Add SupernoteAtelier parser for Atelier .spd files - #33
Merged
Conversation
philips-clanker
marked this pull request as ready for review
July 28, 2026 00:17
philips-clanker
marked this pull request as draft
July 28, 2026 00:17
philips
marked this pull request as ready for review
July 28, 2026 17:17
5 tasks
.spd files (from the Supernote Atelier app) are a different format from .note files: a SQLite database of tiled canvas images plus a small config table, rather than the custom binary layout SupernoteX parses. SupernoteAtelier.open() reads it via sql.js and exposes: - config: every config row as raw bytes - fmtVer, thumbnailBuffer, viewport, canvasSize, layers: decoded from config (fmtVer/viewport/canvasSize are straightforward; layers is a best-effort protobuf decode of the undocumented `ls` value) - surfaces: per-tile PNG blobs for every surface_* table in the file, discovered dynamically rather than assumed to be surface_1/surface_2 - toImage(surfaceName): stitches one surface's tiles into an image, sized/positioned against the tile-grid bounds of every surface in the file so different layers line up and can be composited - toCompositeImage(): flattens every surface into one final image, layered bottom-to-top by layers order (best-effort) -- the simple one-call API for "just give me a finished picture" The .spd schema (especially the `ls` layer list encoding) isn't officially documented. The implementation started from reverse- engineering https://github.com/Ziv-Ink/Atelier-parser, a community tool that writes .spd files, then was corrected against a real device-generated .spd file, which exposed several things that tool's approximation of the format didn't: - Surfaces aren't limited to surface_1/surface_2; real files can have arbitrary surface_{layerId} tables (e.g. a "Reference Layer" used surface_9999). - Layers can have sparse, differently-offset tile coverage, so a naive per-surface tight crop produced images with different origins that couldn't be composited correctly. - image-js defaults a new image's buffer to opaque black, not transparent, so unpasted grid cells were treated as real content and wiped out whatever was layered underneath. - The shared compositeImages() helper (now exported from conversion.ts for reuse) treats a pixel as transparent only when its packed RGBA is exactly zero, which a transparent-but-non-black pixel (e.g. transparent white) fails; tiles are now normalized to zero RGB wherever alpha is zero before compositing. - The tile-id addressing scheme (tid = col * 4096 + row + offset) was confirmed to generalize correctly to a real, non-default canvas size. Test fixtures: tests/input/sample.spd is synthetic, shaped to mirror the real file (sparse per-layer tiles, a surface_9999 reference layer, an empty surface_3, surface.width/surface.height config). tests/input/real-device.spd is a real Atelier export from a Supernote device, used to verify the parser against actual hardware output rather than only a community tool's approximation of the format. Closes #31.
philips-clanker
force-pushed
the
worktree-spd-parser
branch
from
July 28, 2026 17:55
58f2e5f to
6f58256
Compare
SupernoteAtelier.open's sqlJsConfig passthrough already supports this (it's forwarded to sql.js's initSqlJs unchanged), but it wasn't documented. Adds the wasmBinary recipe, validated by the Supernote Obsidian Plugin's .spd prototype, which uses it to avoid sql.js's default locateFile() fetch/readFileSync -- the one thing that otherwise differs between desktop and mobile Obsidian.
pull Bot
pushed a commit
to ben-vargas/supernote-obsidian-plugin
that referenced
this pull request
Jul 28, 2026
Adds a minimal read-only viewer for .spd files (Supernote Atelier app), built on supernote-typescript's SupernoteAtelier parser (philips/supernote-typescript#33). Registers the 'spd' extension to a new SupernoteAtelierView that flattens every layer with toCompositeImage() and shows the result as a single image — no page concept, so none of SupernoteView's zoom/find/thumbnail toolbar or embed support yet. sql.js's wasm binary is embedded into the bundle via esbuild's binary loader (esbuild.config.mjs) rather than fetched/read at runtime, so it works the same on desktop and mobile without a locateFile() callback. Points the supernote-typescript submodule at a local merge of PR philips#33 (worktree-spd-parser) onto main, pushed to spd-prototype-merge-main so the pinned commit resolves for anyone cloning this branch. Closes philips#136.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SupernoteAtelier, a parser for.spdfiles produced by the Supernote Atelier app..notefiles (a custom binary layout, parsed bySupernoteX),.spdfiles are plain SQLite databases (tiled canvas images + a small config table). Reads them viasql.jsand exposes:config— everyconfigrow as raw bytesfmtVer,thumbnailBuffer,viewport(vp.x/vp.y/vp.scale),canvasSize(surface.width/surface.height),layers— decoded fromconfigsurfaces— the per-tile PNG blobs from everysurface_*table in the file, keyed by table name (discovered dynamically)toImage(surfaceName)— stitches one surface's tiles into a single image, sized/positioned against the tile-grid bounds of every surface in the file (not just the requested one), so different layers line up and can be compositedtoCompositeImage()— flattens every surface into one final image directly, layered bottom-to-top bylayersorder (best-effort). The simple one-call API for "just give me a finished picture.".spdschema (especially thels/layers config encoding) isn't officially documented. The initial implementation was reverse-engineered from https://github.com/Ziv-Ink/Atelier-parser, a community tool that writes.spdfiles — then checked against a real device-generated.spdfile, which surfaced real bugs the community tool didn't expose:surface_1/surface_2. Real files can have arbitrarysurface_{layerId}tables (e.g. a "Reference Layer" usedsurface_9999), so surface tables are discovered fromsqlite_masterinstead of assumed.toImagenow sizes/positions against the shared tile grid across all surfaces.toCompositeImagesurfaced two more, both invisible when looking at a single surface's image in isolation and only apparent once actually layering surfaces together:image-jsdefaults a new image's buffer to opaque black, not transparent, so unpasted grid cells were being treated as real content and wiped out whatever was underneath; and the reusedcompositeImagestreats a pixel as transparent only when its packed RGBA is exactly zero, which a transparent-but-non-black pixel (e.g. transparent white) fails, so tiles are now normalized to zero RGB wherever alpha is zero before compositing.tid = col * 4096 + row + offset) generalizes correctly to a real, non-default canvas size.tests/input/sample.spd— synthetic, shaped to mirror the real file (sparse per-layer tiles, asurface_9999reference layer, an emptysurface_3,surface.width/surface.heightconfig).tests/input/real-device.spd— a real Atelier export from a Supernote device (a built-in "Audubon" template background with a couple of sketch strokes on two layers), used to verify the parser against actual hardware output.Closes #31.
Test plan
npm run build(tsc) succeedsnpm run lintpassesnpm test— full suite passes (33/33), includingtests/atelier.test.tsagainst both fixtures