From cfc2da8d000d88fcf94a3da040b399bc2917a834 Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:50:55 -0700 Subject: [PATCH] Fix name tree keys being sorted by locale instead of by their bytes PDFNameTree sorted its keys with localeCompare, but readers look a key up expecting them in ascending byte order (ISO 32000-1, 7.9.6). With destinations named `appendix` and `Chapter1`, `Chapter1` was written second, so PDFium (Chrome) and qpdf could not find it and a link to it went nowhere. Compare the keys by character code instead, which is their byte order for ASCII keys. The EmbeddedFiles and JavaScript name trees are ordered the same way. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + lib/name_tree.js | 5 ++++- tests/unit/tree.spec.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4384a1f1f..068859abd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- Fix links to a named destination going nowhere in readers that look the name up in sorted order, such as Chrome (PDFium) and qpdf, because name tree keys were sorted by locale with `localeCompare` instead of by their bytes: with destinations named `appendix` and `Chapter1`, `Chapter1` was written second and could not be found. The keys of the `EmbeddedFiles` and `JavaScript` name trees are sorted the same way - Escape document information in XMP metadata and omit characters XML 1.0 cannot represent, without changing the PDF Info dictionary. Fixes #1817 - Fix text in a standard font garbling or dropping the characters after a tab, a carriage return or any other character code below `0x10`, which was written into the hex string of the text as a single digit, so the following digits paired up wrongly: a `\r` before a line break showed as `Ð`, and `Tab\there` was read back as `Tab–†W&P`. Fixes #1425, #606 - Reuse the glyph width and kerning tables of a standard font across documents instead of building them again for every document that uses it, which made a short document using two standard fonts more than twice as slow to create diff --git a/lib/name_tree.js b/lib/name_tree.js index 6daf9d8b7..ef2e1599e 100644 --- a/lib/name_tree.js +++ b/lib/name_tree.js @@ -6,7 +6,10 @@ import PDFTree from './tree'; class PDFNameTree extends PDFTree { _compareKeys(a, b) { - return a.localeCompare(b); + // Readers look a key up expecting the keys in ascending byte order + // (ISO 32000-1, 7.9.6). Character codes compare in that order for ASCII + // keys, while `localeCompare` puts `appendix` before `Chapter1`. + return a < b ? -1 : a > b ? 1 : 0; } _keysName() { diff --git a/tests/unit/tree.spec.js b/tests/unit/tree.spec.js index 84b948425..4ce702e34 100644 --- a/tests/unit/tree.spec.js +++ b/tests/unit/tree.spec.js @@ -73,6 +73,24 @@ describe.each([ }); }); +describe('PDFNameTree key order', () => { + test('sorts the keys by their bytes rather than by locale', () => { + const tree = new PDFNameTree(); + tree.add('appendix', new String('one')); + tree.add('Chapter1', new String('two')); + + // readers look keys up in byte order, in which `Chapter1` (0x43) comes + // before `appendix` (0x61); in locale order they could not find it + expect(PDFObject.convert(tree)).toEqual(`<< + /Limits [(Chapter1) (appendix)] + /Names [ + (Chapter1) (two) + (appendix) (one) +] +>>`); + }); +}); + describe('name trees in a document', () => { const names = ['(heading)', '(data.txt)', 'app.alert'];