Skip to content

feat(pdf-server): rasterize imported annotations + form/save consistency follow-ups - #593

Merged
ochafik merged 7 commits into
mainfrom
ochafik/pdf-annotation-canvas-map
Apr 2, 2026
Merged

feat(pdf-server): rasterize imported annotations + form/save consistency follow-ups#593
ochafik merged 7 commits into
mainfrom
ochafik/pdf-annotation-canvas-map

Conversation

@ochafik

@ochafikochafik commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

What

Imported annotations (annotationCanvasMap):

  • New "imported" type for appearance-stream stamps and unmodeled subtypes (Ink, Polygon, …); rasterized via page.render({annotationCanvasMap, isEditing:true, transform:[dpr,…]}) so each annotation's appearance lands in its own canvas at the correct DPR
  • renderImportedAnnotation wraps the canvas (or a transparent click-box if pdf.js didn't divert one) → selectable, draggable, listed in panel
  • AnnotationLayer.render() filtered to Widgets only so stamps don't get click-stealing <section>s in #form-layer

Deletion of baseline annotations:

  • buildAnnotatedPdfBytes(..., removedRefs) walks each page's /Annots and strips matching refs
  • Panel lists removed-baseline entries with REVERT
  • parseAnnotationRef() handles pdf-<num>-<gen> and pdf-<num>R ids

Load/save consistency:

  • Seed baseline form values from getFieldObjects() (AcroForm tree); when widget /V disagrees, push to storage and re-render once so the input shows the AcroForm truth
  • After successful save, reloadPdf() instead of in-place rebase — old pdfDocument no longer drifts vs disk

Tests

parseAnnotationRef cases; removedRefs strip round-trip; "imported" through computeDiff. 277/278 pass.

Follow-up

Moving (not deleting) an "imported" annotation is UI-only — save doesn't re-serialize the appearance stream to a new position yet.

@pkg-pr-new

pkg-pr-newBot commented Apr 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@593

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@593

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@593

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@593

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@593

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@593

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@593

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@593

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@593

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@593

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@593

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@593

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@593

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@593

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@593

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@593

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@593

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@593

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@593

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@593

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@593

commit: 4297d90

@ochafikochafik changed the title feat(pdf-server): rasterize imported annotations via annotationCanvasMapfeat(pdf-server): rasterize imported annotations + form/save consistency follow-upsApr 2, 2026
Adds an 'imported' annotation type for anything in a loaded PDF that we
either don't model (Ink, Polygon, Caret, FileAttachment, ...) or can't
faithfully re-render (Stamp with an appearance stream, e.g. an image
signature). These now:
- Appear in the annotation panel as '<Subtype> (from PDF)'
- Render in our layer as a positioned div whose body is the per-
annotation canvas pdf.js produced via page.render({annotationCanvasMap})
- if pdf.js didn't divert it (no hasOwnCanvas), the box is transparent
over the main-canvas pixel and just captures clicks
- Are selectable and draggable (resize/rotate disabled - bitmap would
just stretch)
- Are skipped by addAnnotationDicts; getAnnotatedPdfBytes already
filters baseline ids, so save leaves the original in the PDF.
Move/delete are UI-only for now (documented).
Link (2) and Popup (16) are excluded - navigational/auxiliary, not
markup.
importPdfjsAnnotation tests added for unsupported-type and
appearance-stamp paths; computeDiff round-trip for 'imported'.
…annotationCanvasMap
StampAnnotation.hasOwnCanvas defaults to noRotate; stamps without that
flag composited onto the main canvas, so deleting the 'imported' overlay
left an unclickable pixel. isEditing forces hasOwnCanvas=true for stamps
(via mustBeViewedWhenEditing) so the appearance lands in the per-id
canvas and our DOM element is the only render.
setDirty(false) updated the title and save button but the panel kept
showing pending-change badges because it diffs against
pdfBaselineAnnotations/FormValues and was never re-rendered.
renderPage applied devicePixelRatio via ctx.scale(dpr,dpr) instead of
page.render's transform parameter. pdf.js sizes annotationCanvasMap
backing buffers as rectW * outputScaleX * viewport.scale, and
outputScaleX is read from transform[0] (defaults 1). So on retina the
per-annotation canvas got a 1x backing while its internal setTransform
(from the SVD of the already-dpr-scaled ctx) was 2x - the appearance
rendered at 2x into a half-sized buffer, showing only the top-left
quarter.
Pass dpr via transform: [dpr,0,0,dpr,0,0] so outputScaleX matches.
Also filter AnnotationLayer.render() to Widget annotations only so it
stops creating empty pointer-events:auto sections for stamps in
#form-layer that could steal clicks from our overlays.
…save
buildAnnotatedPdfBytes takes a removedRefs list and walks each page's
/Annots array (backwards) removing matching PDFRef entries.
getAnnotatedPdfBytes computes that list from baseline annotations no
longer in annotationMap, parsing the ref back from our id via
parseAnnotationRef (handles both pdf-<num>-<gen> and pdf-<num>R).
Panel: removed baseline annotations now stay listed as crossed-out cards
with a revert button (mirrors cleared form fields), so the user can see
and undo the pending deletion before save commits it.
buildFieldNameMap runs AFTER the first renderPage (perf: don't block the
canvas on an O(numPages) scan). When it detects a widget/field-tree
mismatch and pushes the field-tree value into annotationStorage, the
form layer has already rendered the stale widget value. Re-render once
when that happens so the input shows the AcroForm truth.
… in place
Rebasing baselines while keeping the old pdfDocument drifts: subsequent
renders rasterize annotations that were just stripped from disk, and the
field/widget split pdf-lib's save can create isn't visible until reload.
Reload makes 'viewer == disk' an invariant. localStorage cleared first;
file_changed echo suppressed by lastSavedMtime as before.
@ochafik
ochafikforce-pushed the ochafik/pdf-annotation-canvas-map branch from bd5391b to 731b0c7CompareApril 2, 2026 09:03
@ochafik
ochafik merged commit 61fe39b into mainApr 2, 2026
12 of 18 checks passed
@ochafikochafik mentioned this pull request Apr 2, 2026
Sign up for freeto 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.

1 participant

@ochafik