Conversation
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.
Background
For node mode PDF embedding in LuaTeX, luaotfload's fontloader converts CFF and CFF2 charstrings into Type 2 streams. For CFF2 variable fonts, the converter is meant to strip stem hints, so the output is unhinted. CFF2 support in the fontloader has been fixed several times before: commit 9ced93d fixed CFF2 INDEX and FDArray reading, commit 18e81f3 handled multiple
ItemVariationDatasubtables, and commit 9ab4495 fixed hint mask handling in the separate HarfBuzz variable CFF2 path.This PR fixes two bugs in the
justpassconverter in the default and reference fontloaders.Problem
With Source Serif 4 Variable Italic in node mode under luaotfload 3.29, some CFF2 glyphs produce empty or corrupt PDF glyph streams. Greek capital Beta (
U+0392) is one example.The converter should produce a complete unhinted Type 2 stream. Instead, it mishandles the
vstemhmoperator, and it keeps hint masks after removing the stems they refer to. The first bug derails parsing of the input charstring, and the second leaves the output stream inconsistent.Evaluation
justpasschecks the current operatortfor stem operators 1, 3, and 18, but for operator 23 (vstemhm) it checksoperation, which is undefined. Sovstemhmfalls through to generic operator handling, its operands and opcode are copied to the output, and the operand stack is cleared. Since this skipsp_getstem(), those stems are not counted, and a following mask can be computed as too short. When that happens, the parser reads too few mask bytes and treats the rest as charstring instructions. For CFF2, it also leaves in a stem operator that should have been stripped.Even after fixing the above, a second bug remains. The converter drops CFF2 stem declarations but still emits each
hintmaskorcntrmaskalong with its mask bytes. Those masks refer to stems that are not in the output, so the stream can be misinterpreted. To produce unhinted output, the converter has to drop both stems and masks, while still counting input stems and skipping over all input mask bytes.The unconditional mask emission goes back to the ConTeXt fontloader import in commit c98b44c (December 2018). That import added the
justpassstem and mask branches behindif true, with unusedelsebranches that already discard the CFF2 data.The February 2022 import in commit 36750eb switched the stem branch to
if version == "cff", so CFF2 stems going through that branch are now stripped. In the mask branch right next to it, the same import added the matching condition but commented it out:The commit doesn't say why the two branches differ. Both are annotated
cff 1: (when cff2 strip them), and both have a discard path. Using the same condition in both matches the stated intention to strip CFF2 hints.Solution
The fix is split into two commits:
operation == 23tot == 23, sovstemhmgoes through stem counting and version-dependent emission.if truewithif version == "cff". For CFF2, the existingelsebranch skips the mask bytes and clears the operand stack.With these changes, CFF1 keeps its hints,
vstemhmstems are counted correctly, and CFF2 output has both stems and masks stripped. The mask change does not affect CFF1. The dispatch fix does, in a good way: CFF1 mask parsing no longer undercounts stems after avstemhm.Both fixes are applied to
src/fontloader/misc/fontloader-font-cff.lua, the generatedsrc/fontloader/runtime/fontloader-reference.lua, and the bundledsrc/auto/fontloader-2023-12-28.lua. The default configuration selects the bundled loader throughluaotfload-status.lua, so updating it makes the fixes available without a configuration override.Testing Done
In my project, the following test passes with both fixes applied, using LuaHBTeX 1.24.0 from TeX Live 2026:
fonts.hashes.streams.I haven't added an integration test, because Source Serif 4 Variable Italic isn't in luaotfload's test support files.
Notes to Reviewers
Start with
src/fontloader/misc/fontloader-font-cff.lua. The changes insrc/fontloader/runtime/fontloader-reference.luaandsrc/auto/fontloader-2023-12-28.luaapply the same fixes to the reference and default runtime files.Since
fontloader-font-cff.luacomes from ConTeXt, these fixes should also go upstream to ConTeXt so the next fontloader import doesn't undo them. I'd be happy to work on that once this PR is merged.