Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
98bb7ca
Add log dependency
simoncozens Apr 3, 2026
38feba0
Add font-types and num-traits dependencies
simoncozens Apr 3, 2026
84e52da
[not variations] Better Harfbuzz compatibility while subsetting Coverage
simoncozens Feb 23, 2026
6b49ad8
[not variations] Correctly compute PairPos1 coverage
simoncozens Feb 27, 2026
12fa2a1
[Not variations] typo was breaking Khmer
simoncozens Feb 27, 2026
70a8574
[Not variations] Pass through extension lookups, subset their subtables
simoncozens Feb 27, 2026
96e85d6
[Not variations] Fix GDEF subsetting: Varstore offsets are 32 bit, no…
simoncozens Feb 14, 2026
4a599a3
[Not variations] Fix for OS/2 codepage ranges: high codepoints fell t…
simoncozens Mar 3, 2026
4435576
[Not variations] pop_pack may fail when subsetting name table, don't die
simoncozens Mar 1, 2026
2bc4498
[not variations] Support FORCE_LONG_LOCA in head table
simoncozens Feb 24, 2026
5abf280
(Not variations) Implement bidi closure
simoncozens Apr 3, 2026
1643277
Support no-layout and ift-requirements test profiles
simoncozens Feb 26, 2026
7f16a89
Warn when subtable allocation failed
simoncozens Apr 3, 2026
29c6cfd
[write-fonts] Allow access to instructions on composite glyphs
simoncozens Feb 18, 2026
f598d22
[write-fonts] Support manipulation of components and overlaps bit
simoncozens Feb 26, 2026
1f795e4
[skrifa] Allow skera access through a feature to raw computed deltas
simoncozens Feb 12, 2026
e61b701
Add variations solver, basic types
simoncozens Feb 10, 2026
02177a0
Add normalized coords to plan
simoncozens Apr 3, 2026
9b0c4a4
Parse the instancing spec
simoncozens Feb 10, 2026
a50924b
Apply instancing spec to plan
simoncozens Feb 10, 2026
46aaed7
Subset fvar
simoncozens Feb 10, 2026
ca589e7
Subset avar
simoncozens Apr 3, 2026
fff64b4
Add instancing support to integration tests
simoncozens Apr 3, 2026
0f38bcd
Add variations solver, basic types
simoncozens Feb 10, 2026
74bdacb
Instantiate gvar (note: not glyf yet)
simoncozens Feb 11, 2026
44317cd
Start to instantiate glyf
simoncozens Apr 3, 2026
a540c5d
Move head subsetting to new module
simoncozens Feb 15, 2026
4496ba7
More glyf instantiation
simoncozens Apr 3, 2026
6060e52
Instantiate hmtx table
simoncozens Feb 12, 2026
020044b
Now we can partially instance, be careful about subsetting variationr…
simoncozens Feb 14, 2026
13012b9
Deduplicate and coalesce regions after subsetting
simoncozens Feb 14, 2026
0c01aff
Plan warning logging
simoncozens Apr 3, 2026
b33e6ec
Update hmtx while subsetting glyf
simoncozens Feb 24, 2026
321ba12
glyf: Set overlap bits
simoncozens Apr 3, 2026
39e1c5c
Gvar instantiation
simoncozens Apr 3, 2026
a8d36ca
Gather head/maxp information
simoncozens Feb 24, 2026
4bc335e
Instance GDEF and GPOS
simoncozens Feb 14, 2026
ee66b4e
Rework IVS instancing to more closely match Harfbuzz
simoncozens Apr 3, 2026
5592991
Drop HVAR when all pinned
simoncozens Feb 15, 2026
5acb5f4
Subset STAT table
simoncozens Feb 18, 2026
418c23e
Subset MVAR table
simoncozens Feb 19, 2026
a534c12
Support OS/2 avgCharWidth
simoncozens Feb 24, 2026
dbc0357
OS/2 weight and width classes
simoncozens Feb 24, 2026
2b29d23
Warnings fix
simoncozens Apr 3, 2026
10a5d4a
Improve hmtx, subset MVAR
simoncozens Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion skera/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ repository.workspace = true
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
fnv = "1.0.7"
font-types = { workspace = true }
hashbrown = "0.15.1"
log = "0.4.29"
num-traits = "0.2.18"
regex = "1.10.4"
skrifa = { workspace = true }
skrifa = { workspace = true, features = ["skera"] }
thiserror = "2.0"
write-fonts = { workspace = true, features = ["read"] }

Expand Down
255 changes: 255 additions & 0 deletions skera/src/avar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
use font_types::Fixed;
use num_traits::Float;
use skrifa::raw::{tables::avar::SegmentMaps, ReadError, TopLevelTable};
use write_fonts::{read::tables::avar::Avar, types::F2Dot14};

use crate::{
serialize::SerializeErrorFlags,
variations::solver::{renormalize_value, Triple, TripleDistances},
Subset, SubsetError,
};

pub(crate) fn map_coords_2_14(avar: &Avar, coords: Vec<f32>) -> Result<Vec<f32>, ReadError> {
let maps = avar.axis_segment_maps();
coords
.into_iter()
.zip(maps.iter())
.map(|(coord, maybe_map)| {
maybe_map.map(|m| m.apply(Fixed::from_f64(coord as f64)).to_f32())
})
.collect()
}

impl Subset for Avar<'_> {
fn subset(
&self,
plan: &crate::Plan,
_font: &write_fonts::read::FontRef,
s: &mut crate::serialize::Serializer,
_builder: &mut write_fonts::FontBuilder,
) -> Result<(), crate::SubsetError> {
if plan.axes_index_map.is_empty() {
return Err(SubsetError::SubsetTableError(Avar::TAG)); // empty
}
subset_avar(self, plan, s).map_err(|_| SubsetError::SubsetTableError(Avar::TAG))
}
}

fn subset_avar(
avar: &Avar<'_>,
plan: &crate::Plan,
s: &mut crate::serialize::Serializer,
) -> Result<(), SerializeErrorFlags> {
let new_axis_count = plan.axes_index_map.len() as u16;

// Version
s.embed(1_u16)?;
s.embed(0_u16)?;
s.embed(0_u16)?; // reserved
s.embed(new_axis_count)?;

for (i, segment_map) in avar.axis_segment_maps().iter().enumerate() {
let Ok(segment_map) = segment_map else {
return Err(SerializeErrorFlags::SERIALIZE_ERROR_READ_ERROR);
};
if plan.axes_index_map.contains_key(&i) {
let Some(axis_tag) = plan.axes_old_index_tag_map.get(&i) else {
return Err(SerializeErrorFlags::SERIALIZE_ERROR_OTHER);
};
// Subset the mapping
if let Some(axis_range) = plan.axes_location.get(axis_tag) {
let Some(&triple_distances) = plan.axes_triple_distances.get(axis_tag) else {
continue;
};
let unmapped_range: Triple<f64> = unmap_axis_range(axis_range, &segment_map);
let axis_range =
Triple::new(axis_range.minimum, axis_range.middle, axis_range.maximum);
let triple_distances =
TripleDistances::new(triple_distances.negative, triple_distances.positive);
let mut value_mappings = vec![];
for mapping in segment_map.axis_value_maps() {
let mapping_from = mapping.from_coordinate().to_f32() as f64;
if !unmapped_range.contains(mapping_from) {
continue;
}
let mapping_to = mapping.to_coordinate().to_f32() as f64;
let new_mapping = (
renormalize_value(mapping_from, unmapped_range, triple_distances, false),
renormalize_value(mapping_to, axis_range, triple_distances, false),
);
if must_include(new_mapping) {
continue;
}
value_mappings.push(new_mapping);
}
value_mappings.push((-1.0, -1.0));
value_mappings.push((0.0, 0.0));
value_mappings.push((1.0, 1.0));
value_mappings.sort_by_key(|(from, _)| F2Dot14::from_f32(*from as f32).to_bits());
s.embed(value_mappings.len() as u16)?;
for (from, to) in value_mappings {
s.embed(F2Dot14::from_f32(from as f32))?;
s.embed(F2Dot14::from_f32(to as f32))?;
}
} else {
// Just embed it as-is
s.embed(segment_map.position_map_count())?;
for mapping in segment_map.axis_value_maps() {
s.embed(mapping.from_coordinate())?;
s.embed(mapping.to_coordinate())?;
}
}
}
}
Ok(())
}

fn unmap_axis_range(range: &Triple<f64>, segment_maps: &SegmentMaps) -> Triple<f64> {
Triple {
minimum: unmap_float(range.minimum, segment_maps),
middle: unmap_float(range.middle, segment_maps),
maximum: unmap_float(range.maximum, segment_maps),
}
}

enum Direction {
#[allow(dead_code)]
Forward,
Backward,
}

fn unmap_float<F: Float + std::fmt::Debug + Copy + Default + PartialEq>(
f: F,
segment_maps: &SegmentMaps,
) -> F {
map_float(f, Direction::Backward, segment_maps)
}

fn map_float<F: Float + std::fmt::Debug + Copy + Default + PartialEq>(
value: F,
direction: Direction,
segment_maps: &SegmentMaps,
) -> F {
let maps = segment_maps.axis_value_maps();
let len = maps.len();
if len < 2 {
if len == 0 {
return value;
}
let from_coord = F::from(maps[0].from_coordinate().to_bits() as f64 / 16384.0).unwrap();
let to_coord = F::from(maps[0].to_coordinate().to_bits() as f64 / 16384.0).unwrap();
return value - from_coord + to_coord;
}

let get_from_coord_val = |index: usize| match direction {
Direction::Forward => {
F::from(maps[index].from_coordinate().to_bits() as f64 / 16384.0).unwrap()
}
Direction::Backward => {
F::from(maps[index].to_coordinate().to_bits() as f64 / 16384.0).unwrap()
}
};
let get_to_coord_val = |index: usize| match direction {
Direction::Forward => {
F::from(maps[index].to_coordinate().to_bits() as f64 / 16384.0).unwrap()
}
Direction::Backward => {
F::from(maps[index].from_coordinate().to_bits() as f64 / 16384.0).unwrap()
}
};

let mut start = 0usize;
let mut end = len;
if get_from_coord_val(start) == -F::one()
&& get_to_coord_val(start) == -F::one()
&& get_from_coord_val(start + 1) == -F::one()
{
start += 1;
}
if get_from_coord_val(end - 1) == F::one()
&& get_to_coord_val(end - 1) == F::one()
&& get_from_coord_val(end - 2) == F::one()
{
end -= 1;
}

let mut i = start;
while i < end {
if value == get_from_coord_val(i) {
break;
}
i += 1;
}
if i < end {
let mut j = i;
while j + 1 < end {
if value != get_from_coord_val(j + 1) {
break;
}
j += 1;
}

if i == j {
return get_to_coord_val(i);
}
if i + 2 == j {
return get_to_coord_val(i + 1);
}

if value < F::zero() {
return get_to_coord_val(j);
}
if value > F::zero() {
return get_to_coord_val(i);
}

return if get_to_coord_val(i).abs() < get_to_coord_val(j).abs() {
get_to_coord_val(i)
} else {
get_to_coord_val(j)
};
}

let mut i = start;
while i < end {
if value < get_from_coord_val(i) {
break;
}
i += 1;
}

if i == 0 {
return value - get_from_coord_val(0) + get_to_coord_val(0);
}
if i == end {
return value - get_from_coord_val(end - 1) + get_to_coord_val(end - 1);
}

let before = i - 1;
let after = i;
let denom = get_from_coord_val(after) - get_from_coord_val(before);
get_to_coord_val(before)
+ ((get_to_coord_val(after) - get_to_coord_val(before))
* (value - get_from_coord_val(before)))
/ denom
}

const F_EPSILON: f64 = 0.00001; // Epsilon for float comparison

fn float_approx_eq<F: Float + std::fmt::Debug + Copy + Default + PartialEq>(a: F, b: F) -> bool {
(a - b).abs() < F::from(F_EPSILON).unwrap()
}

fn must_include<F: Float + std::fmt::Debug + Copy + Default + PartialEq>(mapping: (F, F)) -> bool {
// Only check for f64, as this is where the `new_mapping` values come from
let neg_one = F::from(-1.0).unwrap();
let zero = F::zero();
let one = F::one();

let map_from = mapping.0;
let map_to = mapping.1;

(float_approx_eq(map_from, neg_one) && float_approx_eq(map_to, neg_one))
|| (float_approx_eq(map_from, zero) && float_approx_eq(map_to, zero))
|| (float_approx_eq(map_from, one) && float_approx_eq(map_to, one))
}
Loading
Loading