use read_fonts::{
tables::{
layout::Condition,
varc::{
DecomposedTransform, MultiItemVariationStore, SparseVariationRegionList, Varc,
VarcComponent, VarcFlags,
},
variations::NO_VARIATION_INDEX,
},
types::{F2Dot14, GlyphId, Matrix},
FontRef, ReadError, TableProvider,
};
use crate::{
collections::SmallVec,
instance::Size,
outline::{cff, glyf, metrics::GlyphHMetrics, pen::PathStyle, DrawError, OutlinePen},
provider::MetadataProvider,
GLYF_COMPOSITE_RECURSION_LIMIT,
};
#[cfg(feature = "libm")]
#[allow(unused_imports)]
use core_maths::CoreFloat;
use super::OutlineKind;
type GlyphStack = SmallVec<GlyphId, 8>;
type CoordVec = SmallVec<F2Dot14, 64>;
type AxisIndexVec = SmallVec<u16, 64>;
type AxisValueVec = SmallVec<f32, 64>;
type DeltaVec = SmallVec<f32, 64>;
type ScalarCacheVec = SmallVec<f32, 128>;
type Affine = Matrix<f32>;
struct Scratchpad {
deltas: DeltaVec,
axis_indices: AxisIndexVec,
axis_values: AxisValueVec,
}
impl Scratchpad {
fn new() -> Self {
Self {
deltas: DeltaVec::new(),
axis_indices: AxisIndexVec::new(),
axis_values: AxisValueVec::new(),
}
}
}
struct VarcSharedContext<'a, 'b> {
font_coords: &'b [F2Dot14],
size: Size,
path_style: PathStyle,
coverage: &'b read_fonts::tables::layout::CoverageTable<'a>,
var_store: Option<&'b MultiItemVariationStore<'a>>,
store_regions: Option<(
&'b MultiItemVariationStore<'a>,
&'b SparseVariationRegionList<'a>,
)>,
}
#[derive(Clone)]
enum BaseOutlines<'a> {
Glyf(glyf::Outlines<'a>),
Cff(cff::Outlines<'a>),
}
impl<'a> BaseOutlines<'a> {
fn glyph_count(&self) -> u32 {
match self {
Self::Glyf(glyf) => glyf.glyph_count() as u32,
Self::Cff(cff) => cff.glyph_count() as u32,
}
}
fn font(&self) -> &FontRef<'a> {
match self {
Self::Glyf(glyf) => &glyf.font,
Self::Cff(cff) => &cff.font,
}
}
fn base_outline_kind(&self, glyph_id: GlyphId) -> Option<OutlineKind<'a>> {
match self {
Self::Glyf(glyf) => Some(OutlineKind::Glyf(
glyf.clone(),
glyf.outline(glyph_id).ok()?,
)),
Self::Cff(cff) => Some(OutlineKind::Cff(
cff.clone(),
glyph_id,
cff.subfont_index(glyph_id),
)),
}
}
fn base_outline_memory(&self, glyph_id: GlyphId) -> usize {
match self {
Self::Glyf(glyf) => glyf
.outline(glyph_id)
.ok()
.map(|outline| outline.required_buffer_size(super::Hinting::None))
.unwrap_or(0),
Self::Cff(..) => 0,
}
}
}
#[derive(Clone)]
pub(crate) struct Outlines<'a> {
varc: Varc<'a>,
coverage: read_fonts::tables::layout::CoverageTable<'a>,
var_store: Option<MultiItemVariationStore<'a>>,
regions: Option<SparseVariationRegionList<'a>>,
base: BaseOutlines<'a>,
glyph_metrics: GlyphHMetrics<'a>,
units_per_em: u16,
axis_count: usize,
}
#[derive(Clone, Copy)]
pub(crate) struct Outline {
pub(crate) glyph_id: GlyphId,
pub(crate) coverage_index: u16,
max_component_memory: usize,
}
impl Outline {
pub fn required_buffer_size(&self) -> usize {
self.max_component_memory
}
}
impl<'a> Outlines<'a> {
pub fn new(font: &FontRef<'a>) -> Option<Self> {
let varc = font.varc().ok()?;
if let Some(glyf) = glyf::Outlines::new(font) {
return Self::from_base(font, varc, BaseOutlines::Glyf(glyf));
}
if let Some(cff) = cff::Outlines::new(font) {
return Self::from_base(font, varc, BaseOutlines::Cff(cff));
}
None
}
fn from_base(font: &FontRef<'a>, varc: Varc<'a>, base: BaseOutlines<'a>) -> Option<Self> {
let glyph_metrics = GlyphHMetrics::new(font)?;
let units_per_em = font.head().ok()?.units_per_em();
let axis_count = font.axes().len();
let coverage = varc.coverage().ok()?;
let var_store = varc.multi_var_store().transpose().ok()?;
let regions = var_store
.as_ref()
.map(|s| s.region_list())
.transpose()
.ok()?;
Some(Self {
varc,
coverage,
var_store,
regions,
base,
glyph_metrics,
units_per_em,
axis_count,
})
}
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
pub fn glyph_count(&self) -> u32 {
self.base.glyph_count()
}
pub fn prefer_interpreter(&self) -> bool {
false
}
pub fn fractional_size_hinting(&self) -> bool {
false
}
pub fn font(&self) -> &FontRef<'a> {
self.base.font()
}
pub(crate) fn fallback_outline_kind(&self, glyph_id: GlyphId) -> Option<OutlineKind<'a>> {
self.base.base_outline_kind(glyph_id)
}
pub fn outline(&self, glyph_id: GlyphId) -> Result<Option<Outline>, ReadError> {
let Some(coverage_index) = self.coverage.get(glyph_id) else {
return Ok(None);
};
let max_component_memory = self.compute_max_component_memory(glyph_id, coverage_index)?;
Ok(Some(Outline {
glyph_id,
coverage_index,
max_component_memory,
}))
}
fn coverage_index(&self, glyph_id: GlyphId) -> Result<Option<u16>, ReadError> {
Ok(self.coverage.get(glyph_id))
}
fn compute_max_component_memory(
&self,
glyph_id: GlyphId,
coverage_index: u16,
) -> Result<usize, ReadError> {
let mut stack = GlyphStack::new();
self.max_component_memory_for_glyph(glyph_id, coverage_index, &mut stack)
}
fn max_component_memory_for_glyph(
&self,
glyph_id: GlyphId,
coverage_index: u16,
stack: &mut GlyphStack,
) -> Result<usize, ReadError> {
if stack.contains(&glyph_id) {
return Ok(0);
}
if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
return Ok(0);
}
stack.push(glyph_id);
let mut max_memory = 0usize;
let glyph = self.varc.glyph(coverage_index as usize)?;
for component in glyph.components() {
let component = component?;
let component_gid = component.gid();
let component_memory = if component_gid == glyph_id {
self.base.base_outline_memory(component_gid)
} else if let Some(coverage_index) = self.coverage_index(component_gid)? {
self.max_component_memory_for_glyph(component_gid, coverage_index, stack)?
} else {
self.base.base_outline_memory(component_gid)
};
max_memory = max_memory.max(component_memory);
}
stack.pop();
Ok(max_memory)
}
pub fn draw(
&self,
outline: &Outline,
buf: &mut [u8],
size: Size,
coords: &[F2Dot14],
path_style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<(), DrawError> {
let mut font_coords = CoordVec::new();
expand_coords(&mut font_coords, self.axis_count, coords);
let mut stack = GlyphStack::new();
let pen: &mut dyn OutlinePen = pen;
let mut scalar_cache = self
.scalar_cache_from_store(self.var_store.as_ref())?
.unwrap();
let mut scratch = Scratchpad::new();
let ctx = VarcSharedContext {
font_coords: &font_coords,
size,
path_style,
coverage: &self.coverage,
var_store: self.var_store.as_ref(),
store_regions: self.var_store.as_ref().zip(self.regions.as_ref()),
};
self.draw_glyph(
outline.glyph_id,
outline.coverage_index,
&font_coords,
Affine::IDENTITY,
&ctx,
buf,
pen,
&mut stack,
&mut scalar_cache,
&mut scratch,
)
}
pub fn draw_unscaled(
&self,
outline: &Outline,
buf: &mut [u8],
coords: &[F2Dot14],
pen: &mut impl OutlinePen,
) -> Result<i32, DrawError> {
let size = Size::unscaled();
self.draw(outline, buf, size, coords, PathStyle::default(), pen)?;
Ok(self.glyph_metrics.advance_width(outline.glyph_id, coords))
}
#[allow(clippy::too_many_arguments)]
fn draw_glyph(
&self,
glyph_id: GlyphId,
coverage_index: u16,
current_coords: &[F2Dot14],
parent_matrix: Affine,
ctx: &VarcSharedContext<'a, '_>,
buf: &mut [u8],
pen: &mut dyn OutlinePen,
stack: &mut GlyphStack,
scalar_cache: &mut ScalarCache,
scratch: &mut Scratchpad,
) -> Result<(), DrawError> {
if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
return Err(DrawError::RecursionLimitExceeded(glyph_id));
}
let glyph = self.varc.glyph(coverage_index as usize)?;
stack.push(glyph_id);
let coverage = ctx.coverage;
let store_regions = ctx.store_regions;
let mut component_coords_buffer = CoordVec::new();
let mut child_scalar_cache: Option<ScalarCache> = None;
for component in glyph.components() {
let component = component?;
if !self.component_condition_met(
&component,
current_coords,
scalar_cache,
scratch,
store_regions,
)? {
continue;
}
let component_gid = component.gid();
let flags = component.flags();
let coords_the_same = !flags.contains(VarcFlags::HAVE_AXES)
&& !flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES);
let component_coords = if coords_the_same {
current_coords
} else {
self.component_coords(
&component,
current_coords,
&mut component_coords_buffer,
scalar_cache,
scratch,
ctx.font_coords,
store_regions,
)?;
component_coords_buffer.as_slice()
};
let mut transform = *component.transform();
self.apply_transform_variations(
&component,
current_coords,
&mut transform,
scalar_cache,
scratch,
store_regions,
)?;
let scale = ctx.size.linear_scale(self.units_per_em);
let matrix = parent_matrix * scale_matrix(transform.matrix(), scale);
if component_gid != glyph_id {
if let Some(coverage_index) = coverage.get(component_gid) {
if !stack.contains(&component_gid) {
if coords_the_same {
self.draw_glyph(
component_gid,
coverage_index,
current_coords,
matrix,
ctx,
buf,
pen,
stack,
scalar_cache,
scratch,
)?;
} else {
if let Some(ref mut cache) = child_scalar_cache {
cache.values.fill(ScalarCache::INVALID);
} else {
child_scalar_cache = self.scalar_cache_from_store(ctx.var_store)?;
}
self.draw_glyph(
component_gid,
coverage_index,
component_coords,
matrix,
ctx,
buf,
pen,
stack,
child_scalar_cache.as_mut().unwrap(),
scratch,
)?;
}
continue;
}
}
}
let mut transform_pen = TransformPen::new(pen, matrix);
self.draw_base_glyph(
component_gid,
component_coords,
ctx.size,
ctx.path_style,
buf,
&mut transform_pen,
)?;
}
stack.pop();
Ok(())
}
fn draw_base_glyph(
&self,
glyph_id: GlyphId,
coords: &[F2Dot14],
size: Size,
path_style: PathStyle,
buf: &mut [u8],
pen: &mut impl OutlinePen,
) -> Result<(), DrawError> {
let Some(kind) = self.base.base_outline_kind(glyph_id) else {
return Err(DrawError::GlyphNotFound(glyph_id));
};
let settings =
crate::outline::DrawSettings::unhinted(size, crate::instance::LocationRef::new(coords))
.with_path_style(path_style)
.with_memory(Some(buf));
crate::outline::OutlineGlyph { kind }.draw(settings, pen)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn component_coords(
&self,
component: &VarcComponent<'_>,
current_coords: &[F2Dot14],
coords: &mut CoordVec,
scalar_cache: &mut ScalarCache,
scratch: &mut Scratchpad,
font_coords: &[F2Dot14],
store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
) -> Result<(), DrawError> {
let flags = component.flags();
if flags.contains(VarcFlags::RESET_UNSPECIFIED_AXES) {
expand_coords(coords, font_coords.len(), font_coords);
} else {
expand_coords(coords, current_coords.len(), current_coords);
}
if !flags.contains(VarcFlags::HAVE_AXES) {
return Ok(());
}
let axis_indices_index = component
.axis_indices_index()
.ok_or(ReadError::MalformedData("Missing axisIndicesIndex"))?;
let num_axes = self.axis_indices(axis_indices_index as usize, &mut scratch.axis_indices)?;
self.axis_values(component, num_axes, &mut scratch.axis_values)?;
if let Some(var_idx) = component.axis_values_var_index() {
let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
compute_tuple_deltas(
store,
regions,
var_idx,
current_coords,
scratch.axis_indices.len(),
scalar_cache,
&mut scratch.deltas,
)?;
for (value, delta) in scratch.axis_values.iter_mut().zip(scratch.deltas.iter()) {
*value += *delta;
}
}
for (axis_index, value) in scratch
.axis_indices
.iter()
.zip(scratch.axis_values.iter().copied())
{
let Some(slot) = coords.get_mut(*axis_index as usize) else {
return Err(DrawError::Read(ReadError::OutOfBounds));
};
let raw = value.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
*slot = F2Dot14::from_bits(raw);
}
Ok(())
}
fn axis_indices(&self, nth: usize, out: &mut AxisIndexVec) -> Result<usize, DrawError> {
let packed = self.varc.axis_indices(nth)?;
out.clear();
for value in packed.iter() {
out.push(value as u16);
}
Ok(out.len())
}
fn axis_values(
&self,
component: &VarcComponent<'_>,
count: usize,
out: &mut AxisValueVec,
) -> Result<(), DrawError> {
let Some(packed) = component.axis_values() else {
out.clear();
return Ok(());
};
out.resize_and_fill(count, 0.0);
for (slot, value) in out.iter_mut().zip(packed.iter().by_ref().take(count)) {
*slot = value as f32;
}
Ok(())
}
fn apply_transform_variations(
&self,
component: &VarcComponent<'_>,
coords: &[F2Dot14],
transform: &mut DecomposedTransform,
scalar_cache: &mut ScalarCache,
scratch: &mut Scratchpad,
store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
) -> Result<(), DrawError> {
let Some(var_idx) = component.transform_var_index() else {
return Ok(());
};
let flags = component.flags();
const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate(
VarcFlags::HAVE_TRANSLATE_X.bits()
| VarcFlags::HAVE_TRANSLATE_Y.bits()
| VarcFlags::HAVE_ROTATION.bits()
| VarcFlags::HAVE_SCALE_X.bits()
| VarcFlags::HAVE_SCALE_Y.bits()
| VarcFlags::HAVE_SKEW_X.bits()
| VarcFlags::HAVE_SKEW_Y.bits()
| VarcFlags::HAVE_TCENTER_X.bits()
| VarcFlags::HAVE_TCENTER_Y.bits(),
);
let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize;
if field_count == 0 {
return Ok(());
}
let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
compute_tuple_deltas(
store,
regions,
var_idx,
coords,
field_count,
scalar_cache,
&mut scratch.deltas,
)?;
let mut delta_iter = scratch.deltas.iter().copied();
if flags.contains(VarcFlags::HAVE_TRANSLATE_X) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_translate_x(transform.translate_x() + delta);
}
if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_translate_y(transform.translate_y() + delta);
}
if flags.contains(VarcFlags::HAVE_ROTATION) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_rotation(transform.rotation() + delta / 4096.0);
}
if flags.contains(VarcFlags::HAVE_SCALE_X) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_scale_x(transform.scale_x() + delta / 1024.0);
}
if flags.contains(VarcFlags::HAVE_SCALE_Y) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_scale_y(transform.scale_y() + delta / 1024.0);
}
const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate(
VarcFlags::HAVE_SKEW_X.bits()
| VarcFlags::HAVE_SKEW_Y.bits()
| VarcFlags::HAVE_TCENTER_X.bits()
| VarcFlags::HAVE_TCENTER_Y.bits(),
);
if flags.intersects(SKEW_OR_CENTER) {
if flags.contains(VarcFlags::HAVE_SKEW_X) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_skew_x(transform.skew_x() + delta / 4096.0);
}
if flags.contains(VarcFlags::HAVE_SKEW_Y) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_skew_y(transform.skew_y() + delta / 4096.0);
}
if flags.contains(VarcFlags::HAVE_TCENTER_X) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_center_x(transform.center_x() + delta);
}
if flags.contains(VarcFlags::HAVE_TCENTER_Y) {
let delta = delta_iter.next().unwrap_or(0.0);
transform.set_center_y(transform.center_y() + delta);
}
}
if !flags.contains(VarcFlags::HAVE_SCALE_Y) {
transform.set_scale_y(transform.scale_x());
}
Ok(())
}
fn component_condition_met(
&self,
component: &VarcComponent<'_>,
coords: &[F2Dot14],
scalar_cache: &mut ScalarCache,
scratch: &mut Scratchpad,
store_regions: Option<(&MultiItemVariationStore<'a>, &SparseVariationRegionList<'a>)>,
) -> Result<bool, DrawError> {
let Some(condition_index) = component.condition_index() else {
return Ok(true);
};
let Some(condition_list) = self.varc.condition_list() else {
return Err(DrawError::Read(ReadError::NullOffset));
};
let condition_list = condition_list?;
let condition = condition_list.conditions().get(condition_index as usize)?;
let (store, regions) = store_regions.ok_or(ReadError::NullOffset)?;
Self::eval_condition(&condition, coords, store, regions, scalar_cache, scratch)
}
fn eval_condition(
condition: &Condition<'a>,
coords: &[F2Dot14],
var_store: &MultiItemVariationStore<'a>,
regions: &SparseVariationRegionList<'a>,
scalar_cache: &mut ScalarCache,
scratch: &mut Scratchpad,
) -> Result<bool, DrawError> {
match condition {
Condition::Format1AxisRange(condition) => {
let axis_index = condition.axis_index() as usize;
let coord = coords.get(axis_index).copied().unwrap_or(F2Dot14::ZERO);
Ok(coord >= condition.filter_range_min_value()
&& coord <= condition.filter_range_max_value())
}
Condition::Format2VariableValue(condition) => {
let default_value = condition.default_value() as f32;
let var_idx = condition.var_index();
compute_tuple_deltas(
var_store,
regions,
var_idx,
coords,
1,
scalar_cache,
&mut scratch.deltas,
)?;
let delta = scratch.deltas.first().copied().unwrap_or(0.0);
Ok(default_value + delta > 0.0)
}
Condition::Format3And(condition) => {
for nested in condition.conditions().iter() {
let nested = nested?;
if !Self::eval_condition(
&nested,
coords,
var_store,
regions,
scalar_cache,
scratch,
)? {
return Ok(false);
}
}
Ok(true)
}
Condition::Format4Or(condition) => {
for nested in condition.conditions().iter() {
let nested = nested?;
if Self::eval_condition(
&nested,
coords,
var_store,
regions,
scalar_cache,
scratch,
)? {
return Ok(true);
}
}
Ok(false)
}
Condition::Format5Negate(condition) => {
let nested = condition.condition()?;
Ok(!Self::eval_condition(
&nested,
coords,
var_store,
regions,
scalar_cache,
scratch,
)?)
}
}
}
fn scalar_cache_from_store(
&self,
store: Option<&MultiItemVariationStore<'a>>,
) -> Result<Option<ScalarCache>, DrawError> {
let Some(store) = store else {
return Ok(None);
};
let region_count = store.region_list()?.region_count() as usize;
Ok(Some(ScalarCache::new(region_count)))
}
}
struct ScalarCache {
values: ScalarCacheVec,
}
impl ScalarCache {
const INVALID: f32 = 2.0;
fn new(count: usize) -> Self {
Self {
values: ScalarCacheVec::with_len(count, Self::INVALID),
}
}
fn get(&self, index: usize) -> f32 {
self.values.get(index).copied().unwrap_or(Self::INVALID)
}
fn set(&mut self, index: usize, value: f32) {
if let Some(slot) = self.values.get_mut(index) {
*slot = value;
}
}
}
fn expand_coords(out: &mut CoordVec, axis_count: usize, coords: &[F2Dot14]) {
out.resize_and_fill(axis_count, F2Dot14::ZERO);
for (slot, value) in out.iter_mut().zip(coords.iter().copied()) {
*slot = value;
}
}
fn compute_tuple_deltas(
store: &MultiItemVariationStore,
regions: &SparseVariationRegionList,
var_idx: u32,
coords: &[F2Dot14],
tuple_len: usize,
cache: &mut ScalarCache,
out: &mut DeltaVec,
) -> Result<(), ReadError> {
out.resize_and_fill(tuple_len, 0.0);
if tuple_len == 0 || var_idx == NO_VARIATION_INDEX {
return Ok(());
}
let outer = (var_idx >> 16) as usize;
let inner = (var_idx & 0xFFFF) as usize;
let data = store
.variation_data()
.get(outer)
.map_err(|_| ReadError::InvalidCollectionIndex(outer as _))?;
let region_indices = data.region_indices();
let mut deltas = data.delta_set(inner)?.fetcher();
let regions = regions.regions();
let out_slice = out.as_mut_slice();
let mut skip = 0;
for region_index in region_indices.iter() {
let region_idx = region_index.get() as usize;
let mut scalar = cache.get(region_idx);
if scalar >= 2.0 {
scalar = regions.get(region_idx)?.compute_scalar_f32(coords);
cache.set(region_idx, scalar);
}
if scalar == 0.0 {
skip += out_slice.len();
} else {
if skip != 0 {
deltas.skip(skip)?;
skip = 0;
}
deltas.add_to_f32_scaled(out_slice, scalar)?;
}
}
Ok(())
}
#[inline(always)]
fn scale_matrix(mut m: Affine, s: f32) -> Affine {
m.dx *= s;
m.dy *= s;
m
}
struct TransformPen<'a, P: OutlinePen + ?Sized> {
pen: &'a mut P,
matrix: Affine,
}
impl<'a, P: OutlinePen + ?Sized> TransformPen<'a, P> {
fn new(pen: &'a mut P, matrix: Affine) -> Self {
Self { pen, matrix }
}
#[inline(always)]
fn transform(&self, x: f32, y: f32) -> (f32, f32) {
self.matrix.transform(x, y)
}
}
impl<P: OutlinePen + ?Sized> OutlinePen for TransformPen<'_, P> {
fn move_to(&mut self, x: f32, y: f32) {
let (x, y) = self.transform(x, y);
self.pen.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
let (x, y) = self.transform(x, y);
self.pen.line_to(x, y);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
let (cx0, cy0) = self.transform(cx0, cy0);
let (x, y) = self.transform(x, y);
self.pen.quad_to(cx0, cy0, x, y);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
let (cx0, cy0) = self.transform(cx0, cy0);
let (cx1, cy1) = self.transform(cx1, cy1);
let (x, y) = self.transform(x, y);
self.pen.curve_to(cx0, cy0, cx1, cy1, x, y);
}
fn close(&mut self) {
self.pen.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::outline::pen::PathElement;
use read_fonts::{FontRef, TableProvider};
fn coord(value: f32) -> F2Dot14 {
F2Dot14::from_f32(value)
}
fn assert_close(actual: f32, expected: f32) {
let diff = (actual - expected).abs();
assert!(
diff <= 1e-6,
"expected {expected}, got {actual}, diff {diff}"
);
}
fn path_head_signature(path: &[PathElement], n: usize) -> Vec<String> {
path.iter()
.take(n)
.map(|el| match *el {
PathElement::MoveTo { x, y } => format!("M{:.2},{:.2}", x, y),
PathElement::LineTo { x, y } => format!("L{:.2},{:.2}", x, y),
PathElement::QuadTo { cx0, cy0, x, y } => {
format!("Q{:.2},{:.2} {:.2},{:.2}", cx0, cy0, x, y)
}
PathElement::CurveTo {
cx0,
cy0,
cx1,
cy1,
x,
y,
} => format!(
"C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
cx0, cy0, cx1, cy1, x, y
),
PathElement::Close => "Z".to_string(),
})
.collect()
}
#[test]
fn expand_coords_pads_and_truncates() {
let mut out = CoordVec::new();
expand_coords(&mut out, 4, &[coord(0.25), coord(-0.5)]);
assert_eq!(
out.as_slice(),
&[coord(0.25), coord(-0.5), F2Dot14::ZERO, F2Dot14::ZERO]
);
expand_coords(&mut out, 1, &[coord(0.25), coord(-0.5)]);
assert_eq!(out.as_slice(), &[coord(0.25)]);
}
#[test]
fn scale_matrix_only_scales_translation() {
let matrix = Matrix::from_elements([1.0, 2.0, 3.0, 4.0, 5.0, -6.0]);
assert_eq!(
scale_matrix(matrix, 10.0).elements(),
[1.0, 2.0, 3.0, 4.0, 50.0, -60.0]
);
}
#[test]
fn compute_tuple_deltas_no_variation_index_is_noop_after_resize() {
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
let varc = font.varc().unwrap();
let store = varc.multi_var_store().unwrap().unwrap();
let regions = store.region_list().unwrap();
let mut cache = ScalarCache::new(regions.region_count() as usize);
let mut out = DeltaVec::new();
out.push(42.0);
compute_tuple_deltas(
&store,
®ions,
NO_VARIATION_INDEX,
&[coord(0.0)],
3,
&mut cache,
&mut out,
)
.unwrap();
assert_eq!(out.as_slice(), &[0.0, 0.0, 0.0]);
compute_tuple_deltas(
&store,
®ions,
NO_VARIATION_INDEX,
&[coord(0.0)],
0,
&mut cache,
&mut out,
)
.unwrap();
assert!(out.is_empty());
}
#[test]
fn compute_tuple_deltas_invalid_outer_index_errors() {
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
let varc = font.varc().unwrap();
let store = varc.multi_var_store().unwrap().unwrap();
let regions = store.region_list().unwrap();
let mut cache = ScalarCache::new(regions.region_count() as usize);
let mut out = DeltaVec::new();
let err = compute_tuple_deltas(&store, ®ions, 0xFFFF_0000, &[], 1, &mut cache, &mut out)
.unwrap_err();
assert!(matches!(err, ReadError::InvalidCollectionIndex(_)));
}
#[test]
fn compute_tuple_deltas_matches_manual_decode() {
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
let varc = font.varc().unwrap();
let store = varc.multi_var_store().unwrap().unwrap();
let regions = store.region_list().unwrap();
let region_list = regions.regions();
let coords = [coord(0.5); 8];
let mut tried = 0usize;
for (outer, data) in store.variation_data().iter().enumerate() {
let data = data.unwrap();
let region_count = data.region_indices().len();
if region_count == 0 {
continue;
}
let delta_set_count = data.delta_sets().unwrap().count() as usize;
for inner in 0..delta_set_count.min(3) {
let decoded = data.delta_set(inner).unwrap().iter().collect::<Vec<_>>();
if decoded.is_empty() || decoded.len() % region_count != 0 {
continue;
}
let tuple_len = decoded.len() / region_count;
let var_idx = ((outer as u32) << 16) | inner as u32;
let mut cache = ScalarCache::new(regions.region_count() as usize);
let mut actual = DeltaVec::new();
compute_tuple_deltas(
&store,
®ions,
var_idx,
&coords,
tuple_len,
&mut cache,
&mut actual,
)
.unwrap();
let first = actual.as_slice().to_vec();
compute_tuple_deltas(
&store,
®ions,
var_idx,
&coords,
tuple_len,
&mut cache,
&mut actual,
)
.unwrap();
assert_eq!(actual.as_slice(), first.as_slice());
let mut expected = vec![0.0f32; tuple_len];
for (region_order, region_idx) in data.region_indices().iter().enumerate() {
let scalar = region_list
.get(region_idx.get() as usize)
.unwrap()
.compute_scalar_f32(&coords);
if scalar == 0.0 {
continue;
}
let base = region_order * tuple_len;
for (i, slot) in expected.iter_mut().enumerate() {
*slot += decoded[base + i] as f32 * scalar;
}
}
assert_eq!(actual.len(), expected.len());
for (a, e) in actual.iter().zip(expected.iter()) {
assert_close(*a, *e);
}
tried += 1;
}
}
assert!(tried > 0, "expected at least one tuple to be exercised");
}
fn apply_transform_variations_reference(
component: &VarcComponent<'_>,
coords: &[F2Dot14],
transform: &mut DecomposedTransform,
var_store: Option<&MultiItemVariationStore<'_>>,
regions: Option<&SparseVariationRegionList<'_>>,
scalar_cache: &mut ScalarCache,
deltas: &mut DeltaVec,
) -> Result<(), DrawError> {
let Some(var_idx) = component.transform_var_index() else {
return Ok(());
};
let flags = component.flags();
const TRANSFORM_MASK: VarcFlags = VarcFlags::from_bits_truncate(
VarcFlags::HAVE_TRANSLATE_X.bits()
| VarcFlags::HAVE_TRANSLATE_Y.bits()
| VarcFlags::HAVE_ROTATION.bits()
| VarcFlags::HAVE_SCALE_X.bits()
| VarcFlags::HAVE_SCALE_Y.bits()
| VarcFlags::HAVE_SKEW_X.bits()
| VarcFlags::HAVE_SKEW_Y.bits()
| VarcFlags::HAVE_TCENTER_X.bits()
| VarcFlags::HAVE_TCENTER_Y.bits(),
);
let field_count = (flags.bits() & TRANSFORM_MASK.bits()).count_ones() as usize;
if field_count == 0 {
return Ok(());
}
let store = var_store.ok_or(ReadError::NullOffset)?;
let regions = regions.ok_or(ReadError::NullOffset)?;
compute_tuple_deltas(
store,
regions,
var_idx,
coords,
field_count,
scalar_cache,
deltas,
)?;
let mut delta_iter = deltas.iter().copied();
if flags.contains(VarcFlags::HAVE_TRANSLATE_X) {
transform.set_translate_x(transform.translate_x() + delta_iter.next().unwrap_or(0.0));
}
if flags.contains(VarcFlags::HAVE_TRANSLATE_Y) {
transform.set_translate_y(transform.translate_y() + delta_iter.next().unwrap_or(0.0));
}
if flags.contains(VarcFlags::HAVE_ROTATION) {
transform
.set_rotation(transform.rotation() + delta_iter.next().unwrap_or(0.0) / 4096.0);
}
if flags.contains(VarcFlags::HAVE_SCALE_X) {
transform.set_scale_x(transform.scale_x() + delta_iter.next().unwrap_or(0.0) / 1024.0);
}
if flags.contains(VarcFlags::HAVE_SCALE_Y) {
transform.set_scale_y(transform.scale_y() + delta_iter.next().unwrap_or(0.0) / 1024.0);
}
const SKEW_OR_CENTER: VarcFlags = VarcFlags::from_bits_truncate(
VarcFlags::HAVE_SKEW_X.bits()
| VarcFlags::HAVE_SKEW_Y.bits()
| VarcFlags::HAVE_TCENTER_X.bits()
| VarcFlags::HAVE_TCENTER_Y.bits(),
);
if flags.intersects(SKEW_OR_CENTER) {
if flags.contains(VarcFlags::HAVE_SKEW_X) {
transform
.set_skew_x(transform.skew_x() + delta_iter.next().unwrap_or(0.0) / 4096.0);
}
if flags.contains(VarcFlags::HAVE_SKEW_Y) {
transform
.set_skew_y(transform.skew_y() + delta_iter.next().unwrap_or(0.0) / 4096.0);
}
if flags.contains(VarcFlags::HAVE_TCENTER_X) {
transform.set_center_x(transform.center_x() + delta_iter.next().unwrap_or(0.0));
}
if flags.contains(VarcFlags::HAVE_TCENTER_Y) {
transform.set_center_y(transform.center_y() + delta_iter.next().unwrap_or(0.0));
}
}
if !flags.contains(VarcFlags::HAVE_SCALE_Y) {
transform.set_scale_y(transform.scale_x());
}
Ok(())
}
#[test]
fn apply_transform_variations_matches_reference_path() {
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
let outlines = Outlines::new(&font).unwrap();
let coverage = outlines.varc.coverage().unwrap();
let var_store = outlines.varc.multi_var_store().transpose().unwrap();
let regions = var_store
.as_ref()
.map(|s| s.region_list())
.transpose()
.unwrap();
let region_count = regions
.as_ref()
.map(|r| r.region_count() as usize)
.unwrap_or(0);
let mut coords = CoordVec::new();
coords.resize_and_fill(outlines.axis_count, F2Dot14::ZERO);
for (i, c) in coords.iter_mut().enumerate() {
*c = match i % 4 {
0 => coord(0.5),
1 => coord(-0.5),
2 => coord(0.25),
_ => coord(-0.25),
};
}
let mut tested = 0usize;
for gid16 in coverage.iter() {
let gid: GlyphId = gid16.into();
let coverage_index = coverage.get(gid).unwrap() as usize;
let glyph = outlines.varc.glyph(coverage_index).unwrap();
for component in glyph.components() {
let component = component.unwrap();
if component.transform_var_index().is_none() {
continue;
}
let mut transform_new = *component.transform();
let mut transform_ref = *component.transform();
let mut cache_new = ScalarCache::new(region_count);
let mut cache_ref = ScalarCache::new(region_count);
let mut deltas_ref = DeltaVec::new();
let mut scratch = Scratchpad::new();
let store_regions = var_store.as_ref().zip(regions.as_ref());
outlines
.apply_transform_variations(
&component,
&coords,
&mut transform_new,
&mut cache_new,
&mut scratch,
store_regions,
)
.unwrap();
apply_transform_variations_reference(
&component,
&coords,
&mut transform_ref,
var_store.as_ref(),
regions.as_ref(),
&mut cache_ref,
&mut deltas_ref,
)
.unwrap();
assert_close(transform_new.translate_x(), transform_ref.translate_x());
assert_close(transform_new.translate_y(), transform_ref.translate_y());
assert_close(transform_new.rotation(), transform_ref.rotation());
assert_close(transform_new.scale_x(), transform_ref.scale_x());
assert_close(transform_new.scale_y(), transform_ref.scale_y());
assert_close(transform_new.skew_x(), transform_ref.skew_x());
assert_close(transform_new.skew_y(), transform_ref.skew_y());
assert_close(transform_new.center_x(), transform_ref.center_x());
assert_close(transform_new.center_y(), transform_ref.center_y());
tested += 1;
if tested >= 32 {
break;
}
}
if tested >= 32 {
break;
}
}
assert!(tested > 0, "expected at least one transformed component");
}
#[test]
fn draw_varc_6868_freetype_path_head_snapshot() {
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
let outlines = Outlines::new(&font).unwrap();
let gid = font.cmap().unwrap().map_codepoint(0x6868_u32).unwrap();
let outline = outlines.outline(gid).unwrap().unwrap();
let mut memory = vec![0u8; outline.required_buffer_size()];
let mut pen = Vec::<PathElement>::new();
outlines
.draw(
&outline,
&mut memory,
Size::unscaled(),
&[],
PathStyle::FreeType,
&mut pen,
)
.unwrap();
let head = path_head_signature(&pen, 8);
assert_eq!(
head,
vec![
"M454.56,574.77".to_string(),
"Q477.58,585.56 499.80,598.25".to_string(),
"Q522.02,610.95 543.36,625.16".to_string(),
"Q564.71,639.37 584.54,655.19".to_string(),
"Q604.38,671.02 623.03,688.41".to_string(),
"Q641.67,705.80 658.41,724.46".to_string(),
"Q675.14,743.12 689.79,763.21".to_string(),
"Q704.43,783.31 717.03,804.56".to_string(),
]
);
}
}