1209 lines
43 KiB
C++
1209 lines
43 KiB
C++
/*
|
|
* Copyright © 2017 Google, Inc.
|
|
*
|
|
* This is part of HarfBuzz, a text shaping library.
|
|
*
|
|
* Permission is hereby granted, without written agreement and without
|
|
* license or royalty fees, to use, copy, modify, and distribute this
|
|
* software and its documentation for any purpose, provided that the
|
|
* above copyright notice and the following two paragraphs appear in
|
|
* all copies of this software.
|
|
*
|
|
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
|
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
|
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
|
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
* DAMAGE.
|
|
*
|
|
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
|
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
|
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
|
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
*
|
|
* Google Author(s): Behdad Esfahbod
|
|
*/
|
|
|
|
#ifndef HB_OT_VAR_AVAR_TABLE_HH
|
|
#define HB_OT_VAR_AVAR_TABLE_HH
|
|
|
|
#include "hb-open-type.hh"
|
|
#include "hb-ot-var-common.hh"
|
|
#include "hb-ot-var-fvar-table.hh"
|
|
|
|
|
|
/*
|
|
* avar -- Axis Variations
|
|
* https://docs.microsoft.com/en-us/typography/opentype/spec/avar
|
|
*/
|
|
|
|
#define HB_OT_TAG_avar HB_TAG('a','v','a','r')
|
|
|
|
|
|
namespace OT {
|
|
|
|
|
|
/* "Spec": https://github.com/be-fonts/boring-expansion-spec/issues/14 */
|
|
struct avarV2Tail
|
|
{
|
|
friend struct avar;
|
|
|
|
bool sanitize (hb_sanitize_context_t *c,
|
|
const void *base) const
|
|
{
|
|
TRACE_SANITIZE (this);
|
|
return_trace (varIdxMap.sanitize (c, base) &&
|
|
varStore.sanitize (c, base));
|
|
}
|
|
|
|
protected:
|
|
Offset32To<DeltaSetIndexMap> varIdxMap; /* Offset from the beginning of 'avar' table. */
|
|
Offset32To<ItemVariationStore> varStore; /* Offset from the beginning of 'avar' table. */
|
|
|
|
public:
|
|
DEFINE_SIZE_STATIC (8);
|
|
};
|
|
|
|
|
|
struct AxisValueMap
|
|
{
|
|
bool sanitize (hb_sanitize_context_t *c) const
|
|
{
|
|
TRACE_SANITIZE (this);
|
|
return_trace (c->check_struct (this));
|
|
}
|
|
|
|
void set_mapping (float from_coord, float to_coord)
|
|
{
|
|
coords[0].set_float (from_coord);
|
|
coords[1].set_float (to_coord);
|
|
}
|
|
|
|
bool is_outside_axis_range (const Triple& axis_range) const
|
|
{
|
|
double from_coord = (double) coords[0].to_float ();
|
|
return !axis_range.contains (from_coord);
|
|
}
|
|
|
|
bool must_include () const
|
|
{
|
|
float from_coord = coords[0].to_float ();
|
|
float to_coord = coords[1].to_float ();
|
|
return (from_coord == -1.f && to_coord == -1.f) ||
|
|
(from_coord == 0.f && to_coord == 0.f) ||
|
|
(from_coord == 1.f && to_coord == 1.f);
|
|
}
|
|
|
|
void instantiate (const Triple& axis_range,
|
|
const Triple& unmapped_range,
|
|
const TripleDistances& triple_distances)
|
|
{
|
|
float from_coord = coords[0].to_float ();
|
|
float to_coord = coords[1].to_float ();
|
|
|
|
from_coord = renormalizeValue ((double) from_coord, unmapped_range, triple_distances);
|
|
to_coord = renormalizeValue ((double) to_coord, axis_range, triple_distances);
|
|
|
|
coords[0].set_float (from_coord);
|
|
coords[1].set_float (to_coord);
|
|
}
|
|
|
|
HB_INTERNAL static int cmp (const void *pa, const void *pb)
|
|
{
|
|
const AxisValueMap *a = (const AxisValueMap *) pa;
|
|
const AxisValueMap *b = (const AxisValueMap *) pb;
|
|
|
|
int a_from = a->coords[0].to_int ();
|
|
int b_from = b->coords[0].to_int ();
|
|
if (a_from != b_from)
|
|
return a_from - b_from;
|
|
|
|
/* this should never be reached. according to the spec, all of the axis
|
|
* value map records for a given axis must have different fromCoord values
|
|
* */
|
|
int a_to = a->coords[1].to_int ();
|
|
int b_to = b->coords[1].to_int ();
|
|
return a_to - b_to;
|
|
}
|
|
|
|
bool serialize (hb_serialize_context_t *c) const
|
|
{
|
|
TRACE_SERIALIZE (this);
|
|
return_trace (c->embed (this));
|
|
}
|
|
|
|
public:
|
|
F2DOT14 coords[2];
|
|
// F2DOT14 fromCoord; /* A normalized coordinate value obtained using
|
|
// * default normalization. */
|
|
// F2DOT14 toCoord; /* The modified, normalized coordinate value. */
|
|
|
|
public:
|
|
DEFINE_SIZE_STATIC (4);
|
|
};
|
|
|
|
struct SegmentMaps : Array16Of<AxisValueMap>
|
|
{
|
|
float map_float (float value, unsigned int from_offset = 0, unsigned int to_offset = 1) const
|
|
{
|
|
#define fromCoord coords[from_offset].to_float ()
|
|
#define toCoord coords[to_offset].to_float ()
|
|
|
|
const auto *map = arrayZ;
|
|
|
|
/* The following special-cases are not part of OpenType, which requires
|
|
* that at least -1, 0, and +1 must be mapped. But we include these as
|
|
* part of a better error recovery scheme. */
|
|
if (len < 2)
|
|
{
|
|
if (!len)
|
|
return value;
|
|
else /* len == 1*/
|
|
return value - map[0].fromCoord + map[0].toCoord;
|
|
}
|
|
|
|
// At least two mappings now.
|
|
|
|
/* CoreText is wild...
|
|
* PingFangUI avar needs all this special-casing...
|
|
* So we implement an extended version of the spec here,
|
|
* which is more robust and more likely to be compatible with
|
|
* the wild. */
|
|
|
|
unsigned start = 0;
|
|
unsigned end = len;
|
|
if (map[start].fromCoord == -1 && map[start].toCoord == -1 && map[start+1].fromCoord == -1)
|
|
start++;
|
|
if (map[end-1].fromCoord == +1 && map[end-1].toCoord == +1 && map[end-2].fromCoord == +1)
|
|
end--;
|
|
|
|
/* Look for exact match first, and do lots of special-casing. */
|
|
unsigned i;
|
|
for (i = start; i < end; i++)
|
|
if (value == map[i].fromCoord)
|
|
break;
|
|
if (i < end)
|
|
{
|
|
// There's at least one exact match. See if there are more.
|
|
unsigned j = i;
|
|
for (; j + 1 < end; j++)
|
|
if (value != map[j + 1].fromCoord)
|
|
break;
|
|
|
|
// [i,j] inclusive are all exact matches:
|
|
|
|
// If there's only one, return it. This is the only spec-compliant case.
|
|
if (i == j)
|
|
return map[i].toCoord;
|
|
// If there's exactly three, return the middle one.
|
|
if (i + 2 == j)
|
|
return map[i + 1].toCoord;
|
|
|
|
// Ignore the middle ones. Return the one mapping closer to 0.
|
|
if (value < 0) return map[j].toCoord;
|
|
if (value > 0) return map[i].toCoord;
|
|
|
|
// Mapping 0? CoreText seems confused. It seems to prefer 0 here...
|
|
// So we'll just return the smallest one. lol
|
|
return fabsf (map[i].toCoord) < fabsf (map[j].toCoord) ? map[i].toCoord : map[j].toCoord;
|
|
|
|
// Mapping 0? Return one not mapping to 0.
|
|
if (map[i].toCoord == 0)
|
|
return map[j].toCoord;
|
|
else
|
|
return map[i].toCoord;
|
|
}
|
|
|
|
/* There's at least two and we're not an exact match. Prepare to lerp. */
|
|
|
|
// Find the segment we're in.
|
|
for (i = start; i < end; i++)
|
|
if (value < map[i].fromCoord)
|
|
break;
|
|
|
|
if (i == start)
|
|
{
|
|
// Value before all segments; Shift.
|
|
return value - map[start].fromCoord + map[start].toCoord;
|
|
}
|
|
if (i == end)
|
|
{
|
|
// Value after all segments; Shift.
|
|
return value - map[end - 1].fromCoord + map[end - 1].toCoord;
|
|
}
|
|
|
|
// Actually interpolate.
|
|
auto &before = map[i-1];
|
|
auto &after = map[i];
|
|
float denom = after.fromCoord - before.fromCoord; // Can't be zero by now.
|
|
return before.toCoord + ((after.toCoord - before.toCoord) * (value - before.fromCoord)) / denom;
|
|
|
|
#undef toCoord
|
|
#undef fromCoord
|
|
}
|
|
|
|
float unmap_float (float value) const { return map_float (value, 1, 0); }
|
|
|
|
|
|
// TODO Kill this.
|
|
Triple unmap_axis_range (const Triple& axis_range) const
|
|
{
|
|
float unmapped_min = unmap_float (axis_range.minimum);
|
|
float unmapped_middle = unmap_float (axis_range.middle);
|
|
float unmapped_max = unmap_float (axis_range.maximum);
|
|
|
|
return Triple{(double) unmapped_min, (double) unmapped_middle, (double) unmapped_max};
|
|
}
|
|
|
|
bool subset (hb_subset_context_t *c, hb_tag_t axis_tag,
|
|
hb_vector_t<AxisValueMap> *out_mappings = nullptr) const
|
|
{
|
|
TRACE_SUBSET (this);
|
|
|
|
/* This function cannot work on avar2 table (and currently doesn't).
|
|
* We should instead keep the design coords in the shape plan and use
|
|
* those. unmap_axis_range needs to be killed. */
|
|
|
|
/* avar mapped normalized axis range. Under avar2, axes_location holds
|
|
* only the self-contained pins for the other tables; avar itself uses
|
|
* the intermediate-space ranges of all restricted axes. */
|
|
const auto &axes_location = c->plan->has_avar2
|
|
? c->plan->old_intermediates
|
|
: c->plan->axes_location;
|
|
Triple *axis_range;
|
|
if (!axes_location.has (axis_tag, &axis_range))
|
|
return c->serializer->embed (*this);
|
|
|
|
TripleDistances *axis_triple_distances;
|
|
if (!c->plan->axes_triple_distances.has (axis_tag, &axis_triple_distances))
|
|
return_trace (false);
|
|
|
|
auto *out = c->serializer->start_embed (this);
|
|
if (unlikely (!c->serializer->extend_min (out))) return_trace (false);
|
|
|
|
Triple unmapped_range = unmap_axis_range (*axis_range);
|
|
|
|
/* create a vector of retained mappings and sort */
|
|
hb_vector_t<AxisValueMap> value_mappings;
|
|
for (const auto& _ : as_array ())
|
|
{
|
|
if (_.is_outside_axis_range (unmapped_range))
|
|
continue;
|
|
AxisValueMap mapping;
|
|
mapping = _;
|
|
mapping.instantiate (*axis_range, unmapped_range, *axis_triple_distances);
|
|
/* (-1, -1), (0, 0), (1, 1) mappings will be added later, so avoid
|
|
* duplicates here */
|
|
if (mapping.must_include ())
|
|
continue;
|
|
value_mappings.push (mapping);
|
|
}
|
|
|
|
AxisValueMap m;
|
|
m.set_mapping (-1.f, -1.f);
|
|
value_mappings.push (m);
|
|
|
|
m.set_mapping (0.f, 0.f);
|
|
value_mappings.push (m);
|
|
|
|
m.set_mapping (1.f, 1.f);
|
|
value_mappings.push (m);
|
|
|
|
value_mappings.qsort ();
|
|
|
|
if (unlikely (value_mappings.in_error ()))
|
|
return_trace (false);
|
|
|
|
for (const auto& _ : value_mappings)
|
|
{
|
|
if (!_.serialize (c->serializer))
|
|
return_trace (false);
|
|
}
|
|
if (!c->serializer->check_assign (out->len, value_mappings.length, HB_SERIALIZE_ERROR_INT_OVERFLOW))
|
|
return_trace (false);
|
|
/* Hand the instantiated mappings to the caller; avar2 offset
|
|
* compensation needs them to locate the new mapping's kinks. */
|
|
if (out_mappings)
|
|
*out_mappings = std::move (value_mappings);
|
|
return_trace (true);
|
|
}
|
|
|
|
public:
|
|
DEFINE_SIZE_ARRAY (2, *this);
|
|
};
|
|
|
|
/* One knot of the avar2 offset-compensation function
|
|
* offset(z) = inv_renorm(z) - z, in new intermediate space. */
|
|
struct avar2_offset_knot_t
|
|
{
|
|
double z;
|
|
double offset;
|
|
};
|
|
|
|
/* Insert a knot keeping the vector sorted by z; first insertion wins on
|
|
* duplicate z. */
|
|
static inline void
|
|
_avar2_add_knot (hb_vector_t<avar2_offset_knot_t> &knots, double z, double offset)
|
|
{
|
|
unsigned i = 0;
|
|
while (i < knots.length && knots.arrayZ[i].z < z) i++;
|
|
if (i < knots.length && knots.arrayZ[i].z == z) return;
|
|
knots.push (avar2_offset_knot_t {0.0, 0.0});
|
|
if (unlikely (knots.in_error ())) return;
|
|
for (unsigned j = knots.length - 1; j > i; j--)
|
|
knots.arrayZ[j] = knots.arrayZ[j - 1];
|
|
knots.arrayZ[i] = avar2_offset_knot_t {z, offset};
|
|
}
|
|
|
|
/* Piecewise-linear evaluation over the sorted knots. Inputs are always
|
|
* within [-1, +1] and anchor knots at -1/0/+1 always exist. */
|
|
static inline double
|
|
_avar2_eval_offset (const hb_vector_t<avar2_offset_knot_t> &knots, double z)
|
|
{
|
|
unsigned len = knots.length;
|
|
for (unsigned i = 0; i < len; i++)
|
|
{
|
|
if (z == knots.arrayZ[i].z) return knots.arrayZ[i].offset;
|
|
if (z < knots.arrayZ[i].z)
|
|
{
|
|
if (!i) return knots.arrayZ[0].offset;
|
|
const auto &before = knots.arrayZ[i - 1];
|
|
const auto &after = knots.arrayZ[i];
|
|
double denom = after.z - before.z;
|
|
return before.offset + (after.offset - before.offset) * (z - before.z) / denom;
|
|
}
|
|
}
|
|
return len ? knots.arrayZ[len - 1].offset : 0.0;
|
|
}
|
|
|
|
/* Piecewise-linear evaluation over instantiated avar v1 mappings, as
|
|
* produced by SegmentMaps::subset (sorted, with -1/0/+1 anchors).
|
|
* Matches SegmentMaps::map_float for such well-formed mappings. */
|
|
static inline double
|
|
_avar2_map_new_mapping (const hb_vector_t<AxisValueMap> &mappings, double v,
|
|
unsigned from_offset = 0, unsigned to_offset = 1)
|
|
{
|
|
unsigned len = mappings.length;
|
|
if (!len) return v;
|
|
|
|
for (unsigned i = 0; i < len; i++)
|
|
{
|
|
double from = (double) mappings.arrayZ[i].coords[from_offset].to_float ();
|
|
if (v == from)
|
|
return (double) mappings.arrayZ[i].coords[to_offset].to_float ();
|
|
if (v < from)
|
|
{
|
|
double to = (double) mappings.arrayZ[i].coords[to_offset].to_float ();
|
|
if (!i) return v - from + to;
|
|
double prev_from = (double) mappings.arrayZ[i - 1].coords[from_offset].to_float ();
|
|
double prev_to = (double) mappings.arrayZ[i - 1].coords[to_offset].to_float ();
|
|
double denom = from - prev_from;
|
|
if (denom == 0.0) return prev_to;
|
|
return prev_to + (to - prev_to) * (v - prev_from) / denom;
|
|
}
|
|
}
|
|
return v - (double) mappings.arrayZ[len - 1].coords[from_offset].to_float ()
|
|
+ (double) mappings.arrayZ[len - 1].coords[to_offset].to_float ();
|
|
}
|
|
|
|
/* Inverse of _avar2_map_new_mapping: pull an output coordinate back to a
|
|
* preimage input coordinate. For a non-strictly-monotone mapping this picks
|
|
* one preimage, which is fine for its only use (augmenting the error
|
|
* estimator's sample set). */
|
|
static inline double
|
|
_avar2_unmap_new_mapping (const hb_vector_t<AxisValueMap> &mappings, double v)
|
|
{
|
|
return _avar2_map_new_mapping (mappings, v, 1, 0);
|
|
}
|
|
|
|
/* Plain (non-avar) fvar-style normalization of a user value against a
|
|
* user-space (min, default, max) triple. */
|
|
static inline double
|
|
_avar2_normalize_value (double v, double min, double def, double max)
|
|
{
|
|
v = hb_clamp (v, min, max);
|
|
if (v == def) return 0.0;
|
|
if (v < def) return def == min ? 0.0 : (v - def) / (def - min);
|
|
return def == max ? 0.0 : (v - def) / (max - def);
|
|
}
|
|
|
|
/* Inverse of the above: map a normalized value back to user space. */
|
|
static inline double
|
|
_avar2_denormalize_value (double v, double min, double def, double max)
|
|
{
|
|
if (v == 0.0) return def;
|
|
return v < 0.0 ? def + v * (def - min) : def + v * (max - def);
|
|
}
|
|
|
|
/* Estimate the residual offset-compensation error for one restricted axis:
|
|
* the max |old-avar1-final - (new-avar1 + offset)| over the retained user
|
|
* range, in F2Dot14 units. offset(z) is the piecewise-linear function
|
|
* through the knots. The residual is dominated by F2Dot14 requantization of
|
|
* a steep retained avar v1 segment (e.g. a moved default compressing part
|
|
* of the axis into a narrow z band); offset compensation cannot remove it.
|
|
* Used only to pick the better knot set and decide whether to warn.
|
|
*
|
|
* Sampled on a uniform grid augmented with the user-space preimages of
|
|
* every kink of the residual (old/new avar v1 breakpoints and offset(z)
|
|
* knots), so the worst kink cannot fall between uniform samples. The
|
|
* pointwise F2Dot14 rounding makes this an estimate rather than an exact
|
|
* bound, but every piecewise-linear extremum is visited. */
|
|
static inline unsigned
|
|
_avar2_estimate_offset_error (const SegmentMaps &old_seg,
|
|
const hb_vector_t<AxisValueMap> &new_mapping,
|
|
double old_min, double old_def, double old_max,
|
|
double new_min, double new_def, double new_max,
|
|
const hb_vector_t<avar2_offset_knot_t> &knots)
|
|
{
|
|
constexpr unsigned samples = 257;
|
|
hb_vector_t<double> us;
|
|
if (unlikely (!us.alloc (samples + old_seg.as_array ().length +
|
|
new_mapping.length + knots.length)))
|
|
return UINT_MAX; /* estimator only; fail towards "worse" */
|
|
for (unsigned i = 0; i < samples; i++)
|
|
us.push (new_min + (new_max - new_min) * i / (samples - 1));
|
|
for (const auto &_ : old_seg.as_array ())
|
|
us.push (_avar2_denormalize_value ((double) _.coords[0].to_float (),
|
|
old_min, old_def, old_max));
|
|
for (const auto &_ : new_mapping)
|
|
us.push (_avar2_denormalize_value ((double) _.coords[0].to_float (),
|
|
new_min, new_def, new_max));
|
|
for (const auto &knot : knots)
|
|
us.push (_avar2_denormalize_value (
|
|
_avar2_unmap_new_mapping (new_mapping, knot.z),
|
|
new_min, new_def, new_max));
|
|
if (unlikely (us.in_error ())) return UINT_MAX;
|
|
|
|
unsigned max_err = 0;
|
|
for (double u : us)
|
|
{
|
|
if (u < new_min || u > new_max) continue;
|
|
double n_old = _avar2_normalize_value (u, old_min, old_def, old_max);
|
|
double old_final = (double) old_seg.map_float ((float) n_old);
|
|
double n_new = _avar2_normalize_value (u, new_min, new_def, new_max);
|
|
double z = _avar2_map_new_mapping (new_mapping, n_new);
|
|
double new_final = z + _avar2_eval_offset (knots, z);
|
|
int err = abs ((int) roundf ((float) (old_final * 16384.0)) -
|
|
(int) roundf ((float) (new_final * 16384.0)));
|
|
if ((unsigned) err > max_err) max_err = err;
|
|
}
|
|
return max_err;
|
|
}
|
|
|
|
struct avar
|
|
{
|
|
static constexpr hb_tag_t tableTag = HB_OT_TAG_avar;
|
|
|
|
bool has_data () const { return version.to_int (); }
|
|
|
|
const SegmentMaps* get_segment_maps () const
|
|
{ return &firstAxisSegmentMaps; }
|
|
|
|
unsigned get_axis_count () const
|
|
{ return axisCount; }
|
|
|
|
bool sanitize (hb_sanitize_context_t *c) const
|
|
{
|
|
TRACE_SANITIZE (this);
|
|
if (!(version.sanitize (c) &&
|
|
hb_barrier () &&
|
|
(version.major == 1
|
|
#ifndef HB_NO_AVAR2
|
|
|| version.major == 2
|
|
#endif
|
|
) &&
|
|
c->check_struct (this)))
|
|
return_trace (false);
|
|
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
unsigned int count = axisCount;
|
|
for (unsigned int i = 0; i < count; i++)
|
|
{
|
|
if (unlikely (!map->sanitize (c)))
|
|
return_trace (false);
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
}
|
|
|
|
#ifndef HB_NO_AVAR2
|
|
if (version.major < 2)
|
|
return_trace (true);
|
|
hb_barrier ();
|
|
|
|
const auto &v2 = * (const avarV2Tail *) map;
|
|
if (unlikely (!v2.sanitize (c, this)))
|
|
return_trace (false);
|
|
#endif
|
|
|
|
return_trace (true);
|
|
}
|
|
|
|
void map_coords_16_16 (int *coords, unsigned int coords_length) const
|
|
{
|
|
unsigned int count = hb_min (coords_length, axisCount);
|
|
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
for (unsigned int i = 0; i < count; i++)
|
|
{
|
|
coords[i] = roundf (map->map_float (coords[i] / 65536.f) * 65536.f);
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
}
|
|
|
|
#ifndef HB_NO_AVAR2
|
|
if (version.major < 2)
|
|
return;
|
|
hb_barrier ();
|
|
|
|
for (; count < axisCount; count++)
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
|
|
const auto &v2 = * (const avarV2Tail *) map;
|
|
|
|
const auto &varidx_map = this+v2.varIdxMap;
|
|
const auto &var_store = this+v2.varStore;
|
|
auto *var_store_cache = var_store.create_cache ();
|
|
|
|
hb_vector_t<int> coords_2_14;
|
|
coords_2_14.resize (coords_length);
|
|
for (unsigned i = 0; i < coords_length; i++)
|
|
coords_2_14[i] = roundf (coords[i] / 4.f); // 16.16 -> 2.14
|
|
|
|
hb_vector_t<int> out;
|
|
out.alloc (coords_length);
|
|
for (unsigned i = 0; i < coords_length; i++)
|
|
{
|
|
int v = coords[i];
|
|
uint32_t varidx = varidx_map.map (i);
|
|
float delta = var_store.get_delta (varidx, coords_2_14.arrayZ, coords_2_14.length, var_store_cache);
|
|
/* Apply the delta unclamped and clamp only the result to [-1, +1],
|
|
* matching fontTools. Since inputs and results are in [-1, +1],
|
|
* deltas beyond ±2 are equivalent to ±2; clamp to that range only
|
|
* to keep the float->int conversion safe. */
|
|
float d = hb_clamp (delta * 4, -(float) (1<<17), +(float) (1<<17)); // 2.14 -> 16.16
|
|
v += (int) roundf (d);
|
|
v = hb_clamp (v, -(1<<16), +(1<<16));
|
|
out.push (v);
|
|
}
|
|
for (unsigned i = 0; i < coords_length; i++)
|
|
coords[i] = out[i];
|
|
|
|
OT::ItemVariationStore::destroy_cache (var_store_cache);
|
|
#endif
|
|
}
|
|
|
|
/* An avar version 2 font is never downgraded to v1: even with a NULL
|
|
* varStore offset (legal; maps like plain v1) the avar2 subsetting path
|
|
* applies, with "no variation" semantics for rows that don't resolve —
|
|
* offset compensation then creates delta rows on demand. */
|
|
bool has_v2_data () const { return version.major > 1; }
|
|
|
|
/* Resolve the avar2 VarStore and VarIdxMap, for the subset planner.
|
|
* Either pointer may be to the Null object (nullable offsets). */
|
|
bool get_v2_store_and_map (const ItemVariationStore **store,
|
|
const DeltaSetIndexMap **varidx_map) const
|
|
{
|
|
#ifndef HB_NO_AVAR2
|
|
if (version.major < 2) return false;
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
const auto &v2 = * (const avarV2Tail *) map;
|
|
*store = &(this+v2.varStore);
|
|
*varidx_map = &(this+v2.varIdxMap);
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// axis normalization is done in 2.14 here
|
|
// TODO: deprecate this API once fonttools is updated to use 16.16 normalization
|
|
bool map_coords_2_14 (float *coords, unsigned int coords_length,
|
|
bool v1_only = false) const
|
|
{
|
|
hb_vector_t<int> coords_2_14;
|
|
if (!v1_only && !coords_2_14.resize (coords_length)) return false;
|
|
unsigned int count = hb_min (coords_length, axisCount);
|
|
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
for (unsigned int i = 0; i < count; i++)
|
|
{
|
|
int v = roundf (map->map_float (coords[i]) * 16384.f);
|
|
if (!v1_only)
|
|
coords_2_14[i] = v;
|
|
coords[i] = v / 16384.f;
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
}
|
|
|
|
if (v1_only)
|
|
return true;
|
|
|
|
#ifndef HB_NO_AVAR2
|
|
if (version.major < 2)
|
|
return true;
|
|
hb_barrier ();
|
|
|
|
for (; count < axisCount; count++)
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
|
|
const auto &v2 = * (const avarV2Tail *) map;
|
|
|
|
const auto &varidx_map = this+v2.varIdxMap;
|
|
const auto &var_store = this+v2.varStore;
|
|
auto *var_store_cache = var_store.create_cache ();
|
|
|
|
for (unsigned i = 0; i < coords_length; i++)
|
|
{
|
|
int v = coords_2_14[i];
|
|
uint32_t varidx = varidx_map.map (i);
|
|
float delta = var_store.get_delta (varidx, coords_2_14.arrayZ, coords_2_14.length, var_store_cache);
|
|
/* As above: apply the delta unclamped (±2 covers every useful case)
|
|
* and clamp the result to [-1, +1], matching fontTools. */
|
|
v += (int) hb_clamp (roundf (delta), -(float) (1<<15), +(float) (1<<15));
|
|
v = hb_clamp (v, -(1<<14), +(1<<14));
|
|
coords[i] = v / 16384.f;
|
|
}
|
|
|
|
OT::ItemVariationStore::destroy_cache (var_store_cache);
|
|
return true;
|
|
#else
|
|
return version.major < 2;
|
|
#endif
|
|
}
|
|
|
|
bool subset (hb_subset_context_t *c) const
|
|
{
|
|
TRACE_SUBSET (this);
|
|
unsigned retained_axis_count = c->plan->axes_index_map.get_population ();
|
|
if (!retained_axis_count) //all axes are pinned/dropped
|
|
return_trace (false);
|
|
|
|
avar *out = c->serializer->allocate_min<avar> ();
|
|
if (unlikely (!out)) return_trace (false);
|
|
|
|
out->version.major = c->plan->has_avar2 ? 2 : 1;
|
|
out->version.minor = 0;
|
|
if (!c->serializer->check_assign (out->axisCount, retained_axis_count, HB_SERIALIZE_ERROR_INT_OVERFLOW))
|
|
return_trace (false);
|
|
|
|
/* For avar2, keep the instantiated v1 mappings around; offset
|
|
* compensation needs them to locate the new mappings' kinks. */
|
|
hb_vector_t<hb_vector_t<AxisValueMap>> new_mappings;
|
|
if (c->plan->has_avar2 && !new_mappings.resize (axisCount))
|
|
return_trace (false);
|
|
|
|
const hb_map_t& axes_index_map = c->plan->axes_index_map;
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
unsigned count = axisCount;
|
|
for (unsigned int i = 0; i < count; i++)
|
|
{
|
|
if (axes_index_map.has (i))
|
|
{
|
|
hb_tag_t *axis_tag;
|
|
if (!c->plan->axes_old_index_tag_map.has (i, &axis_tag))
|
|
return_trace (false);
|
|
|
|
Triple *axis_location;
|
|
if (c->plan->has_avar2 &&
|
|
c->plan->user_axes_location.has (*axis_tag, &axis_location) &&
|
|
axis_location->is_point ())
|
|
{
|
|
/* Pinned axis in avar2 mode: serialize identity segment map
|
|
* {-1->-1, 0->0, 1->1}. The axis is kept in fvar as hidden,
|
|
* so avar needs a segment map entry for it. */
|
|
auto *identity_map = c->serializer->start_embed<SegmentMaps> ();
|
|
if (unlikely (!c->serializer->extend_min (identity_map)))
|
|
return_trace (false);
|
|
AxisValueMap m;
|
|
m.set_mapping (-1.f, -1.f);
|
|
if (!m.serialize (c->serializer)) return_trace (false);
|
|
m.set_mapping (0.f, 0.f);
|
|
if (!m.serialize (c->serializer)) return_trace (false);
|
|
m.set_mapping (1.f, 1.f);
|
|
if (!m.serialize (c->serializer)) return_trace (false);
|
|
if (!c->serializer->check_assign (identity_map->len, 3u,
|
|
HB_SERIALIZE_ERROR_INT_OVERFLOW))
|
|
return_trace (false);
|
|
}
|
|
else
|
|
{
|
|
/* Restricted or free axis: use standard SegmentMaps::subset() */
|
|
if (!map->subset (c, *axis_tag,
|
|
c->plan->has_avar2 ? &new_mappings[i] : nullptr))
|
|
return_trace (false);
|
|
}
|
|
}
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
}
|
|
|
|
if (c->plan->has_avar2)
|
|
return_trace (_subset_avar2 (c, new_mappings));
|
|
|
|
return_trace (true);
|
|
}
|
|
|
|
private:
|
|
struct avar2_index_map_plan_t
|
|
{
|
|
bool init (const hb_vector_t<uint32_t> &varidx_mapping,
|
|
const hb_map_t &axes_index_map,
|
|
unsigned axis_count)
|
|
{
|
|
if (!output_map.alloc (axes_index_map.get_population ()))
|
|
return false;
|
|
|
|
bool has_no_variation = false;
|
|
unsigned max_outer = 0, max_inner = 0;
|
|
for (unsigned i = 0; i < axis_count; i++)
|
|
{
|
|
if (!axes_index_map.has (i)) continue;
|
|
uint32_t varidx = varidx_mapping[i];
|
|
output_map.push (varidx);
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
{
|
|
has_no_variation = true;
|
|
continue;
|
|
}
|
|
max_outer = hb_max (max_outer, varidx >> 16);
|
|
max_inner = hb_max (max_inner, varidx & 0xFFFF);
|
|
}
|
|
if (output_map.in_error () ||
|
|
output_map.length != axes_index_map.get_population ())
|
|
return false;
|
|
|
|
if (has_no_variation)
|
|
{
|
|
width = 4;
|
|
inner_bit_count = 16;
|
|
}
|
|
else
|
|
{
|
|
inner_bit_count = hb_max (1u, hb_bit_storage (max_inner));
|
|
unsigned outer_bit_count = hb_max (1u, hb_bit_storage (max_outer));
|
|
width = hb_clamp ((inner_bit_count + outer_bit_count + 7) / 8, 1u, 4u);
|
|
if (inner_bit_count + outer_bit_count > width * 8)
|
|
inner_bit_count = width * 8 - outer_bit_count;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
unsigned get_inner_bit_count () const { return inner_bit_count; }
|
|
unsigned get_width () const { return width; }
|
|
hb_array_t<const uint32_t> get_output_map () const { return output_map.as_array (); }
|
|
|
|
unsigned inner_bit_count = 1;
|
|
unsigned width = 1;
|
|
hb_vector_t<uint32_t> output_map;
|
|
};
|
|
|
|
bool _subset_avar2 (hb_subset_context_t *c,
|
|
const hb_vector_t<hb_vector_t<AxisValueMap>> &new_mappings) const
|
|
{
|
|
#if defined (HB_NO_VAR) || defined (HB_NO_AVAR2)
|
|
/* Not reachable: the plan never sets has_avar2 in these configurations. */
|
|
return false;
|
|
#else
|
|
|
|
/* 1. Locate original avar2 data, keeping per-axis old segment maps */
|
|
hb_vector_t<const SegmentMaps *> old_seg_maps;
|
|
if (!old_seg_maps.alloc (axisCount)) return false;
|
|
const SegmentMaps *map = &firstAxisSegmentMaps;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
old_seg_maps.push (map);
|
|
map = &StructAfter<SegmentMaps> (*map);
|
|
}
|
|
|
|
const auto &v2 = * (const avarV2Tail *) map;
|
|
const auto &varidx_map = this+v2.varIdxMap;
|
|
const auto &var_store = this+v2.varStore;
|
|
|
|
auto fvar_axes = c->plan->source->table.fvar->get_axes ();
|
|
|
|
/* 2. Compute default deltas by evaluating VarStore at old defaults */
|
|
hb_vector_t<int> default_coords;
|
|
if (!default_coords.resize (axisCount)) return false;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
hb_tag_t *axis_tag;
|
|
if (c->plan->axes_old_index_tag_map.has (i, &axis_tag) &&
|
|
c->plan->old_intermediates.has (*axis_tag))
|
|
{
|
|
float d_i = (float) c->plan->old_intermediates.get (*axis_tag).middle;
|
|
default_coords[i] = roundf (d_i * 16384.f);
|
|
}
|
|
else
|
|
default_coords[i] = 0;
|
|
}
|
|
|
|
auto *store_cache = var_store.create_cache ();
|
|
hb_vector_t<float> default_deltas;
|
|
if (!default_deltas.resize (axisCount)) {
|
|
ItemVariationStore::destroy_cache (store_cache);
|
|
return false;
|
|
}
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
uint32_t varidx = varidx_map.map (i);
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
default_deltas[i] = 0.f;
|
|
else
|
|
default_deltas[i] = var_store.get_delta (varidx, default_coords.arrayZ,
|
|
default_coords.length, store_cache);
|
|
}
|
|
ItemVariationStore::destroy_cache (store_cache);
|
|
|
|
/* 3. Rebase IVS regions */
|
|
item_variations_t item_vars;
|
|
if (!item_vars.create_from_item_varstore (var_store, c->plan->axes_old_index_tag_map))
|
|
return false;
|
|
if (!item_vars.instantiate_tuple_vars (c->plan->old_intermediates,
|
|
c->plan->axes_triple_distances,
|
|
false))
|
|
return false;
|
|
|
|
/* 4. Self-contained pinned axes (whose final coordinate is constant over
|
|
* the retained box) were detected at plan time
|
|
* (_compute_avar2_reachable_ranges) and removed from axes_index_map.
|
|
* They are skipped below; their constant contribution is baked into the
|
|
* other variation tables by standard instancing at the plan's
|
|
* axes_location/normalized_coords. */
|
|
|
|
/* 5. Build per-axis varIdx mapping (may create new VarDatas).
|
|
* Entries (or the implicit identity mapping) that don't resolve to a
|
|
* real store row behave as "no variation" at runtime; normalize them to
|
|
* NO_VARIATIONS_INDEX so the offset loop creates fresh rows instead of
|
|
* writing into nonexistent ones. This also covers a NULL VarStore
|
|
* (never downgraded: rows are created on demand). */
|
|
hb_vector_t<uint32_t> new_varidx_mapping;
|
|
if (!new_varidx_mapping.resize (axisCount)) return false;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
uint32_t varidx = varidx_map.map (i);
|
|
if (varidx != HB_OT_LAYOUT_NO_VARIATIONS_INDEX &&
|
|
!var_store.has_delta_set (varidx))
|
|
varidx = HB_OT_LAYOUT_NO_VARIATIONS_INDEX;
|
|
new_varidx_mapping[i] = varidx;
|
|
}
|
|
|
|
/* 5.5. Privatize shared varIdx delta rows before adding offset
|
|
* compensation. avar2's VarIdxMap may map several fvar axes to the SAME
|
|
* IVS delta row. Writing one axis's offset-compensation deltas into a
|
|
* shared row would corrupt every other axis that reads that row. So give
|
|
* each offset-receiving axis whose row is shared its own private copy of
|
|
* the row (identical contents, preserving the rebased deltas), then
|
|
* repoint its varIdx. Sharers keep the clean row; the varstore
|
|
* optimization pass re-merges identical rows afterwards. */
|
|
hb_hashmap_t<uint32_t, unsigned> varidx_ref_count;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
if (!c->plan->axes_index_map.has (i)) continue; /* self-contained: dropped */
|
|
uint32_t varidx = new_varidx_mapping[i];
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX) continue;
|
|
unsigned *count;
|
|
if (varidx_ref_count.has (varidx, &count))
|
|
(*count)++;
|
|
else if (!varidx_ref_count.set (varidx, 1))
|
|
return false;
|
|
}
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
hb_tag_t *axis_tag_ptr;
|
|
if (!c->plan->axes_old_index_tag_map.has (i, &axis_tag_ptr))
|
|
return false;
|
|
/* Only axes that will receive offset compensation (restricted or
|
|
* pinned) can contaminate a shared row. */
|
|
if (!c->plan->axes_index_map.has (i) ||
|
|
!c->plan->user_axes_location.has (*axis_tag_ptr))
|
|
continue;
|
|
uint32_t varidx = new_varidx_mapping[i];
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
continue; /* gets a fresh, private VarData in the offset loop below */
|
|
unsigned *count;
|
|
if (!varidx_ref_count.has (varidx, &count) || *count <= 1)
|
|
continue; /* sole owner: safe to write offsets in place */
|
|
unsigned outer = varidx >> 16;
|
|
unsigned new_inner = item_vars.duplicate_row (outer, varidx & 0xFFFF);
|
|
if (unlikely (new_inner == (unsigned) -1)) return false;
|
|
new_varidx_mapping[i] = (outer << 16) | new_inner;
|
|
(*count)--;
|
|
}
|
|
|
|
/* 6. Add offset compensation tuples.
|
|
* Track processed (outer,inner) pairs to avoid adding duplicate biases
|
|
* when multiple axes share the same varIdx. */
|
|
hb_set_t processed_varidxes;
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
hb_tag_t *axis_tag_ptr;
|
|
if (!c->plan->axes_old_index_tag_map.has (i, &axis_tag_ptr))
|
|
return false;
|
|
hb_tag_t axis_tag = *axis_tag_ptr;
|
|
|
|
/* Self-contained pinned axes are removed from fvar/avar; their
|
|
* contribution is baked into the variation tables instead. */
|
|
if (!c->plan->axes_index_map.has (i))
|
|
continue;
|
|
|
|
Triple *new_user;
|
|
if (c->plan->user_axes_location.has (axis_tag, &new_user))
|
|
{
|
|
/* This axis is being restricted or pinned */
|
|
Triple *old_int;
|
|
if (!c->plan->old_intermediates.has (axis_tag, &old_int))
|
|
return false;
|
|
|
|
float a_i = (float) old_int->minimum;
|
|
float d_i = (float) old_int->middle;
|
|
float b_i = (float) old_int->maximum;
|
|
|
|
int d_int = roundf (d_i * 16384.f);
|
|
|
|
bool is_pinned = new_user->is_point ();
|
|
|
|
uint32_t varidx = new_varidx_mapping[i];
|
|
unsigned outer, inner, item_count;
|
|
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
{
|
|
/* No existing avar2 mapping. Create new VarData. */
|
|
outer = item_vars.add_vardata (1);
|
|
inner = 0;
|
|
item_count = 1;
|
|
new_varidx_mapping[i] = (outer << 16) | inner;
|
|
default_deltas[i] = 0.f; /* no prior default delta */
|
|
}
|
|
else
|
|
{
|
|
outer = varidx >> 16;
|
|
inner = varidx & 0xFFFF;
|
|
item_count = item_vars.get_item_count (outer);
|
|
}
|
|
|
|
/* Empty-region bias: d_int + round(defaultDelta) */
|
|
int bias = d_int + (int) roundf (default_deltas[i]);
|
|
if (bias != 0)
|
|
{
|
|
hb_hashmap_t<hb_tag_t, Triple> empty_region;
|
|
item_vars.add_tuple (outer, std::move (empty_region),
|
|
inner, bias, item_count);
|
|
}
|
|
|
|
if (!is_pinned)
|
|
{
|
|
/* Offset compensation encodes, as avar2 deltas on this axis, the
|
|
* piecewise-linear function offset(z) = inv_renorm(z) - z, where
|
|
* inv_renorm maps a new intermediate coordinate z back to the old
|
|
* intermediate coordinate. It is known at these knots in the new
|
|
* intermediate space:
|
|
* z = -1 -> a_i + 1 (new minimum)
|
|
* z = 0 -> d_i (new default)
|
|
* z = +1 -> b_i - 1 (new maximum)
|
|
*/
|
|
hb_vector_t<avar2_offset_knot_t> knots;
|
|
_avar2_add_knot (knots, -1.0, (double) a_i + 1.0);
|
|
_avar2_add_knot (knots, 0.0, (double) d_i);
|
|
_avar2_add_knot (knots, 1.0, (double) b_i - 1.0);
|
|
|
|
const hb_vector_t<AxisValueMap> &new_mapping = new_mappings[i];
|
|
|
|
float min_f = 0.f, def_f = 0.f, max_f = 0.f;
|
|
if (likely (i < fvar_axes.length))
|
|
fvar_axes[i].get_coordinates (min_f, def_f, max_f);
|
|
double old_min = (double) min_f;
|
|
double old_def = (double) def_f;
|
|
double old_max = (double) max_f;
|
|
|
|
/* If the axis default MOVED, inv_renorm also kinks where the OLD
|
|
* default lands in the new space (the old intermediate coordinate
|
|
* crosses 0 there), at
|
|
* z = z_old -> -z_old
|
|
* Omitting that knot (as a plain two-tent encoding would) makes
|
|
* interior coordinates wrong. */
|
|
double z_old = _avar2_normalize_value ((double) old_def,
|
|
new_user->minimum,
|
|
new_user->middle,
|
|
new_user->maximum);
|
|
z_old = _avar2_map_new_mapping (new_mapping, z_old);
|
|
z_old = (double) roundf ((float) (z_old * 16384.0)) / 16384.0;
|
|
if (-1.0 < z_old && z_old < 1.0)
|
|
_avar2_add_knot (knots, z_old, -z_old);
|
|
|
|
/* Interior avar v1 breakpoints inside the retained range each put
|
|
* a kink in offset(z). Sampling only {-1, 0, +1, z_old} would
|
|
* linearly interpolate across those kinks. The instantiated
|
|
* mapping keeps exactly the in-range old breakpoints, and the new
|
|
* mapping kinks at each one's output coordinate; add that z with
|
|
* its old intermediate value so offset(z) is reproduced at every
|
|
* kink. */
|
|
hb_vector_t<avar2_offset_knot_t> with_breakpoints (knots);
|
|
for (const auto &m : new_mapping)
|
|
{
|
|
double from = (double) m.coords[0].to_float ();
|
|
if (from == -1.0 || from == 0.0 || from == 1.0)
|
|
continue; /* anchors already seeded */
|
|
double z = (double) m.coords[1].to_float ();
|
|
if (!(-1.0 < z && z < 1.0))
|
|
continue;
|
|
double user = _avar2_denormalize_value (from,
|
|
new_user->minimum,
|
|
new_user->middle,
|
|
new_user->maximum);
|
|
double n_old = _avar2_normalize_value (user, old_min, old_def, old_max);
|
|
double x_old = (double) old_seg_maps[i]->map_float ((float) n_old);
|
|
x_old = (double) roundf ((float) (x_old * 16384.0)) / 16384.0;
|
|
_avar2_add_knot (with_breakpoints, z, x_old - z);
|
|
}
|
|
|
|
if (unlikely (knots.in_error () || with_breakpoints.in_error ()))
|
|
return false;
|
|
|
|
/* Extra tents cost F2Dot14 rounding, so for a steep segment they
|
|
* can add more quantization noise than the structural error they
|
|
* remove. Keep the interior breakpoints only when they do not
|
|
* increase the estimated residual; this makes the collection a
|
|
* strict (never-worse) improvement over the {-1, 0, +1, z_old}
|
|
* anchors. Warn when even the better choice is not bit-exact (a
|
|
* steep retained segment that cannot be reproduced in F2Dot14). */
|
|
unsigned err = _avar2_estimate_offset_error (*old_seg_maps[i], new_mapping,
|
|
old_min, old_def, old_max,
|
|
new_user->minimum,
|
|
new_user->middle,
|
|
new_user->maximum,
|
|
knots);
|
|
if (with_breakpoints.length != knots.length)
|
|
{
|
|
unsigned err_with = _avar2_estimate_offset_error (*old_seg_maps[i], new_mapping,
|
|
old_min, old_def, old_max,
|
|
new_user->minimum,
|
|
new_user->middle,
|
|
new_user->maximum,
|
|
with_breakpoints);
|
|
if (err_with <= err)
|
|
{
|
|
knots = std::move (with_breakpoints);
|
|
err = err_with;
|
|
}
|
|
}
|
|
if (err > 8)
|
|
DEBUG_MSG (SUBSET, nullptr,
|
|
"avar2 offset compensation is approximate for axis %c%c%c%c: "
|
|
"max residual %u F2Dot14 units",
|
|
HB_UNTAG (axis_tag), err);
|
|
|
|
/* Synthesize tents. Adjacent tents evaluate to zero at each
|
|
* other's peaks, so each knot's delta is offset(z) - offset(0);
|
|
* the base value offset(0) = d_i is carried by the empty-region
|
|
* bias above. This reduces to the classic pair of tents
|
|
* (-1,-1,0) / (0,+1,+1) when the default is unchanged and there
|
|
* are no interior knots. */
|
|
for (unsigned k = 0; k < knots.length; k++)
|
|
{
|
|
double z = knots.arrayZ[k].z;
|
|
if (z == 0.0) continue;
|
|
int delta = (int) roundf ((float) ((knots.arrayZ[k].offset - (double) d_i) * 16384.0));
|
|
if (!delta) continue;
|
|
double lower, upper;
|
|
if (z > 0.0)
|
|
{
|
|
lower = knots.arrayZ[k - 1].z;
|
|
upper = k + 1 < knots.length ? knots.arrayZ[k + 1].z : z;
|
|
}
|
|
else
|
|
{
|
|
upper = knots.arrayZ[k + 1].z;
|
|
lower = k > 0 ? knots.arrayZ[k - 1].z : z;
|
|
}
|
|
hb_hashmap_t<hb_tag_t, Triple> region;
|
|
if (unlikely (!region.set (axis_tag, Triple (lower, z, upper))))
|
|
return false;
|
|
item_vars.add_tuple (outer, std::move (region),
|
|
inner, delta, item_count);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Free or private axis — not being restricted.
|
|
* If it has a non-zero default delta, add it back as a bias.
|
|
* Skip if this (outer,inner) was already processed (shared varIdx). */
|
|
uint32_t varidx = new_varidx_mapping[i];
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
continue;
|
|
|
|
if (processed_varidxes.has (varidx))
|
|
continue;
|
|
processed_varidxes.add (varidx);
|
|
|
|
unsigned outer = varidx >> 16;
|
|
unsigned inner = varidx & 0xFFFF;
|
|
int dd = (int) roundf (default_deltas[i]);
|
|
if (dd != 0)
|
|
{
|
|
unsigned item_count = item_vars.get_item_count (outer);
|
|
hb_hashmap_t<hb_tag_t, Triple> empty_region;
|
|
item_vars.add_tuple (outer, std::move (empty_region),
|
|
inner, dd, item_count);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 7. Finalize: build region list + convert to varstore */
|
|
if (!item_vars.build_region_list ()) return false;
|
|
if (!item_vars.as_item_varstore (true /* optimize */,
|
|
false /* use_no_variation_idx */))
|
|
return false;
|
|
|
|
/* 8. Apply varidx_map optimization remapping */
|
|
const auto &opt_varidx_map = item_vars.get_varidx_map ();
|
|
for (unsigned i = 0; i < axisCount; i++)
|
|
{
|
|
uint32_t varidx = new_varidx_mapping[i];
|
|
if (varidx == HB_OT_LAYOUT_NO_VARIATIONS_INDEX)
|
|
continue;
|
|
uint32_t *new_idx;
|
|
if (opt_varidx_map.has (varidx, &new_idx))
|
|
new_varidx_mapping[i] = *new_idx;
|
|
}
|
|
|
|
/* 9. Serialize avarV2Tail. Entries cover the retained axes only;
|
|
* self-contained pinned axes are removed from fvar. */
|
|
avar2_index_map_plan_t index_map_plan;
|
|
if (!index_map_plan.init (new_varidx_mapping,
|
|
c->plan->axes_index_map,
|
|
axisCount))
|
|
return false;
|
|
|
|
auto *tail = c->serializer->allocate_size<avarV2Tail> (avarV2Tail::static_size);
|
|
if (unlikely (!tail)) return false;
|
|
if (!tail->varIdxMap.serialize_serialize (c->serializer, index_map_plan))
|
|
return false;
|
|
if (!tail->varStore.serialize_serialize (c->serializer,
|
|
item_vars.has_long_word (),
|
|
c->plan->axis_tags,
|
|
item_vars.get_region_list (),
|
|
item_vars.get_vardata_encodings ()))
|
|
return false;
|
|
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
public:
|
|
|
|
protected:
|
|
FixedVersion<>version; /* Version of the avar table
|
|
* initially set to 0x00010000u */
|
|
HBUINT16 reserved; /* This field is permanently reserved. Set to 0. */
|
|
HBUINT16 axisCount; /* The number of variation axes in the font. This
|
|
* must be the same number as axisCount in the
|
|
* 'fvar' table. */
|
|
SegmentMaps firstAxisSegmentMaps;
|
|
|
|
public:
|
|
DEFINE_SIZE_MIN (8);
|
|
};
|
|
|
|
} /* namespace OT */
|
|
|
|
|
|
#endif /* HB_OT_VAR_AVAR_TABLE_HH */
|