[compile] Support for beyond-64k fonts#987
Conversation
After postprocessing a compiled TTFont, convert supported tables to their beyond-64k uppercase companions when the glyph order exceeds 64k glyphs. This keeps the behavior in ufo2ft so fontmake and other callers receive a ready-to-save font. The fontTools beyond64k helper is imported lazily, only when conversion is actually needed, so ordinary small-font builds do not require it at import time. Tested with: - /Users/behdad/ft/venv/bin/python -m pytest -q tests/baseCompiler_test.py - /Users/behdad/ft/venv/bin/python -m pytest -q 'tests/integration_test.py::IntegrationTest::test_compile_filters[ufoLib2-compileTTF]' - /Users/behdad/ft/venv/bin/python -m pytest -q 'tests/integration_test.py::IntegrationTest::test_compileVariableTTF[ufoLib2-None]' - black --check Lib/ufo2ft/_compilers/baseCompiler.py tests/baseCompiler_test.py Assisted-by: OpenAI Codex
Replace the mocked beyond-64k helper tests with integration coverage that compiles generated UFOs. The small-font case verifies that compact tables remain in use, while the large-font case verifies that the compiler emits uppercase beyond-64k companion tables after crossing the glyph-count limit. Tested with: - /Users/behdad/ft/venv/bin/python -m pytest -q tests/baseCompiler_test.py - black --check tests/baseCompiler_test.py Assisted-by: OpenAI Codex
|
Probably also worth adding an option to opt into beyond64k spec or otherwise |
There is support for the old |
|
I found an issue with sparse masters (a UFO layer or source with only a subset of glyphs). ufo2ft uppercases each master to the GLYF/LOCA/MAXP companion tables based on that master's own glyph count, and a sparse master may well stay below 64k, so it keeps the lowercase tables while the full masters go uppercase. varLib then rejects import ufoLib2
from fontTools.designspaceLib import DesignSpaceDocument, AxisDescriptor, SourceDescriptor
import ufo2ft
order = ["g%05d" % i for i in range(0x10001)] # 65537 glyphs -> full master is beyond-64k
full = ufoLib2.Font()
full.info.familyName, full.info.styleName = "T", "Regular"
full.info.unitsPerEm, full.info.ascender, full.info.descender = 1000, 800, -200
for n in order:
full.newGlyph(n).width = 500
full.lib["public.glyphOrder"] = order
# sparse layer with fewer than 64k glyphs
sparse = full.newLayer("sparse")
sparse.newGlyph("g00001").width = 600
doc = DesignSpaceDocument()
doc.addAxis(AxisDescriptor(tag="wght", name="Weight", minimum=0, default=0, maximum=1000))
s0 = SourceDescriptor()
s0.font = full
s0.location = {"Weight": 0}
doc.addSource(s0)
s1 = SourceDescriptor()
s1.font = full
s1.layerName = "sparse" # sparse master
s1.location = {"Weight": 1000}
doc.addSource(s1)
ufo2ft.compileVariableTTFs(doc) # VarLibValidationError: Masters must all use the same glyf/GLYF table family |
These exercise the top-level compile* functions, not the BaseCompiler class, so they belong with the other integration tests rather than in a separate module.
_maybe_uppercase_beyond64k ran on every postprocessed font, including the per-master fonts compiled while building a variable font. A sparse master (a layer with only a subset of glyphs) can stay below 64k while the full masters cross it, so it kept the lowercase glyf/loca/maxp tables while the others were upgraded to GLYF/LOCA/MAXP, and varLib then rejected the mismatched table family. Move the uppercasing inside the postProcessorClass guard, which is already nulled for VF interpolation masters (see _compileNeededSources), so the masters keep their lowercase tables and varLib gets a consistent family; the merged VF and standalone outputs still uppercase via their own postprocess. Add a regression test building a beyond-64k VF with a sparse master.
|
I just added a fix for the sparse master issue (including a test). It's safe to move/delay the uppercasing inside the |
_maybe_uppercase_beyond64k guarded on `len(glyphOrder) <= 0x10000`, so a font with exactly 65536 glyphs stayed on the lowercase glyf/maxp tables and overflowed maxp.numGlyphs (uint16) on save. 65536 is a count limit, not a gid-width one: gids 0..0xFFFF still fit, but the count 65536 does not. Guard on `<= 0xFFFF` instead. 65535 was already fine, 65537 already handled, so the existing tests at 65537 could not catch it; add one at exactly 65536.
|
I just fixed an off-by-one in |
PTAL. |
Stop using the obsolete glyphDataFormat=1 output path for TrueType outlines that preserve cubic curves. The allQuadratic option still controls whether preprocessing is allowed to leave cubic curves in place, but final fonts that actually contain cubic glyf data are now converted to the beyond-64k companion table family. Keep the conversion in the final-output postprocess step so variable-font interpolation masters still use a consistent lowercase table family before varLib merges them. Tested with: - PYTHONPATH=Lib /Users/behdad/ft/venv/bin/python -m pytest tests/integration_test.py -q -k 'GLYF_not_allQuadratic or beyond64k' - black --check Lib/ufo2ft/_compilers/baseCompiler.py Lib/ufo2ft/_compilers/ttfCompiler.py Lib/ufo2ft/_compilers/interpolatableTTFCompiler.py Lib/ufo2ft/outlineCompiler.py tests/integration_test.py - git diff --check Assisted-by: OpenAI Codex
Also read cubic flags via glyf[name] in the existing GLYF tests.
Lazily-packed glyphs after a postprocessor reload (useProductionNames / name dropping) have no .flags, so cubics went undetected and wrongly stayed in the lowercase glyf table.
glyf v1 is gone: cubics now go in GLYF and head.glyphDataFormat stays 0. Fix the allQuadratic=False docstrings and hardcode the head field instead of reading a self.glyphDataFormat attribute that no longer exists.
|
Should we always generate upper tables if allQuadratic is not True? |
Why? We'd be forcing GLYF onto what is otherwise a perfectly normal sub-64k quadratic |
Performance was one concern. But my other concern is that by setting allQuadratic to false, one is opting into possibly making fonts that don't work on most platforms currently. Always using GLYF makes it dead obvious of that. Only using GLYF if font has cubics, hides the problem if this allQuadratic value was set by mistake / ignorance. |
Quoting Behdad #987 (comment) > by setting allQuadratic to false, one is opting into possibly making > fonts that don't work on most platforms currently. Always using GLYF > makes it dead obvious of that. Only using GLYF if font has cubics, hides > the problem if this allQuadratic value was set by mistake / ignorance.
|
@behdad yeah I can see the footgun. I changed to do it unconditionally when allQuadratic=False |
After postprocessing a compiled TTFont, convert supported tables to their beyond-64k uppercase companions when the glyph order exceeds 64k glyphs. This keeps the behavior in ufo2ft so fontmake and other callers receive a ready-to-save font.
The fontTools beyond64k helper is imported lazily, only when conversion is actually needed, so ordinary small-font builds do not require it at import time.
This builds upon fonttools/fonttools#4097
Assisted-by: OpenAI Codex