Breaking Changes
This page collects the compatibility changes formerly maintained in the README.
Version 7.x
Upgrading from 6.x? Follow Migrate From v6 To v7 for the upgrade steps. Each change below names the step that covers it.
- The unscoped
muhammarapackage is deprecated and receives no further releases. Install@muhammara/nativeinstead, or use an npm alias when an existingrequire("muhammara")import must remain unchanged. See Choose the Replacement Package and Install as an npm alias. @muhammara/nativeis prebuilt-only. When a matching prebuilt is unavailable, installation fails instead of compiling locally; install@muhammara/native-with-sourcefor bundled source and fallback builds. See Choose the Replacement Package.- Deep imports such as
require("muhammara/lib/Recipe")no longer work, also under an npm alias: the JavaScript layer moved to@muhammara/native-core, and the native packages no longer ship alib/directory. Import from the package root; see Update Imports #556. - Node.js
20 || 22 || 24 || >=25is required. 6.x declared>=17and shipped prebuilds for Node.js 19 to 24, so Node.js 17, 18, 19, 21, and 23 are no longer supported and npm warns or refuses to install on them. Upgrade to a supported Node.js release; see Confirm Prebuilt Coverage. - Windows win32 (32-bit) prebuilds and build tooling were removed. Windows x64 is the current prebuilt target; Windows arm64 is not part of the prebuilt matrix. See Confirm Prebuilt Coverage.
- Native prebuilds now use Node-API 8 and are named
napi-v8-{platform}-{arch}-{libc}.tar.gzinstead ofnode-v{abi}-{platform}-{arch}-{libc}.tar.gz. The installed addon now lives atbinding/napi-v8/muhammara.nodeinstead ofbinding/muhammara.node. Standard npm installs andrequire("@muhammara/native")calls continue to work, but custom mirrors, direct archive downloads, deployment scripts, and direct addon imports that assume the old names or path must use the Node-API names instead. See Migrate from v6 to v7 #750 #504. - Recipe
endPage()no longer leaves the completed page active. Page methods called afterendPage()throw instead of reusing the completed page and its content context: shapes,image(), andlink()withTypeError: No page is active; call createPage() or editPage() first, andtable(),overlay(),setPageBox(),rotate(), andpauseContext()with their own errors.text()draws nothing, andcomment()orannot()makeendPDF()fail. CallcreatePage()oreditPage()before the next page operation. See Migrate from v6 to v7 #608. - Recipe
endPDF()is now idempotent. Repeated calls that previously attempted to finalize the writer again, and could crash, now leave the completed PDF unchanged; a repeatedendPDF(callback)still invokes the callback with the completed output where applicable. Code that relied on another call to flush later changes must create and finalize a new Recipe instead. See Check the Recipe Lifecycle #693. - A failed Recipe
endPDF()now retires the Recipe, aborts its writer, releases its source reader, and rethrows the original error on later calls. Code that retried finalization on the same Recipe must create a new Recipe instead. This prevents failed finalization from retaining source file handles on Windows. See Check the Recipe Lifecycle #381. - Recipe
pauseContext()andresumeContext()throw when there is no matching active or paused page content context. Calls outside a page lifecycle and repeated pause or resume calls used to do nothing silently; callpauseContext()only after creating or editing a page, and callresumeContext()exactly once after a successful pause. See Check the Recipe Lifecycle #608. - Recipe
appendPage()rejects zero, negative, fractional, reversed, and malformed page selections. These values were previously clamped, passed to the low-level writer, or partially interpreted; pass positive one-based integers or ascending two-value ranges instead. Integer endpoints beyond the source still clamp to its final page. See Check the Recipe Lifecycle #548. - Recipe
insertPage()throwsTypeErrorimmediately whenpdfSrcorsrcPageNumberis missing. Incomplete calls previously queued no insertion or failed later duringendPDF(); passafterPageNumber,pdfSrc, and the positive one-basedsrcPageNumbertogether. See Check the Recipe Lifecycle #548. Recipe.fillOpacity()was removed. Existing calls now throw because it is no longer a Recipe method; useRecipe.opacity()to set both fill and stroke alpha. Opacity persists for later vector drawing, so callopacity(1)to restore opaque output. See Migrate from v6 to v7 #618.- The undocumented native
Recipeprototype membersANNOTATION_PREFIX,appendPDFPageFromPDFWithAnnotations(), andappendPDFPagesFromPDFWithAnnotations()were removed. Code that called them now fails because they were internal helpers, not Recipe APIs; useappendPage(),insertPage(), orsplit()for supported page-copying operations. See ReplaceRecipe.fillOpacity()#623. - Recipe
register()throwsFound conflict in Recipe prototypes. <name> already exists.for a plugin named like a method new in v7:deletePage,getCurrentPageInfo,lineStyle,link,opacity,pie,removeText,replaceText,rotate,rotateContent, orsetPageBox. Rename the plugin; see Rename Recipe plugins that collide with new methods #829. - Recipe requires a text
size, or itsfontSizealias, greater than zero and throwsRangeErrornaming the option and the value otherwise.text()previously clamped a negative size to 1pt, drew nothing visible for zero, andtextDimensions()measured with those values, returning nonsensical metrics such as a width of 2147483645.5 forsize: -5. Zero andNaNpreviously fell back to the 14pt default in some paths, hiding the mistake, andInfinitywrote an invalidinffont size into the page. Pass a finite size greater than zero, or omit the option —nullandundefinedstill select the 14pt default. See Migrate from v6 to v7 #733 #798. - Recipe
text()throwsTypeError: charSpace must be a finite numberwhencharSpaceisInfinity,-Infinity,NaN, or not a number, matching Wasm. In 6.xInfinitywrote an invalidinf Tcoperand into the page,NaNsilently drew with no character spacing, and a string or boolean threwWrong Arguments, please provide character spaceafter drawing had started. The check runs before anything is drawn, and the Recipe stays usable. Pass a finite number, or omit the option —nullandundefinedstill mean no spacing. See Pass Valid Recipe Options #812. - Recipe shapes,
text(), andimage()throwTypeError: rotation must be a finite numberwhenrotationis neither a number nor a numeric string, such asNaNor"abc", before anything is drawn. In 6.x the shape was written withNaNtransformation matrices and was missing or broken in viewers. Pass a number, or omitrotation. See Pass Valid Recipe Options #857. - Recipe shapes and
image()throwRangeError: miterLimit must be a number of at least 1for amiterLimitbelow 1 or not a number, before anything is drawn. In 6.x a limit below 1 was written into the PDF, which PDF does not allow, and a non-number was ignored. Pass 1 or more, or omitmiterLimitto keep the default of 1.414. See Pass Valid Recipe Options #857. - Recipe
n_gon()andstar()throwRangeError: n_gon sides must be a finite number no greater than 100000(star points …forstar()) when the side or point count isNaN,Infinity, not a number, or above 100000. An infinite or huge count used to build vertices until the process ran out of memory and aborted, andNaNdrew nothing. Pass a finite count; beyond a few hundred sides a polygon is indistinguishable fromcircle(). See Pass Valid Recipe Options #821. - Recipe
annot()andcomment()throwError: Unknown annotation flag (<name>)when theflagoption is not aRecipe.AnnotFlagvalue, such as a misspelled name. In 6.x the annotation was written without any flag bits. Pass aRecipe.AnnotFlagvalue, a numeric bit mask, or omitflag; see Check Annotation Flags #792. - Recipe
annot()andcomment(), and thehighlight,underline,strikeOut, andsquigglyoptions oftext(), throwTypeError: Invalid annotation optionsfor values that cannot form a valid PDF annotation, as in@muhammara/wasm: anxorythat is neither a finite number nor"center", awidthorheightthat is not a finite number of at least zero, anopacityoutside 0 to 1 (also on a reply), a non-finite border width, aborderDashwith non-numbers, orquadPointsthat are not finite numbers in groups of eight. In 6.xannot()wrote a string size such as"40"into a corrupt/Rect, a negative size as a reversed rectangle,NaNas zero, andopacity: 2as an invalid/CA 2, andcomment()ignoredwidthandheight. Both wrote anxoryofNaNasnanand joined a numeric string such as"50"into the coordinate (5000). The call throws before anything is queued or drawn. Pass numbers in range, or omit the option. Recipelink(), and thelinkoption of text, shapes and images, throwTypeError: URL link requires a URL and valid PDF rectanglefor a URL that is not a string or a rectangle that is not finite, such as aNaNwidth, as Wasm does; 6.x wrote the invalid numbers into the link. See Check Annotation and Link Options #853. - Recipe
annot()andcomment(), and theunderline,strikeOut, andhighlightannotations oftext(), throwTypeError: Unknown annotation color (<value>)whencoloris not a known color. In 6.x an unknown color, such as a misspelled name, silently wrote the default color. Known colors are#rrggbb,%r,g,b, colors registered withchroma(), and CSS color names in any case; a CSS name such as"navy"now writes that color instead of the default. Gray#rrand CMYK#ccmmyykkthrow as well, and numbers, which failed with an internal error in 6.x, now throw thisTypeError.text()checks its markup annotations before drawing any text. Fix the name, or register it withchroma()first. See Check Recipe Colors #796. - An annotation
colorarray with one number is written as a gray, and one with four numbers as a CMYK annotation color. In 6.x both were misread as RGB, so[128]wrote dark blue and CMYK arrays lost a channel. An array with another length, or a value outside 0 to 255, throwsTypeError: Annotation colors need one, three, or four numbers from 0 to 255. Pass one, three, or four numbers from 0 to 255; three-number arrays are unchanged. See Check Recipe Colors #796. - Colors registered with Recipe
chroma()belong to the Recipe that registered them, as in@muhammara/wasm. In 6.x every Recipe in the process shared one color table, so a name registered on one Recipe also resolved in every other Recipe. In another Recipe the name is now unknown: text and shape colors fall back to the default color, and annotation colors throwTypeError: Unknown annotation color (<name>). Register the color withchroma()on each Recipe that uses it. See Check Recipe Colors #799. - Recipe no longer reads
colouras an alias ofcolor. The alias was undocumented and undeclared; a shape or text given onlycolournow uses the default color. Renamecolourtocolor. See Check Recipe Colors #857. - An unknown colorspace throws a
TypeError, as in@muhammara/wasm. The low-level drawing andwriteText()color options throwTypeError: colorspace must be rgb, gray, or cmykfor a numeric or namedcolor; in 6.x a numeric color drew without setting a color and a named color ignored the colorspace. Recipechroma(), text and drawing options throwTypeError: Unknown colorspace: <name>; in 6.xchroma()threw a plainErrorand a named color in an unknown colorspace failed withCannot read properties of undefined. The declaration ofColorOptions.colorspaceno longer accepts anystring, sotscreports a value typedstring. Pass aDeviceColorSpacevalue; see Type Colorspaces #799. - Recipe
annot(x, y, subtype, { width, height })places its rectangle with (x, y) as the top-left corner, likerectangle()andlink(). In 6.x (x, y) was the bottom-left corner, so a Square, Circle, FreeText, or other annotation with aheightnow appearsheightpoints lower on the page. Subtractheightfromyto keep the 6.x position; see Move Recipe annotations to their top-left corner. Highlight, Underline, StrikeOut, and Squiggly created withannot()already hung down fromyand render where they did, but theirRectnow encloses theirQuadPointsinstead of lying above them.comment()withoutwidthandheight, which 6.x ignored, and the markup options oftext()are unchanged #808. - Recipe
line()strokes all of its points as one path, as in@muhammara/wasm; 6.x stroked every segment as its own path. Segments now meet at thelineJoininstead of overlapping their caps, so corners drawn withbuttcaps are closed and a translucent line no longer darkens where segments overlap. A Separation line writes one form XObject instead of one per segment. To keep separate segments, draw each with its ownmoveTo()andlineTo(). See Stroke Recipe Lines as One Path #799. - Recipe
image()withalign: "<horizontal> bottom"places the image half its height below the placement point, as the option has always been documented. 6.x placed it 1.5 times its height above that point, so a bottom-aligned image now appears twice its height lower. Subtract twice the drawn height fromyto keep the 6.x position; see Move bottom-aligned images back where v6 drew them #857. - Recipe
image()frames the image when givenfill,stroke, orcolor, which 6.x ignored for images:fillpaints the image box beneath the image, andstrokeorcoloroutlines it above the image. Code that passes shared style options toimage()now draws a background or a border. Leave these options out of theimage()options to keep the 6.x output; see Check Recipe image and color options #857. - Recipe
image()places a PDF page with a/Rotateentry as viewers display it: the page is turned, and a page turned by 90 or 270 degrees is measured with its width and height swapped. 6.x drew such a page unturned, so the samewidth,height, orscalenow gives a turned page of a different size. Pass the size of the displayed page. See Size Rotated and Placed PDF Pages #857. - A PDF page used as an image is measured as its media box width by height,
as in
@muhammara/wasm. 6.x swapped them: for a 595×842 portrait page,getImageDimensions()returned{ width: 842, height: 595 },drawImage()withtransformation: { width, height }scaled each axis by the other's size, and Recipeimage()drew the page at the wrong size and position, 141 points wide forwidth: 200. Remove code that swapped the values back. See Size Rotated and Placed PDF Pages #856. - Native Recipe
charSpacemeasurements count every character, including leading and trailing whitespace, matching Wasm. 6.x trimmed boundary whitespace before counting, so text such as" Label "withcharSpacenow measures wider, wraps earlier and aligns differently. Trim the text before passing it where boundary whitespace should not add spacing; see Trim Boundary Whitespace FromcharSpaceText #661 #543. - Recipe HTML text keeps text outside any element on one line with its
neighboring inline elements, and keeps one space between them:
x <b>a</b> yrenders as one linex a y, as it already did inside<p>. Previously 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()now returns a<br>as an object withlineBreak: trueinstead of apobject holding placeholder text. See Wrap Recipe HTML Text Explicitly #667. - Recipe
table()derives its columns from every record, not just the first, keepsorderandcolumnsentries even when no record has that field, and uses exactly the listedcolumnswhen noorderis given. A columnrendererresult now also sizes its row. Existing tables can gain columns, reorder them, or grow taller rows, and a misspelledorderorcolumnsname now draws an empty column instead of being dropped; list the intended columns withorderorcolumnsto keep a fixed layout. See Migrate from v6 to v7 #666. - Recipe table sizing includes vertical padding, minimum/fixed cell heights,
and rendered HTML. Rows can grow taller and continue earlier; adjust the cell
sizing or continuation area. If an
overflowcallback continues into an area too small for the pending row and repeated header,table()now throwsRangeErrorinstead of drawing beyond the bounds. Returntrueto stop or provide enough space; see the table migration steps #666. - Recipe
table()passes""instead ofnullto a columnrendererfor anullvalue, and leaves the text cursor at the table's left edge and bottom. In 6.x anullvalue made the table fail with an internalTypeError, and the cursor stayed after the last cell, so atext()call without coordinates aftertable()now starts below the table. Check for""in renderers, and pass coordinates to the nexttext(); see the table migration steps #666. - Three Recipe declarations that 6.x typed more loosely are narrower:
rectangle()rotationOriginis a two-number tuple instead ofnumber[], the textoverflowcallback must returnbooleanor overflow instructions instead ofvoid(avoidcallback already failed at runtime), andlineTo()options no longer declare thefillthat 6.x ignored. Such code failstsc; annotate the origin as[number, number], returntruefromoverflowto stop, and dropfill. See Migrate from v6 to v7 #654. - Low-level declarations that 6.x code compiled against are narrower, so such
code fails
tsc: PDFReader#getXrefPosition()takes no argument; 6.x required one and ignored it. Drop the argument.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. See Type Low-Level Declarations.InfoDictionary#getAdditionalInfoEntries()is declared without its ignoredkeyparameter, sogetAdditionalInfoEntries("Company")failstscwithExpected 0 arguments. The call always returned every entry; drop the argument and read the key from the result. The call also works without an argument now; 6.x threw unless it got one. See Type Low-Level Declarations #799 #792.DocumentCopyingContext#getSourceDocumentParser()is declared without theinputandoptionsparameters that its runtime never used, sogetSourceDocumentParser("source.pdf")failstscwithExpected 0 arguments. The call always returned the parser of the copying context's source document; drop the arguments. See Type Low-Level Declarations #320.WriteTextOptionsno longer declaresstrikeOutandlineWidth, whichwriteText()never read. Passing them failstscwith an excess-property error; remove them, draw the line withdrawPath(), or use the Recipetext()strikeOutoption. See Type Low-Level Declarations #799.- The TypeScript declarations of
toPDF*()andtoNumber()on PDF objects now includeundefined, which they return for a different object type. Strict builds that use the result directly fail withObject is possibly 'undefined'; check the result, orgetType(), before using it. See Type Low-Level Declarations #792. - Low-level shape helpers now honor
type: "clip", ending the path withW nwithout painting it. Previously"clip"did nothing and unrecognized types incorrectly clipped. Any othertypevalue, such as the typo"fil",false,0, or"", now throwsTypeError: Unknown drawing type; use "stroke", "fill", "clip" or nullinstead of clipping with a strayW;nullnow ends the path unpainted withn, where 6.x clipped it too, andtype: undefinedstrokes like an omittedtype. Use"clip"explicitly and scope it withq()/Q(); use"stroke"or"fill"when painting is intended. See the drawing migration #750 #792. - Shape helpers and
writeText()now finish input conversion before emitting operators. Failed getters or coercions no longer leave partial graphics/text output; correct the input and retry rather than relying on that partial output. Coordinates, dimensions, stroke widths, and text sizes must convert to finite numbers; calculated circle and underline geometry must also remain finite.drawPath()requires at least two complete pairs and rejects malformed or extra arguments instead of drawing a prefix. ReplaceNaN/infinities with finite values, reduce overflowing geometry, and supply complete pairs. See the drawing migration #750. - Low-level
drawPath(),drawCircle(),drawSquare(),drawRectangle(), andwriteText()throwTypeError: Colors must be a 24-bit number, a color name, or a #rrggbb stringwhen a stringcoloris neither a CSS color name nor#rrggbb, such as a misspelled name or hex without the#. In 6.x those colors were drawn black, and so was every#rrggbbstring. Pass a CSS color name, a#rrggbbstring, or a 24-bit number such as0xff0000. See Check Low-Level Drawing Options #796. - The low-level drawing helpers and
writeText()throwTypeError: only a numeric color can use the gray or cmyk colorspacefor a color name or#rrggbbstring withcolorspace: "gray"or"cmyk", as in@muhammara/wasm. Such a color is RGB; in 6.x it drew in RGB and the colorspace was ignored. Dropcolorspacefor a string color, or pass the gray or CMYK color as a number; see Check Low-Level Drawing Options and Draw in Gray and CMYK #799. Tj(),Quote(),DoubleQuote()andTJ()throw aTypeErrorwhen a glyph list contains an item that is not a[glyphId, unicodeCodePoint]array. In 6.x such items were skipped silently, soTJ(["ab", -100, "c"])drew nothing. Pass theTJitems as separate arguments:TJ("ab", -100, "c").TJ("a", -1, glyphs)also throws now instead of dropping the final glyph list as if it were options. See Check Low-Level Drawing Options #792.UsedFont#calculateTextDimensions()throws aTypeErrorwhen the font size is not a finite number greater than zero, matching@muhammara/wasm. 6.x converted the size to an unsigned integer, so0,NaN, and infinite sizes measured as zero and a negative size wrapped to a huge integer. Pass a size greater than zero, or omit it to measure at size 1. See Check Low-Level Drawing Options #798.- Custom write streams, including
logtargets, now receive each chunk as aBufferinstead of an array of numbers, andPDFRStreamForFile#read()andPDFRStreamForBuffer#read()return aBuffer, as doesread()on the byte readers fromstartReadingFromStream(),startReadingFromStreamForPlainCopying(),getParserStream(), andgetSourceDocumentStream(). Code that calls array methods such asconcat,push, orspliceon those bytes, or checksArray.isArray, now misbehaves or throws, and TypeScript implementations declaringwrite(bytes: number[])fail to compile. Use Buffer operations, orArray.from(bytes)where an array is required.ReadStream#read()is declared as returningUint8Array | number[], so code typing its result asnumber[]failstsc. Output also arrives in batched chunks of up to 64 KiB, with the last one delivered when the writer ends; do not expect onewritecall per PDF token.writemust return the full chunk length: returning less now fails the writer (creation, later writes,end(), andshutdown()throw) instead of being ignored. See Accept Buffers in custom streams #324. - Custom-stream
getCurrentPosition()results now throwTypeErrorif numeric conversion produces a non-finite value or a value outside[-2^63, 2^63). Previously these values could produce corrupt PDF offsets. Return the actual finite byte position within that range. Numeric strings and other successful number coercions remain supported. See the stream contract and Accept Buffers in Custom Streams #750. - Stateful
PDFWritercalls afterend()orshutdown()now throwError("PDF writer has ended")instead of accessing closed resources or crashing. Failed finalization also retires the writer. Create a new writer withcreateWriter()orcreateWriterToModify(), or resume a saved state withcreateWriterToContinue();new PDFWriter()alone is not active. Repeatedend()remains a no-op. See Writer lifecycle and Handle Writer and Reader Errors #693. PDFWriter#end()throwsError: End the active objects context operation before ending the PDFwhile a dictionary started withstartDictionary()is still open, as@muhammara/wasmdoes. The writer stays usable: end the dictionary and callend()again. In 6.xend()succeeded and wrote the cross-reference table and trailer inside the open dictionary, so the PDF was damaged. See Handle Writer and Reader Errors #815.appendPDFPagesFromPDF()now ends its writer when copying pages fails. Previously callers could continue and produce a corrupted document; create a fresh writer and retry with a valid source. A source that cannot be opened, parsed, or decrypted, and page ranges outside the source, still throw without ending the writer, because nothing was written. See Handle Writer and Reader Errors #750 #828.PDFReadermethods that take a page index or object ID —parseNewObject(),getPageObjectID(),parsePageDictionary(),parsePage(), andgetXrefEntry()— reject anything that is not a non-negative integer below 2^32. In 6.x a fractional or out-of-range argument was converted silently:reader.parsePage(1.5)read the second page,reader.getPageObjectID(-1)returned0, andNaNorInfinityread index 0. Such values now throwTypeError: Page index must be a non-negative integer(orObject ID must be a non-negative integer). Round or validate the value before passing it, bounding page indices withgetPagesCount()and object IDs withgetObjectsCount(). See Handle Writer and Reader Errors #581.
Version 6.x
- Node.js 17 and 18 are no longer supported, and no prebuilds are published for them. Upgrade to a supported Node.js release, or stay on 5.x.
- Electron versions before 36 are no longer supported, and no prebuilds are published for them. Upgrade to Electron 36 or newer, or stay on 5.x.
Version 5.x
- Node.js 16 and earlier prebuilds were removed.
- Electron 23 and earlier prebuilds were removed.
- Building from source requires a C++20-capable compiler. CI validates GCC 11.
- Official Docker builds use a GCC Bookworm environment, lowering the required
GLIBCXXversion to 3.4.30.
Version 4.x
- Node.js 15 and earlier and Electron 15 and earlier prebuilds were removed.
- Ubuntu 18.04 was removed from GitHub Actions. Its older glibc can affect use of prebuilt binaries; building from source remains an option.
Version 3.x
- Node.js 11 and earlier and Electron 11 and earlier prebuilds were removed.
- The misspelled
eTokenSepratorexport was renamed toeTokenSeparator.
Version 2.x
- Older Node.js and Electron versions may be incompatible because of the node-pre-gyp upgrade.