Repository navigation
Subset hinted CFF2 fonts, and finish the instancer's metrics - #294
Merged
Merged
Conversation
Subsetting a hinted CFF2 font failed: the per-body scanner cleared the operand stack at `blend`, so blended stem hints never counted, the hint masks were read as empty, and their bytes passed for operators. Source Serif 4 VF failed 553 of its 1,463 single-glyph subsets this way. The scanner also started every subroutine at zero stems, so a CFF1 mask in a subroutine that counts its caller's stems was misread too. The subsetter now runs each kept glyph's charstring through its subroutine calls, as HarfBuzz's subsetter interprets it: a call runs the callee with the caller's stack and stem count, the first mask fixes the mask size, a CFF2 `blend` drops as many deltas as the store gives the `vsindex` in effect (starting from the Private DICT's), subroutines nest at most 10 deep, and the stack holds at most 513 operands. Each body is read once, at its first call, and the calls found then are renumbered. The walk charges 64 tokens per byte of the table, plus 2^22. A renumbered call whose new number does not fit its push is written in its shortest form and the bytes after it move, as HarfBuzz re-encodes calls, instead of failing; CFF2 no longer falls back to keeping every subroutine. The public per-body helpers (`scan_subr_calls`, `compute_kept_subrs`, `renumber_charstring`) are unchanged. Part of #290.
A full CFF2 instance wrote each resolved blend as `master + sum(scalar * delta)` and kept the fraction, in 16.16 fixed form: Source Serif 4 VF at wght 700 came to a 614 KB `CFF2` table where HarfBuzz writes 203 KB. HarfBuzz's instancer rounds every blended value to a whole unit, halves away from zero, in double precision. The bake now keeps its operand stack in f64 and rounds each blend once its deltas are in; a source operand with a fraction keeps its exact 16.16 value. The charstrings of the Source Serif 4 instance are now byte-identical to HarfBuzz's. The bake also starts each charstring at the `vsindex` its Private DICT sets, as the core and HarfBuzz read it, instead of 0. Part of #290.
`instance` left `OS/2.usWeightClass`, `usWidthClass`, `xAvgCharWidth` and `post.italicAngle` at the source's values, and a CFF2 instance kept the source's left side bearings (1,375 of Source Serif 4's 1,464 glyphs at wght 700), `head` box and `hhea` extremes. As HarfBuzz's instancer does: - `usWeightClass` follows a pinned `wght` (rounded, clamped to 1 to 1000), `usWidthClass` a pinned `wdth` (through the spec's width percentages), and `italicAngle` a pinned `slnt` (clamped to -90 to 90). `instance` takes each pinned axis' location as the user value of its normalized coordinate; a kept axis leaves its field. - `xAvgCharWidth` is the mean of the instance's advances that are not zero, in full and partial instances. - A CFF2 instance measures each glyph at the outline coordinates, the box of its outline with control points before the blends round, and takes its left side bearing, the `head` box and the `hhea` extremes from those boxes (`update_instance_metrics_map_from_cff2`). A glyph without a box keeps its bearing. A partial instance measures its new default outlines the same way. The glyphs are drawn after the charstring bake, which ran the same subroutines within its budget; the partial bake now runs before the metrics too. Part of #290.
`instance` takes normalized coordinates, so callers had to normalize
each axis with `VariationAxis::normalize` themselves. `instance_user`
takes each axis' setting by tag in the units `fvar` gives, as HarfBuzz's
instancer takes them:
- `AxisLimit::Pin(v)` pins the axis at `v`, clamped to its range;
`AxisLimit::Default` pins it at its default; `AxisLimit::Keep` keeps
it variable.
- `AxisLimit::Range { min, default, max }` follows
`hb_subset_input_set_axis_range`: a range of one value pins, the
axis' whole range with its own default keeps the axis (and records
the default for `usWeightClass` and the like, as HarfBuzz does), and
any narrower range or moved default fails, since the instancer cannot
narrow an axis.
- Axes the input does not name are pinned at their defaults, or kept
when `keep_unnamed_axes` is set, as HarfBuzz and fontTools keep them.
Each pinned value is normalized as HarfBuzz normalizes it, and the
instance derives both of HarfBuzz's coordinate sets (outline and plan)
from it, as `instance` does; the output matches `instance` at the same
normalized coordinates byte for byte. `instance` keeps working
unchanged.
Part of #290.
HarfBuzz defines `roundf(x)` as `floor(x + 0.5)`: halves round toward positive infinity, not away from zero. The CFF2 glyph boxes, the `OS/2` weight and width classes, `xAvgCharWidth` (from the double-precision mean) and `post.italicAngle` now round that way. A negative box end at an exact half, -2.5 say, goes to -2. The blends of a CFF2 bake keep C `round`, halves away from zero, which HarfBuzz's interpreter calls there. Part of #290.
Review of #294 (B1, B2). A `blend` over a subtable with no regions (a `vsindex` past the store, or a VarData without regions) leaves its `n` values on the stack, so it can run again and again for two tokens while the full CFF2 bake rounds all `n` values each time and the subset walk marks them. The bake also had no operand-stack limit, so `n` could be as large as the charstring. A 9,980-byte font took 50 s to fail the bake's budget, a 4 MB one over 180 s, and a 4 MB font took 50 s to subset. Both now charge one token for each value a blend leaves (the deltas were charged as they were pushed), and the bake stops at 513 operands, the CFF2 limit HarfBuzz's interpreter enforces. The same fonts now fail or finish in under a second. Real fonts spend at most 1.8% of the bake budget (Noto Sans KR VF, from 1.4%). Part of #290.
Review of #294 (B3). In a CID-keyed CFF1 subset, a global subroutine that calls a local is duplicated per Font DICT, and each duplicate is renumbered from the calls the walk recorded when it first read the global. That reading resolved the call against the first Font DICT's local INDEX, so a Font DICT whose local count gives another bias had its duplicate point at the wrong local: the reviewer's font drew a line to (0, 77) instead of (0, 555). A call's source subroutine is now worked out again from the number it pushes and the bias of the Font DICT that runs the body, for every body. Each kept subroutine's new index is its position among the kept ones, found by binary search, so the renumber tables no longer take an entry per source subroutine and Font DICT. Part of #290.
Review of #294 (S1). The walk gave every kept Font DICT a slot per entry of its local Subrs INDEX, and the CID-keyed CFF1 subsetter read the Private DICT and Subrs INDEX of every Font DICT, even when they all name one. 256 Font DICTs over one 65,535-entry INDEX (a 269 KB font) peaked at 301 MB and took 1.4 s. The walk now keeps a map of the subroutines it reached, per Font DICT and for the globals, and a set of the globals each Font DICT reached. The CID-keyed subsetter reads a Private DICT and its Subrs INDEX once for every Font DICT that names it, as the CFF2 parser does. That font now peaks at 6 MB and takes 0.02 s. Part of #290.
Review of #294 (nit). Both are new in this release, so marking them `#[non_exhaustive]` now costs nothing, and lets a later release add an axis setting (narrowing an axis, say) or an input field without breaking callers. `UserInstanceInput` gains `new`, `with_axis`, `with_unnamed_axes_kept` and `with_var_tables_dropped` to build one outside the crate; its fields stay public. Part of #290.
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.
Closes #290.
Subsets hinted CFF2 fonts (N1), counts a glyph's stems across subroutine calls (A18), finishes the instancer's
OS/2,postand CFF2 metrics (B12), and adds a user-space entry point to the instancer (B13). Everything is insidecrates/sigilbuzz-subset. No new dependencies; MSRV 1.81.References: HarfBuzz 14.5.0 (
hb-subset-cff-common.hh,hb-cff-interp-cs-common.hh,hb-cff2-interp-cs.hh,hb-subset-cff2.cc,hb-subset-plan-var.cc,hb-ot-os2-table.hh,hb-ot-post-table.hh,hb-ot-hmtx-table.hh), through uharfbuzz 0.56.2, and fontTools 4.66.1.vsindexusWeightClass,usWidthClass,xAvgCharWidth,italicAngle; CFF2 side bearings,headbox,hheaextremes from the outlinesinstance_user,AxisLimit,UserInstanceInputroundf(floor(x + 0.5)) doesAxisLimitandUserInstanceInputare#[non_exhaustive];UserInstanceInputbuildersReview fixes (d05e37c)
New commits on top of da932e8; nothing pushed before was rewritten. Timings are wall time with peak working set, release builds; "main" is fce2d02 and "PR head" da932e8. The reviewer's fonts are from
review294/(copied).B1 and B2: blend amplification (12bb477). A
blendover a subtable with no regions (avsindexpast the store, or a VarData without regions) leaves itsnvalues, so it could repeat for two tokens while the full CFF2 bake rounded allnvalues every time and the subset walk marked them; the bake also had no stack limit. Both now charge a token per value a blend leaves (the deltas were charged when pushed), as the reviewer suggested, and the bake stops at 513 operands, the CFF2 limit. Real fonts spend at most 1.8% of the bake budget (Noto Sans KR VF, from 1.4%; Source Serif 4 1.5%).Fonts that main instanced and that now fail: drawamp0_50_0_400, drawamp_50_0 and bakebomb_flat_512 (budget), drawamp0_600_1000000_400 and bakebomb_flat_5000 (more than 513 operands), and of the earlier reviews' crafted fonts
chainP50.otf(budget) andstale.otf(more than 513 operands). HarfBuzz's instancer drops theCFF2table of all of them but bakebomb_flat_512 (one 60 KB glyph running512 blend20,000 times: 10 million value touches, which HarfBuzz does not charge).B3: cross-FD duplicates (c2c1bd1). A global's calls were recorded once, resolved against the local INDEX of the first FD that ran it, and every FD's duplicate was renumbered from them; an FD whose local count gives another bias then called the wrong local. A call's source subroutine is now worked out from the number it pushes and the bias of the FD that runs the body. Kept subroutines take their position among the kept ones by binary search, so the renumber tables no longer hold an entry per source subroutine per FD.
lineTo (0, 77)for(0, 555)), 1 failsS1: memory (4f4b62f). The walk kept a slot per entry of each kept FD's Subrs INDEX; it now keeps maps of the subroutines it reached. The CID-keyed CFF1 subsetter reads a Private DICT and its Subrs INDEX once for all the FDs that name it (as the CFF2 parser already did).
Nit (d05e37c).
AxisLimitandUserInstanceInputare#[non_exhaustive].UserInstanceInput::new(),with_axis,with_unnamed_axes_keptandwith_var_tables_droppedbuild one; the fields stay public.Tests.
a_blend_charges_the_values_it_leaves,font_dicts_keep_only_the_locals_they_reach(walk),bake_charges_each_blended_value,bake_stacks_at_most_513_operands(CFF2 bake),cid_cross_fd_duplicates_resolve_locals_with_their_fds_bias(two FDs with 1 and 1,240 locals sharing a global that calls a local). All five fail against da932e8.Re-verification on d05e37c. The CFF corpus, TrueType corpus, Source Serif 4 and NectoMono checks, the 22 B12 comparisons, the instance diffs, the hostile suites and the gate below are from this head; every number in them came out the same as on da932e8. The reviewer's
verify.py, with this head, finds no program difference in 467 subsets (56,708 glyphs) of the 37 non-Typodermic fonts and 3,416 subsets (144,965 glyphs) of every third Typodermic font.N1 and A18: subsetting hinted CFF and CFF2
Problem. The subsetter scanned each charstring and subroutine body on its own. It cleared the stack at
blend, so a blended stem hint (... 7 blend hstemhm) counted no stems, and it started every subroutine at zero stems, so a hint mask in a subroutine that counts its caller's stems was sized wrong. Either way the mask bytes were read as operators and operands: "CFF unknown charstring operator", "callsubr without operand", or call sites in the wrong place.Fix (3ad21bb). A new walk (
cff/walk.rs) runs each kept glyph's charstring as HarfBuzz's subsetter interprets it:vstem), asdetermine_hintmask_sizedoes.blendpopsn, thennrows of one delta per region of thevsindexin effect, leavingnvalues. Region counts come from the VariationStore, cached pervsindex; thevsindexstarts at the one the glyph's Private DICT sets. Avsindexpast the store's subtables blends no deltas, as HarfBuzz reads a missing subtable.kMaxCallLimit), a subroutine reaching itself fails, and the stack holds at most 513 operands.The subroutines reached are the ones kept: per FD for CID CFF1 and CFF2, with the cross-FD global duplication of CID CFF1 now worked out from the walk's call lists and the globals each FD reached.
A renumbered call whose new number cannot keep its push's width (a
-107..=107value in a 2-byte push, or a wider value) is written in its shortest form and the bytes after it move, as HarfBuzz re-encodes calls. Before, a non-CID CFF1 subset failed with "CFF renumber: cannot pad operand to original width" and a CFF2 subset fell back to keeping every subroutine.Budget. The walk charges one token per operand or operator it reads, inside subroutines too, and one per value a
blendleaves: 64 per byte of theCFForCFF2table, plus 2^22, the same scale as the CFF2 bake. Past it the subset fails with "CFF charstring work budget exceeded". Every other new loop is linear in the input (each body is read once; call lists are at most one entry per byte), and the walk's memory follows the subroutines it reaches.The public per-body helpers (
scan_subr_calls,compute_kept_subrs,renumber_charstring) are unchanged and document their per-body limits; the subsetter no longer uses them.CFF unknown charstring operatorTests.
cff/walk/tests.rs(17 tests on hand-built charstrings and stores: blended stems, Private DICTvsindex,vsindexpast the store and negative, a missing store, stems and implicitvstems across calls, the mask fixed at the first mask, a body read once,endcharin a subroutine, globals per FD, 10 deep and 11 deep, recursion, 513 and 514 operands, a spent budget, and ten malformed cases), plus five orchestration round trips for non-CID CFF1, CID CFF1 (a cross-FD global holding a mask) and CFF2 (blended stems, Private DICTvsindex, a mask in a subroutine). All five fail against main's code.B12: the instancer's metrics
OS/2 and post (e56f4db, da932e8). As
hb-ot-os2-table.hhandhb-ot-post-table.hhdo:usWeightClassfrom a pinnedwght(rounded, clamped to 1 to 1000),usWidthClassfrom a pinnedwdth(map_wdth_to_widthclass),post.italicAnglefrom a pinnedslnt(clamped to -90 to 90), andxAvgCharWidththe mean of the instance's advances that are not zero (full and partial instances).instancetakes each pinned axis' location as the user value of its normalized coordinate; a kept axis leaves its field. ARangegiven toinstance_userrecords its default, as HarfBuzz does.usWidthClassis not in the issue but is the same mechanism and was needed for parity on Roboto Flex.CFF2 rounding (525b689). HarfBuzz's flattener rounds every blended value to a whole unit (
round, halves away from zero) when it has coordinates. The bake now keeps its stack in f64 and does the same; a source operand with a fraction keeps its exact 16.16 value. Its charstrings now equal HarfBuzz's byte for byte (197,577 bytes of Source Serif 4 at wght 700). The bake also starts at the Private DICT'svsindex.CFF2 outline metrics (e56f4db, da932e8). As
update_instance_metrics_map_from_cff2: each glyph of the source is drawn at the outline coordinates (the core's CFF2 outlines), and its box is the control box (curve control points included, a contour's start counted once a segment leaves it) before the blends round, its ends rounded halves up. A glyph with a box takes its left end as its left side bearing; one without keeps the source's.headtakes the union of the boxes;hheatakes the largest advance and the smallest left and right side bearings and largest extent over every glyph, one without a box as zero wide. A partial CFF2 instance measures its new default outlines the same way, as HarfBuzz does. The glyphs are drawn after the bake, which ran the same subroutines within its budget; the partial bake now runs before the metrics too.Against HarfBuzz's instancer, 22 locations (all fields; "before" is main, "after" this PR):
wght=700wght=250,opsz=48wght=650,opsz=keepopsz=12,wght=keepwght=400wght=700wght=350wght=100wght=700slnt=-10,wdth=75wdth=137.5,wght=300,opsz=72wght=700,opsz=keep,slnt=keepslnt=-5,wdth=keepwght=700,slnt=-15slnt=-7.5,CASL=1,wght=keepMONO=1,wght=550wght=500,opsz=72opsz=144,SOFT=keepwght=300wght=700opsz=20,wght=keepwght=550Every "after" value equals HarfBuzz's. "outlines != HB" draws every glyph of both instances through HarfBuzz at their default. The two partial Source Serif 4 rows still differ in their default outlines: see Not done.
Sizes (full instances; the partial rows did not change):
wght=700wght=250,opsz=48wght=400wght=700wght=350The 36 CFF2 bytes left on Source Serif 4 are Private DICTs: HarfBuzz writes
BlueScaleas 0 (this PR keeps 0.0375), and in FD 1 it rounds blendedBlueValuesper delta where this crate rounds the absolute values as fontTools does (483 499 717 737against HarfBuzz's483 498 716 736). The rest of the file difference isGPOS(402,776 bytes against 185,628: not repacked) andSTAT.B13:
instance_userFix (9580c2b).
instance_user(face, &UserInstanceInput)takes axis settings by tag infvar's units:AxisLimit::Pin(v)clamps to the axis' range ashb_subset_input_pin_axis_locationdoes (a NaN pins the default);AxisLimit::Defaultpins the default;AxisLimit::Keepkeeps the axis.AxisLimit::Range { min, default, max }followshb_subset_input_set_axis_range's clamping: a range of one value pins, the whole axis with its own default keeps the axis (and setsusWeightClassand the like from the default, as HarfBuzz does), and a narrower range or a moved default returnsSubsetError::Unsupported.wghtgives a static font;keep_unnamed_axes: truekeeps them, as HarfBuzz and fontTools do. An unknown tag is an error.Both types are
#[non_exhaustive];UserInstanceInput::new().with_axis(*b"wght", AxisLimit::Pin(700.0))builds an input. Each pinned value goes throughVariationAxis::normalize, and the instance derives both coordinate sets (outline and plan) from it asinstancedoes. In all 22 locations above,instance_userwith user values andinstancewithVariationAxis::normalized coordinates wrote the same bytes.instanceis unchanged.Verification
All against HarfBuzz 14.5.0 (uharfbuzz 0.56.2) and fontTools 4.66.1, release builds of a harness against main (fce2d02) and this PR.
Source Serif 4 VF. All 1,463 single-glyph subsets and 200 random multi-glyph subsets succeed. Each kept glyph draws the same as the source through the sigilbuzz core at 5 locations (default, all axes min, all max, 0.5, mixed); fontTools decompiles every charstring and its desubroutinized programs equal the source's token for token; fontTools' and HarfBuzz's drawings (default and 3 locations) equal the source's.
Hinted CFF1 with masks in subroutines (NectoMono Regular): 486 singles and 100 multi-glyph subsets, the same checks; desubroutinized programs, hint masks included, are identical to the source.
CFF corpus, subsets against main. 779 CFF and CFF2 fonts (731 Typodermic CFF1 fonts, most with masks in subroutines; Aika, Fivo Sans, Sora, NectoMono; Source Serif 4 VF, Noto Sans KR VF .otf; the repo fixtures and earlier crafted files): 12 random sets, "Hello" and all glyphs, layout on and off, 21,650 subsets.
The 58: in 32 main's output was wrong (a glyph's program or drawing differed from the source, or fontTools could not decompile it) and the new one is right; 26 are Source Sans 3 VF fixture subsets that main wrote by keeping all 473 subroutines (the dropped fallback) and that are now pruned ("Hello": 12,368 to 2,588 bytes), both right. The 31: 24 are
CidCff1Synthetic.otfcalling a subroutine that does not exist ("calls dropped subroutine" is now "subroutine index out of range"); 7 are large Source Serif 4 subsets that now get past CFF2 and stop at the existingGPOS PairPos rewrite: an offset exceeds 64 KiB.CFF outputs against their sources. Every 4th font of that corpus (195 fonts, 5,404 subsets): fontTools decompiles every charstring, the desubroutinized programs equal the source glyphs' token for token, and fontTools and HarfBuzz draw every glyph as the source. (2 sources fontTools cannot read are skipped.)
TrueType subsets. 364 fonts (326 in
C:\Windows\Fonts, the test corpus, the repo fixtures), 8 random sets, "Hello" and all glyphs, layout on and off: 7,080 outputs byte-identical to main, 180 identical errors, no difference.Instances against main. 17 variable fonts (the issue's six plus Hahmlet, Nabla, Noto Sans KR VF .ttf, Rubik, the Source Sans 3 VF fixture, Cascadia Code and Mono, Segoe UI Variable, Bahnschrift), 3 full locations each and 2 partial for multi-axis fonts (71 runs). With
OS/2bytes 2 to 7,postbytes 4 to 7 andhead.checkSumAdjustmentmasked, everyglyffont is byte-identical to main in every table; CFF2 fonts differ only inCFF2(full instances),hmtx,headandhhea.glyfinstances take the same time; CFF2 instances take longer for the outline measurement (Noto Sans KR VF .otf, 24,731 glyphs: 0.16 to 0.25 s on main, 0.26 to 0.34 s here).Hostile inputs. 11 new crafted fonts for the walk and metrics (
mk_walk_hostile.py: a 9-level 10-way subroutine fan-out with stems and masks in CFF1 and with blended stems in CFF2, chains 10 and 11 deep each declaring a stem, a recursive subroutine, 400,000 masks in one charstring, 300 operands pushed twice, blends asking for 32,000 values and a 5,000-region subtable,vsindex300 and -5, blends taking the caller's operands, 20,000 glyphs each running a 6-level fan-out), subsets and full, user and partial instances: 26 runs, no panic, slowest 0.99 s. The 278 crafted fonts of the earlier reviews (Instancer gvar and BASE parity with HarfBuzz, and seac in the subset closure #284, Move pinned-only deltas into a partial instance's defaults #286): 717 runs, no panic, slowest 0.51 s. The reviewer's 17 fonts: 53 runs, no panic, all under 2 s but one partial instance of blendamp_4mb (7.9 s, a budget error), which takes as long on main (6.8 s): the partial bake runs out its budget of 64 tokens per byte of a 4 MB table.Fuzzing. The
subsettarget (which subsets and instances) in Docker for 600 s on d05e37c, seeded with the repo fixtures and four of the crafted and hinted fonts: 168,982 runs, no crash, peak RSS 642 MB. (Before the review fixes, on 9580c2b: 141,792 runs, no crash.)Gate on d05e37c:
cargo fmt --check,cargo +1.99.0 clippy(all features and no default features, all targets,-D warnings),cargo test --workspace --all-features(3,723 passed),cargo build --no-default-features, and MSRVcargo +1.81 check(lib, no default features; lib and bins, all features) with a fallback lockfile.Not done
wght=650,opsz=keepis 2.59 MB (CFF2 2.11 MB) against HarfBuzz's 0.96 MB (0.53 MB), and its default outlines differ from HarfBuzz's. HarfBuzz 14.5's partial CFF2 path (cff2_instancing_plan_t) rounds each folded default and each projected delta through its own VarData transforms; matching it means redesigning the partial bake, so it is left for a follow-up. This PR only gives partial CFF2 instances HarfBuzz's side bearings,headbox,hheaextremes andOS/2andpostfields.vmtxtop side bearings fromVORGand the glyph boxes, andvhea's extremes from a negative box height (so itsminBottomSideBearingandyMaxExtentcome out asadv - tsb + heightandtsb - height); itsVORGvalues differ from this crate's too. Noto Sans KR VF .otf at wght 700 differs in 24,435vmtxrows and itsVORGrecords, as on main.GPOSrepacking are unchanged.AxisLimit::Rangecannot narrow an axis or move its default (L4 instancing); it returnsUnsupported.vsindexpast the store, the walk blends no deltas (HarfBuzz); the core infers the count from the stack depth.subsetfuzz target was not extended to callinstance_user(it is outside the crate).CHANGELOG lines
Ready to paste into the 0.24.0 section:
Added:
sigilbuzz-subset:instance_userinstances a variable font at axis values in user units, given by axis tag, as HarfBuzz's instancer takes them (Subset hinted CFF2 fonts, and finish the instancer's metrics #290).AxisLimitpins an axis at a value (clamped to its range) or at its default, keeps it, or takes a HarfBuzz-style range; a range that would narrow an axis or move its default returnsSubsetError::Unsupported. Axes the input does not name are pinned at their defaults, or kept withkeep_unnamed_axes. Build the input withUserInstanceInput::new()and itswith_methods; both types are#[non_exhaustive]. Callers no longer normalize each axis withVariationAxis::normalize, and the output equalsinstanceat the same normalized coordinates.Changed:
sigilbuzz-subset: the CFF and CFF2 subsetter runs each kept glyph through its subroutine calls, as HarfBuzz's subsetter does, and fails a charstring whose subroutines nest more than 10 deep or reach themselves, that stacks more than 513 operands, or that takes more than 64 tokens per byte of the table plus 2^22, ablendcharging a token per value it leaves (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). Its memory follows the subroutines the kept glyphs reach, and the Font DICTs of a CID-keyed font that name one Private DICT share one read of it. The public per-body helpersscan_subr_calls,compute_kept_subrsandrenumber_charstringare unchanged.sigilbuzz-subset: a full CFF2 instance fails a charstring that stacks more than 513 operands, the CFF2 limit, and its bake charges a token per value eachblendleaves (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). Crafted fonts that repeat blends without regions fail the budget in under a second instead of running for minutes; real fonts spend under 2% of it.sigilbuzz-subset: a renumbered subroutine call that does not fit its push is written in its shortest form instead of failing the subset with "cannot pad operand to original width", and a CFF2 subset no longer keeps every subroutine when that happens (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). The Source Sans 3 VF fixture's "Hello" subset drops from 12,368 to 2,588 bytes.Fixed:
sigilbuzz-subset: hinted CFF2 fonts subset (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). Blended stem hints were not counted, so hint masks were misread: Source Serif 4 VF failed "Hello" and 553 of its 1,463 single-glyph subsets with "CFF unknown charstring operator". All of them now subset and draw as the source in sigilbuzz, fontTools and HarfBuzz.sigilbuzz-subset: a hint mask in a subroutine counts the stems its callers declared (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). Hinted CFF1 fonts with masks in subroutines failed or kept the wrong subroutines; across 779 CFF fonts, 2,965 of 21,650 test subsets that failed now succeed, and 32 that succeeded with wrong outlines are now right.sigilbuzz-subset:instancesetsOS/2.usWeightClass,usWidthClassandxAvgCharWidthandpost.italicAnglefrom the pinnedwght,wdthandslntand the instance's advances, as HarfBuzz's instancer does (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). They kept the source's values.sigilbuzz-subset: a full CFF2 instance rounds each blended value to a whole unit, as HarfBuzz does, and starts each charstring at its Private DICT'svsindex(Subset hinted CFF2 fonts, and finish the instancer's metrics #290). Source Serif 4 VF at wght 700 drops from 1.07 MB to 0.65 MB (itsCFF2from 614 KB to 203 KB, HarfBuzz 203 KB), and its charstrings equal HarfBuzz's.sigilbuzz-subset: a CFF2 instance, full or partial, takes its left side bearings,headbox andhheaextremes from its outlines, as HarfBuzz does (Subset hinted CFF2 fonts, and finish the instancer's metrics #290). They kept the source's: 1,375 of Source Serif 4 VF's 1,464 side bearings at wght 700.