diff --git a/fontbe/src/glyphs.rs b/fontbe/src/glyphs.rs index b3361355e..bddfe93a2 100644 --- a/fontbe/src/glyphs.rs +++ b/fontbe/src/glyphs.rs @@ -1,17 +1,24 @@ //! 'glyf' Glyph binary compilation -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::{ + cmp, + collections::{BTreeSet, HashMap, HashSet}, +}; use fontdrasil::{orchestration::Work, types::GlyphName}; use fontir::{coords::NormalizedLocation, ir}; -use kurbo::{cubics_to_quadratic_splines, BezPath, CubicBez, PathEl}; -use log::{error, trace, warn}; +use kurbo::{cubics_to_quadratic_splines, Affine, BezPath, CubicBez, PathEl, Rect}; +use log::{trace, warn}; -use write_fonts::tables::glyf::SimpleGlyph; +use read_fonts::{ + tables::glyf::{self, Anchor, Transform}, + types::{F2Dot14, GlyphId}, +}; +use write_fonts::tables::glyf::{Bbox, Component, ComponentFlags, CompositeGlyph, SimpleGlyph}; use crate::{ error::{Error, GlyphProblem}, - orchestration::{BeWork, Context}, + orchestration::{BeWork, Context, Glyph}, }; struct GlyphWork { @@ -22,6 +29,86 @@ pub fn create_glyph_work(glyph_name: GlyphName) -> Box { Box::new(GlyphWork { glyph_name }) } +fn create_component( + context: &Context, + ref_glyph_name: &GlyphName, + transform: &Affine, +) -> Result<(Component, Bbox), GlyphProblem> { + // Obtain glyph id from static metadata + let gid = context + .ir + .get_static_metadata() + .glyph_id(ref_glyph_name) + .ok_or(GlyphProblem::NotInGlyphOrder)?; + let gid = GlyphId::new(gid as u16); + + // No known source does point anchoring so we just our transform into a 2x2 + offset + let [a, b, c, d, e, f] = transform.as_coeffs(); + let component = Component::new( + gid, + Anchor::Offset { + x: e as i16, + y: f as i16, + }, + Transform { + xx: F2Dot14::from_f32(a as f32), + yx: F2Dot14::from_f32(b as f32), + xy: F2Dot14::from_f32(c as f32), + yy: F2Dot14::from_f32(d as f32), + }, + ComponentFlags::default(), + ); + + // Bbox computation is postponed to glyph merge to ensure all glyphs are available to query + Ok((component, Bbox::default())) +} + +fn create_composite( + context: &Context, + glyph_name: &GlyphName, + default_location: &NormalizedLocation, + components: &HashMap<(GlyphName, NormalizedLocation), Affine>, +) -> Result { + let mut errors = vec![]; + let components_at_default = components + .iter() + .filter_map(|((ref_glyph_name, loc), transform)| { + if default_location == loc { + Some((ref_glyph_name, transform)) + } else { + None + } + }) + .filter_map(|(ref_glyph_name, transform)| { + create_component(context, ref_glyph_name, transform) + .map_err(|problem| { + errors.push(Error::ComponentError { + glyph: glyph_name.clone(), + referenced_glyph: ref_glyph_name.clone(), + problem, + }) + }) + .ok() + }); + + let composite = CompositeGlyph::try_from_iter(components_at_default) + .map_err(|_| { + errors.push(Error::GlyphError( + glyph_name.clone(), + GlyphProblem::NoComponents, + )) + }) + .ok(); + + if !errors.is_empty() { + return Err(Error::ComponentErrors { + glyph: glyph_name.clone(), + errors, + }); + } + Ok(composite.unwrap()) +} + impl Work for GlyphWork { fn exec(&self, context: &Context) -> Result<(), Error> { trace!("BE glyph work for {}", self.glyph_name); @@ -38,8 +125,9 @@ impl Work for GlyphWork { // TODO refine (submodel) var model if glyph locations is a subset of var model locations match glyph { - CheckedGlyph::Composite { name, .. } => { - error!("setting no glyph for {name}; composites not implemented yet"); + CheckedGlyph::Composite { name, components } => { + let composite = create_composite(context, &name, default_location, &components)?; + context.set_glyph(name, composite.into()); } CheckedGlyph::Contour { name, contours } => { // Draw the default outline of our simple glyph @@ -173,6 +261,7 @@ fn cubics_to_quadratics(glyph: CheckedGlyph) -> CheckedGlyph { enum CheckedGlyph { Composite { name: GlyphName, + components: HashMap<(GlyphName, NormalizedLocation), Affine>, }, Contour { name: GlyphName, @@ -261,7 +350,17 @@ impl TryFrom<&ir::Glyph> for CheckedGlyph { .collect(); CheckedGlyph::Contour { name, contours } } else { - CheckedGlyph::Composite { name } + let components = glyph + .sources + .iter() + .flat_map(|(location, instance)| { + instance + .components + .iter() + .map(|c| ((c.base.clone(), location.clone()), c.transform)) + }) + .collect(); + CheckedGlyph::Composite { name, components } }) } } @@ -275,3 +374,169 @@ fn path_el_type(el: &PathEl) -> &'static str { PathEl::ClosePath => "Z", } } + +fn affine_for(component: &Component) -> Affine { + let glyf::Anchor::Offset { x: dx, y: dy} = component.anchor else { + panic!("Only offset anchor is supported"); + }; + Affine::new([ + component.transform.xx.to_f32().into(), + component.transform.yx.to_f32().into(), + component.transform.xy.to_f32().into(), + component.transform.yy.to_f32().into(), + dx.into(), + dy.into(), + ]) +} + +fn bbox2rect(bbox: Bbox) -> Rect { + Rect { + x0: bbox.x_min.into(), + y0: bbox.y_min.into(), + x1: bbox.x_max.into(), + y1: bbox.y_max.into(), + } +} + +fn rect2bbox(rect: Rect) -> Bbox { + Bbox { + x_min: rect.min_x() as i16, + y_min: rect.min_y() as i16, + x_max: rect.max_x() as i16, + y_max: rect.max_y() as i16, + } +} + +struct GlyphMergeWork {} + +pub fn create_glyph_merge_work() -> Box { + Box::new(GlyphMergeWork {}) +} + +fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { + let static_metadata = context.ir.get_static_metadata(); + let glyph_order = &static_metadata.glyph_order; + + let glyphs: HashMap<_, _> = glyph_order + .iter() + .map(|gn| (gn, context.get_glyph(gn))) + .collect(); + + // Simple glyphs have bbox set. Composites don't. + // Ultimately composites are made up of simple glyphs, lets figure out the boxes + let mut bbox_acquired: HashMap = HashMap::new(); + let mut composites = glyphs + .iter() + .filter_map(|(name, glyph)| { + let glyph = glyph.as_ref(); + match glyph { + Glyph::Composite(composite) => Some(((*name).clone(), composite.clone())), + Glyph::Simple(..) => None, + } + }) + .collect::>(); + + trace!("Resolve bbox for {} composites", composites.len()); + while !composites.is_empty() { + let pending = composites.len(); + + // Hopefully we can figure out some of those bboxes! + for (glyph_name, composite) in composites.iter() { + let mut missing_boxes = false; + let boxes: Vec = composite + .components() + .filter_map(|c| { + if missing_boxes { + return None; // can't succeed + } + let ref_glyph_name = glyph_order.get_index(c.glyph.to_u16() as usize).unwrap(); + let bbox = bbox_acquired.get(ref_glyph_name).copied().or_else(|| { + glyphs + .get(ref_glyph_name) + .map(|g| g.as_ref().clone()) + .and_then(|g| match g { + Glyph::Composite(..) => None, + Glyph::Simple(simple_glyph) => Some(simple_glyph.bbox), + }) + }); + if bbox.is_none() { + trace!("Can't compute bbox for {glyph_name} because bbox for {ref_glyph_name} isn't ready yet"); + missing_boxes = true; + return None; // maybe next time? + }; + + // The transform we get here has changed because it got turned into F2Dot14 and i16 parts + // We could go get the "real" transform from IR...? + let affine = affine_for(c); + let transformed_box = affine.transform_rect_bbox(bbox2rect(bbox.unwrap())); + Some(rect2bbox(transformed_box)) + }) + .collect(); + if missing_boxes { + trace!("bbox for {glyph_name} not yet resolveable"); + continue; + } + + let bbox = boxes + .into_iter() + .reduce(|acc, e| Bbox { + x_min: cmp::min(acc.x_min, e.x_min), + y_min: cmp::min(acc.y_min, e.y_min), + x_max: cmp::max(acc.x_max, e.x_max), + y_max: cmp::max(acc.y_max, e.y_max), + }) + .unwrap(); + trace!("bbox for {glyph_name} {bbox:?}"); + bbox_acquired.insert(glyph_name.clone(), bbox); + } + + // Kerplode if we didn't make any progress this spin + composites.retain(|(gn, _)| !bbox_acquired.contains_key(gn)); + if pending == composites.len() { + panic!("Unable to make progress on composite bbox, stuck at\n{composites:?}"); + } + } + + // It'd be a shame to just throw away those nice boxes + for (glyph_name, bbox) in bbox_acquired.into_iter() { + let mut glyph = (*context.get_glyph(&glyph_name)).clone(); + let Glyph::Composite(composite) = &mut glyph else { + panic!("{glyph_name} is not a composite; we shouldn't be trying to update it"); + }; + composite.bbox = bbox; + context.set_glyph(glyph_name, glyph); + } + + Ok(()) +} + +impl Work for GlyphMergeWork { + /// Generate [glyf](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf) + /// and [loca](https://learn.microsoft.com/en-us/typography/opentype/spec/loca). + /// + /// We've already generated all the binary glyphs so all we have to do here is glue everything together. + fn exec(&self, context: &Context) -> Result<(), Error> { + compute_composite_bboxes(context)?; + + let static_metadata = context.ir.get_static_metadata(); + let glyph_order = &static_metadata.glyph_order; + + // Glue together glyf and loca + // This isn't overly memory efficient but ... fonts aren't *that* big (yet?) + let mut loca = vec![0]; + let mut glyf: Vec = Vec::new(); + glyf.reserve(1024 * 1024); + glyph_order + .iter() + .map(|gn| context.get_glyph(gn)) + .for_each(|g| { + let bytes = g.to_bytes(); + loca.push(loca.last().unwrap() + bytes.len() as u32); + glyf.extend(bytes); + }); + + context.set_glyf_loca((glyf, loca)); + + Ok(()) + } +} diff --git a/fontbe/src/orchestration.rs b/fontbe/src/orchestration.rs index bef95aefc..456233ec2 100644 --- a/fontbe/src/orchestration.rs +++ b/fontbe/src/orchestration.rs @@ -14,6 +14,13 @@ use write_fonts::{from_obj::FromTableRef, tables::glyf::CompositeGlyph}; use crate::{error::Error, paths::Paths}; +/// What exactly is being assembled from glyphs? +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GlyphMerge { + Glyf, + Loca, +} + /// Unique identifier of work. /// /// If there are no fields work is unique. @@ -22,10 +29,13 @@ use crate::{error::Error, paths::Paths}; pub enum WorkId { Features, Glyph(GlyphName), - GlyphMerge, + GlyphMerge(GlyphMerge), FinalMerge, } +const GLYF_WORK_ID: WorkId = WorkId::GlyphMerge(GlyphMerge::Glyf); +const LOCA_WORK_ID: WorkId = WorkId::GlyphMerge(GlyphMerge::Loca); + // Identifies work of any type, FE, BE, ... future optimization passes, w/e. // Useful because BE work can very reasonably depend on FE work #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -92,6 +102,7 @@ impl Glyph { } pub type BeWork = dyn Work + Send; +type GlyfLoca = (Vec, Vec); /// Read/write access to data for async work. /// @@ -114,6 +125,8 @@ pub struct Context { // TODO: variations glyphs: Arc>>>, + + glyf_loca: Arc>>>, } impl Context { @@ -125,6 +138,7 @@ impl Context { acl, features: self.features.clone(), glyphs: self.glyphs.clone(), + glyf_loca: self.glyf_loca.clone(), } } @@ -136,6 +150,7 @@ impl Context { acl: AccessControlList::read_only(), features: Arc::from(RwLock::new(None)), glyphs: Arc::from(RwLock::new(HashMap::new())), + glyf_loca: Arc::from(RwLock::new(None)), } } @@ -232,4 +247,48 @@ impl Context { self.maybe_persist(&self.paths.target_file(&id), &glyph.to_bytes()); self.set_cached_glyph(glyph_name, glyph); } + + fn set_cached_glyf_loca(&self, glyf_loca: GlyfLoca) { + let mut wl = self.glyf_loca.write(); + *wl = Some(Arc::from(glyf_loca)); + } + + pub fn get_glyf_loca(&self) -> Arc { + let ids = [GLYF_WORK_ID.into(), LOCA_WORK_ID.into()]; + self.acl.assert_read_access_to_all(&ids); + { + let rl = self.glyf_loca.read(); + if rl.is_some() { + return rl.as_ref().unwrap().clone(); + } + } + + let loca = self + .restore(&self.paths.target_file(&LOCA_WORK_ID)) + .chunks_exact(std::mem::size_of::()) + .map(|bytes| u32::from_be_bytes(bytes.try_into().unwrap())) + .collect(); + let glyf = self.restore(&self.paths.target_file(&GLYF_WORK_ID)); + + self.set_cached_glyf_loca((glyf, loca)); + let rl = self.glyf_loca.read(); + rl.as_ref().expect(MISSING_DATA).clone() + } + + pub fn set_glyf_loca(&self, glyf_loca: GlyfLoca) { + let ids = [GLYF_WORK_ID.into(), LOCA_WORK_ID.into()]; + self.acl.assert_write_access_to_all(&ids); + + let (glyf, loca) = glyf_loca; + self.maybe_persist(&self.paths.target_file(&GLYF_WORK_ID), &glyf); + self.maybe_persist( + &self.paths.target_file(&LOCA_WORK_ID), + &loca + .iter() + .flat_map(|v| v.to_be_bytes()) + .collect::>(), + ); + + self.set_cached_glyf_loca((glyf, loca)); + } } diff --git a/fontbe/src/paths.rs b/fontbe/src/paths.rs index 0d7d8bbb3..594a380c7 100644 --- a/fontbe/src/paths.rs +++ b/fontbe/src/paths.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use fontdrasil::paths::glyph_file; -use crate::orchestration::WorkId; +use crate::orchestration::{GlyphMerge, WorkId}; #[derive(Debug, Clone)] pub struct Paths { @@ -45,7 +45,8 @@ impl Paths { match id { WorkId::Features => self.build_dir.join("features.ttf"), WorkId::Glyph(name) => self.glyph_file(name.as_str()), - WorkId::GlyphMerge => self.build_dir.join("glyf.ttf"), + WorkId::GlyphMerge(GlyphMerge::Glyf) => self.build_dir.join("glyf.ttf"), + WorkId::GlyphMerge(GlyphMerge::Loca) => self.build_dir.join("loca.u32_be"), WorkId::FinalMerge => self.build_dir.join("font.ttf"), } } diff --git a/fontc/src/main.rs b/fontc/src/main.rs index adcc1d184..00bc720b7 100644 --- a/fontc/src/main.rs +++ b/fontc/src/main.rs @@ -11,8 +11,8 @@ use clap::Parser; use crossbeam_channel::{Receiver, TryRecvError}; use fontbe::{ features::FeatureWork, - glyphs::create_glyph_work, - orchestration::{AnyWorkId, Context as BeContext, WorkId as BeWorkIdentifier}, + glyphs::{create_glyph_merge_work, create_glyph_work}, + orchestration::{AnyWorkId, Context as BeContext, GlyphMerge, WorkId as BeWorkIdentifier}, paths::Paths as BePaths, }; use fontc::{ @@ -396,6 +396,52 @@ fn add_glyph_ir_jobs( Ok(()) } +fn add_glyph_merge_be_job( + change_detector: &mut ChangeDetector, + workload: &mut Workload, +) -> Result<(), Error> { + let glyphs_changed = change_detector.glyphs_changed(); + + // If no glyph has changed there isn't a lot of merging to do + if !glyphs_changed.is_empty() { + let mut dependencies: HashSet<_> = glyphs_changed + .iter() + .map(|gn| BeWorkIdentifier::Glyph(gn.clone()).into()) + .collect(); + dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); + + let id: AnyWorkId = BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf).into(); + // Write the merged glyphs and write individual glyphs that are updated, such as composites with bboxes + let write_access: AccessFn<_> = Arc::new(|id| { + matches!( + id, + AnyWorkId::Be(BeWorkIdentifier::GlyphMerge(..)) + | AnyWorkId::Be(BeWorkIdentifier::Glyph(..)) + ) + }); + workload.insert( + id, + Job { + work: create_glyph_merge_work().into(), + dependencies, + // We need to read all glyphs, even unchanged ones, plus static metadata + read_access: ReadAccess::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Be(BeWorkIdentifier::GlyphMerge(..)) + | AnyWorkId::Be(BeWorkIdentifier::Glyph(..)) + ) + })), + write_access, + }, + ); + } else { + workload.mark_success(BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf)); + workload.mark_success(BeWorkIdentifier::GlyphMerge(GlyphMerge::Loca)); + } + Ok(()) +} + fn add_glyph_be_job(workload: &mut Workload, fe_root: &FeContext, glyph_name: GlyphName) { let glyph_ir = fe_root.get_glyph_ir(&glyph_name); @@ -410,6 +456,11 @@ fn add_glyph_be_job(workload: &mut Workload, fe_root: &FeContext, glyph_name: Gl let id = AnyWorkId::Be(BeWorkIdentifier::Glyph(glyph_name.clone())); + // this job should already be a dependency of the glyph merge; if not terrible things will happen + if !workload.is_dependency(&BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf).into(), &id) { + panic!("BE glyph '{glyph_name}' is being built but not participating in glyph merge",); + } + let write_access = access_one(id.clone()); workload.insert( id, @@ -496,6 +547,13 @@ impl Workload { self.job_count += 1; } + fn is_dependency(&mut self, id: &AnyWorkId, dep: &AnyWorkId) -> bool { + self.jobs_pending + .get(id) + .map(|job| job.dependencies.contains(dep)) + .unwrap_or(false) + } + fn mark_success(&mut self, id: impl Into) { if self.success.insert(id.into()) { self.pre_success += 1; @@ -522,9 +580,22 @@ impl Workload { debug!("Updating graph for new glyph {glyph_name}"); + self.jobs_pending + .get_mut(&AnyWorkId::Be(BeWorkIdentifier::GlyphMerge( + GlyphMerge::Glyf, + ))) + .unwrap() + .dependencies + .insert(id.clone()); + add_glyph_be_job(self, fe_root, glyph_name.clone()); } } + + // When Glyf merges mark Loca too + AnyWorkId::Be(BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf)) => self.mark_success( + AnyWorkId::Be(BeWorkIdentifier::GlyphMerge(GlyphMerge::Loca)), + ), _ => (), } } @@ -679,6 +750,7 @@ fn create_workload(change_detector: &mut ChangeDetector) -> Result binary add_feature_be_job(change_detector, &mut workload)?; + add_glyph_merge_be_job(change_detector, &mut workload)?; Ok(workload) } @@ -740,7 +812,7 @@ mod tests { use filetime::FileTime; use fontbe::{ - orchestration::{AnyWorkId, Context as BeContext, WorkId as BeWorkIdentifier}, + orchestration::{AnyWorkId, Context as BeContext, GlyphMerge, WorkId as BeWorkIdentifier}, paths::Paths as BePaths, }; use fontc::work::AnyContext; @@ -753,15 +825,19 @@ mod tests { }; use indexmap::IndexSet; use read_fonts::{ - tables::glyf::{self, SimpleGlyph}, - FontData, FontRead, + tables::{ + glyf::{self, CompositeGlyph, Glyf, SimpleGlyph}, + loca::Loca, + }, + types::{F2Dot14, GlyphId}, + FontData, FontRead, FontReadWithArgs, }; use tempfile::{tempdir, TempDir}; use crate::{ add_feature_be_job, add_feature_ir_job, add_finalize_static_metadata_ir_job, - add_glyph_ir_jobs, add_init_static_metadata_ir_job, finish_successfully, init, require_dir, - Args, ChangeDetector, Config, Workload, + add_glyph_ir_jobs, add_glyph_merge_be_job, add_init_static_metadata_ir_job, + finish_successfully, init, require_dir, Args, ChangeDetector, Config, Workload, }; fn testdata_dir() -> PathBuf { @@ -784,6 +860,7 @@ mod tests { } struct TestCompile { + build_dir: PathBuf, work_completed: HashSet, glyphs_changed: IndexSet, glyphs_deleted: IndexSet, @@ -798,6 +875,7 @@ mod tests { be_context: BeContext, ) -> TestCompile { TestCompile { + build_dir: change_detector.be_paths.build_dir().to_path_buf(), work_completed: HashSet::new(), glyphs_changed: change_detector.glyphs_changed(), glyphs_deleted: change_detector.glyphs_deleted(), @@ -805,6 +883,20 @@ mod tests { _be_context: be_context, } } + + fn get_glyph_index(&self, name: &str) -> u32 { + self.fe_context + .get_static_metadata() + .glyph_id(&name.into()) + .unwrap() + } + + fn raw_glyf_loca(&self) -> (Vec, Vec) { + ( + read_file(&self.build_dir.join("glyf.ttf")), + read_file(&self.build_dir.join("loca.u32_be")), + ) + } } #[test] @@ -868,6 +960,8 @@ mod tests { add_feature_ir_job(&mut change_detector, &mut workload).unwrap(); add_feature_be_job(&mut change_detector, &mut workload).unwrap(); + add_glyph_merge_be_job(&mut change_detector, &mut workload).unwrap(); + // Try to do the work // As we currently don't stress dependencies just run one by one // This will likely need to change when we start doing things like glyphs with components @@ -941,6 +1035,8 @@ mod tests { BeWorkIdentifier::Features.into(), BeWorkIdentifier::Glyph("bar".into()).into(), BeWorkIdentifier::Glyph("plus".into()).into(), + BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf).into(), + BeWorkIdentifier::GlyphMerge(GlyphMerge::Loca).into(), ]), result.work_completed ); @@ -980,6 +1076,8 @@ mod tests { HashSet::from([ FeWorkIdentifier::Glyph("bar".into()).into(), BeWorkIdentifier::Glyph("bar".into()).into(), + BeWorkIdentifier::GlyphMerge(GlyphMerge::Glyf).into(), + BeWorkIdentifier::GlyphMerge(GlyphMerge::Loca).into(), ]), result.work_completed ); @@ -1059,9 +1157,14 @@ mod tests { #[test] fn resolve_contour_and_composite_glyph_in_non_legacy_mode() { let temp_dir = tempdir().unwrap(); - let (_, inst) = build_contour_and_composite_glyph(&temp_dir, false); + let (glyph, inst) = build_contour_and_composite_glyph(&temp_dir, false); assert!(inst.contours.is_empty(), "{inst:?}"); assert_eq!(2, inst.components.len(), "{inst:?}"); + + let raw_glyph = glyph_bytes(temp_dir.path(), glyph.name.as_str()); + let glyph = CompositeGlyph::read(FontData::new(&raw_glyph)).unwrap(); + // -1: composite, per https://learn.microsoft.com/en-us/typography/opentype/spec/glyf + assert_eq!(-1, glyph.number_of_contours()); } #[test] @@ -1092,10 +1195,13 @@ mod tests { ); } - fn glyphs(build_dir: &Path, glyph_order: &IndexSet) -> Vec> { - glyph_order - .iter() - .map(|name| glyph_bytes(build_dir, name.as_str())) + fn glyphs<'a>(raw_glyf: &'a [u8], raw_loca: &'a [u8]) -> Vec> { + let glyf = Glyf::read(FontData::new(raw_glyf)).unwrap(); + let loca = Loca::read_with_args(FontData::new(raw_loca), &true).unwrap(); + + (0..loca.len()) + .map(|gid| loca.get_glyf(GlyphId::new(gid as u16), &glyf)) + .map(|r| r.unwrap().unwrap()) .collect() } @@ -1103,25 +1209,115 @@ mod tests { fn compile_simple_glyphs_to_glyf_loca() { let temp_dir = tempdir().unwrap(); let build_dir = temp_dir.path(); - let result = compile(test_args(build_dir, "static.designspace")); + compile(test_args(build_dir, "static.designspace")); + + let raw_glyf = read_file(&build_dir.join("glyf.ttf")); + let raw_loca = read_file(&build_dir.join("loca.u32_be")); // See resources/testdata/Static-Regular.ufo/glyphs // bar, 4 points, 1 contour // plus, 12 points, 1 contour assert_eq!( vec![(4, 1), (12, 1)], - glyphs( - build_dir, - &result.fe_context.get_static_metadata().glyph_order - ) - .iter() - .map(|raw_glyph| { - match glyf::Glyph::read(FontData::new(raw_glyph)).unwrap() { + glyphs(&raw_glyf, &raw_loca) + .iter() + .map(|g| match g { glyf::Glyph::Simple(glyph) => (glyph.num_points(), glyph.number_of_contours()), glyf::Glyph::Composite(glyph) => (0, glyph.number_of_contours()), - } - }) - .collect::>() + }) + .collect::>() + ); + } + + #[test] + fn compile_composite_glyphs_has_expected_glyph_types() { + let temp_dir = tempdir().unwrap(); + let build_dir = temp_dir.path(); + let result = compile(test_args(build_dir, "glyphs2/Component.glyphs")); + let (raw_glyf, raw_loca) = result.raw_glyf_loca(); + + // Per source, glyphs should be period, comma, non_uniform_scale + // Period is simple, the other two use it as a component + let glyphs = glyphs(&raw_glyf, &raw_loca); + assert!(glyphs.len() > 1, "{glyphs:#?}"); + let period_idx = result.get_glyph_index("period"); + assert!(matches!(glyphs[0], glyf::Glyph::Simple(..)), "{glyphs:#?}"); + for (idx, glyph) in glyphs.iter().enumerate() { + if idx == period_idx.try_into().unwrap() { + assert!( + matches!(glyphs[idx], glyf::Glyph::Simple(..)), + "glyphs[{idx}] should be simple\n{glyph:#?}\nAll:\n{glyphs:#?}" + ); + } else { + assert!( + matches!(glyphs[idx], glyf::Glyph::Composite(..)), + "glyphs[{idx}] should be composite\n{glyph:#?}\nAll:\n{glyphs:#?}" + ); + } + } + } + + #[test] + fn compile_composite_glyphs_to_glyf_loca() { + let temp_dir = tempdir().unwrap(); + let build_dir = temp_dir.path(); + let result = compile(test_args(build_dir, "glyphs2/Component.glyphs")); + let (raw_glyf, raw_loca) = result.raw_glyf_loca(); + + // non-uniform scaling of period + let period_idx = result.get_glyph_index("period"); + let comma_idx = result.get_glyph_index("non_uniform_scale"); + let glyphs = glyphs(&raw_glyf, &raw_loca); + let glyf::Glyph::Composite(glyph) = &glyphs[comma_idx as usize] else { + panic!("Expected a composite\n{glyphs:#?}"); + }; + let component = glyph.components().next().unwrap(); + assert_eq!(period_idx, component.glyph.to_u16() as u32); + + // If we all work together we're a real transform! + assert_eq!(glyf::Anchor::Offset { x: -233, y: -129 }, component.anchor); + assert_eq!( + glyf::Transform { + xx: F2Dot14::from_f32(0.84519), + yx: F2Dot14::from_f32(0.58921), + xy: F2Dot14::from_f32(-1.16109), + yy: F2Dot14::from_f32(1.66553) + }, + component.transform + ); + } + + #[test] + fn compile_composite_glyphs_to_glyf_loca_applies_transform() { + let temp_dir = tempdir().unwrap(); + let build_dir = temp_dir.path(); + let result = compile(test_args(build_dir, "glyphs2/Component.glyphs")); + let (raw_glyf, raw_loca) = result.raw_glyf_loca(); + + let simple_transform_idx = result.get_glyph_index("simple_transform"); + let glyphs = glyphs(&raw_glyf, &raw_loca); + let glyf::Glyph::Composite(glyph) = &glyphs[simple_transform_idx as usize] else { + panic!("Expected a composite\n{glyphs:#?}"); + }; + + let component = glyph.components().next().unwrap(); + assert_eq!(glyf::Anchor::Offset { x: 50, y: 50 }, component.anchor); + assert_eq!( + glyf::Transform { + xx: F2Dot14::from_f32(2.0), + yx: F2Dot14::from_f32(0.0), + xy: F2Dot14::from_f32(0.0), + yy: F2Dot14::from_f32(1.5) + }, + component.transform + ); + + // Have ye bbox? + // Original period bbox is (250,50) to (375,100), then transform above is applied + // Result *should* be [550, 125, 800, 200] but F2Dot14/i16 representation changes it + assert_eq!( + [549, 125, 799, 200], + [glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()] ); } } diff --git a/resources/testdata/glyphs2/Component.glyphs b/resources/testdata/glyphs2/Component.glyphs index 1e521874f..78e0680c7 100644 --- a/resources/testdata/glyphs2/Component.glyphs +++ b/resources/testdata/glyphs2/Component.glyphs @@ -34,10 +34,10 @@ paths = ( { closed = 1; nodes = ( -"238 0 LINE", -"362 0 LINE", -"362 112 LINE", -"238 112 LINE" +"250 50 LINE", +"375 50 LINE", +"375 100 LINE", +"250 100 LINE" ); } ); @@ -79,6 +79,23 @@ width = 600; } ); unicode = 002C; +}, +{ +glyphname = simple_transform; +lastChange = "2023-01-20 20:22:39 +0000"; +layers = ( +{ +components = ( +{ +name = period; +transform = "{2, 0, 0, 1.5, 50, 50}"; +} +); +layerId = m01; +width = 600; +} +); +unicode = 002C; } ); unitsPerEm = 1000;