Skip to content

Add addTextOnlyPdfPage(): a PDF page with recognized text but no image - #34

Merged
philips merged 1 commit into
mainfrom
text-only-pdf-page
Jul 28, 2026
Merged

Add addTextOnlyPdfPage(): a PDF page with recognized text but no image#34
philips merged 1 commit into
mainfrom
text-only-pdf-page

Conversation

@philips-clanker

Copy link
Copy Markdown
Collaborator

Summary

  • Adds addTextOnlyPdfPage(ctx, page, pageWidth, pageHeight, options?): builds a PDF page with the invisible recognized-text (RTR) layer, same as addPdfPage(), but embeds no image at all.
  • Refactors the recognition-text-drawing loop out of addPdfPage() into a shared drawRecognitionText() helper used by both functions, so they can't drift out of sync.
  • Exports the new function from src/index.ts.

Why

addPdfPage() is correct for a PDF the user actually opens or exports — it needs the real page image. But some callers only build a PDF to hand to pdf.js purely for getTextContent()/getViewport() (e.g. building a search/selectable text layer over a page that's actually displayed some other way, like a directly-rendered <canvas>) and never call pdf.js's render() against it. For those, the embedded image is pure dead weight — and not a cheap kind. pdf-lib's PNG embedding always fully decodes the PNG and recompresses it from scratch at .save() time (PngEmbedder calls PNG.load() then re-flateStreams the raw RGB/alpha planes), regardless of the source PNG already being compressed. This is real, size-proportional cost.

Profiled against a real 12-page, ~1404x1872px note with several user-uploaded-background pages:

  • addPdfPage (image + text): ~2.9s total (embedPng ~0.8s, .save() ~2.1s), ~4.5MB output.
  • addTextOnlyPdfPage (text only): ~40ms total, ~43KB output.
  • Extracted text identical either way (verified via pdf-parse).

This is being consumed by philips/supernote-obsidian-plugin's SupernoteView, which was unconditionally paying the full image-embedding cost on every single note open just to get a text layer for search/selection, even though the image was never rendered.

Test plan

  • npx vitest run (submodule) — 26 passed, including one new test asserting addTextOnlyPdfPage produces the same extracted text as addPdfPage for the same note, and that its output is under 1/10th the byte size.
  • npm run lint — clean
  • npm run build (tsc) — clean

addPdfPage() embeds the full page image plus the invisible RTR text
layer, correct for a PDF the user actually opens/exports. But some
callers only need the resulting PDF to hand to pdf.js for
getTextContent()/getViewport() (e.g. to build a search/select text layer
over a page that's actually displayed some other way, like a directly
rendered canvas) and never call pdf.js's render() against it — for them,
the embedded image is pure dead weight. pdf-lib's PNG embedding always
fully decodes and recompresses the image at save() time regardless of
whether the source PNG was already compressed, which is real,
size-proportional cost: profiling against a real 12-page, ~1404x1872,
partly user-background note showed ~2.9s and ~4.5MB for the full
image+text PDF, versus ~40ms and ~43KB for text-only, with identical
extracted text.

Refactors the recognition-text-drawing loop out of addPdfPage() into a
shared drawRecognitionText() so both functions stay in sync.
philips-clanker added a commit to philips/supernote-obsidian-plugin that referenced this pull request Jul 28, 2026
The PDF built for pdf.js's getTextContent()/getViewport() (search and
text selection) was unconditionally paying the full cost of embedding
every page's real image, even though pdf.js's render() is never called
against it — the image is never actually shown. pdf-lib's PNG embedding
always fully decodes and recompresses the source PNG at save() time
regardless of it already being compressed, which is real,
size-proportional cost.

Bump the supernote-typescript submodule to pick up
addTextOnlyPdfPage() (philips/supernote-typescript#34) and use it here
instead of assemblePdfFromImages(). Profiled against a real 12-page note
with several user-uploaded-background pages: ~2.9s/~4.5MB before, down
to ~40ms/~43KB after, with identical extracted/searchable text. This was
the dominant cost behind "opening an already-cached note still feels
slow" — confirmed by splitting the earlier timing logs into per-phase
numbers and finding the "decode" and "assemble" timings were nearly
identical, i.e. competing for the same main thread.

assemblePdfFromImages() (real images) is unchanged and still used by the
user-facing "export/attach as PDF" commands, which need actual page
content in their output.
@philips
philips merged commit 52a9f25 into main Jul 28, 2026
1 check passed
pull Bot pushed a commit to ben-vargas/supernote-obsidian-plugin that referenced this pull request Jul 28, 2026
The PDF built for pdf.js's getTextContent()/getViewport() (search and
text selection) was unconditionally paying the full cost of embedding
every page's real image, even though pdf.js's render() is never called
against it — the image is never actually shown. pdf-lib's PNG embedding
always fully decodes and recompresses the source PNG at save() time
regardless of it already being compressed, which is real,
size-proportional cost.

Bump the supernote-typescript submodule to pick up
addTextOnlyPdfPage() (philips/supernote-typescript#34) and use it here
instead of assemblePdfFromImages(). Profiled against a real 12-page note
with several user-uploaded-background pages: ~2.9s/~4.5MB before, down
to ~40ms/~43KB after, with identical extracted/searchable text.

Also moves the pdfDocPromise kickoff to after the page-render loop
instead of before it: it's fire-and-forget but still runs on the same
single JS thread, so starting it first let it compete with (and inflate
the apparent cost of) the render loop.

assemblePdfFromImages() (real images) is unchanged and still used by the
user-facing "export/attach as PDF" commands, which need actual page
content in their output. Also ignores personal .note test files dropped
at the repo root for manual testing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants