Migrate From v6 To v7
v7 replaces the single unscoped muhammara package with packages published
under the @muhammara organization on npm:
| Package | Contents |
|---|---|
@muhammara/native |
Prebuilt native addon only |
@muhammara/native-with-source |
Native addon plus the C++ source tree for local and Electron builds |
@muhammara/native-core |
Shared JavaScript layer; a dependency of both, never installed direct |
For most Node.js applications the migration is a dependency rename, an import rename, a supported Node.js version, and a check that a prebuilt binary exists for your platform (steps 1–7). Steps 8–16 cover Recipe behavior, step 17 TypeScript declarations, and steps 18–20 the low-level API; each affects only code that uses those features. The Breaking Changes page lists every change and the step that covers it.
Upgrading from 5.x or older
This guide starts from 6.x. Read the Breaking Changes sections for every major version after yours first, for example Version 6.x when you come from 5.x, and then follow the steps below.
Why Upgrade
v7 passes PDF bytes between the native addon and JavaScript as Buffer chunks
instead of arrays of numbers
#324. Workflows that
keep PDFs in memory or write through JavaScript streams become much faster and
use far less memory. Workflows that read and write file paths are unchanged.
Editing a 48 MB PDF:
| Operation | muhammara 6.0.6 |
@muhammara/native 7 |
|---|---|---|
Buffer-mode Recipe: edit every page, then endPDF() |
6.4 s, 656 MB | 0.40 s, 258 MB |
createWriterToModify() from a Buffer into a JS stream |
7.3 s, 559 MB | 0.10 s, 255 MB |
File-path Recipe: edit every page, then endPDF() |
0.21 s, 92 MB | 0.21 s, 117 MB |
recrypt() between file paths |
0.35 s, 69 MB | 0.33 s, 71 MB |
Encrypting with recrypt() from a PDFRStreamForBuffer into a
PDFWStreamForBuffer grew quadratically with file size in v6, because RC4
output reached JavaScript one byte at a time:
| PDF size | muhammara 6.0.6 |
@muhammara/native 7 |
|---|---|---|
| 0.48 MB | 13.4 s, 172 MB | 0.01 s, 72 MB |
| 0.96 MB | 60.4 s, 201 MB | 0.01 s, 75 MB |
| 4.8 MB | did not finish in 10 minutes | 0.06 s, 90 MB |
Measured on Linux x64 with Node.js 25 against generated, uncompressed PDFs. Times and peak resident memory are the median of three runs.
1. Choose The Replacement Package
| v6 usage | v7 package |
|---|---|
npm install muhammara on a platform with a matching prebuilt binary |
@muhammara/native |
| Installs that compiled locally, Electron rebuilds, or platforms without a prebuilt binary | @muhammara/native-with-source |
Both native packages expose the same API, the same TypeScript declarations, and the same native binary metadata. They differ only in whether the C++ source is included.
v6 shipped the prebuilt binaries and the C++ source in one package, so an
install without a matching prebuilt binary compiled from source. v7 separates
the two. @muhammara/native fails installation with a pointer to
@muhammara/native-with-source when no prebuilt binary matches; it never
compiles.
2. Replace The Dependency
Use the source-capable package instead when a matching prebuilt binary is unavailable or Electron must rebuild the addon:
To keep the scoped import name while using the source-capable package, use an npm alias:
3. Update Imports
// v6
var muhammara = require("muhammara");
var Recipe = require("muhammara").Recipe;
// v7
var muhammara = require("@muhammara/native");
var Recipe = require("@muhammara/native").Recipe;
Recipe remains bundled, so no separate hummus-recipe or muhammara-recipe
package is needed.
The shared JavaScript layer now lives in @muhammara/native-core, which both
native packages install as a dependency. Paths under muhammara/lib/, such as
require("muhammara/lib/Recipe"), were never public API and no longer resolve,
also under the npm alias below; import from the package root instead. Do not import
@muhammara/native-core from an application; it needs an addon supplied by an
implementation package.
Stage The Rename
To postpone the import changes, alias the old package name. Existing
require("muhammara") calls keep working while the dependency is already v7:
Treat this as a temporary step. The alias hides the package name from the dependency tree and makes future upgrades harder to reason about.
4. Update TypeScript Imports
v6 shipped an ambient declare module "muhammara" block. v7 attaches the
declarations to each package, exports them with export =, and defines no
global namespace:
import muhammara = require("@muhammara/native");
declare const writer: muhammara.PDFWriter;
declare const recipe: muhammara.Recipe;
Only the module name changes: import, import = require(), and module
augmentation forms that named "muhammara" compile once they name the new
package, or unchanged under the npm alias.
5. Confirm Prebuilt Coverage
v7 supports Node.js 20 || 22 || 24 || >=25. 6.x declared >=17 and shipped
prebuilds for Node.js 19 to 24, so Node.js 17, 18, 19, 21, and 23 are no longer
supported. Move to a supported Node.js release before upgrading.
Windows win32 (32-bit) prebuilds and build tooling were removed; use Windows
x64. Check that your platform, architecture, and runtime are listed in the
prebuilt support matrix. Install
@muhammara/native-with-source for any combination that is not.
6. Update Native Binary Tooling
v7 uses one Node-API 8 binary across all supported Node.js and Electron versions. An ordinary npm install and public package import need no change beyond the package rename described above. The native binary metadata, archive name, and installed path do change:
| v6 | v7 | |
|---|---|---|
| Prebuild archive | node-v{abi}-{platform}-{arch}-{libc}.tar.gz |
napi-v8-{platform}-{arch}-{libc}.tar.gz |
| Installed addon | binding/muhammara.node |
binding/napi-v8/muhammara.node |
| Runtime selection | Separate archive for each Node.js or Electron ABI | One archive for every Node-API 8+ runtime |
Update custom binary mirrors and direct-download deployment scripts to carry
the napi-v8-* archives. Stop copying or importing the addon through a
hard-coded binding/muhammara.node path; import the package so node-pre-gyp
resolves its declared module path. Tooling that intentionally inspects the
binary can read binary.module_path, binary.package_name, and
binary.napi_versions from the selected package's package.json rather than
duplicating these values.
The real napi_versions: [8] metadata also allows package analyzers such as
Turbopack to identify the addon as Node-API compatible.
7. Rebuild Electron Applications
Electron applications must install the source-capable package before running
@electron/rebuild, because the rebuild tool runs node-gyp directly and needs
the bundled source tree. See the
Electron support policy.
8. Reactivate Pages After endPage()
In v7, Recipe.endPage() clears the completed page and its content context.
Calls that draw on or configure a page must follow createPage() or
editPage() rather than relying on the completed page remaining active:
recipe.endPage();
// Create another page before adding more content.
recipe.createPage("letter");
recipe.text("Next page", 72, 72);
When modifying a PDF, call editPage() with the one-based page number before
resuming page operations:
After endPage(), shapes, image(), and link() throw
TypeError: No page is active; call createPage() or editPage() first, and
table(), overlay(), setPageBox(), rotate(), and pauseContext() throw
their own errors. text() draws nothing, and comment() or annot() make
endPDF() fail.
Check The Recipe Lifecycle
Other Recipe lifecycle calls that 6.x tolerated now throw or behave differently:
endPDF()finishes the document once. A repeated call leaves the completed PDF unchanged instead of finalizing it again, which could crash 6.x; a repeatedendPDF(callback)still receives the completed output. Create a new Recipe for changes made afterendPDF().- A failed
endPDF()retires the Recipe: its writer is aborted, its source file is released, and later calls rethrow the original error. Fix the cause and build the document again with a new Recipe instead of retrying. pauseContext()throws without an active page content context, andresumeContext()throws without a paused one. CallpauseContext()only aftercreatePage()oreditPage(), andresumeContext()once after each successful pause.appendPage()rejects zero, negative, fractional, reversed, and malformed page selections instead of clamping them. Pass positive one-based integers or ascending two-value ranges such as[2, 5]; integer endpoints beyond the source still clamp to its last page.insertPage(afterPageNumber, pdfSrc, srcPageNumber)throws aTypeErrorright away whenpdfSrcorsrcPageNumberis missing, where 6.x queued nothing or failed inendPDF(). Pass all three arguments.
9. Replace Recipe.fillOpacity()
Recipe.fillOpacity() was removed. Replace it with Recipe.opacity(), which
sets both fill and stroke alpha:
// v6
recipe.fillOpacity(0.5);
// v7
recipe.opacity(0.5);
// Restore opaque drawing after translucent content.
recipe.opacity(1);
The undocumented ANNOTATION_PREFIX, appendPDFPageFromPDFWithAnnotations(),
and appendPDFPagesFromPDFWithAnnotations() members of the Recipe prototype
were internal helpers and are gone too. Copy pages with appendPage(),
insertPage(), or split() instead.
10. Rename Recipe Plugins That Collide With New Methods
v7 adds these Recipe methods: deletePage, getCurrentPageInfo,
lineStyle, link, opacity, pie, removeText, replaceText, rotate,
rotateContent, and setPageBox. register() refuses to replace an existing
method, so registering a plugin with one of these names now throws
Found conflict in Recipe prototypes. <name> already exists.
#829.
Rename the plugin, or use the built-in method if it does what your plugin did:
// v6: a plugin named pie.
recipe.register("pie", drawPie);
// v7: register it under a name Recipe does not use.
recipe.register("drawPieChart", drawPie);
11. Pass Valid Recipe Options
v7 Recipe checks numeric options before it draws anything and throws where 6.x wrote invalid PDF operators, drew nothing, or failed halfway through a call. The Recipe stays usable after such an error. A computed value is the usual source of these errors; guard it, or leave the option out to keep its default.
| Option | v7 throws for | 6.x behavior |
|---|---|---|
Text size or fontSize |
anything but a finite number greater than zero (RangeError) |
clamped negatives to 1pt, fell back to 14pt, or wrote inf |
Text charSpace |
NaN, Infinity, or a non-number (TypeError) |
wrote inf Tc, ignored NaN, or threw after drawing started |
rotation of shapes, text, images |
a value that is neither a number nor a numeric string (TypeError) |
wrote NaN matrices, so the shape was missing or broken |
miterLimit of shapes and images |
a value below 1 or not a number (RangeError) |
wrote the invalid limit, or ignored a non-number |
n_gon() sides, star() points |
NaN, Infinity, a non-number, or more than 100000 (RangeError) |
ran out of memory and aborted, or drew nothing |
null and undefined still select the default for each option: 14pt for
size, no spacing for charSpace, no rotation, and a miter limit of 1.414.
var size = scale * base; // may compute 0 or NaN
recipe.text("Hello", 72, 72, size > 0 ? { size: size } : {});
In v6, textDimensions("Hello", { size: -5 }) reported a width of
2147483645.5; it now throws like text() does.
Check Annotation Flags
annot() and comment() now throw Error: Unknown annotation flag (<name>)
for a flag that is not a Recipe.AnnotFlag value, where 6.x wrote the
annotation without flags. Fix a misspelled name, use a Recipe.AnnotFlag
value, or pass a numeric bit mask, which 7.x also accepts.
Check Annotation And Link Options
annot(), comment(), and the highlight, underline, strikeOut, and
squiggly options of text() throw TypeError: Invalid annotation options
before anything is queued or drawn when:
xoryis neither a finite number nor"center";widthorheightis not a finite number of at least zero;opacity, also on a reply, is outside 0 to 1;- a border width is not finite, or
borderDashcontains non-numbers; quadPointsare not finite numbers in groups of eight.
6.x wrote such values into the PDF: a string size such as "40" into a corrupt
/Rect, a negative size as a reversed rectangle, NaN as nan or zero,
opacity: 2 as an invalid /CA 2, and a numeric string such as "50" joined
into the coordinate (5000). Pass numbers in range, or omit the option.
link() and the link option of text, shapes, and images, new in v7, throw
TypeError: URL link requires a URL and valid PDF rectangle the same way for a
URL that is not a string or a rectangle that is not finite.
12. Check Recipe Colors
Recipe resolves colors more strictly, as @muhammara/wasm does:
- Annotation colors in
annot(),comment(), and theunderline,strikeOut, andhighlightoptions oftext()must be known. An unknown name, a gray#rror CMYK#ccmmyykkstring, or a number throwsTypeError: Unknown annotation color (<value>), where 6.x wrote the default color. Known colors are#rrggbb,%r,g,b, names registered withchroma(), and CSS color names in any case; a CSS name such as"navy"now writes that color instead of the default. - An annotation
colorarray with one number is a gray and one with four numbers a CMYK color. 6.x read both as RGB, so[128]wrote dark blue. Other lengths and values outside 0 to 255 throw aTypeError; three-number arrays are unchanged. - Colors registered with
chroma()belong to the Recipe that registered them. In 6.x every Recipe in the process shared one color table. Register the color on each Recipe that uses it; otherwise text and shape colors fall back to the default color, and annotation colors throw. - Recipe no longer reads the undocumented
colouralias. Renamecolourtocolor, or the shape or text is drawn in the default color. - An unknown colorspace in
chroma(), text, or drawing options throwsTypeError: Unknown colorspace: <name>; see Type Colorspaces.
var first = new Recipe("new", "first.pdf");
first.chroma("brand", "#003366");
var second = new Recipe("new", "second.pdf");
// v7: register the color again on every Recipe that uses it.
second.chroma("brand", "#003366");
13. Check Recipe Shape And Annotation Placement
Move Recipe Annotations To Their Top-Left Corner
annot(x, y, subtype, { width, height }) now places the annotation
rectangle with (x, y) as its top-left corner, like rectangle() and link()
#808. In v6, (x, y) was the bottom-left corner, so the rectangle extended
height points up from y. A Square, Circle, FreeText, or other annotation
with a height now appears height points lower.
To keep the v6 position, subtract the height from y:
// v6: the Square spans y 70..100.
recipe.annot(50, 100, "Square", { width: 120, height: 30 });
// v7: pass the top edge to cover the same area.
recipe.annot(50, 100 - 30, "Square", { width: 120, height: 30 });
Highlight, Underline, StrikeOut, and Squiggly annotations need no change. In
v6 their QuadPoints, which viewers draw, already hung down from y, while
their Rect lay above them. Both now cover the same area below y. Annotations
without a height, such as a 6.x comment(), and the highlight, underline,
strikeOut, and squiggly options of text() stay where they were.
Stroke Recipe Lines As One Path
line() strokes all of its points as one path instead of one path per
segment #799.
Segments now meet at the lineJoin instead of overlapping their caps, so
corners drawn with butt caps are closed, and a translucent line no longer
darkens where segments overlap. To keep separate segments, draw each one on
its own:
// v7: one path through every point.
recipe.line(
[
[50, 50],
[150, 50],
[150, 150],
],
{ color: "#003366", lineWidth: 4 },
);
// Separate segments, as v6 drew them.
recipe.moveTo(50, 50).lineTo(150, 50, { color: "#003366", lineWidth: 4 });
recipe.moveTo(150, 50).lineTo(150, 150, { color: "#003366", lineWidth: 4 });
14. Check Recipe Images
Move Bottom-Aligned Images Back Where v6 Drew Them
image() with align: "left bottom", "center bottom", or "right bottom"
now moves the image down by half its height from its top-left placement, as
the option has always been documented #857. v6 moved it up by 1.5 times its
height instead, so the image now appears twice its drawn height lower.
To keep the v6 position, subtract twice the drawn height from y. With both
width and height given, the drawn height is height unless
keepAspectRatio fits the image into a smaller one:
// v6: this image appeared with its top edge 150 points above y = 400.
recipe.image("photo.jpg", 100, 400, {
width: 200,
height: 100,
keepAspectRatio: false,
align: "left bottom",
});
// v7: move y up by twice the drawn height to draw it at the same place.
recipe.image("photo.jpg", 100, 400 - 2 * 100, {
width: 200,
height: 100,
keepAspectRatio: false,
align: "left bottom",
});
When only width or scale is given, compute the drawn height from the image
size, which writer.getImageDimensions() returns.
Check Recipe Image And Color Options
image() now uses fill, stroke, and color, which 6.x ignored for images:
fill paints the image box beneath the image, and stroke or color outlines
it #857. If you pass
one options object to both shapes and images, leave these options out of the
image() call to keep the 6.x output:
var style = { color: "#003366", opacity: 0.8 };
recipe.rectangle(50, 50, 100, 40, style);
// v7: without color, the image is drawn without an outline, as in v6.
var { color, ...imageStyle } = style;
recipe.image("photo.jpg", 50, 120, { ...imageStyle, width: 100 });
Size Rotated And Placed PDF Pages
A PDF page used as an image is measured as its media box width by height
#856. 6.x swapped the
two: for a 595×842 portrait page, getImageDimensions() returned
{ width: 842, height: 595 }, drawImage() with
transformation: { width, height } scaled each axis by the other's size, and
Recipe image() drew the page at the wrong size and position. Remove code that
swapped the values back.
Recipe image() also turns a PDF page by its /Rotate entry, as viewers
display it, and measures a page turned by 90 or 270 degrees with its width and
height swapped #857.
6.x drew such a page unturned. Pass the width, height, or scale for the
displayed page.
15. Check Recipe Text Layout
Trim Boundary Whitespace From charSpace Text
v7 Recipe character-spacing measurements count every character of the text,
including leading and trailing whitespace such as spaces, tabs and non-breaking
spaces (U+00A0), matching Wasm. v6 trimmed boundary whitespace before
counting, so text with charSpace can now measure wider, wrap earlier, or
align differently: " Label " adds two more charSpace gaps than in v6.
Characters outside the Basic Multilingual Plane, such as emoji, now count once
instead of twice.
If boundary whitespace should not add spacing, trim the text before passing
it, and move x where the indent must stay visible:
Wrap Recipe HTML Text Explicitly
Recipe text with html: true keeps text outside any element on one line with
its neighboring inline elements, with one space between them:
x <b>a</b> y renders as the single line x a y, as it already did inside
<p> #667. In v6 each
top-level text run and inline element started its own line and lost its
leading space. Wrap content in <p> elements, or add <br>, where separate
lines are intended:
htmlToTextObjects() returns a <br> as an object with lineBreak: true
instead of a p object holding placeholder text.
16. Choose Table Columns Explicitly
v7 Recipe.table() builds its columns from every record instead of only the
first one, keeps order and columns entries even when no record has that
field, and uses exactly the listed columns when there is no order. In v6, a
field missing from the first record was dropped, and a columns list longer
than the first record's fields was ignored in favor of those fields. A column
renderer result now also sizes its row. Tables can therefore gain columns,
change column order, or get taller rows. A misspelled order or columns name
now draws an empty column instead of being dropped silently; check the names
against your records.
To keep a fixed set of columns, list them with order or columns:
recipe.table(50, 52, people, {
order: ["name", "city"],
columns: [
{ name: "name", text: "Name", width: 180 },
{ name: "city", text: "City", width: 160 },
],
});
Row and header sizing now includes vertical padding, minHeight, fixed
height, and rendered HTML rather than just unpadded plain text. A default
cell's 2pt top and bottom padding therefore adds 4pt to its row height. To make
rows more compact, set column cell.padding and header.cell.padding
explicitly; remove or reduce any unnecessary minHeight/height. HTML cells
reserve the space their rendered lines need.
Review overflow callbacks after adjusting sizing. The callback receives the
Recipe as this and its first argument, and is called once per pending row.
If it continues, the destination must fit both the repeated header and the
entire row within the table height and page bottom margin. Otherwise table()
throws RangeError before drawing that header or row. Return true to stop,
move the next segment upward, choose a taller page/table area, or split an
oversized record into multiple rows. See Create Multi-Page Tables.
Array-form order preserves exact field names, including whitespace and
empty-string keys; the comma-separated form trims surrounding whitespace.
A column renderer now receives "" for a null value; in 6.x a null value
made the table fail with an internal TypeError. After drawing, table() leaves the text cursor
at the table's left edge and bottom, where 6.x left it after the last cell, so
pass coordinates to a following text() call. Tables with empty contents or no
discovered columns preserve the cursor.
17. Update TypeScript Declarations
v7 declares the native API more precisely. A few 6.x declarations were broader
than what the runtime accepts, so TypeScript code written against them can fail
to compile in the following cases. Recipe members that 6.x did not declare at
all, such as register(), table(), chroma(), and metadata, are new
declarations and need no migration.
Type Text Overflow Callbacks
Text overflow callbacks were typed () => void in 6.x, but a callback that
returns nothing already failed at runtime. They now receive the active Recipe
and must return true to stop, false to continue, or an object selecting the
next layout and/or column. A column can be an index or an [x, y] position:
recipe.layout("article", 72, 72, 468, 600, { columns: 2, gap: 18 });
recipe.text(longText, {
layout: "article",
overflow: (currentRecipe) => {
currentRecipe.endPage().createPage("letter");
return { layout: "article", column: 0 };
},
});
Type Vector Options
rectangle() rotationOrigin is a two-number tuple instead of number[].
Annotate a reusable origin as [number, number]:
var origin: [number, number] = [100, 100];
recipe.rectangle(50, 50, 100, 40, { rotation: 30, rotationOrigin: origin });
lineTo() options no longer declare fill, which 6.x declared but ignored.
Remove it; fill a closed path with polygon() instead.
Type Colorspaces
The low-level ColorOptions.colorspace no longer accepts a value typed
string, and an unknown colorspace such as "lab" throws a TypeError at
runtime: Unknown colorspace: lab from Recipe, and
colorspace must be rgb, gray, or cmyk from the low-level drawing and
writeText() options. Use muhammara.DeviceColorSpace values:
context.drawRectangle(10, 10, 100, 40, {
type: "fill",
color: 0xff000000,
colorspace: muhammara.DeviceColorSpace.CMYK,
});
Type Low-Level Declarations
These low-level declarations are narrower than in 6.x:
PDFReader#getXrefPosition()takes no argument; 6.x required one and ignored it. Drop the argument.InfoDictionary#getAdditionalInfoEntries()takes nokey; the call always returned every entry. Drop the argument and read the key from the result.DocumentCopyingContext#getSourceDocumentParser()takes no arguments; it always returned the parser of the copying context's source document.WriteTextOptionsno longer declaresstrikeOutandlineWidth, whichwriteText()never read. Remove them, draw the line withdrawPath(), or use the Recipetext()strikeOutoption.toPDF*()andtoNumber()on PDF objects may returnundefined, which they do for a different object type. Check the result, orgetType(), before using it.PDFWStreamForBuffer#buffermay benull; check it before use.InfoDictionary#trappedand theJ()andj()arguments take0,1, or2instead of anynumber; pass a literal, or aLineCapStyleconstant forJ().eTokenSeparatorSpace,eTokenSeparatorEndLine, andeTokenSeparatorNoneare constants only, no longer types; writetypeof muhammara.eTokenSeparatorSpacewhere a type is needed.ReadStream#read()is declared as returningUint8Array | number[]; see step 19.
18. Check Low-Level Drawing Options
The drawPath, drawCircle, drawSquare, and drawRectangle helpers now
interpret type: "clip" as clipping without painting. Previously that spelling
did not apply a clip, while unknown strings incorrectly entered the clip branch.
Any other type, including misspellings such as "fil" and falsy values such
as false, 0, or "", now throws
TypeError: Unknown drawing type; use "stroke", "fill", "clip" or null. Replace
it with "clip" when clipping is intended, "stroke"/"fill" when drawing an
outline or filled shape is intended, or null to end the path unpainted.
type: undefined strokes, as an omitted type does; in 6.x it clipped too.
Clipping now ends the path (W n). Do not rely on a later painting operator to
paint that same path: draw the shape again with a painting type if necessary.
Save graphics state with q() before the clip, draw the content that should be
clipped, then restore it with Q() so later content is unaffected. See
Draw Primitives.
Drawing helpers and writeText() also convert their inputs before emitting
operators. If an option getter or numeric conversion throws, correct the input
and retry; failed calls no longer leave partial drawing output in the stream.
Coordinates, dimensions, stroke widths, and writeText() font sizes must
convert to finite numbers. Replace NaN and infinities with finite values and
reduce values whose circle or underline calculations overflow. Finite numeric
coercions remain supported. Supply at least two complete coordinate pairs to
drawPath(); malformed pairs and extra arguments now throw instead of silently
drawing a prefix. A failed call emits no operators and can be retried after
correcting the input.
Colors are checked too:
- A string
colormust be a CSS color name or a#rrggbbstring. 6.x drew every string color black, including valid#rrggbbvalues, so such shapes now appear in their color. Other strings, such as hex without the#, throw aTypeError; pass a CSS name,#rrggbb, or a 24-bit number such as0xff0000. - A gray or CMYK color must be a number. A color name or
#rrggbbstring is RGB, so v7 throwsonly a numeric color can use the gray or cmyk colorspacewhere 6.x drew it in RGB; dropcolorspacefor such a color. See Draw in Gray and CMYK.
Text operators and measurements are stricter:
Tj(),Quote(),DoubleQuote(), andTJ()throw aTypeErrorfor a glyph list item that is not a[glyphId, unicodeCodePoint]array, where 6.x skipped it. PassTJitems as separate arguments:TJ("ab", -100, "c")instead ofTJ(["ab", -100, "c"]).TJ()with text items also throws when a glyph list comes last instead of dropping it as if it were options.UsedFont#calculateTextDimensions()throws aTypeErrorfor a font size that is not a finite number greater than zero. 6.x measured0,NaN, and infinite sizes as zero and wrapped a negative size to a huge integer. Pass a size greater than zero, or omit it to measure at size 1.
19. Accept Buffers In Custom Streams
Custom write streams passed to createWriter, createWriterToModify,
recrypt, or the log option now receive each chunk as a Buffer instead of
an array of numbers. The built-in PDFRStreamForFile and PDFRStreamForBuffer
also return Buffer chunks from read(), and so do the byte readers returned
by startReadingFromStream(), startReadingFromStreamForPlainCopying(),
getParserStream(), and getSourceDocumentStream(). This makes large in-memory and
Buffer-mode Recipe work several times faster and far smaller
#324.
Streams that only read bytes.length, index bytes, or pass the chunk to
Buffer.from() keep working. Code that relies on array methods does not:
// v6: bytes was an array of numbers.
write: function (bytes) {
this.data = this.data.concat(bytes);
return bytes.length;
},
// v7: bytes is a Buffer, which may be kept after write() returns.
write: function (bytes) {
this.chunks.push(bytes);
return bytes.length;
},
Output is batched into chunks of up to 64 KiB, and the final chunk is delivered
when the writer ends, recrypt() returns, or the writer is shut down or
aborted. Read the collected output after those calls rather than while pages
are still being written.
write must return the full length of the chunk it received. In v6 a smaller
return value was ignored; now it fails the writer: creating it, later writes,
end(), and shutdown() throw. Return bytes.length once the chunk is
accepted, and throw from write to report a real failure.
Replace concat, push(...bytes), splice, and Array.isArray checks with
Buffer operations, or call Array.from(bytes) where an array is still needed.
TypeScript implementations of WriteStream or the log option must declare
write(bytes: Buffer). Custom read streams may keep returning arrays; returning
a Uint8Array or Buffer is now also accepted and faster. ReadStream#read()
is declared as returning Uint8Array | number[], so code that types its result
as number[] needs Array.from() or a wider type.
getCurrentPosition() of a custom stream must return a finite byte position
within [-2^63, 2^63). A result that converts to NaN, an infinity, or a
value outside that range throws a TypeError instead of writing corrupt PDF
offsets; numeric strings and other finite coercions still work. See the
stream contract.
20. Handle Writer And Reader Errors
The low-level writer and reader stop on errors that 6.x passed over:
- Stateful
PDFWritercalls afterend()orshutdown(), including after a failed finalization, throwError("PDF writer has ended")instead of touching closed resources or crashing. Create a new writer withcreateWriter()orcreateWriterToModify(), or resume a saved state withcreateWriterToContinue(); a barenew PDFWriter()is not active. Repeatedend()remains a no-op. See Writer lifecycle. end()throwsError: End the active objects context operation before ending the PDFwhile a dictionary started withstartDictionary()is still open. 6.x wrote the cross-reference table and trailer inside it and damaged the PDF. End the dictionary and callend()again; the writer stays usable.appendPDFPagesFromPDF()ends its writer when copying pages fails, because the document may already be partly written. Create a fresh writer and retry with a valid source. A source that cannot be opened, parsed, or decrypted, and a page range outside the source, throw before anything is written and leave the writer usable.PDFReadermethods that take a page index or object ID —parseNewObject(),getPageObjectID(),parsePageDictionary(),parsePage(), andgetXrefEntry()— accept only a non-negative integer below 2^32 and otherwise throwTypeError: Page index must be a non-negative integer(orObject ID must be a non-negative integer). In 6.xreader.parsePage(1.5)read the second page andNaNread index 0. Round or validate the value first, bounding page indices withgetPagesCount()and object IDs withgetObjectsCount().
What Does Not Change
- The entry points
createWriter(),createReader(),createWriterToModify(), andnew Recipe()keep their names. Changes to individual methods and options that can affect existing code are listed on the Breaking Changes page; the steps above cover the ones that need a code change. - Prebuilt binaries still install through
node-pre-gyp; only their archive names change, as described in step 6.
Version 6 Status
The unscoped muhammara package (1.x to 6.x) is deprecated on npm. Its
deprecation message points to @muhammara/native and to this guide, and it
receives no further releases. Existing v6
installations keep working; it is simply a different package from
@muhammara/native.
For the full list of compatibility changes, including those of 5.x and older, see Breaking Changes.