Skip to content

Fix name tree keys being sorted by locale instead of by their bytes - #1827

Merged
blikblum merged 1 commit into
foliojs:masterfrom
MaxFreedomPollard:fix/name-tree-key-order
Oct 22, 2026
Merged

blikblum merged 1 commit into
foliojs:masterfrom
MaxFreedomPollard:fix/name-tree-key-order

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix. No linked issue.

PDFNameTree sorts its keys with localeCompare, which collates by locale and puts appendix before Chapter1. ISO 32000-1, 7.9.6 wants the keys in ascending order by their bytes, shorter keys first when one begins with the other, and readers rely on that order to look a key up. In byte order Chapter1 (0x43) comes before appendix (0x61). So this:

doc.text('Chapter 1', { goTo: 'Chapter1' });
doc.text('Appendix', { goTo: 'appendix' });
doc.addPage().text('...', { destination: 'Chapter1' });
doc.addPage().text('...', { destination: 'appendix' });

writes /Names [(appendix) [...] (Chapter1) [...]], and the link to Chapter1 goes nowhere in PDFium, the engine behind Chrome's PDF viewer. PDFium reads the keys in order and stops at the first one greater than the name it wants (if (iCompare > 0) break; in core/fpdfdoc/cpdf_nametree.cpp). qpdf's name tree lookup misses keys in the same way.

The fix compares the keys by character code, which is their byte order for ASCII keys. Keys that contain a character outside ASCII are written as UTF-16BE and compare among themselves in the same way. In a tree that mixes the two, the keys come out in the order PDFium and qpdf use when they compare the decoded keys. The EmbeddedFiles and JavaScript name trees go through the same comparison.

Verification

I generated documents with the library and looked the names up with PDFium 156.0.8076.0 (pypdfium2), qpdf 12.4.2 (pikepdf) and MuPDF 1.28.5 (PyMuPDF):

master this branch
appendix, Chapter1, chapter2: PDFium goTo links Chapter1 dead all resolve
same names: qpdf NameTree lookups appendix and Chapter1 not found all found
295 random ASCII names: dead PDFium links / failed qpdf lookups 286 / 251 0 / 0
147 random non-ASCII names: dead PDFium links / failed qpdf lookups 96 / 122 0 / 0

On this branch the written keys are in ascending byte order for the ASCII and non-ASCII sets. MuPDF resolves every link on both. Encrypted documents (RC4 and AES-256) give the same results.

I could not run yarn test, because I could not install the dependencies in my environment (no access to the npm registry). Instead I loaded lib/ directly in Node 22 with small stand-ins for the third-party imports, and ran the spec files through a minimal shim of the vitest globals. The new test in tests/unit/tree.spec.js fails on master with the localeCompare order and passes here. The other tests in tree.spec.js, trailer.spec.js and attachments.spec.js pass on both. Prettier and ESLint were not run either; the change follows the formatting of the code around it.

Checklist:

  • Unit Tests
  • Documentation - N/A
  • Update CHANGELOG.md
  • Ready to be merged

🤖 Generated with Claude Code

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 <[email protected]>
@blikblum
blikblum merged commit 7e6145b into foliojs:master Oct 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants