[subset] Preserve overflowing COLR partial-instance defaults Folding a moved default into F2DOT14 can wrap alpha, scale, sweep angles, or gradient stops before retained axes have applied their deltas. Keep base fields unchanged and retain constant tuples in COLR's rebased item variation store when the default does not fit. Constant regions can also contribute deltas at default coordinates, so check referenced COLR rows before taking the pinned-at-default shortcut. Add generated fixtures and regressions for explicit and implicit maps, changed defaults, constant regions, and composed instancing. All 254 configured Meson tests pass. Assisted-by: Codex
diff --git a/src/OT/Color/COLR/COLR.hh b/src/OT/Color/COLR/COLR.hh index 794e33d..73fd58c 100644 --- a/src/OT/Color/COLR/COLR.hh +++ b/src/OT/Color/COLR/COLR.hh
@@ -331,11 +331,19 @@ DEFINE_SIZE_STATIC (6); }; +static bool colr_f2dot14_overflows (F2DOT14 value, float delta) +{ + float bits = roundf (value.to_float (delta) * 16384.f); + return bits < -32768.f || bits >= 32768.f; +} + template <typename T> struct Variable { static constexpr bool is_variable = true; + const T& get_value () const { return value; } + Variable<T>* copy (hb_serialize_context_t *c) const { TRACE_SERIALIZE (this); @@ -494,7 +502,7 @@ auto *out = c->serializer->embed (*this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->stopOffset.set_float (stopOffset.to_float(instancer (varIdxBase, 0))); float a = alpha.to_float (instancer (varIdxBase, 1)); @@ -546,6 +554,15 @@ template <template<typename> class Var> struct ColorLine { + bool default_overflows (const ItemVarStoreInstancer &instancer) const + { + for (const auto &stop : stops.iter ()) + if (colr_f2dot14_overflows (stop.get_value ().stopOffset, instancer (stop.varIdxBase, 0)) || + colr_f2dot14_overflows (stop.get_value ().alpha, instancer (stop.varIdxBase, 1))) + return true; + return false; + } + void dependv1 (hb_colrv1_depend_context_t * c) const {} void closurev1 (hb_colrv1_closure_context_t* c) const @@ -667,7 +684,7 @@ TRACE_SUBSET (this); auto *out = c->serializer->embed (*this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->xx.set_float (xx.to_float(instancer (varIdxBase, 0))); out->yx.set_float (yx.to_float(instancer (varIdxBase, 1))); @@ -750,7 +767,7 @@ auto *out = c->serializer->embed (*this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { float a = alpha.to_float (instancer (varIdxBase, 0)); out->alpha.set_float (c->plan->all_axes_pinned ? hb_clamp (a, 0.f, 1.f) : a); @@ -813,7 +830,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->x0 = x0 + (int) roundf (instancer (varIdxBase, 0)); out->y0 = y0 + (int) roundf (instancer (varIdxBase, 1)); @@ -885,7 +902,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->x0 = x0 + (int) roundf (instancer (varIdxBase, 0)); out->y0 = y0 + (int) roundf (instancer (varIdxBase, 1)); @@ -957,7 +974,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->centerX = centerX + (int) roundf (instancer (varIdxBase, 0)); out->centerY = centerY + (int) roundf (instancer (varIdxBase, 1)); @@ -1126,7 +1143,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->dx = dx + (int) roundf (instancer (varIdxBase, 0)); out->dy = dy + (int) roundf (instancer (varIdxBase, 1)); @@ -1238,7 +1255,7 @@ { TRACE_SUBSET (this); if (c->plan->all_axes_pinned && instancer && - !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { float sx = roundf (scaleX.to_float (instancer (varIdxBase, 0)) * 16384) / 16384; float sy = roundf (scaleY.to_float (instancer (varIdxBase, 1)) * 16384) / 16384; @@ -1248,7 +1265,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->scaleX.set_float (scaleX.to_float (instancer (varIdxBase, 0))); out->scaleY.set_float (scaleY.to_float (instancer (varIdxBase, 1))); @@ -1296,7 +1313,7 @@ { TRACE_SUBSET (this); if (c->plan->all_axes_pinned && instancer && - !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { float sx = roundf (scaleX.to_float (instancer (varIdxBase, 0)) * 16384) / 16384; float sy = roundf (scaleY.to_float (instancer (varIdxBase, 1)) * 16384) / 16384; @@ -1309,7 +1326,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->scaleX.set_float (scaleX.to_float (instancer (varIdxBase, 0))); out->scaleY.set_float (scaleY.to_float (instancer (varIdxBase, 1))); @@ -1363,7 +1380,7 @@ { TRACE_SUBSET (this); if (c->plan->all_axes_pinned && instancer && - !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { float s = roundf (scale.to_float (instancer (varIdxBase, 0)) * 16384) / 16384; if (s < -2 || s >= 2) @@ -1372,7 +1389,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) out->scale.set_float (scale.to_float (instancer (varIdxBase, 0))); if (format == 21 && c->plan->all_axes_pinned) @@ -1415,7 +1432,7 @@ { TRACE_SUBSET (this); if (c->plan->all_axes_pinned && instancer && - !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { float s = roundf (scale.to_float (instancer (varIdxBase, 0)) * 16384) / 16384; if (s < -2 || s >= 2) @@ -1427,7 +1444,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->scale.set_float (scale.to_float (instancer (varIdxBase, 0))); out->centerX = centerX + (int) roundf (instancer (varIdxBase, 1)); @@ -1480,7 +1497,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) out->angle.set_float (angle.to_float (instancer (varIdxBase, 0))); if (format == 25 && c->plan->all_axes_pinned) @@ -1525,7 +1542,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->angle.set_float (angle.to_float (instancer (varIdxBase, 0))); out->centerX = centerX + (int) roundf (instancer (varIdxBase, 1)); @@ -1578,7 +1595,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->xSkewAngle.set_float (xSkewAngle.to_float (instancer (varIdxBase, 0))); out->ySkewAngle.set_float (ySkewAngle.to_float (instancer (varIdxBase, 1))); @@ -1628,7 +1645,7 @@ auto *out = c->serializer->embed (this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->xSkewAngle.set_float (xSkewAngle.to_float (instancer (varIdxBase, 0))); out->ySkewAngle.set_float (ySkewAngle.to_float (instancer (varIdxBase, 1))); @@ -1784,7 +1801,7 @@ auto *out = c->serializer->embed (*this); if (unlikely (!out)) return_trace (false); - if (instancer && !c->plan->pinned_at_default && varIdxBase != VarIdx::NO_VARIATION) + if (instancer && (!c->plan->pinned_at_default || c->plan->colr_has_default_deltas) && varIdxBase != VarIdx::NO_VARIATION) { out->xMin = xMin + (int) roundf (instancer (varIdxBase, 0)); out->yMin = yMin + (int) roundf (instancer (varIdxBase, 1)); @@ -2047,6 +2064,44 @@ struct Paint { + bool default_overflows (const ItemVarStoreInstancer &instancer) const + { + switch (u.format.v) + { + case 3: + return colr_f2dot14_overflows (u.paintformat3.get_value ().alpha, instancer (u.paintformat3.varIdxBase, 0)); + case 5: + { + const auto &paint = u.paintformat5.get_value (); + return (&paint+paint.colorLine).default_overflows (instancer); + } + case 7: + { + const auto &paint = u.paintformat7.get_value (); + return (&paint+paint.colorLine).default_overflows (instancer); + } + case 9: + { + const auto &paint = u.paintformat9.get_value (); + unsigned base = u.paintformat9.varIdxBase; + return (&paint+paint.colorLine).default_overflows (instancer) || + colr_f2dot14_overflows (paint.startAngle, instancer (base, 2)) || + colr_f2dot14_overflows (paint.endAngle, instancer (base, 3)); + } + case 17: + return colr_f2dot14_overflows (u.paintformat17.get_value ().scaleX, instancer (u.paintformat17.varIdxBase, 0)) || + colr_f2dot14_overflows (u.paintformat17.get_value ().scaleY, instancer (u.paintformat17.varIdxBase, 1)); + case 19: + return colr_f2dot14_overflows (u.paintformat19.get_value ().scaleX, instancer (u.paintformat19.varIdxBase, 0)) || + colr_f2dot14_overflows (u.paintformat19.get_value ().scaleY, instancer (u.paintformat19.varIdxBase, 1)); + case 21: + return colr_f2dot14_overflows (u.paintformat21.get_value ().scale, instancer (u.paintformat21.varIdxBase, 0)); + case 23: + return colr_f2dot14_overflows (u.paintformat23.get_value ().scale, instancer (u.paintformat23.varIdxBase, 0)); + default: + return false; + } + } template <typename ...Ts> bool sanitize (hb_sanitize_context_t *c, Ts&&... ds) const @@ -2427,8 +2482,10 @@ hb_set_t *layer_indices, hb_set_t *palette_indices, hb_set_t *variation_indices, - hb_set_t *delta_set_indices) const - { colr->closure_forV1 (glyphset, layer_indices, palette_indices, variation_indices, delta_set_indices); } + hb_set_t *delta_set_indices, + hb_array_t<const int> coords = {}, + bool *preserve_defaults = nullptr) const + { colr->closure_forV1 (glyphset, layer_indices, palette_indices, variation_indices, delta_set_indices, coords, preserve_defaults); } bool has_var_store () const { return colr->has_var_store (); } @@ -2538,7 +2595,9 @@ hb_set_t *layer_indices, hb_set_t *palette_indices, hb_set_t *variation_indices, - hb_set_t *delta_set_indices) const + hb_set_t *delta_set_indices, + hb_array_t<const int> coords = {}, + bool *preserve_defaults = nullptr) const { if (version < 1) return; hb_barrier (); @@ -2563,6 +2622,17 @@ for (const ClipRecord &clip_record : cliplist.clips.iter()) clip_record.closurev1 (&c, &cliplist); + if (preserve_defaults && coords) + { + ItemVarStoreInstancer instancer (get_var_store_ptr (), get_delta_set_index_map_ptr (), coords); + for (unsigned offset : c.visited_paint) + if (StructAtOffset<Paint> (this, offset).default_overflows (instancer)) + { + *preserve_defaults = true; + break; + } + } + // if a DeltaSetIndexMap is included, collected variation indices are // actually delta set indices, we need to map them into variation indices if (has_delta_set_index_map ()) @@ -2706,7 +2776,8 @@ if (!item_vars.instantiate (var_store, c->plan, optimize, /* optimization */ optimize, /* use_no_variation_idx = false */ - c->plan->colrv1_varstore_inner_maps.as_array ())) + c->plan->colrv1_varstore_inner_maps.as_array (), + false, c->plan->colr_preserve_default_deltas)) return_trace (false); /* do not serialize varStore if there's no variation data after @@ -2855,9 +2926,9 @@ * after instancing */ if (!subset_varstore (c, colr_prime)) return_trace (false); - ItemVarStoreInstancer instancer (c->plan->normalized_coords ? get_var_store_ptr () : nullptr, + ItemVarStoreInstancer instancer (c->plan->normalized_coords && !c->plan->colr_preserve_default_deltas ? get_var_store_ptr () : nullptr, c->plan->normalized_coords ? get_delta_set_index_map_ptr () : nullptr, - c->plan->normalized_coords.as_array ()); + c->plan->colr_preserve_default_deltas ? hb_array_t<const int> () : c->plan->normalized_coords.as_array ()); if (!colr_prime->baseGlyphList.serialize_subset (c, baseGlyphList, this, instancer)) return_trace (false);
diff --git a/src/hb-ot-var-common.hh b/src/hb-ot-var-common.hh index 19ec3d4..44ac28a 100644 --- a/src/hb-ot-var-common.hh +++ b/src/hb-ot-var-common.hh
@@ -1185,7 +1185,8 @@ /* merge tuple variations with overlapping tents, if iup delta optimization * is enabled, add default deltas to contour_points */ - bool merge_tuple_variations (contour_point_vector_t* contour_points = nullptr) + bool merge_tuple_variations (contour_point_vector_t* contour_points = nullptr, + bool preserve_constants = false) { hb_vector_t<tuple_delta_t> new_vars; // The pre-allocation is essential for address stability of pointers @@ -1196,7 +1197,7 @@ for (tuple_delta_t& var : tuple_vars) { /* if all axes are pinned, drop the tuple variation */ - if (var.axis_tuples.is_empty ()) + if (var.axis_tuples.is_empty () && !preserve_constants) { /* if iup_delta_optimize is enabled, add deltas to contour coords */ if (contour_points && !contour_points->add_deltas (var.deltas_x, @@ -1326,7 +1327,8 @@ optimize_scratch_t &scratch, hb_alloc_pool_t *pool = nullptr, contour_point_vector_t* contour_points = nullptr, - bool optimize = false) + bool optimize = false, + bool preserve_constants = false) { if (!tuple_vars) return true; if (!change_tuple_variations_axis_limits (normalized_axes_location, axes_triple_distances, pool)) @@ -1343,7 +1345,7 @@ if (optimize && !contour_points) return false; - if (!merge_tuple_variations (optimize ? contour_points : nullptr)) + if (!merge_tuple_variations (optimize ? contour_points : nullptr, preserve_constants)) return false; if (optimize && !iup_optimize (*contour_points, scratch)) return false; @@ -1852,7 +1854,8 @@ bool optimize=true, bool use_no_variation_idx=true, const hb_array_t <const hb_inc_bimap_t> inner_maps = hb_array_t<const hb_inc_bimap_t> (), - bool layout_conditions = false) + bool layout_conditions = false, + bool preserve_constants = false) { if (!create_from_item_varstore (varStore, plan->axes_old_index_tag_map, inner_maps)) return false; @@ -1861,7 +1864,7 @@ for (tuple_variations_t& tuple_vars : vars) tuple_vars.cull_unreachable (plan->avar2_reachable_ranges); if (!instantiate_tuple_vars (plan->axes_location, plan->axes_triple_distances, - !layout_conditions)) + !layout_conditions, preserve_constants)) return false; if (layout_conditions && (!instantiate_condition_values (plan, varStore) || !build_region_list ())) @@ -2023,11 +2026,13 @@ bool instantiate_tuple_vars (const hb_hashmap_t<hb_tag_t, Triple>& normalized_axes_location, const hb_hashmap_t<hb_tag_t, TripleDistances>& axes_triple_distances, - bool build_regions = true) + bool build_regions = true, + bool preserve_constants = false) { optimize_scratch_t scratch; for (tuple_variations_t& tuple_vars : vars) - if (!tuple_vars.instantiate (normalized_axes_location, axes_triple_distances, scratch)) + if (!tuple_vars.instantiate (normalized_axes_location, axes_triple_distances, scratch, + nullptr, nullptr, false, preserve_constants)) return false; return !build_regions || build_region_list ();
diff --git a/src/hb-subset-plan.cc b/src/hb-subset-plan.cc index e34e13b..21f031f 100644 --- a/src/hb-subset-plan.cc +++ b/src/hb-subset-plan.cc
@@ -125,7 +125,9 @@ //closure for COLRv1 hb_set_t variation_indices, delta_set_indices; - colr.closure_forV1 (glyphs_colred, &layer_indices, &palette_indices, &variation_indices, &delta_set_indices); + colr.closure_forV1 (glyphs_colred, &layer_indices, &palette_indices, &variation_indices, &delta_set_indices, + plan->all_axes_pinned ? hb_array_t<const int> () : plan->normalized_coords.as_array (), + &plan->colr_preserve_default_deltas); colr.closure_V0palette_indices (glyphs_colred, &palette_indices); remap_indexes (&layer_indices, &plan->colrv1_layers); @@ -135,6 +137,13 @@ if (!colr.has_var_store () || !variation_indices) return; const OT::ItemVariationStore &var_store = colr.get_var_store (); + if (plan->pinned_at_default && plan->normalized_coords) + for (unsigned index : variation_indices) + if (var_store.get_delta<float> (index, plan->normalized_coords.as_array ()) != 0.f) + { + plan->colr_has_default_deltas = true; + break; + } // generated inner_maps is used by ItemVariationStore serialize(), which is subset only unsigned subtable_count = var_store.get_sub_table_count (); generate_varstore_inner_maps (variation_indices, subtable_count, plan->colrv1_varstore_inner_maps); @@ -670,6 +679,8 @@ all_axes_pinned = false; pinned_at_default = true; has_gdef_varstore = false; + colr_preserve_default_deltas = false; + colr_has_default_deltas = false; has_avar2 = false; #ifdef HB_EXPERIMENTAL_API
diff --git a/src/hb-subset-plan.hh b/src/hb-subset-plan.hh index 9b20b33..2bc6dbd 100644 --- a/src/hb-subset-plan.hh +++ b/src/hb-subset-plan.hh
@@ -160,6 +160,8 @@ bool all_axes_pinned; bool pinned_at_default; + bool colr_preserve_default_deltas; + bool colr_has_default_deltas; bool has_seac; // whether to insert a catch-all FeatureVariationRecord
diff --git a/test/api/fonts/colr-constant-bias.ttf b/test/api/fonts/colr-constant-bias.ttf new file mode 100644 index 0000000..b0ad49a --- /dev/null +++ b/test/api/fonts/colr-constant-bias.ttf Binary files differ
diff --git a/test/api/fonts/colr-partial-overflow-mapped.ttf b/test/api/fonts/colr-partial-overflow-mapped.ttf new file mode 100644 index 0000000..7b1ded1 --- /dev/null +++ b/test/api/fonts/colr-partial-overflow-mapped.ttf Binary files differ
diff --git a/test/api/fonts/colr-partial-overflow.ttf b/test/api/fonts/colr-partial-overflow.ttf new file mode 100644 index 0000000..7b1ded1 --- /dev/null +++ b/test/api/fonts/colr-partial-overflow.ttf Binary files differ
diff --git a/test/api/fonts/generate-colr-partial-overflow-fonts.py b/test/api/fonts/generate-colr-partial-overflow-fonts.py new file mode 100644 index 0000000..0a70dda --- /dev/null +++ b/test/api/fonts/generate-colr-partial-overflow-fonts.py
@@ -0,0 +1,76 @@ +"""Generate COLRv1 default biases that cannot be stored in compact fields.""" + +from pathlib import Path +from fontTools.fontBuilder import FontBuilder +from fontTools.colorLib.builder import buildCOLR, buildCPAL +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.varLib.builder import ( + buildVarRegionList, + buildVarData, + buildVarStore, + buildDeltaSetIndexMap, +) + +fb = FontBuilder(1000, isTTF=True) +names = [".notdef", "a", "b", "c"] +fb.setupGlyphOrder(names) +fb.setupCharacterMap({97: "a", 98: "b", 99: "c"}) +fb.setupGlyf({name: TTGlyphPen(None).glyph() for name in names}) +fb.setupHorizontalMetrics({name: (500, 0) for name in names}) +fb.setupHorizontalHeader(ascent=800, descent=-200) +fb.setupNameTable({"familyName": "COLR Partial Overflow", "styleName": "Regular"}) +fb.setupOS2(sTypoAscender=800, sTypoDescender=-200, usWinAscent=800, usWinDescent=200) +fb.setupPost() +fb.setupFvar([("wght", 0, 0, 1, "Weight"), ("wdth", 0, 0, 1, "Width")], []) +regions = buildVarRegionList( + [{"wght": (0, 1, 1)}, {"wdth": (0, 1, 1)}], ["wght", "wdth"] +) +store = buildVarStore( + regions, [buildVarData([0, 1], [[32768, -49152], [0, 0], [49152, -49152], [0, 0]])] +) +solid = dict(Format=2, PaletteIndex=0, Alpha=1) +paints = { + "a": dict(Format=3, PaletteIndex=0, Alpha=1, VarIndexBase=0), + "b": dict(Format=17, Paint=solid, scaleX=1.5, scaleY=1, VarIndexBase=0), + "c": dict( + Format=5, + ColorLine=dict( + Extend="pad", + ColorStop=[ + dict(StopOffset=0, PaletteIndex=0, Alpha=1, VarIndexBase=2), + dict(StopOffset=1, PaletteIndex=0, Alpha=1, VarIndexBase=0xFFFFFFFF), + ], + ), + x0=0, + y0=0, + x1=400, + y1=0, + x2=0, + y2=700, + VarIndexBase=0xFFFFFFFF, + ), +} +fb.font["COLR"] = buildCOLR( + paints, + version=1, + glyphMap=fb.font.getReverseGlyphMap(), + varStore=store, + clipBoxes={name: (0, 0, 400, 700) for name in paints}, +) +fb.font["CPAL"] = buildCPAL([[(1, 0, 0, 1)]]) +fb.font.recalcTimestamp = False +fb.font["head"].created = fb.font["head"].modified = 2082844800 +root = Path(__file__).parent +fb.save(root / "colr-partial-overflow.ttf") +fb.font["COLR"].table.VarIndexMap = buildDeltaSetIndexMap(list(range(4))) +fb.save(root / "colr-partial-overflow-mapped.ttf") +regions = buildVarRegionList([{}], ["wght", "wdth"]) +store = buildVarStore(regions, [buildVarData([0], [[32768]])]) +fb.font["COLR"] = buildCOLR( + {"a": dict(Format=3, PaletteIndex=0, Alpha=0, VarIndexBase=0)}, + version=1, + glyphMap=fb.font.getReverseGlyphMap(), + varStore=store, + clipBoxes={"a": (0, 0, 400, 700)}, +) +fb.save(root / "colr-constant-bias.ttf")
diff --git a/test/api/test-subset-colr.c b/test/api/test-subset-colr.c index 692f204..9e07096 100644 --- a/test/api/test-subset-colr.c +++ b/test/api/test-subset-colr.c
@@ -186,6 +186,86 @@ hb_face_destroy (face); } +static hb_face_t * +instance_colr (hb_face_t *face, float weight, float width, hb_bool_t pin_weight, hb_bool_t pin_width) +{ + hb_subset_input_t *input = hb_subset_input_create_or_fail (); + hb_set_add_range (hb_subset_input_glyph_set (input), 0, 3); + if (pin_weight) + g_assert_true (hb_subset_input_pin_axis_location (input, face, HB_TAG ('w','g','h','t'), weight)); + if (pin_width) + g_assert_true (hb_subset_input_pin_axis_location (input, face, HB_TAG ('w','d','t','h'), width)); + return hb_subset_test_create_subset (face, input); +} + +static void +test_subset_colr_partial_overflow (void) +{ + const char *files[] = {"fonts/colr-partial-overflow.ttf", "fonts/colr-partial-overflow-mapped.ttf"}; + for (unsigned file = 0; file < G_N_ELEMENTS (files); file++) + { + hb_face_t *face = hb_test_open_font_file (files[file]); + hb_face_t *partial = instance_colr (face, 1, 0, true, false); + /* Retained deltas bring alpha, scale, and stop offsets back into range. */ + const float widths[] = {0.5f, 0.75f, 1.f}; + for (unsigned i = 0; i < G_N_ELEMENTS (widths); i++) + { + hb_face_t *direct = instance_colr (face, 1, widths[i], true, true); + hb_face_t *composed = instance_colr (partial, 0, widths[i], false, true); + hb_subset_test_check (direct, composed, HB_TAG ('C','O','L','R')); + hb_face_destroy (composed); + hb_face_destroy (direct); + } + hb_face_destroy (partial); + + hb_subset_input_t *input = hb_subset_input_create_or_fail (); + hb_set_add_range (hb_subset_input_glyph_set (input), 0, 3); + g_assert_true (hb_subset_input_set_axis_range (input, face, HB_TAG ('w','g','h','t'), 0, 1, 0.5f)); + partial = hb_subset_test_create_subset (face, input); + for (unsigned i = 0; i < G_N_ELEMENTS (widths); i++) + { + float weight = i * 0.5f; + hb_face_t *direct = instance_colr (face, weight, widths[i], true, true); + hb_face_t *composed = instance_colr (partial, weight, widths[i], true, true); + hb_subset_test_check (direct, composed, HB_TAG ('C','O','L','R')); + hb_face_destroy (composed); + hb_face_destroy (direct); + } + hb_face_destroy (partial); + hb_face_destroy (face); + } +} + +static void +test_subset_colr_constant_default (void) +{ + hb_face_t *face = hb_test_open_font_file ("fonts/colr-constant-bias.ttf"); + for (unsigned i = 0; i < 2; i++) + { + hb_face_t *subset = instance_colr (face, i, 0, true, true); + hb_blob_t *blob = hb_face_reference_table (subset, HB_TAG ('C','O','L','R')); + unsigned length; + const unsigned char *data = (const unsigned char *) hb_blob_get_data (blob, &length); + g_assert_cmpuint (length, >=, 34); + unsigned list = read_uint_be (data + 14, 4); + g_assert_cmpuint (list + 10, <=, length); + unsigned paint = list + read_uint_be (data + list + 6, 4); + g_assert_cmpuint (paint + 5, <=, length); + g_assert_cmpuint (data[paint], ==, 2); /* PaintSolid */ + g_assert_cmpuint (read_uint_be (data + paint + 3, 2), ==, 16384); + hb_blob_destroy (blob); + hb_face_destroy (subset); + } + hb_face_t *partial = instance_colr (face, 0, 0, true, false); + hb_face_t *direct = instance_colr (face, 0, 0, true, true); + hb_face_t *composed = instance_colr (partial, 0, 0, false, true); + hb_subset_test_check (direct, composed, HB_TAG ('C','O','L','R')); + hb_face_destroy (composed); + hb_face_destroy (direct); + hb_face_destroy (partial); + hb_face_destroy (face); +} + int main (int argc, char **argv) { @@ -196,6 +276,8 @@ hb_test_add (test_subset_colr_keep_mixed_glyph); hb_test_add (test_subset_colr_keep_no_colr_glyph); hb_test_add (test_subset_colr_wide_scales); + hb_test_add (test_subset_colr_partial_overflow); + hb_test_add (test_subset_colr_constant_default); return hb_test_run(); }