@trupdf/content-edit
Page-content-stream editing. Detects editable text / image blocks on a page, queues pending edits, and bakes them directly into the PDF — edits live in the page content stream, not in annotation dictionaries, so they survive every save mode and render identically in Acrobat, Chrome, Firefox, Edge, and Apple Preview.
ContentEditEngine
import { ContentEditEngine } from '@trupdf/content-edit';
// Or grab the viewer's own instance: engine.getContentEditEngine()
const editEngine = new ContentEditEngine(); // stateless; share across documents
const blocks = await editEngine.detectBlocks(page, { documentKey: pdfDoc });
const newBytes = await editEngine.bake(oldBytes, store.list());| Method | Purpose |
|---|---|
detectBlocks(page, options?) | Returns EditableBlock[] the UI overlays as editable regions. Cached per (documentKey, page) when options.documentKey is passed. |
bake(originalBytes, edits, options?) | Applies a sequence of PendingEdits to the content stream; returns new PDF bytes |
invalidateDetectionCache(documentKey, page?) | Drop cached detection for one page or the whole document |
Construct with { removal } to swap the removal adapter (see below).
ContentEditStore
UI-facing store of pending edits with undo/redo. Flushed by
ViewerEngine.applyContentEdits() (which SaveEngine.saveAll runs
before the form and annotation passes).
const store = engine.getContentEditStore();
const edit = store.push({ kind: 'insert-text', page: 1, xPdf: 50, yPdf: 700, widthPdf: 400,
text: 'Draft', font: { key: 'Helvetica', size: 11 }, color: { r: 0, g: 0, b: 0 } });
store.update(edit.id, { xPdf: 60 });
store.undo(); store.redo();
store.list(); // ReadonlyArray<PendingEdit>
store.on('changed', (snapshot) => /* re-render overlay */);Edit shapes
PendingEdit is a discriminated union:
type PendingEdit =
| InsertTextEdit // kind: 'insert-text'
| InsertImageEdit // kind: 'insert-image'
| ReplaceTextEdit // kind: 'replace-text'
| ReplaceImageEdit // kind: 'replace-image'
| DeleteEdit; // kind: 'delete'
type StandardFontKey =
| 'Helvetica' | 'HelveticaBold' | 'HelveticaOblique' | 'HelveticaBoldOblique'
| 'TimesRoman' | 'TimesRomanBold' | 'TimesRomanItalic' | 'TimesRomanBoldItalic'
| 'Courier' | 'CourierBold' | 'CourierOblique' | 'CourierBoldOblique';For simple overlay additions without detection, @trupdf/core also
exports a lightweight applyEdits(bytes, edits) helper (add text /
image / rectangle).
Removal adapters
Replacing or deleting existing content goes through a removal adapter:
import {
ContentStreamRewriterAdapter, // default — structural operator rewrite
WhiteOutRemovalAdapter,
PdfiumRemovalAdapter,
} from '@trupdf/content-edit'; // WhiteOutRemovalAdapter + PdfiumRemovalAdapter also ship in @trupdf/viewer
const editEngine = new ContentEditEngine({ removal: new WhiteOutRemovalAdapter() });| Adapter | Behaviour |
|---|---|
| Content-stream rewriter (default) | Structurally removes the affected operators from the page content stream |
| White-out adapter | Masks the original bbox with a filled rectangle — maximum cross-viewer safety |
| Native-engine adapter | True object removal via the native PDF engine (WASM); plug in your own engine handle |
The bake pipeline is adapter-agnostic — implement the RemovalAdapter
interface to bring your own backend.
Font resolution
Baking a text edit resolves its font in three tiers:
- Original resource reuse — when the detected block carried a reusable font reference (subset-tagged base font + inverted character map + advance widths), the new text is encoded and shown through the font dictionary the source run already used — no embedding, exact typeface. This is the only tier that preserves composite subset fonts, i.e. most office-generated PDFs.
- Original program re-embed — when the block carried the document's embedded font program and it has a glyph for every character of the new text, the program is re-embedded (subsetted) so the saved PDF keeps the document's own typeface. The embedder is loaded lazily on the first save that needs it — never on the viewer's critical path.
- Standard 14 fallback —
matchStandardFontpicks the closest Standard 14 variant from the run's real PostScript name (subset tag stripped) plus bold/italic/serif/monospace classifier flags. Override at edit time by passingfontexplicitly. Text is sanitized against the face's WinAnsi character set so an unencodable character degrades to?instead of aborting the save.
While typing, the live edit UI renders with a CSS font chain built
from the detected face (buildCssFontChain) so the editor shows the
page's own glyphs. A face change from the style panel drops the
reuse/re-embed tiers (the captured face no longer matches);
size-only changes keep them.
Capturing per-block font intel requires extended font properties from
the render engine; ViewerEngine enables this automatically outside
secure mode.
Secure mode
The Edit Content tool requires reading existing text, which secure mode forbids — it short-circuits to a disabled state. Insert Paragraph and Insert Image still work in secure mode because they don't read existing content.