From 6c058ee63818ceb69d11fe4287f02ce48a6f15f7 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:39:09 -0500 Subject: [PATCH] Allow zero dash lengths when not every length is zero --- CHANGELOG.md | 1 + docs/vector.md | 6 ++++-- lib/mixins/vector.js | 10 ++++++++-- tests/unit/vector.spec.js | 40 +++++++++++++++++++++++++++++++++------ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6b9729a..7834b058e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- Fix `dash()` rejecting a length of `0`, e.g. `dash(0, { space: 2 })` or `dash([2, 0, 3])`, which the PDF specification allows as long as not every length is zero, so a dotted line can be drawn with a `round` line cap. A dash array of only zeros still throws. Fixes #1060 - Fix `doc.table()` parsing a font passed as binary data (e.g. registered with `registerFont(name, uint8Array)`) again for every cell, when it restores the font after measuring and drawing the cell, which made tables with such fonts slower. A font loaded from the same `Uint8Array` is now reused without parsing it again - Add `lineBreak: 'whitespace'` to `doc.text()`, which breaks a line only at whitespace, a soft hyphen or a zero-width space. The Unicode line breaking rules the wrapper follows also allow a break after a hyphen or a slash, so `design-system` or `https://github.com/foliojs/pdfkit` could be cut at a line end, and a text extractor that joins the lines, such as a résumé parser, reads two words - Add an `objectStreams` option, which packs the objects that are not streams into compressed object streams and writes a cross-reference stream instead of the xref table, making documents with many such objects, e.g. tagged ones, much smaller. It requires a `pdfVersion` of `1.5` or higher, and cannot be used with encryption or PDF/A-1. Fixes #1810 diff --git a/docs/vector.md b/docs/vector.md index 2a96b512a..f9ca55783 100644 --- a/docs/vector.md +++ b/docs/vector.md @@ -150,8 +150,10 @@ The output of this example looks like this. The `dash` method allows you to create non-continuous dashed lines. It takes a length specifying how long each dash should be, as well as an optional hash -describing the additional properties `space` and `phase`. Lengths must be positive -numbers; `dash` will throw if passed invalid lengths. +describing the additional properties `space` and `phase`. Lengths must be +non-negative numbers, and not all zero; `dash` will throw if passed invalid +lengths. A length of `0` draws a dot when used with a `round` line cap, e.g. +`doc.lineCap('round').dash(0, {space: 4})` for a dotted line. The `space` option defines the length of the space between each dash, and the `phase` option defines the starting point of the sequence of dashes. By default the `space` diff --git a/lib/mixins/vector.js b/lib/mixins/vector.js index b608c83ea..0e7b230fd 100644 --- a/lib/mixins/vector.js +++ b/lib/mixins/vector.js @@ -67,12 +67,18 @@ export default { length = [length, options.space || length]; } - const valid = length.every((x) => Number.isFinite(x) && x > 0); + // A dash array must be made of non-negative numbers that are not all + // zero; an all-zero array makes some viewers reject the file (#949). + // A zero-length dash is fine otherwise, e.g. [0 2] with a round line + // cap draws a dotted line. + const valid = + length.every((x) => Number.isFinite(x) && x >= 0) && + (length.length === 0 || length.some((x) => x > 0)); if (!valid) { throw new Error( `dash(${JSON.stringify(originalLength)}, ${JSON.stringify( options, - )}) invalid, lengths must be numeric and greater than zero`, + )}) invalid, lengths must be numeric and non-negative, and not all zero`, ); } diff --git a/tests/unit/vector.spec.js b/tests/unit/vector.spec.js index d297f4628..ed938cd3a 100644 --- a/tests/unit/vector.spec.js +++ b/tests/unit/vector.spec.js @@ -78,6 +78,28 @@ describe('Vector Graphics', () => { ]); }); + test('with zero length and space option', () => { + const docData = logData(document); + const vectorStream = Buffer.from( + '1 0 0 -1 0 792 cm\n50 20 m\n[0 2] 0 d\nS\n', + 'binary', + ); + + document.moveTo(50, 20).dash(0, { space: 2 }).stroke(); + document.end(); + + expect(docData).toContainChunk([ + `5 0 obj`, + `<< +/Length 38 +>>`, + `stream`, + vectorStream, + `\nendstream`, + `endobj`, + ]); + }); + test('with phase option', () => { const docData = logData(document); const vectorStream = Buffer.from( @@ -117,7 +139,7 @@ describe('Vector Graphics', () => { const doc = new PDFDocument(); expect(() => doc.dash(0)).toThrow( - 'dash(0, {}) invalid, lengths must be numeric and greater than zero', + 'dash(0, {}) invalid, lengths must be numeric and non-negative, and not all zero', ); }); @@ -125,7 +147,7 @@ describe('Vector Graphics', () => { const doc = new PDFDocument(); expect(() => doc.dash(-1)).toThrow( - 'dash(-1, {}) invalid, lengths must be numeric and greater than zero', + 'dash(-1, {}) invalid, lengths must be numeric and non-negative, and not all zero', ); }); @@ -133,7 +155,7 @@ describe('Vector Graphics', () => { const doc = new PDFDocument(); expect(() => doc.dash(null)).toThrow( - 'dash(null, {}) invalid, lengths must be numeric and greater than zero', + 'dash(null, {}) invalid, lengths must be numeric and non-negative, and not all zero', ); }); @@ -143,11 +165,17 @@ describe('Vector Graphics', () => { expect(() => doc.dash([2, 3])).not.toThrow(); }); - test('length array containing zeros throws', () => { + test('length array containing zeros', () => { + const doc = new PDFDocument(); + + expect(() => doc.dash([2, 0, 3])).not.toThrow(); + }); + + test('length array of only zeros throws', () => { const doc = new PDFDocument(); - expect(() => doc.dash([2, 0, 3])).toThrow( - 'dash([2,0,3], {}) invalid, lengths must be numeric and greater than zero', + expect(() => doc.dash([0, 0])).toThrow( + 'dash([0,0], {}) invalid, lengths must be numeric and non-negative, and not all zero', ); }); });