Skip to content

Pin the bundled font paths so PDF export is not blank (#371, #251, #287) - #402

Merged
chadek merged 1 commit into
nextcloud:masterfrom
chadek:fix/pdf-blank-font-paths
Sep 15, 2026
Merged

chadek merged 1 commit into
nextcloud:masterfrom
chadek:fix/pdf-blank-font-paths

Conversation

@chadek

@chadek chadek commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Exporting or printing to PDF produces a PDF with no text in it. Reported since 2022 and still open: #371, #251, #287, and mentioned in passing in #288.

What it is

server/FileConverter/bin/font_selection.bin stores an absolute path to every bundled font, written by whichever machine ran allfontsgen — the build machine, for an app store install. When one of those paths does not resolve, x2t's PDF writer prints Can't load fontfile on stdout, writes a PDF with no glyphs in it, and exits 0. Nothing is logged and nothing is thrown, so from everywhere else in the system a blank PDF and a good one are the same successful conversion.

Same input, same binary, same exit code:

font_selection.bin holds x2t exit PDF size text characters
paths that do not resolve here 0 1 937 B 0
paths pinned to ../../../core-fonts/ 0 12 804 B the text that went in

The build already pinned AllFonts.js to paths relative to x2t's working directory and left font_selection.bin alone, on the belief — written into the Makefile — that its paths "are not used for loading". They are not used by the change-replay path that comment is about, which reads AllFonts.js; they are used by the PDF writer.

What this does

Stops shipping one machine's paths. x2t accepts relative paths in font_selection.bin — verified before committing to the approach, since AllFonts.js accepting them does not prove it — so it is now pinned the same way AllFonts.js already was. allthemesgen, which runs right after and renders the theme thumbnails, resolves them too: it never chdirs and the Makefile already runs it from FileConverter/bin for this reason.

The file is not sed-able — its strings are length-prefixed with a little-endian int32, so replacing a path without updating its prefix corrupts everything after it. The rewriter is lib/Document/FontPaths.php, shared between tools/pin-font-paths.php at build time and FontManager at runtime so both produce the same thing. Only paths under core-fonts are touched; custom fonts live in appdata, outside the app directory, and keep the absolute paths they are found at. A PDF converted with both embeds both.

Makes the rebuild trustworthy, which fixes the silent-failure half regardless. finishFontList() checked only that an AllFonts.js existed containing /core-fonts/ — a stale one left over from the build satisfies that, so a rebuild that wrote nothing at all reported success — and never looked at font_selection.bin at all. It now checks that this run wrote both files, that file_put_contents succeeded, and that every pinned path resolves from the converter directory; and it refuses up front, naming the file and the user, when it cannot write. That last case is the shape of #145 ("allfontsgen: not found") and #278 ("Segmentation fault while rebuilding fonts"), each of which leaves the user with blank PDFs and no diagnostic.

Makes the failure visible. ConverterBinary now scans x2t's stdout for Can't load fontfile and logs it rather than discarding the one stream that mentions it. occ documentserver:fonts returned 1 on success and 0 on failure, so anything scripting it was told the opposite of what happened. The upgrade repair step puts the actual error in its warning instead of a generic sentence.

Tests

tests/rig/pdf_test.py, which the rig had no equivalent of — that is why this survived the v0.4.0 work. It asserts on extracted text rather than file size, and covers all three parts: a converted PDF has the text in it, a font that cannot be loaded reaches the log, and a rebuild that cannot write fails loudly. Confirmed it fails against the pre-fix font file (FAIL - "PdfFontProbe" is not in the 1937 byte PDF).

tests/Document/FontPathsTest.php covers the binary rewriter, where a mistake would produce exactly the same silent symptom.

CI now installs poppler-utils for pdftotext. The Makefile change invalidates the integration job's document-server cache key, so the next run rebuilds the tree rather than reusing one with unpinned paths.

Suite

The full rig suite is green on this branch — formats, smoke, flush-live, autosave, leave, twotab, coedit, coedit-xlsx, forcesave, chat, pdf. coedit-xlsx is the one that exercises the other consumer of these files: spreadsheet change replay reads AllFonts.js, which is the failure finishFontList was originally written for.

Left as a draft pending review. Nothing has been posted to #371/#251/#287.

🤖 Generated with Claude Code

…extcloud#251, nextcloud#287)

font_selection.bin stores an absolute path to every bundled font, written by
whichever machine ran allfontsgen - the build machine, for an app store
install. When one of those paths does not resolve, x2t's PDF writer prints
"Can't load fontfile" on stdout, writes a PDF with no glyphs in it, and exits
0. Nothing downstream can tell that from a good conversion, which is why
exporting to PDF has been blank since 2022 with no diagnostic anywhere.

The build already pinned AllFonts.js to paths relative to x2t's working
directory and left font_selection.bin alone, on the belief - written into the
Makefile - that its paths were not used for loading. They are, by the PDF
writer. x2t accepts relative paths in that file, so it is now pinned the same
way: same input, same binary, 1 937 bytes with no text before, 12 804 bytes
with the text after.

font_selection.bin is not sed-able - its strings are length-prefixed with a
little-endian int32 - so the rewriter lives in FontPaths, shared between
tools/pin-font-paths.php at build time and FontManager at runtime, and only
touches paths under core-fonts. Custom fonts live in appdata, outside the app
directory, and keep the absolute paths they are found at.

The rest is the silence around it:

- finishFontList() checked only that some AllFonts.js existed containing
  "/core-fonts/", which a stale file from the build satisfies, so a rebuild
  that wrote nothing at all reported success. It now checks that this run
  wrote both files, that file_put_contents succeeded, and that every pinned
  path resolves - and refuses up front, naming the file and the user, when it
  cannot write. This is the shape of nextcloud#145 and nextcloud#278.
- ConverterBinary logs the fonts x2t says it could not load, rather than
  discarding the one stream that mentions them.
- documentserver:fonts returned 1 on success and 0 on failure, so anything
  scripting it was told the opposite of what happened.
- The upgrade repair step puts the actual error in its warning.

tests/rig/pdf_test.py covers all three: a converted PDF has the text in it, a
font that cannot be loaded reaches the log, and a rebuild that cannot write
fails loudly. FontPathsTest covers the binary rewriter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Fabrice Meyer <meyer.fabrice@gmx.fr>
@chadek
chadek force-pushed the fix/pdf-blank-font-paths branch from 96fe665 to bc07ff6 Compare September 15, 2026 17:22
@chadek
chadek marked this pull request as ready for review September 15, 2026 17:23
@chadek
chadek merged commit 8fea4eb into nextcloud:master Sep 15, 2026
8 of 9 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.

1 participant