From 0eca44e6e819f9e156eee0de2cbbcffd3a59e47b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sun, 18 Oct 2026 15:33:50 -0600 Subject: [PATCH] [truetype] Use the blend axis count for private VARC axes. Static VARC fonts can have private variation axes without an 'fvar' table or public MM metadata. Use the blend's axis count when validating 'avar' and item variation stores, avoiding a null 'mmvar' dereference when loading HVAR or VVAR for a VARC component. * src/truetype/ttgxvar.c (ft_var_load_avar, tt_var_load_item_variation_store): Use blend->num_axis. * tests/varc/make-hvvar-fonts.py: Generate HVAR and VVAR fixtures from the static VARC/gvar font using only Python's standard library. * tests/meson.build: Generate the fixtures and pass them to the VARC test. * tests/varc/main.c (test_font, main): Test generated fixture paths. Tests: all five Meson regression tests pass. Fixes issue #1474. Assisted-by: OpenAI Codex --- src/truetype/ttgxvar.c | 4 +- tests/meson.build | 8 ++++ tests/varc/main.c | 20 +++++++-- tests/varc/make-hvvar-fonts.py | 76 ++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 tests/varc/make-hvvar-fonts.py diff --git a/src/truetype/ttgxvar.c b/src/truetype/ttgxvar.c index d3f8c30fa..6017eaf4d 100644 --- a/src/truetype/ttgxvar.c +++ b/src/truetype/ttgxvar.c @@ -413,7 +413,7 @@ FT_TRACE2(( "loaded\n" )); - if ( axisCount != (FT_Long)blend->mmvar->num_axis ) + if ( axisCount != (FT_Long)blend->num_axis ) { FT_TRACE2(( "ft_var_load_avar:" " number of axes in `avar' and `fvar'\n" )); @@ -571,7 +571,7 @@ FT_READ_USHORT( region_count ) ) goto Exit; - if ( axis_count != (FT_Long)blend->mmvar->num_axis ) + if ( axis_count != (FT_Long)blend->num_axis ) { FT_TRACE2(( "tt_var_load_item_variation_store:" " number of axes in item variation store\n" )); diff --git a/tests/meson.build b/tests/meson.build index 4448bb1e2..c6ea91f0c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -15,6 +15,12 @@ test('issue-1063', ### +varc_hvvar_fonts = custom_target('varc-hvvar-fonts', + input: files('data/varc-static-gvar.ttf'), + output: ['varc-static-hvar.ttf', 'varc-static-vvar.ttf'], + command: [python_exe, files('varc/make-hvvar-fonts.py'), '@INPUT@', '@OUTPUT@'], +) + test_varc = executable('varc', files([ 'varc/main.c' ]), dependencies: freetype_dep, @@ -23,6 +29,8 @@ test_varc = executable('varc', test('varc', test_varc, env: test_env, + depends: varc_hvvar_fonts, + args: [varc_hvvar_fonts[0], varc_hvvar_fonts[1]], suite: 'regression') ### diff --git a/tests/varc/main.c b/tests/varc/main.c index 6981f498a..53843fce5 100644 --- a/tests/varc/main.c +++ b/tests/varc/main.c @@ -29,8 +29,11 @@ test_font( FT_Library library, int ret = 1; - snprintf( filepath, sizeof ( filepath ), "%s/%s", - testdata_dir, test_font->filename ); + if ( testdata_dir ) + snprintf( filepath, sizeof ( filepath ), "%s/%s", + testdata_dir, test_font->filename ); + else + snprintf( filepath, sizeof ( filepath ), "%s", test_font->filename ); if ( FT_New_Face( library, filepath, 0, &face ) ) { @@ -244,7 +247,8 @@ Exit: int -main( void ) +main( int argc, + char** argv ) { static const TestFont test_fonts[] = { @@ -276,6 +280,16 @@ main( void ) i++ ) ret |= test_font( library, testdata_dir, &test_fonts[i] ); + /* Generated HVAR/VVAR fixtures retain the static gvar font's outline. */ + for ( i = 1; i < (FT_UInt)argc; i++ ) + { + TestFont font = { NULL, 1, 3, 50, 3, 0 }; + + + font.filename = argv[i]; + ret |= test_font( library, NULL, &font ); + } + ret |= test_reset_without_axes( library, testdata_dir ); ret |= test_value_conditions( library, testdata_dir ); ret |= test_variation_store( library, testdata_dir ); diff --git a/tests/varc/make-hvvar-fonts.py b/tests/varc/make-hvvar-fonts.py new file mode 100644 index 000000000..86c8c3d01 --- /dev/null +++ b/tests/varc/make-hvvar-fonts.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Add HVAR or VVAR to the static VARC/gvar fixture using only stdlib. + +The component sets the private axis to a nonzero coordinate. Loading its +metrics must therefore parse an item variation store without an 'fvar'. +Distributed under the FreeType project license, LICENSE.TXT. +""" + +from pathlib import Path +import struct +import sys + + +def checksum(data): + data += bytes(-len(data) % 4) + return sum(struct.unpack(">" + "I" * (len(data) // 4), data)) & 0xFFFFFFFF + + +def read_tables(data): + count, = struct.unpack_from(">H", data, 4) + tables = {} + for index in range(count): + tag, _, offset, size = struct.unpack_from(">4sIII", data, 12 + 16 * index) + tables[tag] = data[offset:offset + size] + return tables + + +def make_font(tables, vertical): + tables = tables.copy() + axis_count, = struct.unpack_from(">H", tables[b"gvar"], 4) + glyph_count, = struct.unpack_from(">H", tables[b"maxp"], 4) + assert b"fvar" not in tables and axis_count == 1 + + # One positive-axis region and one delta set per glyph, with implicit + # glyph-to-delta mapping. The component is glyph 2. + store = struct.pack(">HIHI", 1, 12, 1, 22) + store += struct.pack(">HHhhh", 1, 1, 0, 16384, 16384) + store += struct.pack(">4H", glyph_count, 1, 1, 0) + store += struct.pack(">" + "h" * glyph_count, + *(25 if i == 2 else 0 for i in range(glyph_count))) + if vertical: + tables[b"VVAR"] = struct.pack(">HH5I", 1, 0, 24, 0, 0, 0, 0) + store + vhea = bytearray(tables[b"hhea"]) + struct.pack_into(">H", vhea, 10, 1000) # advanceHeightMax + struct.pack_into(">H", vhea, 34, glyph_count) + tables[b"vhea"] = bytes(vhea) + tables[b"vmtx"] = struct.pack(">Hh", 1000, 0) * glyph_count + else: + tables[b"HVAR"] = struct.pack(">HH4I", 1, 0, 20, 0, 0, 0) + store + + head = bytearray(tables[b"head"]) + struct.pack_into(">I", head, 8, 0) + tables[b"head"] = bytes(head) + count = len(tables) + power = 1 << (count.bit_length() - 1) + font = bytearray(struct.pack(">I4H", 0x10000, count, power * 16, + power.bit_length() - 1, count * 16 - power * 16)) + font += bytes(16 * count) + for index, (tag, data) in enumerate(sorted(tables.items())): + offset = len(font) + struct.pack_into(">4sIII", font, 12 + 16 * index, + tag, checksum(data), offset, len(data)) + font += data + bytes(-len(data) % 4) + if tag == b"head": + head_offset = offset + struct.pack_into(">I", font, head_offset + 8, + (0xB1B0AFBA - checksum(font)) & 0xFFFFFFFF) + return font + + +if __name__ == "__main__": + if len(sys.argv) != 4: + raise SystemExit("usage: make-hvvar-fonts.py input.ttf hvar.ttf vvar.ttf") + tables = read_tables(Path(sys.argv[1]).read_bytes()) + for vertical, output in enumerate(sys.argv[2:]): + Path(output).write_bytes(make_font(tables, vertical))