feat(plot): import PDFs, HEIC and camera photos, identified by content - #77
Merged
Merged
Conversation
The import button said "+ Image" but meant "SVG, PNG, JPEG or WebP", and it decided which by looking at the file's *name*. Half an operator's sources are neither: the print-and-cut files come as PDF, phones produce HEIC, and a sketch on paper is a photograph that has to be taken, not found. Files are now identified by their bytes. Extensions lie, MIME types are often just the extension in disguise, and the cost of being wrong is a silent empty artwork rather than an error. The sniffer is pure, so each format's signature is pinned by a test rather than discovered on a machine with vinyl in it. PDF is a first-class source. A page with paths becomes lines at the size the page declares, keeping its page position so Place on page and the registration wizard work exactly as they do for an SVG; a page without paths is a scan, and goes to the conversion wizard; a multi-page file asks which page. The operator decoder is pure — it runs on a plain operator list, testable without a PDF or a DOM — and the hard-coded pdf.js op codes are asserted against the library's own at load time, because a renumbering would not break the build, it would just make every PDF import as blank. The y-flip happens in the decoder, at the boundary, so everything downstream keeps the one convention it assumes. Two things learned by running it: - Rasterising uses *print* intent although nothing is printed. pdf.js drives its display render loop with requestAnimationFrame, which does not fire in a background tab — an import started and then left alone would hang for ever. - The page is painted white first. A PDF page has no background of its own, so transparent areas read as black and the whole page traces as one solid blob. pdf.js is imported on demand: it is by far the largest dependency here and most sessions never open a PDF, so it must not sit in the bundle every client downloads to jog the machine. The library and its worker come out as separate chunks. Also fixes a packaging bug this uncovered: the daemon served `.mjs` as application/octet-stream, which browsers refuse to execute as a module. A lazily-loaded chunk would have worked perfectly in the dev server and failed only from the .deb. HEIC is named rather than merely rejected — most browsers cannot decode it, and "convert it on your phone" is useful where "could not read that image" is not. Phones get a Take photo button (the file input's capture attribute) so a sketch can be traced standing at the machine; the placement controls stay on desktop. Verified against the packaged build: a vector PDF imports at 88.2 × 211.7 mm with the right page offset, a three-page PDF asks which page and imports page 2, a filled path imports as its outline, a page whose only content is an image opens the wizard, and the camera input appears only at phone width. Closes #56 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 tasks
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.
Closes #56.
Why
The import button said "+ Image" but meant "SVG, PNG, JPEG or WebP", and it decided which by looking at the file's name. Half an operator's sources are neither: the print-and-cut files come as PDF, phones produce HEIC, and a sketch on paper is a photograph that has to be taken, not found.
Files are identified by their bytes
Extensions lie, MIME types are often just the extension in disguise, and the cost of being wrong is a silent empty artwork rather than an error.
src/plot/sniff.tsis pure, so each format's signature is pinned by a test rather than discovered on a machine with vinyl in it. The MIME type survives only as a tiebreak where the bytes are genuinely ambiguous (a long XML prologue pushing<svgpast the sniffed window) — and never overrides a signature that disagrees.PDF is a first-class source
The operator decoder is pure (it runs on a plain operator list, testable without a PDF or a DOM), and the hard-coded pdf.js op codes are asserted against the library's own at load time: a renumbering in a future version wouldn't break the build, it would just make every PDF import as blank. The y-flip happens in the decoder, at the boundary, so everything downstream keeps the single convention it already assumes.
Two things learned by actually running it:
requestAnimationFrame, which doesn't fire in a background tab — an import started and then left alone would hang for ever.pdf.js is imported on demand — it's by far the largest dependency here and most sessions never open a PDF, so it must not sit in the bundle every client downloads to jog the machine. The library (431 kB) and its worker (1.3 MB) come out as separate chunks; the main bundle is unchanged.
Fixed in passing
The daemon served
.mjsasapplication/octet-stream, which browsers refuse to execute as a module. A lazily-loaded chunk would have worked perfectly in the dev server and failed only from the.deb..wasmand.mapwere missing too.Verification
mise run cigreen — 409 tests, 24 of them new across sniffing and PDF decoding (page size, y-flip, closed contours, cubic and quadratic curves, transform apply/restore/nesting, text counted, images reported, degenerate subpath dropped, an unknown opcode stopping the chunk rather than misreading coordinates).Driven against the packaged build: a vector PDF imports at 88.2 × 211.7 mm with the right page offset (17.6, 50.1); a three-page PDF asks which page and imports page 2; a filled path imports as its outline; a page whose only content is an image XObject opens the wizard; and the camera input appears only at phone width.
🤖 Generated with Claude Code