From 5375bff642e5eff45af69ef915d0fa396be917e1 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sat, 1 Aug 2026 16:06:39 +0200 Subject: [PATCH] Don't write numbers in exponential notation when saving a pdf `toFixed(10)` switches to the exponential notation from 1e21 on, which isn't valid PDF syntax, and removing the trailing zeros then dropped a digit of the exponent: 1e30 was written "1e+3" and 1e100 "1e+1". Such a number, necessarily an integer, is now written with all its digits. The trailing zeros are removed with a backward scan, since `toFixed(10)` always produces exactly 10 decimals. Below the 1e21 limit its output is at most 33 characters long, so the previous `$`-anchored regex wasn't a performance issue. --- src/core/writer.js | 29 ++++++++++++++++++++++++++--- test/unit/writer_spec.js | 14 ++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/core/writer.js b/src/core/writer.js index 3395d1a02..d12292d94 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -148,6 +148,31 @@ async function writeArray(array, buffer, transform) { buffer.push("]"); } +// The exponential notation isn't valid in a PDF, hence a number is always +// written with all its digits. +function numberToPDFString(value) { + // `toFixed` uses the exponential notation from 1e21 on, so such a number is + // written thanks to BigInt: it's necessarily an integer, and `isInteger` also + // rules out NaN and ±Infinity for which BigInt would throw. + if (Number.isInteger(value) && Math.abs(value) >= 1e21) { + return BigInt(value).toString(); + } + + // Below that limit `toFixed(10)` never uses the exponential notation (unlike + // `toString` which uses it under 1e-6) and it rounds the value: it always + // adds 10 decimals, hence scan them backwards to remove the trailing zeros, + // and then the dot itself when none of the decimals is left. + const str = value.toFixed(10); + let end = str.length; + while (str[end - 1] === "0") { + end--; + } + if (str[end - 1] === ".") { + end--; + } + return str.slice(0, end); +} + async function writeValue(value, buffer, transform) { if (value instanceof Name) { buffer.push(`/${escapePDFName(value.name)}`); @@ -165,9 +190,7 @@ async function writeValue(value, buffer, transform) { // matrices (e.g. [0.000008 0 0 0.000008 0 0]). // The numbers must be "rounded" only when pdf.js is producing them and the // current transformation matrix is well known. - // toFixed(10) avoids scientific notation and rounds; the replace removes - // trailing zeros (and a trailing dot for integers). - buffer.push(value.toFixed(10).replace(/\.?0+$/, "")); + buffer.push(numberToPDFString(value)); } else if (typeof value === "boolean") { buffer.push(value.toString()); } else if (value instanceof Dict) { diff --git a/test/unit/writer_spec.js b/test/unit/writer_spec.js index b6d12cdd3..68a3ad23e 100644 --- a/test/unit/writer_spec.js +++ b/test/unit/writer_spec.js @@ -295,10 +295,20 @@ describe("Writer", function () { }); it("should not use scientific notation for very large numbers", async function () { - // JavaScript produces scientific notation above ~1e21 but such values - // are unlikely in PDFs; values below that threshold must be plain. + // JavaScript's toString() and toFixed() produce scientific notation from + // 1e21 on, which is invalid PDF: such a number must be written with all + // its digits, which are the exact ones of the underlying double. expect(await serialize(1e10)).toEqual("10000000000"); expect(await serialize(1.5e6)).toEqual("1500000"); + expect(await serialize(1e20)).toEqual("100000000000000000000"); + expect(await serialize(1e21)).toEqual("1000000000000000000000"); + // Removing the trailing zeros of the exponent used to change the value: + // "1e+30" was written "1e+3" and "1e+100" was written "1e+1". + expect(await serialize(1e30)).toEqual("1000000000000000019884624838656"); + expect(await serialize(-1e30)).toEqual( + "-1000000000000000019884624838656" + ); + expect((await serialize(1e100)).length).toEqual(101); }); it("should round to at most 10 decimal places", async function () {