Skip to content

Commit 40a0f38

Browse files
authored
Fix precision rounding issues in LineWrapper (foliojs#1583)
Handle JS quirks with large decimal precision checks resulting from the calculations of next lines in the LineWrapper
1 parent 2511122 commit 40a0f38

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## pdfkit changelog
22

3+
### Unreleased
4+
5+
- Fix precision rounding issues in LineWrapper
6+
37
### [v0.16.0] - 2024-12-29
48

59
- Update fontkit to 2.0

lib/line_wrapper.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EventEmitter } from 'events';
22
import LineBreaker from 'linebreak';
3+
import { PDFNumber } from './utils';
34

45
const SOFT_HYPHEN = '\u00AD';
56
const HYPHEN = '-';
@@ -26,9 +27,9 @@ class LineWrapper extends EventEmitter {
2627
// calculate the maximum Y position the text can appear at
2728
if (options.height != null) {
2829
this.height = options.height;
29-
this.maxY = this.startY + options.height;
30+
this.maxY = PDFNumber(this.startY + options.height);
3031
} else {
31-
this.maxY = this.document.page.maxY();
32+
this.maxY = PDFNumber(this.document.page.maxY());
3233
}
3334

3435
// handle paragraph indents
@@ -230,7 +231,7 @@ class LineWrapper extends EventEmitter {
230231
if (
231232
this.height != null &&
232233
this.ellipsis &&
233-
this.document.y + lh * 2 > this.maxY &&
234+
PDFNumber(this.document.y + lh * 2) > this.maxY &&
234235
this.column >= this.columns
235236
) {
236237
if (this.ellipsis === true) {
@@ -274,7 +275,7 @@ class LineWrapper extends EventEmitter {
274275

275276
// if we've reached the edge of the page,
276277
// continue on a new page or column
277-
if (this.document.y + lh > this.maxY) {
278+
if (PDFNumber(this.document.y + lh) > this.maxY) {
278279
const shouldContinue = this.nextSection();
279280

280281
// stop if we reached the maximum height

lib/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function PDFNumber(n) {
2+
// PDF numbers are strictly 32bit
3+
// so convert this number to the nearest 32bit number
4+
// @see ISO 32000-1 Annex C.2 (real numbers)
5+
return Math.fround(n);
6+
}

tests/unit/line_wrapper.spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import PDFDocument from "../../lib/document";
2+
import LineWrapper from '../../lib/line_wrapper';
3+
4+
describe("LineWrapper", () => {
5+
let document;
6+
7+
beforeEach(() => {
8+
document = new PDFDocument({
9+
compress: false,
10+
margin: 0,
11+
});
12+
});
13+
14+
test("ellipsis is present only on last line of multiline text", () => {
15+
// There is a weird edge case where ellipsis occurs on lines
16+
// in the middle of text due to number rounding errors
17+
//
18+
// There is probably a simpler combination of values but this is one I found in the wild
19+
document.y = 402.1999999999999;
20+
document.fontSize(7.26643598615917)
21+
const wrapper = new LineWrapper(document, {width: 300, height: 50.399999999999864, ellipsis: true})
22+
let wrapperOutput = "";
23+
wrapper.on("line", (buffer) => {
24+
wrapperOutput += buffer;
25+
document.y += document.currentLineHeight(true)
26+
})
27+
wrapper.wrap("- A\n- B\n- C\n- D\n- E\n- F", {})
28+
expect(wrapperOutput).toBe("- A\n- B\n- C\n- D\n- E\n- F");
29+
})
30+
31+
test("line break is handled correctly when at weird heights", () => {
32+
// There is probably a simpler combination of values but this is one I found in the wild
33+
document.y = 1/3;
34+
document.fontSize(Math.fround(42.3/3));
35+
let lineHeight = document.currentLineHeight(true);
36+
const wrapper = new LineWrapper(document, {width: 300, height:lineHeight*3})
37+
let wrapperOutput = "";
38+
wrapper.on("line", (buffer) => {
39+
wrapperOutput += buffer;
40+
document.y += lineHeight
41+
})
42+
// Limit to 3/4 lines
43+
wrapper.wrap("A\nB\nC\nD", {})
44+
expect(wrapperOutput).toBe("A\nB\nC\n");
45+
});
46+
47+
test("line break is handled correctly with ellipsis", () => {
48+
// There is probably a simpler combination of values but this is one I found in the wild
49+
document.y = 1/3;
50+
document.fontSize(Math.fround(42.3/3));
51+
let lineHeight = document.currentLineHeight(true);
52+
const wrapper = new LineWrapper(document, {width: 300, height:lineHeight*3, ellipsis: true})
53+
let wrapperOutput = "";
54+
wrapper.on("line", (buffer) => {
55+
wrapperOutput += buffer;
56+
document.y += lineHeight
57+
})
58+
// Limit to 3/4 lines
59+
wrapper.wrap("A\nB\nC\nD", {})
60+
expect(wrapperOutput).toBe("A\nB\nC…");
61+
});
62+
});

0 commit comments

Comments
 (0)