Skip to content

Read and write a markup annotation's comment as rich text - #978

Merged
bobsingor merged 2 commits into
mainfrom
feature/markup-rich-text
Nov 4, 2026
Merged

bobsingor merged 2 commits into
mainfrom
feature/markup-rich-text

Conversation

@bobsingor

Copy link
Copy Markdown
Contributor

What changes

/RC is every markup annotation's comment (ISO 32000-2 §12.5.6.2): the formatted text its popup shows. Only free text had richText, so a note's formatted comment couldn't be read or written. Editing one's text also had a bug: the update set /Contents and left Acrobat's old /RC. Acrobat shows /RC when it's there, so it kept showing the old text. (Checked on a note written by Acrobat 26.)

Now every markup kind (text, line, square, circle, polygon, polyline, the four text markups, caret, stamp, ink, file attachment, redact) has richText, the same RichTextDocument free text has:

await page.annotations.create({
  subtype: 'text',
  rect,
  richText: { paragraphs: [{ runs: [{ text: 'Check ' }, { text: 'this', style: { weight: 700 } }] }] },
});
await page.annotations.update(note.ref, { subtype: 'text', contents: 'Plain words now' });
await page.annotations.update(note.ref, { subtype: 'text', richText: null });
  • A plain comment reads richText: null; contents is always the plain text.
  • Rich text brings its contents.
  • contents alone on a formatted comment rewrites its paragraphs over its body, so /RC follows instead of going stale. richText: null drops the formatting and keeps the text.
  • contents and richText given together must agree, on every kind now, not only free text. Line breaks count the same however they're spelled (Acrobat's \n, our \r).
  • A comment written by Acrobat names no font, so it reads as Helvetica, and a Helvetica body is written back naming none. An Acrobat note comes back the same after a round trip.

By layer:

  • Runtime: pinned to f1ba46f0d. It brings Write a markup annotation's comment as rich text runtime#110 (EPDFAnnot_SetRichTextJSON writes /RC and /Contents on every markup kind, and nothing else, as Acrobat does), Svelte and npm package #111 (linearized test files go through the layer) and the S16 upstream sync (readAttachmentContent throws WASM error. #112). No function is added.
  • Engine core:
    • markupBaseFields gives the markup kinds richText; free text keeps its own.
    • resolve/comment.ts holds the agreement check and the follow rules for drafts and patches.
    • The rich text schemas move to dto/RichText.schema.ts and become named wire components.
  • Engine services:
    • Each markup reader reads its comment, and a write sets it through EPDFAnnot_SetRichTextJSON; null removes /RC.
    • Fixed: a redact's overlay font now reads from its /DA alone (EPDFAnnot_GetDefaultAppearanceFontFace), not from its rich text. That rich text is the comment, so a comment in another font changed the overlay's font.
  • Contract and SDKs: api:sync. The two schemas are named once, which makes the TypeScript SDK 439 lines smaller.
  • Docs: the engine annotations page and "Formatted comments" on the notes page, synced to both sites.
  • Changesets: engine-core, engine-services, cloudpdf-contract and cloudpdf-sdk, all minor.

Not in this PR:

  • The viewer side: the comment card showing formatting, and a B/I/U box when writing a comment.
  • A fork follow-up for redacts. The fork reads a comment's /RC over the annotation's /DA defaults, and a redact is the only comment kind with a /DA. So a redact's comment with no font of its own reads in the overlay's font, size and color, where Acrobat shows its default.

Testing

  • Fork (Write a markup annotation's comment as rich text runtime#110, released): Acrobat's note read as rich text, a comment written as Acrobat writes one, Acrobat's comment written back unchanged, and a link and a popup refused.

  • Conformance on the local (wasm) and cloud engines:

    • a note's comment reads back with its plain text, and a plain one reads null;
    • new contents rewrite a formatted comment, and null makes it plain;
    • a redact whose comment is in Times keeps its Courier overlay. This test fails without the reader fix.
  • Export hashes: both pinned export hashes moved, because a read's richText now sits beside the base fields. Moving it back to the end reproduces the old hashes exactly.

  • Suites, on a runtime built from the release's libraries (darwin-arm64, wasm32), with main merged (Format the text typed next at a caret, and a selected box's whole text #975):

    Suite Result
    @embedpdf/engine 1924 passed
    @cloudpdf/engine 1121 passed
    engine-core 1043 passed
    core-annotation 670 passed
    plugin-annotation 289 passed

    The full engine runs came before the last hash update; their export and mutation files were rerun after it.

  • Checks: typecheck of the 72 packages that depend on engine-core (118 turbo tasks) and api:check.

The release brings fork PRs 110 to 112: a markup annotation's comment
written as rich text (EPDFAnnot_SetRichTextJSON writes /RC and /Contents
on every markup kind), linearized files opened through the layer in
layer-mode tests, and upstream PDFium up to 2b52a82ab (Nov 2), where CMYK
images under a soft mask or a blend mode convert like any other. No
function is added; runtime-src moves to the pin.
/RC is every markup annotation's comment (ISO 32000-2 §12.5.6.2). Every
markup kind now has `richText`, the RichTextDocument free text has, or
null for a plain comment, with `contents` its plain projection. Rich
text brings its contents; contents alone on a formatted comment rewrite
its paragraphs over its body, so /RC follows instead of keeping the old
text other PDF apps show; richText: null drops the formatting. Contents
and rich text given together must agree, line breaks however spelled.

engine-services: each markup kind's reader reads its comment's /RC, and
a write sets /RC with /Contents through EPDFAnnot_SetRichTextJSON,
leaving the appearance. A comment body that names no font reads as
Helvetica and is written back naming none. A redact's overlay font now
reads from its /DA alone (EPDFAnnot_GetDefaultAppearanceFontFace): its
rich text is its comment, and a comment in another font changed it. The
rich text schemas move to dto/RichText.schema.ts and are named wire
components, so the contract and SDK carry the model once. The pinned
export hashes move because a read's richText now sits beside the base
fields.
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cloudpdf-com Building Building Preview Nov 4, 2026 11:46am UTC
embed-pdf-snippet Error Error Nov 4, 2026 11:46am UTC
embed-pdf-viewer-react-mui Error Error Nov 4, 2026 11:46am UTC
embed-pdf-viewer-svelte-tailwind Error Error Nov 4, 2026 11:46am UTC
embed-pdf-viewer-vue-vuetify Error Error Nov 4, 2026 11:46am UTC
embed-pdf-website Building Building Preview Nov 4, 2026 11:46am UTC

Request Review

This branch had an error being deployed

4 failed and 2 active deployments
Preview – cloudpdf-com — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
Preview – embed-pdf-website — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
Preview – embed-pdf-snippet — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
Preview – embed-pdf-viewer-vue-vuetify — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
Preview – embed-pdf-viewer-svelte-tailwind — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
Preview – embed-pdf-viewer-react-mui — 7b91e04a Deployed Nov 4, 2026 by vercel[bot]
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.

1 participant