Skip to content

Low-Level Writer, Reader, And Modifier

Writer Lifecycle

mergePDFPagesToPage (including its async variant) invokes its optional callback with no arguments and globalThis as this, matching native on fresh and modifying writers. Bound functions retain their bound receiver; arrow functions retain their lexical this.

Finish drawing before calling writer.end(). After finalization, disposal, or a finalization failure, stateful writer methods throw Error("PDF writer has ended"), matching native. Async methods reject their promises with the same error. Create a new writer for further output and consume borrowed resources before ending their writer.

appendPDFPagesFromPDF also ends a writer or modifier when copying pages fails; create a fresh writer and retry with valid source bytes. Source bytes that cannot be parsed, encrypted input, and page ranges outside the source are rejected before anything is written, so the writer stays usable.

createPDFDate() and createPDFTextString() create independent values and remain usable after cleanup. dispose() is idempotent; Wasm end() still throws on a second call, whereas native end() is a no-op. Recipe uses the writer guard internally, so there is no additional Recipe method for it.

Drawing Helpers And Clipping

Page and form contexts on new and modifying writers expose drawPath, drawCircle, drawSquare, and drawRectangle. Their type option accepts "stroke" (the default), "fill", or "clip". Clipping intersects the current clipping region without painting the shape and emits W n to end the path. close: true closes the path first. Scope the clip with q() before defining it and Q() after the drawing it should affect. Any other type except null throws TypeError: Unknown drawing type; use "stroke", "fill", "clip" or null before anything is written, as in native.

Pass an RGB color to these helpers or writeText() as a 24-bit number, a CSS color name in any case, or a #rrggbb string. Any other color string, including hex without the #, throws a TypeError before anything is drawn.

For colorspace: "gray" or "cmyk", pass the color as a number: one byte for gray and 0xCCMMYYKK for CMYK. A color name, #rrggbb string, or [r, g, b] array is RGB, so combining it with gray or CMYK throws TypeError: only a numeric color can use the gray or cmyk colorspace, as in native. See Draw in Gray and CMYK.

An explicit type: null ends the path without painting, ignores width and close, and applies a supplied color only to the non-stroking graphics state, matching native. Omit type or use "stroke" for an outline; null does not select the default.

DrawingPathType names these values at runtime and in the TypeScript declarations, so a misspelled paint mode fails to compile, and at runtime it throws instead of producing unpainted geometry.

These helpers validate coordinates and snapshot drawing options before emitting geometry or graphics-state operators. writeText likewise reads its font, size, color, and underline options before starting text output. Throwing option getters propagate their original exception without partial output from the call. Wasm still requires finite numeric coordinates and a font from the same writer; native retains its historical numeric coercions.

Stroke widths and text sizes must also be finite. Circle control points and underline endpoints are checked for overflow before drawing. Paths must contain at least two complete finite coordinate pairs, without holes or extra arguments. Invalid calls throw without emitting operators; correct the values and retry on the same context.

Named Values

DrawingPathType, ImageFit, ObjectReplacementScope, DeviceColorSpace, PageBox, PDFImageType, and EEncoding are frozen objects of accepted option strings, for example DrawingPathType.FILL or ImageFit.OVERFLOW; LineCapStyle and ETokenSeparator name numeric operands. @muhammara/native and @muhammara/wasm export them with the same names and members, each with a same-named TypeScript type. The plain values stay accepted.

Create A PDF

Create a writer, draw in PDF's bottom-left coordinate system, then retain the returned bytes:

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

var muhammara = await createMuhammaraWasm();
var page = new muhammara.PDFPage(0, 0, 595, 842);
var writer = muhammara.createWriter({ compress: true });
muhammara.registerFont("inter", fontBytes);
var font = writer.getFontForBytes("inter");
var content = writer.startPageContentContext(page);

content.BT().Tf(font, 24).Tm(1, 0, 0, 1, 72, 720).Tj("Hello").ET();
writer.writePage(page);
var pdfBytes = writer.end();

createReader(bytes) exposes page counts, page information, PDF objects, streams, xref data, and text extraction with each operation's decoded Unicode text and raw character codes. Call end() when the reader is no longer needed; parser and object handles are owned by it and become invalid afterwards. Every reader method that takes a page index or object ID — parsePage, parsePageDictionary, getPageObjectID, extractPageText, extractPageContentItems, parseNewObject, and getXrefEntry — requires a non-negative integer below 2^32 and throws a TypeError otherwise, exactly as the native reader does.

createWriterToModify(bytes, options?) appends pages or changes an existing page through createPageModifier(index?, ensureContentEncapsulation?). Its end() returns a new Uint8Array. createModifier(bytes) is the compact drawing facade.

A writer created with createWriterToModify also exposes replaceObject(pageIndex, sourceObjectId, replacementObjectId, options?), which repoints every direct reference to sourceObjectId in the zero-based page's dictionary at replacementObjectId. Only that page is rewritten; pass { scope: "global" } to apply the replacement across every page.

var modifyingWriter = muhammara.createWriterToModify(pdfBytes);
modifyingWriter.replaceObject(0, contentsId, replacementId, {
  scope: "global",
});

PDFRStreamForBuffer, PDFWStreamForBuffer, and the ByteReader/ByteWriter aliases are byte adapters, not Node or Web streams. A reader adapter's read(amount) returns a copy of at most amount bytes as a Uint8Array, and its setPosition() and setPositionFromEnd() calls clamp the resulting position to the available byte range. A writer adapter exposes buffer, toUint8Array(), toArrayBuffer(), and toBlob().

Stream readers returned by startReadingFromStream(), startReadingFromStreamForPlainCopying(), getParserStream(), and getSourceDocumentStream() return each read(amount) as a Uint8Array and allocate Wasm resources. Call their idempotent dispose() method as soon as reading finishes. Disposing one of these byte readers does not end its parent PDF reader; ending the parent remains fallback cleanup for byte readers that were not disposed explicitly.

For exact signatures, lifecycle rules, content operators, object contexts, copying contexts, images, forms, and modifier APIs, use the TypeScript reference.

Task-oriented low-level guides: