[subset/cff2] Compute per-VarData blend transforms for partial instancing Port of fontTools' instantiateCFF2 (fonttools/fonttools#3506) design, with a simplification: the instancing solver acts on regions independently of per-blend delta values, so instead of lifting every blend's deltas into the store and reading them back, instance each VarData once with identity delta rows. The surviving tuples then form a fixed linear map from old per-region deltas to new per-region deltas; the fold-into-default part is the old store's region scalars at the new default location. Dormant so far: computed for partial-instancing requests but not yet consumed; blends are still (wrongly) fully flattened.
diff --git a/src/hb-subset-cff2.cc b/src/hb-subset-cff2.cc index 9514a45..1fe32f0 100644 --- a/src/hb-subset-cff2.cc +++ b/src/hb-subset-cff2.cc
@@ -30,6 +30,7 @@ #include "hb-open-type.hh" #include "hb-ot-cff2-table.hh" +#include "hb-ot-var-common.hh" #include "hb-set.h" #include "hb-subset-plan.hh" #include "hb-subset-cff-common.hh" @@ -489,6 +490,177 @@ }; +/* + * Partial instancing (some axes pinned or limited, others kept). + * + * This is a port of fontTools' instantiateCFF2 (fonttools/fonttools#3506), + * with one simplification: the variation-store instancing solver acts on + * regions independently of the per-blend delta values, so instead of lifting + * every blend's deltas into the store, instancing it, and reading the rows + * back, we instance each VarData once with identity delta rows. The + * instantiated tuples then give, per VarData, a fixed linear map from old + * per-region deltas to new per-region deltas. The contribution that folds + * into each blend's default value is the old store's region scalars + * evaluated at the new default location (plan->normalized_coords); the + * surviving rebased regions all evaluate to zero there. + */ +struct cff2_instancing_plan_t +{ + struct vardata_transform_t + { + /* new_deltas[j] = Σ_i matrix[j][i] * old_deltas[i] */ + hb_vector_t<hb_vector_t<float>> matrix; + /* Default-value fold-in scalars, one per old region: the old store's + * region scalars at the new default location. */ + hb_vector_t<float> gains; + /* Index into the new global region list, per new region. */ + hb_vector_t<unsigned> region_indices; + unsigned new_major = 0; + bool alive () const { return matrix.length; } + }; + + hb_vector_t<vardata_transform_t> transforms; /* per old VarData (vsindex) */ + hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>> new_regions; /* global region list */ + unsigned new_major_count = 0; + + bool has_variations () const { return new_major_count; } + + const vardata_transform_t *get_transform (unsigned major) const + { return major < transforms.length ? &transforms.arrayZ[major] : nullptr; } + + /* The new vsindex a private dict's (or charstring's) old ivs maps to; + * dead VarDatas fall back to 0, their blends having become static. */ + unsigned remap_ivs (unsigned old_ivs) const + { + const vardata_transform_t *t = get_transform (old_ivs); + return (t && t->alive ()) ? t->new_major : 0; + } + + double fold_deltas (unsigned old_ivs, hb_array_t<const number_t> deltas) const + { + const vardata_transform_t *t = get_transform (old_ivs); + if (unlikely (!t || t->gains.length != deltas.length)) return 0.; + double v = 0.; + for (unsigned i = 0; i < deltas.length; i++) + v += (double) t->gains.arrayZ[i] * deltas.arrayZ[i].to_real (); + return v; + } + + bool create (const CFF2ItemVariationStore *cff2VarStore, + const hb_subset_plan_t *plan) + { + const OT::ItemVariationStore &varStore = cff2VarStore->varStore; + unsigned major_count = varStore.get_sub_table_count (); + if (!transforms.resize (major_count)) return false; + + hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>> orig_regions; + if (!varStore.get_region_list ().get_var_regions (plan->axes_old_index_tag_map, orig_regions)) + return false; + + /* Instance each VarData with identity delta rows; the surviving tuples' + * delta columns are the old-region → new-region transform. */ + hb_vector_t<OT::tuple_variations_t> tuple_vars_list; + if (!tuple_vars_list.resize (major_count)) return false; + OT::optimize_scratch_t scratch; + for (unsigned m = 0; m < major_count; m++) + { + const OT::VarData &var_data = varStore.get_sub_table (m); + unsigned k = var_data.get_region_index_count (); + vardata_transform_t &t = transforms.arrayZ[m]; + + if (!t.gains.resize_exact (k)) return false; + varStore.get_region_scalars (m, plan->normalized_coords.arrayZ, + plan->normalized_coords.length, + t.gains.arrayZ, k); + + OT::tuple_variations_t &tuples = tuple_vars_list.arrayZ[m]; + for (unsigned r = 0; r < k; r++) + { + OT::tuple_delta_t tuple; + if (!tuple.deltas_x.resize (k) || + !tuple.indices.resize (k)) + return false; + for (unsigned i = 0; i < k; i++) + tuple.indices.arrayZ[i] = true; + tuple.deltas_x.arrayZ[r] = 1.f; + unsigned region_index = var_data.get_region_index (r); + if (unlikely (region_index >= orig_regions.length)) return false; + tuple.axis_tuples = orig_regions.arrayZ[region_index]; + if (unlikely (tuple.axis_tuples.in_error ())) return false; + tuples.tuple_vars.push (std::move (tuple)); + } + if (unlikely (tuples.tuple_vars.in_error ())) return false; + + if (!tuples.instantiate (plan->axes_location, plan->axes_triple_distances, scratch)) + return false; + } + + /* Build the new global region list: surviving original regions first, + * in their original order, then solver-split new regions in order of + * appearance (fontTools' rebuildRegions). Keys hash by content, so + * pointers into different owners compare fine. */ + hb_hashmap_t<const hb_hashmap_t<hb_tag_t, Triple>*, unsigned> region_idx_map; + hb_vector_t<const hb_hashmap_t<hb_tag_t, Triple>*> region_ptrs; + for (const auto &tuples : tuple_vars_list) + for (const auto &tuple : tuples.tuple_vars) + if (!region_idx_map.has (&tuple.axis_tuples)) + if (unlikely (!region_idx_map.set (&tuple.axis_tuples, (unsigned) -1))) + return false; + for (const auto &r : orig_regions) + { + unsigned *idx; + if (region_idx_map.has (&r, &idx) && *idx == (unsigned) -1) + { + if (unlikely (!region_idx_map.set (&r, region_ptrs.length))) return false; + region_ptrs.push (&r); + } + } + for (const auto &tuples : tuple_vars_list) + for (const auto &tuple : tuples.tuple_vars) + { + unsigned *idx; + if (region_idx_map.has (&tuple.axis_tuples, &idx) && *idx == (unsigned) -1) + { + if (unlikely (!region_idx_map.set (&tuple.axis_tuples, region_ptrs.length))) return false; + region_ptrs.push (&tuple.axis_tuples); + } + } + if (unlikely (region_ptrs.in_error ())) return false; + + /* Fill in per-VarData transforms and assign new vsindexes. */ + for (unsigned m = 0; m < major_count; m++) + { + vardata_transform_t &t = transforms.arrayZ[m]; + const auto &tuples = tuple_vars_list.arrayZ[m]; + unsigned k = t.gains.length; + for (const auto &tuple : tuples.tuple_vars) + { + if (unlikely (tuple.deltas_x.length != k)) return false; + unsigned *idx; + if (unlikely (!region_idx_map.has (&tuple.axis_tuples, &idx))) return false; + t.region_indices.push (*idx); + hb_vector_t<float> col; + if (unlikely (!col.resize_exact (k))) return false; + for (unsigned i = 0; i < k; i++) + col.arrayZ[i] = tuple.deltas_x.arrayZ[i]; + t.matrix.push (std::move (col)); + } + if (unlikely (t.matrix.in_error () || t.region_indices.in_error ())) return false; + if (t.alive ()) + t.new_major = new_major_count++; + } + + if (!new_regions.resize (region_ptrs.length)) return false; + for (unsigned i = 0; i < region_ptrs.length; i++) + { + new_regions.arrayZ[i] = *region_ptrs.arrayZ[i]; + if (unlikely (new_regions.arrayZ[i].in_error ())) return false; + } + + return true; + } +}; + namespace OT { struct cff2_subset_plan { @@ -510,6 +682,14 @@ desubroutinize = plan->flags & HB_SUBSET_FLAGS_DESUBROUTINIZE || pinned; // For instancing we need this path + /* Partial instancing: some axes pinned or limited, others kept. */ + partial_instancing = plan->normalized_coords && !plan->all_axes_pinned && + acc.varStore != &Null (CFF2ItemVariationStore) && + acc.varStore->varStore.get_sub_table_count (); + if (partial_instancing && + unlikely (!instancing.create (acc.varStore, plan))) + return false; + /* Enable command capture for CFF2→CFF1 conversion (for specialization) */ capture_commands = pinned; @@ -604,6 +784,9 @@ bool drop_hints = false; bool desubroutinize = false; + bool partial_instancing = false; + cff2_instancing_plan_t instancing; + unsigned min_charstrings_off_size = 0; hb_array_t<int> normalized_coords; // For instantiation