Skip to content

Modify And Inspect PDFs

Construct Recipe with a Uint8Array or ArrayBuffer to modify an existing PDF. For a Blob or File, await arrayBuffer() first. Select a one-based page, finish its editing context with endPage(), and receive new bytes from endPDF(); the input object is never overwritten.

import { createRecipe } from "@muhammara/wasm";

var Recipe = await createRecipe();
var inputBytes = new Recipe()
  .createPage(420, 300)
  .text("Original", 40, 50)
  .endPage()
  .endPDF();

var recipe = new Recipe(inputBytes);
var geometry = recipe.pageInfo(1);
var outputBytes = recipe
  .editPage(1)
  .rectangle(24, 24, geometry.width - 48, geometry.height - 48, {
    stroke: "#2563eb",
    lineWidth: 2,
  })
  .text("Added to existing bytes", 40, 90)
  .endPage()
  .endPDF();

Editing uses a byte-backed modifier, including for rotated pages and pages with non-zero MediaBox origins. pauseContext() and resumeContext() split an edit into separate appended content contexts. Both return the Recipe for valid transitions and throw for unmatched calls. Password-protected source editing is not available; decrypt first with the low-level byte-first recrypt() API.

Delete Source Pages

deletePage(pageNumber) and deletePage([pageNumbers]) remove one or more one-based pages from the original source when endPDF() finalizes the Recipe. At least one page must remain. Deletion cannot be combined with createPage(), appendPage(), or insertPage() in the same Recipe.

Deletion preserves retained page objects and adjusts page labels, but it is an incremental update rather than secure erasure of the removed content. A page that outlines, links, form widgets, tagged-PDF structure or the open action still reference is refused unless you pass deletePage(pageNumbers, { pruneReferences: true }), which removes those references. See Delete Pages for a complete byte-input example and what pruning changes.

Inspect Without Replacing Output State

read(bytes) and readAsync(blob) report page count and geometry without turning those bytes into the Recipe's output document. info() returns the document Info metadata currently known to Recipe. getPageInfo() is the native-compatible accessor; during source editing it returns the writable output Info dictionary rather than the parsed source values. pageInfo() and getCurrentPageInfo() return page geometry.

structure("json") returns a browser-safe summary rather than writing a diagnostic file. Calling structure() finalizes the Recipe through endPDF(), so perform all page and document operations first.

import { createRecipe } from "@muhammara/wasm";

var Recipe = await createRecipe();
var sourceBytes = new Recipe()
  .createPage(240, 160)
  .text("Inspect these bytes", 24, 36)
  .endPage()
  .endPDF();
var inspector = new Recipe();
var metadata = await inspector.readAsync(
  new Blob([sourceBytes], { type: "application/pdf" }),
);

console.log(metadata.pages, metadata[1].mediaBox);
console.log(new Recipe(sourceBytes).structure("json"));

Replace Text

replaceText(text, replacement, pageNumber) rewrites text-showing operands in an existing page's single content stream. It leaves text position and font unchanged.

import { createRecipe } from "@muhammara/wasm";

var Recipe = await createRecipe();
// The embedded font is subset, so the page must already use the
// replacement's glyphs.
var sourceBytes = new Recipe({ compress: false })
  .createPage(300, 160)
  .text("Before", 30, 50)
  .text("After", 30, 90)
  .endPage()
  .endPDF();

var outputBytes = new Recipe(sourceBytes)
  .replaceText("Before", "After", 1)
  .endPDF();

text is compared with each Tj operand decoded through the font selected by Tf: the font's /ToUnicode CMap first, then a simple font's /Encoding and /Differences. This matches text written with composite (Type0) fonts, such as the hex glyph IDs Muhammara writes, as well as simple fonts. text and replacement can be any Unicode strings. The replacement is encoded through the same font and written back as a literal or hex string, like the original operand. Content outside the replaced operands keeps its exact bytes.

Matching follows the font's encoding, not the raw bytes. A Type 1 font without an /Encoding uses StandardEncoding, where the byte ' is the right single quotation mark, so (don't) Tj matches "don’t" rather than "don't". Use the text field of extractPageText() to see how a page's text decodes.

The replacement can only use glyphs the font already has. Embedded fonts are usually subset to the glyphs of their original text, so a replacement with any other character throws an error that names the missing characters, for example font FN1 has no glyph for "A", "t". A font that cannot be read, or whose /Widths array is malformed, so it cannot tell which glyphs exist, throws an error too. Replacing text with a new font is not supported.

Only whole Tj operands match. Text split across several show operations, and TJ, ', and " operands, are not replaced. Pages with more than one content stream are rejected with an error. When nothing matches, the page is left unchanged. See Replace Text In An Existing PDF for a worked example and what to do when nothing matches or a glyph is missing.

Remove Text

removeText(pageNumber, options) drops every text-showing operator (Tj, TJ, ', ") from an existing page, for example before placing a fresh OCR text layer. Graphics, images, and marked content stay in place, and all of the page's content streams are handled. pageNumber is a required one-based page number.

var outputBytes = new Recipe(sourceBytes)
  .removeText(1, { forms: true })
  .endPDF();

With { forms: true }, text inside the Form XObjects the page paints is removed too, including text that an earlier editPage() added. Without it, only the page's own content streams change. Annotation appearances, such as form field values, are never changed.

The source streams are rewritten in place: a content stream or form shared with another page loses its text there as well. Text added with editPage() in the same Recipe is kept, whichever call comes first. Like page deletion, removal is an incremental update, so the old text bytes can remain in the returned data; do not use it to redact sensitive content.