Repository navigation
Fix name tree keys being sorted by locale instead of by their bytes - #1827
Merged
blikblum merged 1 commit intoOct 22, 2026
Merged
Conversation
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What kind of change does this PR introduce?
Bug fix. No linked issue.
PDFNameTreesorts its keys withlocaleCompare, which collates by locale and putsappendixbeforeChapter1. 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 orderChapter1(0x43) comes beforeappendix(0x61). So this:writes
/Names [(appendix) [...] (Chapter1) [...]], and the link toChapter1goes 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;incore/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
EmbeddedFilesandJavaScriptname 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):
masterappendix,Chapter1,chapter2: PDFium goTo linksChapter1deadNameTreelookupsappendixandChapter1not foundOn 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 loadedlib/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 intests/unit/tree.spec.jsfails onmasterwith thelocaleCompareorder and passes here. The other tests intree.spec.js,trailer.spec.jsandattachments.spec.jspass on both. Prettier and ESLint were not run either; the change follows the formatting of the code around it.Checklist:
🤖 Generated with Claude Code