diff --git a/Cargo.toml b/Cargo.toml index 1f3de57c..340299b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ parking_lot = "0.12.1" fea-rs = "0.9.0" font-types = { version = "0.3.2", features = ["serde"] } read-fonts = "0.7.0" -write-fonts = "0.10.0" +write-fonts = "0.10.1" skrifa = "0.6.0" bitflags = "2.0" diff --git a/fontbe/src/avar.rs b/fontbe/src/avar.rs index 0e36c9e2..3fc16bbf 100644 --- a/fontbe/src/avar.rs +++ b/fontbe/src/avar.rs @@ -1,19 +1,21 @@ //! Generates a [avar](https://learn.microsoft.com/en-us/typography/opentype/spec/avar) table. use font_types::F2Dot14; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; use fontir::{ coords::{CoordConverter, DesignCoord}, ir::Axis, + orchestration::WorkId as FeWorkId, }; use log::debug; use write_fonts::tables::avar::{Avar, AxisValueMap, SegmentMaps}; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct AvarWork {} pub fn create_avar_work() -> Box { @@ -58,29 +60,36 @@ fn to_segment_map(axis: &Axis) -> SegmentMaps { SegmentMaps::new(mappings) } -impl Work for AvarWork { - fn id(&self) -> WorkId { - WorkId::Avar +impl Work for AvarWork { + fn id(&self) -> AnyWorkId { + WorkId::Avar.into() + } + + fn read_access(&self) -> Access { + Access::One(FeWorkId::StaticMetadata.into()) } /// Generate [avar](https://learn.microsoft.com/en-us/typography/opentype/spec/avar) /// /// See also fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_init_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); // Guard clause: don't produce avar for a static font if static_metadata.variable_axes.is_empty() { debug!("Skip avar; this is not a variable font"); return Ok(()); } - context.set_avar(Avar::new( - static_metadata - .variable_axes - .iter() - .map(to_segment_map) - .filter(|sm| !sm.axis_value_maps.is_empty()) - .collect(), - )); + context.avar.set_unconditionally( + Avar::new( + static_metadata + .variable_axes + .iter() + .map(to_segment_map) + .filter(|sm| !sm.axis_value_maps.is_empty()) + .collect(), + ) + .into(), + ); Ok(()) } } diff --git a/fontbe/src/cmap.rs b/fontbe/src/cmap.rs index 988e8741..b0279f3d 100644 --- a/fontbe/src/cmap.rs +++ b/fontbe/src/cmap.rs @@ -1,34 +1,47 @@ //! Generates a [cmap](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap) table. -use fontdrasil::orchestration::Work; +use std::sync::Arc; + +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; + use read_fonts::types::GlyphId; use write_fonts::tables::cmap::Cmap; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct CmapWork {} pub fn create_cmap_work() -> Box { Box::new(CmapWork {}) } -impl Work for CmapWork { - fn id(&self) -> WorkId { - WorkId::Cmap +impl Work for CmapWork { + fn id(&self) -> AnyWorkId { + WorkId::Cmap.into() + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::GlyphOrder) | AnyWorkId::Fe(FeWorkId::Glyph(..)) + ) + })) } /// Generate [cmap](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap) fn exec(&self, context: &Context) -> Result<(), Error> { // cmap only accomodates single codepoint : glyph mappings; collect all of those - let static_metadata = context.ir.get_final_static_metadata(); + let glyph_order = context.ir.glyph_order.get(); - let mappings = static_metadata - .glyph_order + let mappings = glyph_order .iter() - .map(|glyph_name| context.ir.get_glyph_ir(glyph_name)) + .map(|glyph_name| context.ir.glyphs.get(&FeWorkId::Glyph(glyph_name.clone()))) .enumerate() .flat_map(|(gid, glyph)| { glyph @@ -44,7 +57,7 @@ impl Work for CmapWork { }); let cmap = Cmap::from_mappings(mappings); - context.set_cmap(cmap); + context.cmap.set_unconditionally(cmap.into()); Ok(()) } } diff --git a/fontbe/src/features.rs b/fontbe/src/features.rs index 10ccd9af..f94b576c 100644 --- a/fontbe/src/features.rs +++ b/fontbe/src/features.rs @@ -1,6 +1,7 @@ //! Feature binary compilation. use std::{ + collections::HashSet, ffi::{OsStr, OsString}, fmt::Display, fs, @@ -12,16 +13,20 @@ use fea_rs::{ parse::{SourceLoadError, SourceResolver}, Compiler, GlyphMap, GlyphName as FeaRsGlyphName, }; -use fontir::{ir::Features, orchestration::Flags}; +use fontir::{ + ir::Features, + orchestration::{Flags, WorkId as FeWorkId}, +}; use log::{debug, error, warn}; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] pub struct FeatureWork {} // I did not want to make a struct @@ -111,19 +116,26 @@ fn write_debug_fea(context: &Context, is_error: bool, why: &str, fea_content: &s }; } -impl Work for FeatureWork { - fn id(&self) -> WorkId { - WorkId::Features +impl Work for FeatureWork { + fn id(&self) -> AnyWorkId { + WorkId::Features.into() + } + + fn read_access(&self) -> Access { + Access::Set(HashSet::from([ + FeWorkId::Features.into(), + FeWorkId::GlyphOrder.into(), + ])) } - fn also_completes(&self) -> Vec { - vec![WorkId::Gpos, WorkId::Gsub] + fn also_completes(&self) -> Vec { + vec![WorkId::Gpos.into(), WorkId::Gsub.into()] } fn exec(&self, context: &Context) -> Result<(), Error> { - let features = context.ir.get_features(); + let features = context.ir.features.get(); if !matches!(*features, Features::Empty) { - let glyph_order = &context.ir.get_final_static_metadata().glyph_order; + let glyph_order = &context.ir.glyph_order.get(); if glyph_order.is_empty() { warn!("Glyph order is empty; feature compile improbable"); } @@ -146,17 +158,24 @@ impl Work for FeatureWork { result.gsub.is_some() ); if let Some(gpos) = result.gpos { - context.set_gpos(gpos); + context.gpos.set_unconditionally(gpos.into()); } if let Some(gsub) = result.gsub { - context.set_gsub(gsub); + context.gsub.set_unconditionally(gsub.into()); } } else { debug!("No fea file, dull compile"); } // Enables the assumption that if the file exists features were compiled if context.flags.contains(Flags::EMIT_IR) { - fs::write(context.paths.target_file(&WorkId::Features), "1").map_err(Error::IoError)?; + fs::write( + context + .persistent_storage + .paths + .target_file(&WorkId::Features), + "1", + ) + .map_err(Error::IoError)?; } Ok(()) } diff --git a/fontbe/src/font.rs b/fontbe/src/font.rs index 3d1a938f..6931147b 100644 --- a/fontbe/src/font.rs +++ b/fontbe/src/font.rs @@ -1,6 +1,9 @@ //! Merge tables into a font -use fontdrasil::orchestration::Work; +use std::collections::HashSet; + +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use log::debug; use read_fonts::{ tables::{ @@ -15,9 +18,10 @@ use write_fonts::FontBuilder; use crate::{ error::Error, - orchestration::{to_bytes, BeWork, Bytes, Context, WorkId}, + orchestration::{to_bytes, AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct FontWork {} pub fn create_font_work() -> Box { @@ -50,52 +54,76 @@ const TABLES_TO_MERGE: &[(WorkId, Tag, TableType)] = &[ fn has(context: &Context, id: WorkId) -> bool { match id { - WorkId::Avar => context.has_avar(), - WorkId::Cmap => context.has_cmap(), - WorkId::Fvar => context.has_fvar(), - WorkId::Head => context.has_head(), - WorkId::Hhea => context.has_hhea(), - WorkId::Hmtx => context.has_hmtx(), - WorkId::Glyf => context.has_glyf_loca(), - WorkId::Gpos => context.has_gpos(), - WorkId::Gsub => context.has_gsub(), - WorkId::Gvar => context.has_gvar(), - WorkId::Loca => context.has_glyf_loca(), - WorkId::Maxp => context.has_maxp(), - WorkId::Name => context.has_name(), - WorkId::Os2 => context.has_os2(), - WorkId::Post => context.has_post(), - WorkId::Stat => context.has_stat(), + WorkId::Avar => context.avar.try_get().is_some(), + WorkId::Cmap => context.cmap.try_get().is_some(), + WorkId::Fvar => context.fvar.try_get().is_some(), + WorkId::Head => context.head.try_get().is_some(), + WorkId::Hhea => context.hhea.try_get().is_some(), + WorkId::Hmtx => context.hmtx.try_get().is_some(), + WorkId::Glyf => context.glyf.try_get().is_some(), + WorkId::Gpos => context.gpos.try_get().is_some(), + WorkId::Gsub => context.gsub.try_get().is_some(), + WorkId::Gvar => context.gvar.try_get().is_some(), + WorkId::Loca => context.loca.try_get().is_some(), + WorkId::Maxp => context.maxp.try_get().is_some(), + WorkId::Name => context.name.try_get().is_some(), + WorkId::Os2 => context.os2.try_get().is_some(), + WorkId::Post => context.post.try_get().is_some(), + WorkId::Stat => context.stat.try_get().is_some(), _ => false, } } fn bytes_for(context: &Context, id: WorkId) -> Result, Error> { + // TODO: to_vec copies :( let bytes = match id { - WorkId::Avar => to_bytes(context.get_avar().as_ref()), - WorkId::Cmap => to_bytes(&*context.get_cmap()), - WorkId::Fvar => to_bytes(&*context.get_fvar()), - WorkId::Head => to_bytes(&*context.get_head()), - WorkId::Hhea => to_bytes(&*context.get_hhea()), - WorkId::Hmtx => context.get_hmtx().get().to_vec(), - WorkId::Glyf => context.get_glyf_loca().glyf.clone(), - WorkId::Gpos => to_bytes(&*context.get_gpos()), - WorkId::Gsub => to_bytes(&*context.get_gsub()), - WorkId::Gvar => context.get_gvar().get().to_vec(), - WorkId::Loca => context.get_glyf_loca().raw_loca.clone(), - WorkId::Maxp => to_bytes(&*context.get_maxp()), - WorkId::Name => to_bytes(&*context.get_name()), - WorkId::Os2 => to_bytes(&*context.get_os2()), - WorkId::Post => to_bytes(&*context.get_post()), - WorkId::Stat => to_bytes(&*context.get_stat()), + WorkId::Avar => to_bytes(context.avar.get().as_ref()), + WorkId::Cmap => to_bytes(context.cmap.get().as_ref()), + WorkId::Fvar => to_bytes(context.fvar.get().as_ref()), + WorkId::Head => to_bytes(context.head.get().as_ref()), + WorkId::Hhea => to_bytes(context.hhea.get().as_ref()), + WorkId::Hmtx => context.hmtx.get().as_ref().get().to_vec(), + WorkId::Glyf => context.glyf.get().as_ref().get().to_vec(), + WorkId::Gpos => to_bytes(context.gpos.get().as_ref()), + WorkId::Gsub => to_bytes(context.gsub.get().as_ref()), + WorkId::Gvar => context.gvar.get().as_ref().get().to_vec(), + WorkId::Loca => context.loca.get().as_ref().get().to_vec(), + WorkId::Maxp => to_bytes(context.maxp.get().as_ref()), + WorkId::Name => to_bytes(context.name.get().as_ref()), + WorkId::Os2 => to_bytes(context.os2.get().as_ref()), + WorkId::Post => to_bytes(context.post.get().as_ref()), + WorkId::Stat => to_bytes(context.stat.get().as_ref()), _ => panic!("Missing a match for {id:?}"), }; Ok(bytes) } -impl Work for FontWork { - fn id(&self) -> WorkId { - WorkId::Font +impl Work for FontWork { + fn id(&self) -> AnyWorkId { + WorkId::Font.into() + } + + fn read_access(&self) -> Access { + Access::Set(HashSet::from([ + WorkId::Avar.into(), + WorkId::Cmap.into(), + WorkId::Fvar.into(), + WorkId::Head.into(), + WorkId::Hhea.into(), + WorkId::Hmtx.into(), + WorkId::Glyf.into(), + WorkId::Gpos.into(), + WorkId::Gsub.into(), + WorkId::Gvar.into(), + WorkId::Loca.into(), + WorkId::Maxp.into(), + WorkId::Name.into(), + WorkId::Os2.into(), + WorkId::Post.into(), + WorkId::Stat.into(), + FeWorkId::StaticMetadata.into(), + WorkId::LocaFormat.into(), + ])) } /// Glue binary tables into a font @@ -104,11 +132,7 @@ impl Work for FontWork { let mut builder = FontBuilder::default(); // A fancier implementation would mmap the files. We basic. - let is_static = context - .ir - .get_init_static_metadata() - .variable_axes - .is_empty(); + let is_static = context.ir.static_metadata.get().variable_axes.is_empty(); for (work_id, tag, table_type) in TABLES_TO_MERGE { if is_static && matches!(table_type, TableType::Variable) { debug!("Skip {tag} because this is a static font"); @@ -126,7 +150,7 @@ impl Work for FontWork { debug!("Building font"); let font = builder.build(); debug!("Assembled {} byte font", font.len()); - context.set_font(Bytes::new(font)); + context.font.set_unconditionally(font.into()); Ok(()) } } diff --git a/fontbe/src/fvar.rs b/fontbe/src/fvar.rs index df08e5fa..d21f8242 100644 --- a/fontbe/src/fvar.rs +++ b/fontbe/src/fvar.rs @@ -1,20 +1,20 @@ //! Generates a [fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar) table. -use std::collections::HashMap; - use font_types::Fixed; -use fontdrasil::orchestration::Work; -use fontir::ir::StaticMetadata; +use fontdrasil::orchestration::{Access, Work}; +use fontir::{ir::StaticMetadata, orchestration::WorkId as FeWorkId}; use log::trace; +use std::collections::HashMap; use write_fonts::tables::fvar::{AxisInstanceArrays, Fvar, InstanceRecord, VariationAxisRecord}; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; const HIDDEN_AXIS: u16 = 0x0001; +#[derive(Debug)] struct FvarWork {} pub fn create_fvar_work() -> Box { @@ -81,15 +81,19 @@ fn generate_fvar(static_metadata: &StaticMetadata) -> Option { Some(Fvar::new(axes_and_instances)) } -impl Work for FvarWork { - fn id(&self) -> WorkId { - WorkId::Fvar +impl Work for FvarWork { + fn id(&self) -> AnyWorkId { + WorkId::Fvar.into() + } + + fn read_access(&self) -> Access { + Access::One(FeWorkId::StaticMetadata.into()) } /// Generate [fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar) fn exec(&self, context: &Context) -> Result<(), Error> { - if let Some(fvar) = generate_fvar(&context.ir.get_init_static_metadata()) { - context.set_fvar(fvar); + if let Some(fvar) = generate_fvar(&context.ir.static_metadata.get()) { + context.fvar.set_unconditionally(fvar.into()); } Ok(()) } @@ -110,7 +114,6 @@ mod tests { axes.to_vec(), Default::default(), Default::default(), - Default::default(), ) .unwrap() } diff --git a/fontbe/src/glyphs.rs b/fontbe/src/glyphs.rs index 6932b340..f077ef60 100644 --- a/fontbe/src/glyphs.rs +++ b/fontbe/src/glyphs.rs @@ -3,10 +3,16 @@ //! Each glyph is built in isolation and then the fragments are collected //! and glued together to form a final table. -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + sync::Arc, +}; -use fontdrasil::{orchestration::Work, types::GlyphName}; -use fontir::{coords::NormalizedLocation, ir}; +use fontdrasil::{ + orchestration::{Access, Work}, + types::GlyphName, +}; +use fontir::{coords::NormalizedLocation, ir, orchestration::WorkId as FeWorkId}; use kurbo::{cubics_to_quadratic_splines, Affine, BezPath, CubicBez, PathEl, Point, Rect}; use log::{log_enabled, trace, warn}; @@ -26,9 +32,10 @@ use write_fonts::{ use crate::{ error::{Error, GlyphProblem}, - orchestration::{BeWork, Context, GlyfLoca, Glyph, GvarFragment, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, Glyph, GvarFragment, LocaFormat, WorkId}, }; +#[derive(Debug)] struct GlyphWork { glyph_name: GlyphName, } @@ -73,7 +80,8 @@ fn create_component( // Obtain glyph id from static metadata let gid = context .ir - .get_final_static_metadata() + .glyph_order + .get() .glyph_id(ref_glyph_name) .ok_or(GlyphProblem::NotInGlyphOrder)?; let gid = GlyphId::new(gid as u16); @@ -134,7 +142,10 @@ fn create_composite( }) .map(|(mut component, bbox)| { if !set_use_my_metrics { - let component_glyph = context.ir.get_glyph_ir(ref_glyph_name); + let component_glyph = context + .ir + .glyphs + .get(&FeWorkId::Glyph(ref_glyph_name.clone())); if let Some(default_component) = component_glyph.sources().get(default_location) { @@ -224,22 +235,43 @@ fn point_seqs_for_composite_glyph(ir_glyph: &ir::Glyph) -> HashMap for GlyphWork { - fn id(&self) -> WorkId { - WorkId::GlyfFragment(self.glyph_name.clone()) +impl Work for GlyphWork { + fn id(&self) -> AnyWorkId { + WorkId::GlyfFragment(self.glyph_name.clone()).into() + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::StaticMetadata) + | AnyWorkId::Fe(FeWorkId::GlyphOrder) + | AnyWorkId::Fe(FeWorkId::Glyph(..)) + ) + })) + } + + fn write_access(&self) -> Access { + Access::Set(HashSet::from([ + WorkId::GlyfFragment(self.glyph_name.clone()).into(), + WorkId::GvarFragment(self.glyph_name.clone()).into(), + ])) } - fn also_completes(&self) -> Vec { - vec![WorkId::GvarFragment(self.glyph_name.clone())] + fn also_completes(&self) -> Vec { + vec![WorkId::GvarFragment(self.glyph_name.clone()).into()] } fn exec(&self, context: &Context) -> Result<(), Error> { trace!("BE glyph work for '{}'", self.glyph_name); - let static_metadata = context.ir.get_final_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); let var_model = &static_metadata.variation_model; let default_location = static_metadata.default_location(); - let ir_glyph = &*context.ir.get_glyph_ir(&self.glyph_name); + let ir_glyph = &*context + .ir + .glyphs + .get(&FeWorkId::Glyph(self.glyph_name.clone())); let glyph: CheckedGlyph = ir_glyph.try_into()?; // Hopefully in time https://github.com/harfbuzz/boring-expansion-spec means we can drop this @@ -249,7 +281,9 @@ impl Work for GlyphWork { let (name, point_seqs, contour_ends) = match glyph { CheckedGlyph::Composite { name, components } => { let composite = create_composite(context, ir_glyph, default_location, &components)?; - context.set_glyph(name.clone(), composite.into()); + context + .glyphs + .set_unconditionally(Glyph::new_composite(name.clone(), composite)); let point_seqs = point_seqs_for_composite_glyph(ir_glyph); (name, point_seqs, Vec::new()) } @@ -275,7 +309,9 @@ impl Work for GlyphWork { let Some(base_glyph) = instances.get(default_location) else { return Err(Error::GlyphError(ir_glyph.name.clone(), GlyphProblem::MissingDefault)); }; - context.set_glyph(name.clone(), base_glyph.clone().into()); + context + .glyphs + .set_unconditionally(Glyph::new_simple(name.clone(), base_glyph.clone())); let mut contour_end = 0; let mut contour_ends = Vec::with_capacity(base_glyph.contours().len()); @@ -322,7 +358,10 @@ impl Work for GlyphWork { .collect::, _>>() .map_err(|e| Error::IupError(ir_glyph.name.clone(), e))?; - context.set_gvar_fragment(name, GvarFragment { deltas }); + context.gvar_fragments.set_unconditionally(GvarFragment { + glyph_name: name, + deltas, + }); Ok(()) } @@ -603,6 +642,7 @@ fn bbox2rect(bbox: Bbox) -> Rect { } } +#[derive(Debug)] struct GlyfLocaWork {} pub fn create_glyf_loca_work() -> Box { @@ -610,26 +650,24 @@ pub fn create_glyf_loca_work() -> Box { } fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_final_static_metadata(); - let glyph_order = &static_metadata.glyph_order; + let glyph_order = context.ir.glyph_order.get(); let glyphs: HashMap<_, _> = glyph_order .iter() - .map(|gn| (gn, context.get_glyph(gn))) + .map(|gn| { + ( + gn, + context.glyphs.get(&WorkId::GlyfFragment(gn.clone()).into()), + ) + }) .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, - } - }) + .values() + .filter(|glyph| matches!(glyph.as_ref(), Glyph::Composite(..))) .collect::>(); trace!("Resolve bbox for {} composites", composites.len()); @@ -637,7 +675,10 @@ fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { let pending = composites.len(); // Hopefully we can figure out some of those bboxes! - for (glyph_name, composite) in composites.iter() { + for composite in composites.iter() { + let Glyph::Composite(glyph_name, composite) = composite.as_ref() else { + panic!("Only composites should be in our vector of composites!!"); + }; let mut missing_boxes = false; let boxes: Vec<_> = composite .components() @@ -646,14 +687,14 @@ fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { if missing_boxes { return None; // can't succeed } - let ref_glyph_name = glyph_order.get_index(c.glyph.to_u16() as usize).unwrap(); + let ref_glyph_name = glyph_order.glyph_name(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(bbox2rect(simple_glyph.bbox)), + Glyph::Simple(_, simple_glyph) => Some(bbox2rect(simple_glyph.bbox)), }) }); if bbox.is_none() { @@ -680,7 +721,7 @@ fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { } // Kerplode if we didn't make any progress this spin - composites.retain(|(gn, _)| !bbox_acquired.contains_key(gn)); + composites.retain(|composite| !bbox_acquired.contains_key(composite.glyph_name())); if pending == composites.len() { panic!("Unable to make progress on composite bbox, stuck at\n{composites:?}"); } @@ -688,24 +729,50 @@ fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { // 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 { + let mut glyph = (*context + .glyphs + .get(&WorkId::GlyfFragment(glyph_name.clone()).into())) + .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.into(); // delay conversion to Bbox to avoid accumulating rounding error - context.set_glyph(glyph_name, glyph); + context.glyphs.set_unconditionally(glyph); } Ok(()) } -impl Work for GlyfLocaWork { - fn id(&self) -> WorkId { - WorkId::Glyf +impl Work for GlyfLocaWork { + fn id(&self) -> AnyWorkId { + WorkId::Glyf.into() } - fn also_completes(&self) -> Vec { - vec![WorkId::Loca, WorkId::LocaFormat] + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::StaticMetadata) + | AnyWorkId::Fe(FeWorkId::GlyphOrder) + | AnyWorkId::Be(WorkId::GlyfFragment(..)) + ) + })) + } + + fn write_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Be(WorkId::Glyf) + | AnyWorkId::Be(WorkId::Loca) + | AnyWorkId::Be(WorkId::LocaFormat) + | AnyWorkId::Be(WorkId::GlyfFragment(..)) + ) + })) + } + + fn also_completes(&self) -> Vec { + vec![WorkId::Loca.into(), WorkId::LocaFormat.into()] } /// Generate [glyf](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf) @@ -715,26 +782,37 @@ impl Work for GlyfLocaWork { fn exec(&self, context: &Context) -> Result<(), Error> { compute_composite_bboxes(context)?; - let static_metadata = context.ir.get_final_static_metadata(); - let glyph_order = &static_metadata.glyph_order; + let glyph_order = context.ir.glyph_order.get(); // Glue together glyf and loca - // We generate a long offset loca here, intent is the final merge can make it small - // and update the head.indexToLocFormat if it wishes. + // Build loca as u32 first, then shrink if it'll fit // 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); // initial size, will grow as needed glyph_order .iter() - .map(|gn| context.get_glyph(gn)) + .map(|gn| context.glyphs.get(&WorkId::GlyfFragment(gn.clone()).into())) .for_each(|g| { let bytes = g.to_bytes(); loca.push(loca.last().unwrap() + bytes.len() as u32); glyf.extend(bytes); }); - context.set_glyf_loca(GlyfLoca::new(glyf, loca)); + let loca_format = LocaFormat::new(&loca); + let loca: Vec = match loca_format { + LocaFormat::Short => loca + .iter() + .flat_map(|offset| ((offset >> 1) as u16).to_be_bytes()) + .collect(), + LocaFormat::Long => loca + .iter() + .flat_map(|offset| offset.to_be_bytes()) + .collect(), + }; + context.loca_format.set(loca_format); + context.glyf.set(glyf.into()); + context.loca.set(loca.into()); Ok(()) } diff --git a/fontbe/src/gvar.rs b/fontbe/src/gvar.rs index b4801854..13c0c3e0 100644 --- a/fontbe/src/gvar.rs +++ b/fontbe/src/gvar.rs @@ -1,8 +1,13 @@ //! Generates a [gvar](https://learn.microsoft.com/en-us/typography/opentype/spec/gvar) table. +use std::sync::Arc; + use font_types::GlyphId; -use fontdrasil::{orchestration::Work, types::GlyphName}; -use fontir::ir::StaticMetadata; +use fontdrasil::{ + orchestration::{Access, Work}, + types::GlyphName, +}; +use fontir::{ir::GlyphOrder, orchestration::WorkId as FeWorkId}; use write_fonts::{ dump_table, tables::gvar::{GlyphDeltas, GlyphVariations, Gvar}, @@ -10,9 +15,10 @@ use write_fonts::{ use crate::{ error::Error, - orchestration::{BeWork, Bytes, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct GvarWork {} pub fn create_gvar_work() -> Box { @@ -20,11 +26,10 @@ pub fn create_gvar_work() -> Box { } fn make_variations( - static_metadata: &StaticMetadata, + glyph_order: &GlyphOrder, get_deltas: impl Fn(&GlyphName) -> Vec, ) -> Vec { - static_metadata - .glyph_order + glyph_order .iter() .enumerate() .filter_map(|(gid, gn)| { @@ -38,26 +43,40 @@ fn make_variations( .collect() } -impl Work for GvarWork { - fn id(&self) -> WorkId { - WorkId::Gvar +impl Work for GvarWork { + fn id(&self) -> AnyWorkId { + WorkId::Gvar.into() + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::GlyphOrder) | AnyWorkId::Be(WorkId::GvarFragment(..)) + ) + })) } /// Generate [gvar](https://learn.microsoft.com/en-us/typography/opentype/spec/gvar) fn exec(&self, context: &Context) -> Result<(), Error> { // We built the gvar fragments alongside glyphs, now we need to glue them together into a gvar table - let static_metadata = context.ir.get_final_static_metadata(); + let glyph_order = context.ir.glyph_order.get(); - let variations: Vec<_> = make_variations(&static_metadata, |gid| { - context.get_gvar_fragment(gid).to_deltas() + let variations: Vec<_> = make_variations(&glyph_order, |glyph_name| { + context + .gvar_fragments + .get(&WorkId::GvarFragment(glyph_name.clone()).into()) + .to_deltas() }); let gvar = Gvar::new(variations).map_err(Error::GvarError)?; - let raw_gvar = Bytes::new(dump_table(&gvar).map_err(|e| Error::DumpTableError { - e, - context: "gvar".into(), - })?); - context.set_gvar(raw_gvar); + let raw_gvar = dump_table(&gvar) + .map_err(|e| Error::DumpTableError { + e, + context: "gvar".into(), + })? + .into(); + context.gvar.set_unconditionally(raw_gvar); Ok(()) } @@ -66,34 +85,20 @@ impl Work for GvarWork { #[cfg(test)] mod tests { use font_types::F2Dot14; - use fontdrasil::types::GlyphName; - use fontir::ir::StaticMetadata; - use indexmap::IndexSet; + use fontir::ir::GlyphOrder; use write_fonts::tables::{gvar::GlyphDeltas, variations::Tuple}; - use crate::test_util::axis; - use super::make_variations; - fn create_static_metadata(glyph_order: &[&str]) -> StaticMetadata { - StaticMetadata::new( - 1000, - Default::default(), - [axis(400.0, 400.0, 700.0)].to_vec(), - Default::default(), - IndexSet::from_iter(glyph_order.iter().map(|n| GlyphName::from(*n))), - Default::default(), - ) - .unwrap() - } - #[test] fn skips_empty_variations() { let glyph_with_var = "has_var"; let glyph_without_var = "no_var"; - let static_metadata = create_static_metadata(&[glyph_with_var, glyph_without_var]); + let mut glyph_order = GlyphOrder::new(); + glyph_order.insert(glyph_with_var.into()); + glyph_order.insert(glyph_without_var.into()); - let variations = make_variations(&static_metadata, |name| { + let variations = make_variations(&glyph_order, |name| { match name.as_str() { v if v == glyph_without_var => Vec::new(), // At the maximum extent (normalized pos 1.0) of our axis, add +1, +1 diff --git a/fontbe/src/head.rs b/fontbe/src/head.rs index 6993171b..3d78f568 100644 --- a/fontbe/src/head.rs +++ b/fontbe/src/head.rs @@ -1,18 +1,20 @@ //! Generates a [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head) table. -use std::env; +use std::{collections::HashSet, env}; use chrono::{DateTime, TimeZone, Utc}; use font_types::{Fixed, LongDateTime}; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use log::warn; use write_fonts::tables::head::Head; use crate::{ error::Error, - orchestration::{BeWork, Context, LocaFormat, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, LocaFormat, WorkId}, }; +#[derive(Debug)] struct HeadWork {} pub fn create_head_work() -> Box { @@ -86,15 +88,23 @@ fn apply_created_modified(head: &mut Head, created: Option>) { head.modified = LongDateTime::new(now); } -impl Work for HeadWork { - fn id(&self) -> WorkId { - WorkId::Head +impl Work for HeadWork { + fn id(&self) -> AnyWorkId { + WorkId::Head.into() + } + + fn read_access(&self) -> Access { + Access::Set(HashSet::from([ + FeWorkId::StaticMetadata.into(), + WorkId::Glyf.into(), + WorkId::LocaFormat.into(), + ])) } /// Generate [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head) fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_final_static_metadata(); - let loca_format = context.get_loca_format(); + let static_metadata = context.ir.static_metadata.get(); + let loca_format = context.loca_format.get(); let mut head = init_head( static_metadata.units_per_em, *loca_format, @@ -107,7 +117,7 @@ impl Work for HeadWork { static_metadata.misc.version_minor, ); apply_created_modified(&mut head, static_metadata.misc.created); - context.set_head(head); + context.head.set_unconditionally(head.into()); // Defer x/y Min/Max to metrics and limits job diff --git a/fontbe/src/metrics_and_limits.rs b/fontbe/src/metrics_and_limits.rs index 8b50383b..dedf9f74 100644 --- a/fontbe/src/metrics_and_limits.rs +++ b/fontbe/src/metrics_and_limits.rs @@ -4,10 +4,12 @@ use std::{ cmp::{max, min}, collections::{HashMap, HashSet}, + sync::Arc, }; use font_types::GlyphId; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use read_fonts::types::FWord; use write_fonts::{ dump_table, @@ -23,9 +25,10 @@ use write_fonts::{ use crate::{ error::Error, - orchestration::{BeWork, Bytes, Context, Glyph, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, Glyph, WorkId}, }; +#[derive(Debug)] struct MetricAndLimitWork {} pub fn create_metric_and_limit_work() -> Box { @@ -109,7 +112,7 @@ impl FontLimits { self.bbox = self.bbox.map(|b| b.union(bbox)).or(Some(bbox)); match glyph { - Glyph::Simple(simple) => { + Glyph::Simple(_, simple) => { let num_points = simple.contours().iter().map(Contour::len).sum::() as u16; let num_contours = simple.contours().len() as u16; self.max_points = max(self.max_points, num_points); @@ -126,7 +129,7 @@ impl FontLimits { }, ) } - Glyph::Composite(composite) => { + Glyph::Composite(_, composite) => { let num_components = composite.components().len() as u16; self.max_component_elements = max(self.max_component_elements, num_components); let components = Some(composite.components().iter().map(|c| c.glyph).collect()); @@ -191,13 +194,38 @@ impl FontLimits { } } -impl Work for MetricAndLimitWork { - fn id(&self) -> WorkId { - WorkId::Hmtx +impl Work for MetricAndLimitWork { + fn id(&self) -> AnyWorkId { + WorkId::Hmtx.into() + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::Glyph(..)) + | AnyWorkId::Fe(FeWorkId::GlobalMetrics) + | AnyWorkId::Fe(FeWorkId::GlyphOrder) + | AnyWorkId::Be(WorkId::GlyfFragment(..)) + | AnyWorkId::Be(WorkId::Head) + ) + })) + } + + fn write_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Be(WorkId::Hmtx) + | AnyWorkId::Be(WorkId::Hhea) + | AnyWorkId::Be(WorkId::Maxp) + | AnyWorkId::Be(WorkId::Head) + ) + })) } - fn also_completes(&self) -> Vec { - vec![WorkId::Hhea, WorkId::Maxp] + fn also_completes(&self) -> Vec { + vec![WorkId::Hhea.into(), WorkId::Maxp.into()] } /// Generate: @@ -208,27 +236,29 @@ impl Work for MetricAndLimitWork { /// /// Touchup [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head) fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_final_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); + let glyph_order = context.ir.glyph_order.get(); let default_metrics = context .ir - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); let mut glyph_limits = FontLimits::default(); - let mut long_metrics: Vec = static_metadata - .glyph_order + let mut long_metrics: Vec = glyph_order .iter() .enumerate() .map(|(gid, gn)| { let gid = GlyphId::new(gid as u16); let advance: u16 = context .ir - .get_glyph_ir(gn) + .glyphs + .get(&FeWorkId::Glyph(gn.clone())) .default_instance() .width .ot_round(); - let glyph = context.get_glyph(gn); + let glyph = context.glyphs.get(&WorkId::GlyfFragment(gn.clone()).into()); glyph_limits.update(gid, advance, &glyph); LongMetric { advance, @@ -286,20 +316,22 @@ impl Work for MetricAndLimitWork { })?, ..Default::default() }; - context.set_hhea(hhea); + context.hhea.set_unconditionally(hhea.into()); // Send hmtx out into the world let hmtx = Hmtx::new(long_metrics, lsbs); - let raw_hmtx = Bytes::new(dump_table(&hmtx).map_err(|e| Error::DumpTableError { - e, - context: "hmtx".into(), - })?); - context.set_hmtx(raw_hmtx); + let raw_hmtx = dump_table(&hmtx) + .map_err(|e| Error::DumpTableError { + e, + context: "hmtx".into(), + })? + .into(); + context.hmtx.set_unconditionally(raw_hmtx); // Might as well do maxp while we're here let composite_limits = glyph_limits.update_composite_limits(); let maxp = Maxp { - num_glyphs: static_metadata.glyph_order.len().try_into().unwrap(), + num_glyphs: glyph_order.len().try_into().unwrap(), // maxp computes it's version based on whether fields are set // if you fail to set any of them it gets angry with you so set all of them max_points: Some(glyph_limits.max_points), @@ -316,16 +348,16 @@ impl Work for MetricAndLimitWork { max_component_elements: Some(glyph_limits.max_component_elements), max_component_depth: Some(composite_limits.max_depth), }; - context.set_maxp(maxp); + context.maxp.set_unconditionally(maxp.into()); // Set x/y min/max in head - let mut head = (*context.get_head()).clone(); + let mut head = context.head.get().0.clone(); let bbox = glyph_limits.bbox.unwrap_or_default(); head.x_min = bbox.x_min; head.y_min = bbox.y_min; head.x_max = bbox.x_max; head.y_max = bbox.y_max; - context.set_head(head); + context.head.set_unconditionally(head.into()); Ok(()) } @@ -348,6 +380,7 @@ mod tests { GlyphId::new(0), 0, &crate::orchestration::Glyph::Simple( + "don't care".into(), SimpleGlyph::from_kurbo( &BezPath::from_svg("M-437,611 L-334,715 L-334,611 Z").unwrap(), ) diff --git a/fontbe/src/name.rs b/fontbe/src/name.rs index 6ff91b9f..c748f213 100644 --- a/fontbe/src/name.rs +++ b/fontbe/src/name.rs @@ -1,6 +1,7 @@ //! Generates a [name](https://learn.microsoft.com/en-us/typography/opentype/spec/name) table. -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use write_fonts::{ tables::name::{Name, NameRecord}, OffsetMarker, @@ -8,23 +9,28 @@ use write_fonts::{ use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct NameWork {} pub fn create_name_work() -> Box { Box::new(NameWork {}) } -impl Work for NameWork { - fn id(&self) -> WorkId { - WorkId::Name +impl Work for NameWork { + fn id(&self) -> AnyWorkId { + WorkId::Name.into() + } + + fn read_access(&self) -> Access { + Access::One(FeWorkId::StaticMetadata.into()) } /// Generate [name](https://learn.microsoft.com/en-us/typography/opentype/spec/name) fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_init_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); let name_records = static_metadata .names @@ -38,7 +44,9 @@ impl Work for NameWork { }) .collect::>(); - context.set_name(Name::new(name_records.into_iter().collect())); + context + .name + .set_unconditionally(Name::new(name_records.into_iter().collect()).into()); Ok(()) } } diff --git a/fontbe/src/orchestration.rs b/fontbe/src/orchestration.rs index 26564425..4025a7b6 100644 --- a/fontbe/src/orchestration.rs +++ b/fontbe/src/orchestration.rs @@ -1,20 +1,26 @@ //! Helps coordinate the graph execution for BE -use std::{collections::HashMap, fs, io, path::Path, sync::Arc}; +use std::{ + fs::File, + io::{self, BufReader, BufWriter, Read, Write}, + path::{Path, PathBuf}, + sync::Arc, +}; use font_types::{F2Dot14, Tag}; use fontdrasil::{ - orchestration::{Access, AccessControlList, Work, MISSING_DATA}, + orchestration::{Access, AccessControlList, Identifier, Work}, types::GlyphName, }; use fontir::{ - context_accessors, - orchestration::{Context as FeContext, ContextItem, Flags, WorkId as FeWorkIdentifier}, + orchestration::{ + Context as FeContext, ContextItem, ContextMap, Flags, IdAware, Persistable, + PersistentStorage, WorkId as FeWorkIdentifier, + }, variations::VariationRegion, }; use kurbo::Vec2; use log::trace; -use parking_lot::RwLock; use read_fonts::{FontData, FontRead}; use serde::{Deserialize, Serialize}; @@ -81,6 +87,8 @@ pub enum WorkId { Stat, } +impl Identifier for WorkId {} + // 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)] @@ -89,6 +97,8 @@ pub enum AnyWorkId { Be(WorkId), } +impl Identifier for AnyWorkId {} + impl AnyWorkId { pub fn unwrap_be(&self) -> &WorkId { match self { @@ -117,54 +127,132 @@ impl From for AnyWorkId { } } +impl From<&FeWorkIdentifier> for AnyWorkId { + fn from(id: &FeWorkIdentifier) -> Self { + AnyWorkId::Fe(id.clone()) + } +} + +impl From<&WorkId> for AnyWorkId { + fn from(id: &WorkId) -> Self { + AnyWorkId::Be(id.clone()) + } +} + /// #[derive(Debug, Clone)] pub enum Glyph { - Simple(SimpleGlyph), - Composite(CompositeGlyph), + Simple(GlyphName, SimpleGlyph), + Composite(GlyphName, CompositeGlyph), } -impl From for Glyph { - fn from(value: SimpleGlyph) -> Self { - Glyph::Simple(value) +impl Glyph { + pub(crate) fn new_simple(glyph_name: GlyphName, simple: SimpleGlyph) -> Glyph { + Glyph::Simple(glyph_name, simple) } -} -impl From for Glyph { - fn from(value: CompositeGlyph) -> Self { - Glyph::Composite(value) + pub(crate) fn new_composite(glyph_name: GlyphName, composite: CompositeGlyph) -> Glyph { + Glyph::Composite(glyph_name, composite) + } + + pub(crate) fn glyph_name(&self) -> &GlyphName { + match self { + Glyph::Simple(name, _) | Glyph::Composite(name, _) => name, + } } -} -impl Glyph { pub fn to_bytes(&self) -> Vec { match self { - Glyph::Simple(table) => dump_table(table), - Glyph::Composite(table) => dump_table(table), + Glyph::Simple(_, table) => dump_table(table), + Glyph::Composite(_, table) => dump_table(table), } .unwrap() } pub fn bbox(&self) -> Bbox { match self { - Glyph::Simple(table) => table.bbox, - Glyph::Composite(table) => table.bbox, + Glyph::Simple(_, table) => table.bbox, + Glyph::Composite(_, table) => table.bbox, } } pub fn is_empty(&self) -> bool { match self { - Glyph::Simple(table) => table.contours().is_empty(), - Glyph::Composite(table) => table.components().is_empty(), + Glyph::Simple(_, table) => table.contours().is_empty(), + Glyph::Composite(_, table) => table.components().is_empty(), } } } +impl IdAware for Glyph { + fn id(&self) -> AnyWorkId { + AnyWorkId::Be(WorkId::GlyfFragment(self.glyph_name().clone())) + } +} + +#[derive(Serialize, Deserialize)] +struct GlyphPersistable { + name: GlyphName, + simple: bool, + glyph: Vec, +} + +impl From<&Glyph> for GlyphPersistable { + fn from(value: &Glyph) -> Self { + match value { + Glyph::Simple(name, table) => GlyphPersistable { + name: name.clone(), + simple: true, + glyph: dump_table(table).unwrap(), + }, + Glyph::Composite(name, table) => GlyphPersistable { + name: name.clone(), + simple: false, + glyph: dump_table(table).unwrap(), + }, + } + } +} + +impl From for Glyph { + fn from(value: GlyphPersistable) -> Self { + match value.simple { + true => { + let glyph = + read_fonts::tables::glyf::SimpleGlyph::read(FontData::new(&value.glyph)) + .unwrap(); + let glyph = SimpleGlyph::from_table_ref(&glyph); + Glyph::Simple(value.name, glyph) + } + false => { + let glyph = + read_fonts::tables::glyf::CompositeGlyph::read(FontData::new(&value.glyph)) + .unwrap(); + let glyph = CompositeGlyph::from_table_ref(&glyph); + Glyph::Composite(value.name, glyph) + } + } + } +} + +impl Persistable for Glyph { + fn read(from: &mut dyn Read) -> Self { + bincode::deserialize_from::<&mut dyn Read, GlyphPersistable>(from) + .unwrap() + .into() + } + + fn write(&self, to: &mut dyn Write) { + bincode::serialize_into::<&mut dyn Write, GlyphPersistable>(to, &self.into()).unwrap(); + } +} + /// Unusually we store something other than the binary gvar per glyph. /// /// #[derive(Serialize, Deserialize, Debug)] pub struct GvarFragment { + pub glyph_name: GlyphName, /// None entries are safe to omit per IUP pub deltas: Vec<(VariationRegion, Vec>)>, } @@ -202,6 +290,22 @@ impl GvarFragment { } } +impl IdAware for GvarFragment { + fn id(&self) -> AnyWorkId { + AnyWorkId::Be(WorkId::GvarFragment(self.glyph_name.clone())) + } +} + +impl Persistable for GvarFragment { + fn read(from: &mut dyn Read) -> Self { + bincode::deserialize_from(from).unwrap() + } + + fn write(&self, to: &mut dyn io::Write) { + bincode::serialize_into(to, &self).unwrap(); + } +} + /// #[derive(Debug, Default)] struct TupleBuilder { @@ -255,85 +359,88 @@ impl LocaFormat { } } -// Free function of specific form to fit macro -pub fn loca_format_from_file(file: &Path) -> LocaFormat { - trace!("loca_format_from_file"); - let bytes = fs::read(file).unwrap(); - match bytes.first() { - Some(0) => LocaFormat::Short, - Some(1) => LocaFormat::Long, - _ => { - panic!("serialized LocaFormat is invalid") +impl Persistable for LocaFormat { + fn read(from: &mut dyn Read) -> Self { + let mut buf = Vec::new(); + from.read_to_end(&mut buf).unwrap(); + match buf.first() { + Some(0) => LocaFormat::Short, + Some(1) => LocaFormat::Long, + _ => { + panic!("serialized LocaFormat is invalid") + } } } -} -// Free function of specific form to fit macro -fn loca_format_to_bytes(format: &LocaFormat) -> Vec { - vec![*format as u8] + fn write(&self, to: &mut dyn io::Write) { + to.write_all(&[*self as u8]).unwrap(); + } } -pub type BeWork = dyn Work + Send; -pub struct GlyfLoca { - pub glyf: Vec, - pub raw_loca: Vec, - pub loca: Vec, +pub type BeWork = dyn Work + Send; + +/// Allows us to implement [Persistable] w/o violating the orphan rules +/// +/// Other than that just kind of gets in the way +pub struct BeValue(pub T); + +impl Persistable for BeValue +where + for<'a> T: FontRead<'a> + FontWrite + Validate, +{ + fn read(from: &mut dyn Read) -> Self { + let mut buf = Vec::new(); + from.read_to_end(&mut buf).unwrap(); + T::read(FontData::new(&buf)).unwrap().into() + } + + fn write(&self, to: &mut dyn io::Write) { + let bytes = dump_table(&self.0).unwrap(); + to.write_all(&bytes).unwrap(); + } } -fn raw_loca(loca: &[u32]) -> Vec { - let format = LocaFormat::new(loca); - if format == LocaFormat::Short { - loca.iter() - .flat_map(|offset| ((offset >> 1) as u16).to_be_bytes()) - .collect() - } else { - loca.iter() - .flat_map(|offset| offset.to_be_bytes()) - .collect() +impl From for BeValue +where + T: FontWrite + Validate, +{ + fn from(value: T) -> Self { + BeValue(value) } } -impl GlyfLoca { - pub fn new(glyf: Vec, loca: Vec) -> Self { - Self { - glyf, - raw_loca: raw_loca(&loca), - loca, - } +pub struct BePersistentStorage { + active: bool, + pub(crate) paths: Paths, +} + +impl PersistentStorage for BePersistentStorage { + fn active(&self) -> bool { + self.active } - pub fn read(format: LocaFormat, paths: &Paths) -> Self { - let glyf = read_entire_file(&paths.target_file(&WorkId::Glyf)); - let raw_loca = read_entire_file(&paths.target_file(&WorkId::Loca)); - let loca = if format == LocaFormat::Short { - raw_loca - .chunks_exact(std::mem::size_of::()) - .map(|bytes| u16::from_be_bytes(bytes.try_into().unwrap()) as u32 * 2) - .collect() - } else { - raw_loca - .chunks_exact(std::mem::size_of::()) - .map(|bytes| u32::from_be_bytes(bytes.try_into().unwrap())) - .collect() - }; - Self { - glyf, - raw_loca, - loca, + fn reader(&self, id: &AnyWorkId) -> Option> { + let file = self.paths.target_file(id.unwrap_be()); + if !file.exists() { + return None; } + let raw_file = File::open(file.clone()) + .map_err(|e| panic!("Unable to write {file:?} {e}")) + .unwrap(); + Some(Box::from(BufReader::new(raw_file))) } - fn write(&self, paths: &Paths) { - persist(&paths.target_file(&WorkId::Glyf), &self.glyf); - persist(&paths.target_file(&WorkId::Loca), &self.raw_loca); + fn writer(&self, id: &AnyWorkId) -> Box { + let file = self.paths.target_file(id.unwrap_be()); + let raw_file = File::create(file.clone()) + .map_err(|e| panic!("Unable to write {file:?} {e}")) + .unwrap(); + Box::from(BufWriter::new(raw_file)) } } -fn persist(file: &Path, content: &[u8]) { - fs::write(file, content) - .map_err(|e| panic!("Unable to write {file:?} {e}")) - .unwrap(); -} +type BeContextItem = ContextItem; +type BeContextMap = ContextMap; /// Read/write access to data for async work. /// @@ -343,91 +450,99 @@ fn persist(file: &Path, content: &[u8]) { pub struct Context { pub flags: Flags, - pub paths: Arc, + pub persistent_storage: Arc, // The final, fully populated, read-only FE context pub ir: Arc, - acl: AccessControlList, - // work results we've completed or restored from disk - // We create individual caches so we can return typed results from get fns - glyphs: Arc>>>, - gvar_fragments: Arc>>>, - - glyf_loca: ContextItem, - avar: ContextItem, - cmap: ContextItem, - fvar: ContextItem, - gsub: ContextItem, - gpos: ContextItem, - gvar: ContextItem, - post: ContextItem, - loca_format: ContextItem, - maxp: ContextItem, - name: ContextItem, - os2: ContextItem, - head: ContextItem, - hhea: ContextItem, - hmtx: ContextItem, - stat: ContextItem, - font: ContextItem, + pub gvar_fragments: BeContextMap, + pub glyphs: BeContextMap, + + pub avar: BeContextItem>, + pub cmap: BeContextItem>, + pub fvar: BeContextItem>, + pub glyf: BeContextItem, + pub gsub: BeContextItem>, + pub gpos: BeContextItem>, + pub gvar: BeContextItem, + pub post: BeContextItem>, + pub loca: BeContextItem, + pub loca_format: BeContextItem, + pub maxp: BeContextItem>, + pub name: BeContextItem>, + pub os2: BeContextItem>, + pub head: BeContextItem>, + pub hhea: BeContextItem>, + pub hmtx: BeContextItem, + pub stat: BeContextItem>, + pub font: BeContextItem, } impl Context { fn copy(&self, acl: AccessControlList) -> Context { + let acl = Arc::from(acl); Context { flags: self.flags, - paths: self.paths.clone(), + persistent_storage: self.persistent_storage.clone(), ir: self.ir.clone(), - acl, - glyphs: self.glyphs.clone(), - gvar_fragments: self.gvar_fragments.clone(), - glyf_loca: self.glyf_loca.clone(), - avar: self.avar.clone(), - cmap: self.cmap.clone(), - fvar: self.fvar.clone(), - gsub: self.gsub.clone(), - gpos: self.gpos.clone(), - gvar: self.gvar.clone(), - post: self.post.clone(), - loca_format: self.loca_format.clone(), - maxp: self.maxp.clone(), - name: self.name.clone(), - os2: self.os2.clone(), - head: self.head.clone(), - hhea: self.hhea.clone(), - hmtx: self.hmtx.clone(), - stat: self.stat.clone(), - font: self.font.clone(), + glyphs: self.glyphs.clone_with_acl(acl.clone()), + gvar_fragments: self.gvar_fragments.clone_with_acl(acl.clone()), + avar: self.avar.clone_with_acl(acl.clone()), + cmap: self.cmap.clone_with_acl(acl.clone()), + fvar: self.fvar.clone_with_acl(acl.clone()), + glyf: self.glyf.clone_with_acl(acl.clone()), + gsub: self.gsub.clone_with_acl(acl.clone()), + gpos: self.gpos.clone_with_acl(acl.clone()), + gvar: self.gvar.clone_with_acl(acl.clone()), + post: self.post.clone_with_acl(acl.clone()), + loca: self.loca.clone_with_acl(acl.clone()), + loca_format: self.loca_format.clone_with_acl(acl.clone()), + maxp: self.maxp.clone_with_acl(acl.clone()), + name: self.name.clone_with_acl(acl.clone()), + os2: self.os2.clone_with_acl(acl.clone()), + head: self.head.clone_with_acl(acl.clone()), + hhea: self.hhea.clone_with_acl(acl.clone()), + hmtx: self.hmtx.clone_with_acl(acl.clone()), + stat: self.stat.clone_with_acl(acl.clone()), + font: self.font.clone_with_acl(acl), } } pub fn new_root(flags: Flags, paths: Paths, ir: &fontir::orchestration::Context) -> Context { + let acl = Arc::from(AccessControlList::read_only()); + let persistent_storage = Arc::from(BePersistentStorage { + active: flags.contains(Flags::EMIT_IR), + paths, + }); Context { flags, - paths: Arc::from(paths), + persistent_storage: persistent_storage.clone(), ir: Arc::from(ir.read_only()), - acl: AccessControlList::read_only(), - glyphs: Arc::from(RwLock::new(HashMap::new())), - gvar_fragments: Arc::from(RwLock::new(HashMap::new())), - glyf_loca: Arc::from(RwLock::new(None)), - avar: Arc::from(RwLock::new(None)), - cmap: Arc::from(RwLock::new(None)), - fvar: Arc::from(RwLock::new(None)), - gpos: Arc::from(RwLock::new(None)), - gsub: Arc::from(RwLock::new(None)), - gvar: Arc::from(RwLock::new(None)), - post: Arc::from(RwLock::new(None)), - loca_format: Arc::from(RwLock::new(None)), - maxp: Arc::from(RwLock::new(None)), - name: Arc::from(RwLock::new(None)), - os2: Arc::from(RwLock::new(None)), - head: Arc::from(RwLock::new(None)), - hhea: Arc::from(RwLock::new(None)), - hmtx: Arc::from(RwLock::new(None)), - stat: Arc::from(RwLock::new(None)), - font: Arc::from(RwLock::new(None)), + glyphs: ContextMap::new(acl.clone(), persistent_storage.clone()), + gvar_fragments: ContextMap::new(acl.clone(), persistent_storage.clone()), + avar: ContextItem::new(WorkId::Avar.into(), acl.clone(), persistent_storage.clone()), + cmap: ContextItem::new(WorkId::Cmap.into(), acl.clone(), persistent_storage.clone()), + fvar: ContextItem::new(WorkId::Fvar.into(), acl.clone(), persistent_storage.clone()), + glyf: ContextItem::new(WorkId::Glyf.into(), acl.clone(), persistent_storage.clone()), + gpos: ContextItem::new(WorkId::Gpos.into(), acl.clone(), persistent_storage.clone()), + gsub: ContextItem::new(WorkId::Gsub.into(), acl.clone(), persistent_storage.clone()), + gvar: ContextItem::new(WorkId::Gvar.into(), acl.clone(), persistent_storage.clone()), + post: ContextItem::new(WorkId::Post.into(), acl.clone(), persistent_storage.clone()), + loca: ContextItem::new(WorkId::Loca.into(), acl.clone(), persistent_storage.clone()), + loca_format: ContextItem::new( + WorkId::LocaFormat.into(), + acl.clone(), + persistent_storage.clone(), + ), + maxp: ContextItem::new(WorkId::Maxp.into(), acl.clone(), persistent_storage.clone()), + name: ContextItem::new(WorkId::Name.into(), acl.clone(), persistent_storage.clone()), + os2: ContextItem::new(WorkId::Os2.into(), acl.clone(), persistent_storage.clone()), + head: ContextItem::new(WorkId::Head.into(), acl.clone(), persistent_storage.clone()), + hhea: ContextItem::new(WorkId::Hhea.into(), acl.clone(), persistent_storage.clone()), + hmtx: ContextItem::new(WorkId::Hmtx.into(), acl.clone(), persistent_storage.clone()), + stat: ContextItem::new(WorkId::Stat.into(), acl.clone(), persistent_storage.clone()), + font: ContextItem::new(WorkId::Font.into(), acl, persistent_storage), } } @@ -445,227 +560,60 @@ impl Context { /// A reasonable place to write extra files to help someone debugging pub fn debug_dir(&self) -> &Path { - self.paths.debug_dir() + self.persistent_storage.paths.debug_dir() } - // we need a self.persist for macros - fn persist(&self, file: &Path, content: &[u8]) { - persist(file, content); + pub fn font_file(&self) -> PathBuf { + self.persistent_storage.paths.target_file(&WorkId::Font) } - - pub fn read_raw(&self, id: WorkId) -> Result, io::Error> { - self.acl.assert_read_access(&id.clone().into()); - fs::read(self.paths.target_file(&id)) - } - - fn set_cached_glyph(&self, glyph_name: GlyphName, glyph: Glyph) { - let mut wl = self.glyphs.write(); - wl.insert(glyph_name, Arc::from(glyph)); - } - - pub fn get_glyph(&self, glyph_name: &GlyphName) -> Arc { - let id = WorkId::GlyfFragment(glyph_name.clone()); - self.acl.assert_read_access(&id.clone().into()); - { - let rl = self.glyphs.read(); - if let Some(glyph) = rl.get(glyph_name) { - return glyph.clone(); - } - } - - // Vec[u8] => read type => write type == all the right type - let glyph = read_entire_file(&self.paths.target_file(&id)); - let glyph = read_fonts::tables::glyf::SimpleGlyph::read(FontData::new(&glyph)).unwrap(); - let glyph = SimpleGlyph::from_table_ref(&glyph); - - self.set_cached_glyph(glyph_name.clone(), glyph.into()); - let rl = self.glyphs.read(); - rl.get(glyph_name).expect(MISSING_DATA).clone() - } - - pub fn set_glyph(&self, glyph_name: GlyphName, glyph: Glyph) { - let id = WorkId::GlyfFragment(glyph_name.clone()); - self.acl.assert_write_access(&id.clone().into()); - if self.flags.contains(Flags::EMIT_IR) { - self.persist(&self.paths.target_file(&id), &glyph.to_bytes()); - } - self.set_cached_glyph(glyph_name, glyph); - } - - fn set_cached_gvar_fragment(&self, glyph_name: GlyphName, variations: GvarFragment) { - let mut wl = self.gvar_fragments.write(); - wl.insert(glyph_name, Arc::from(variations)); - } - - pub fn get_gvar_fragment(&self, glyph_name: &GlyphName) -> Arc { - let id = WorkId::GvarFragment(glyph_name.clone()); - self.acl.assert_read_access(&id.clone().into()); - { - let rl = self.gvar_fragments.read(); - if let Some(variations) = rl.get(glyph_name) { - return variations.clone(); - } - } - - let gvar_fragment = read_entire_file(&self.paths.target_file(&id)); - let variations: GvarFragment = bincode::deserialize(&gvar_fragment).unwrap(); - - self.set_cached_gvar_fragment(glyph_name.clone(), variations); - let rl = self.gvar_fragments.read(); - rl.get(glyph_name).expect(MISSING_DATA).clone() - } - - pub fn set_gvar_fragment(&self, glyph_name: GlyphName, gvar_fragment: GvarFragment) { - let id = WorkId::GvarFragment(glyph_name.clone()); - self.acl.assert_write_access(&id.clone().into()); - - if self.flags.contains(Flags::EMIT_IR) { - let bytes = bincode::serialize(&gvar_fragment).unwrap(); - self.persist(&self.paths.target_file(&id), &bytes); - } - self.set_cached_gvar_fragment(glyph_name, gvar_fragment); - } - - pub fn get_glyf_loca(&self) -> Arc { - let ids = [ - WorkId::Glyf.into(), - WorkId::Loca.into(), - WorkId::LocaFormat.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 format = self.get_loca_format(); - set_cached( - &self.glyf_loca, - GlyfLoca::read(*format, self.paths.as_ref()), - ); - let rl = self.glyf_loca.read(); - rl.as_ref().expect(MISSING_DATA).clone() - } - - pub fn has_glyf_loca(&self) -> bool { - let ids = [ - WorkId::Glyf.into(), - WorkId::Loca.into(), - WorkId::LocaFormat.into(), - ]; - self.acl.assert_read_access_to_all(&ids); - { - let rl = self.glyf_loca.read(); - if rl.is_some() { - return true; - } - } - ids.iter() - .all(|id| self.paths.target_file(id.unwrap_be()).is_file()) - } - - pub fn set_glyf_loca(&self, glyf_loca: GlyfLoca) { - let ids = [ - WorkId::Glyf.into(), - WorkId::Loca.into(), - WorkId::LocaFormat.into(), - ]; - self.acl.assert_write_access_to_all(&ids); - - let loca_format = LocaFormat::new(&glyf_loca.loca); - if self.flags.contains(Flags::EMIT_IR) { - glyf_loca.write(self.paths.as_ref()); - } - - self.set_loca_format(loca_format); - set_cached(&self.glyf_loca, glyf_loca); - } - - // Lovely little typed accessors - context_accessors! { get_avar, set_avar, has_avar, avar, Avar, WorkId::Avar, from_file, to_bytes } - context_accessors! { get_cmap, set_cmap, has_cmap, cmap, Cmap, WorkId::Cmap, from_file, to_bytes } - context_accessors! { get_fvar, set_fvar, has_fvar, fvar, Fvar, WorkId::Fvar, from_file, to_bytes } - context_accessors! { get_loca_format, set_loca_format, has_loca_format, loca_format, LocaFormat, WorkId::LocaFormat, loca_format_from_file, loca_format_to_bytes } - context_accessors! { get_maxp, set_maxp, has_maxp, maxp, Maxp, WorkId::Maxp, from_file, to_bytes } - context_accessors! { get_name, set_name, has_name, name, Name, WorkId::Name, from_file, to_bytes } - context_accessors! { get_os2, set_os2, has_os2, os2, Os2, WorkId::Os2, from_file, to_bytes } - context_accessors! { get_post, set_post, has_post, post, Post, WorkId::Post, from_file, to_bytes } - context_accessors! { get_head, set_head, has_head, head, Head, WorkId::Head, from_file, to_bytes } - context_accessors! { get_hhea, set_hhea, has_hhea, hhea, Hhea, WorkId::Hhea, from_file, to_bytes } - context_accessors! { get_gpos, set_gpos, has_gpos, gpos, Gpos, WorkId::Gpos, from_file, to_bytes } - context_accessors! { get_gsub, set_gsub, has_gsub, gsub, Gsub, WorkId::Gsub, from_file, to_bytes } - context_accessors! { get_stat, set_stat, has_stat, stat, Stat, WorkId::Stat, from_file, to_bytes } - - // Accessors where value is raw bytes - context_accessors! { get_gvar, set_gvar, has_gvar, gvar, Bytes, WorkId::Gvar, raw_from_file, raw_to_bytes } - context_accessors! { get_hmtx, set_hmtx, has_hmtx, hmtx, Bytes, WorkId::Hmtx, raw_from_file, raw_to_bytes } - context_accessors! { get_font, set_font, has_font, font, Bytes, WorkId::Font, raw_from_file, raw_to_bytes } -} - -fn set_cached(lock: &Arc>>>, value: T) { - let mut wl = lock.write(); - *wl = Some(Arc::from(value)); -} - -fn from_file(file: &Path) -> T -where - for<'a> T: FontRead<'a>, -{ - let buf = read_entire_file(file); - T::read(FontData::new(&buf)).unwrap() } +#[derive(PartialEq)] pub struct Bytes { buf: Vec, } impl Bytes { - pub(crate) fn new(buf: Vec) -> Bytes { - Bytes { buf } - } - pub fn get(&self) -> &[u8] { &self.buf } } -// Free fn because that lets it fit into the context_accessors macro. -fn raw_from_file(file: &Path) -> Bytes { - let buf = read_entire_file(file); - Bytes { buf } +impl From> for Bytes { + fn from(buf: Vec) -> Self { + Bytes { buf } + } } -fn raw_to_bytes(table: &Bytes) -> Vec { - table.buf.clone() +impl Persistable for Bytes { + fn read(from: &mut dyn Read) -> Self { + let mut buf = Vec::new(); + from.read_to_end(&mut buf).unwrap(); + buf.into() + } + + fn write(&self, to: &mut dyn io::Write) { + to.write_all(&self.buf).unwrap(); + } } -pub(crate) fn to_bytes(table: &T) -> Vec +pub(crate) fn to_bytes(table: &BeValue) -> Vec where T: FontWrite + Validate, { - dump_table(table).unwrap() -} - -fn read_entire_file(file: &Path) -> Vec { - fs::read(file) - .map_err(|e| panic!("Unable to read {file:?} {e}")) - .unwrap() + dump_table(&table.0).unwrap() } #[cfg(test)] mod tests { + use crate::orchestration::LocaFormat; use font_types::Tag; use fontir::{ coords::NormalizedCoord, variations::{Tent, VariationRegion}, }; - use tempfile::tempdir; - - use crate::{orchestration::LocaFormat, paths::Paths}; - use super::{GlyfLoca, GvarFragment}; + use super::GvarFragment; #[test] fn no_glyphs_is_short() { @@ -690,19 +638,6 @@ mod tests { ); } - #[test] - fn round_trip_glyf_loca() { - let glyf = (0..16_u8).collect::>(); - let loca = vec![0, 4, 16]; - let gl = GlyfLoca::new(glyf.clone(), loca.clone()); - let tmp = tempdir().unwrap(); - let paths = Paths::new(tmp.path()); - gl.write(&paths); - - let gl = GlyfLoca::read(LocaFormat::Short, &paths); - assert_eq!((glyf, loca), (gl.glyf, gl.loca)); - } - fn non_default_region() -> VariationRegion { let mut region = VariationRegion::default(); region.insert( @@ -719,6 +654,7 @@ mod tests { #[test] fn keeps_if_some_deltas() { let deltas = GvarFragment { + glyph_name: "blah".into(), deltas: vec![( non_default_region(), vec![None, Some((1.0, 0.0).into()), None], @@ -731,6 +667,7 @@ mod tests { #[test] fn drops_nop_deltas() { let deltas = GvarFragment { + glyph_name: "blah".into(), deltas: vec![(non_default_region(), vec![None, None, None])], } .to_deltas(); diff --git a/fontbe/src/os2.rs b/fontbe/src/os2.rs index 34c89d42..e183c359 100644 --- a/fontbe/src/os2.rs +++ b/fontbe/src/os2.rs @@ -1,9 +1,9 @@ //! Generates a [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2) table. -use std::{cmp::Ordering, collections::HashSet}; +use std::{cmp::Ordering, collections::HashSet, sync::Arc}; -use fontdrasil::orchestration::Work; -use fontir::ir::GlobalMetricsInstance; +use fontdrasil::orchestration::{Access, Work}; +use fontir::{ir::GlobalMetricsInstance, orchestration::WorkId as FeWorkId}; use log::warn; use read_fonts::{tables::hmtx::Hmtx, FontData, TopLevelTable}; use write_fonts::{ @@ -26,7 +26,7 @@ use write_fonts::{ use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; /// Used to build a bitfield. @@ -210,6 +210,7 @@ const UNICODE_RANGES: &[(u32, u32, u32)] = &[ (0x100000, 0x10FFFD, 90), // Private Use (plane 16) ]; +#[derive(Debug)] struct Os2Work {} pub fn create_os2_work() -> Box { @@ -218,13 +219,13 @@ pub fn create_os2_work() -> Box { /// fn x_avg_char_width(context: &Context) -> Result { - let static_metadata = context.ir.get_final_static_metadata(); - let hhea = context.get_hhea(); - let raw_hmtx = context.get_hmtx(); - let num_glyphs = static_metadata.glyph_order.len() as u64; + let glyph_order = context.ir.glyph_order.get(); + let hhea = context.hhea.get(); + let raw_hmtx = context.hmtx.get(); + let num_glyphs = glyph_order.len() as u64; let hmtx = Hmtx::read( FontData::new(raw_hmtx.get()), - hhea.number_of_long_metrics, + hhea.0.number_of_long_metrics, num_glyphs as u16, ) .map_err(|_| Error::InvalidTableBytes(Hmtx::TAG))?; @@ -247,7 +248,7 @@ fn x_avg_char_width(context: &Context) -> Result { .map(|m| m.advance() as u64) .unwrap_or_default(); let (count, total) = if last_advance > 0 { - let num_short = num_glyphs - hhea.number_of_long_metrics as u64; + let num_short = num_glyphs - hhea.0.number_of_long_metrics as u64; (count + num_short, total + num_short * last_advance) } else { (count, total) @@ -747,17 +748,15 @@ impl MaxContext for &SubstitutionLookup { fn apply_max_context(os2: &mut Os2, context: &Context) { let mut max_context: u16 = 0; - if context.has_gsub() { - let gsub = context.get_gsub(); - let lookups = &gsub.lookup_list.lookups; + if let Some(gsub) = context.gsub.try_get() { + let lookups = &gsub.0.lookup_list.lookups; max_context = max_context.max(lookups.iter().fold(0, |max_ctx, lookup| { max_ctx.max((lookup as &SubstitutionLookup).max_context()) })); } - if context.has_gpos() { - let gpos = context.get_gpos(); - let lookups = &gpos.lookup_list.lookups; + if let Some(gpos) = context.gpos.try_get() { + let lookups = &gpos.0.lookup_list.lookups; warn!( "max context for gpos not implemented, {} position lookups being ignored", lookups.len() @@ -768,27 +767,51 @@ fn apply_max_context(os2: &mut Os2, context: &Context) { } fn codepoints(context: &Context) -> HashSet { - let static_metadata = context.ir.get_final_static_metadata(); + let glyph_order = context.ir.glyph_order.get(); let mut codepoints = HashSet::new(); - for glyph_name in static_metadata.glyph_order.iter() { - codepoints.extend(context.ir.get_glyph_ir(glyph_name).codepoints.iter()); + for glyph_name in glyph_order.iter() { + codepoints.extend( + context + .ir + .glyphs + .get(&FeWorkId::Glyph(glyph_name.clone())) + .codepoints + .iter(), + ); } codepoints } -impl Work for Os2Work { - fn id(&self) -> WorkId { - WorkId::Os2 +impl Work for Os2Work { + fn id(&self) -> AnyWorkId { + WorkId::Os2.into() + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + AnyWorkId::Fe(FeWorkId::Glyph(..)) + | AnyWorkId::Fe(FeWorkId::StaticMetadata) + | AnyWorkId::Fe(FeWorkId::GlyphOrder) + | AnyWorkId::Fe(FeWorkId::GlobalMetrics) + | AnyWorkId::Be(WorkId::Hhea) + | AnyWorkId::Be(WorkId::Hmtx) + | AnyWorkId::Be(WorkId::Gpos) + | AnyWorkId::Be(WorkId::Gsub) + ) + })) } /// Generate [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2) fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_final_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); let metrics = context .ir - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); let codepoints = codepoints(context); @@ -813,7 +836,7 @@ impl Work for Os2Work { apply_max_context(&mut os2, context); - context.set_os2(os2); + context.os2.set_unconditionally(os2.into()); Ok(()) } } diff --git a/fontbe/src/post.rs b/fontbe/src/post.rs index c87728ca..0247004c 100644 --- a/fontbe/src/post.rs +++ b/fontbe/src/post.rs @@ -1,23 +1,34 @@ //! Generates a [post](https://learn.microsoft.com/en-us/typography/opentype/spec/post) table. +use std::collections::HashSet; + use font_types::FWord; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use write_fonts::tables::post::Post; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct PostWork {} pub fn create_post_work() -> Box { Box::new(PostWork {}) } -impl Work for PostWork { - fn id(&self) -> WorkId { - WorkId::Post +impl Work for PostWork { + fn id(&self) -> AnyWorkId { + WorkId::Post.into() + } + + fn read_access(&self) -> Access { + Access::Set(HashSet::from([ + FeWorkId::StaticMetadata.into(), + FeWorkId::GlyphOrder.into(), + ])) } /// Generate [post](https://learn.microsoft.com/en-us/typography/opentype/spec/post) @@ -25,11 +36,12 @@ impl Work for PostWork { // For now we build a v2 table by default, like fontmake does. // TODO optionally drop glyph names with format 3.0. // TODO a more serious post - let static_metadata = context.ir.get_final_static_metadata(); - let mut post = Post::new_v2(static_metadata.glyph_order.iter().map(|g| g.as_str())); + let static_metadata = context.ir.static_metadata.get(); + let glyph_order = context.ir.glyph_order.get(); + let mut post = Post::new_v2(glyph_order.iter().map(|g| g.as_str())); post.underline_position = FWord::new(static_metadata.misc.underline_position.0 as i16); post.underline_thickness = FWord::new(static_metadata.misc.underline_thickness.0 as i16); - context.set_post(post); + context.post.set_unconditionally(post.into()); Ok(()) } } diff --git a/fontbe/src/stat.rs b/fontbe/src/stat.rs index 02f9c149..15a3b6db 100644 --- a/fontbe/src/stat.rs +++ b/fontbe/src/stat.rs @@ -3,24 +3,30 @@ use std::collections::HashMap; use font_types::NameId; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; +use fontir::orchestration::WorkId as FeWorkId; use log::trace; use write_fonts::tables::stat::{AxisRecord, Stat}; use crate::{ error::Error, - orchestration::{BeWork, Context, WorkId}, + orchestration::{AnyWorkId, BeWork, Context, WorkId}, }; +#[derive(Debug)] struct StatWork {} pub fn create_stat_work() -> Box { Box::new(StatWork {}) } -impl Work for StatWork { - fn id(&self) -> WorkId { - WorkId::Stat +impl Work for StatWork { + fn id(&self) -> AnyWorkId { + WorkId::Stat.into() + } + + fn read_access(&self) -> Access { + Access::One(FeWorkId::StaticMetadata.into()) } /// Generate [stat](https://learn.microsoft.com/en-us/typography/opentype/spec/stat) @@ -28,7 +34,7 @@ impl Work for StatWork { /// See /// Note that we support only a very simple STAT at time of writing. fn exec(&self, context: &Context) -> Result<(), Error> { - let static_metadata = context.ir.get_init_static_metadata(); + let static_metadata = context.ir.static_metadata.get(); // Guard clause: don't produce fvar for a static font if static_metadata.variable_axes.is_empty() { @@ -45,21 +51,24 @@ impl Work for StatWork { .map(|(key, name)| (name, key.name_id)) .collect(); - context.set_stat(Stat { - design_axes: static_metadata - .variable_axes - .iter() - .enumerate() - .map(|(idx, a)| AxisRecord { - axis_tag: a.tag, - axis_name_id: *reverse_names.get(&a.name).unwrap(), - axis_ordering: idx as u16, - }) - .collect::>() - .into(), - elided_fallback_name_id: Some(NameId::SUBFAMILY_NAME), - ..Default::default() - }); + context.stat.set_unconditionally( + Stat { + design_axes: static_metadata + .variable_axes + .iter() + .enumerate() + .map(|(idx, a)| AxisRecord { + axis_tag: a.tag, + axis_name_id: *reverse_names.get(&a.name).unwrap(), + axis_ordering: idx as u16, + }) + .collect::>() + .into(), + elided_fallback_name_id: Some(NameId::SUBFAMILY_NAME), + ..Default::default() + } + .into(), + ); Ok(()) } diff --git a/fontc/src/change_detector.rs b/fontc/src/change_detector.rs index 2b25f735..45976030 100644 --- a/fontc/src/change_detector.rs +++ b/fontc/src/change_detector.rs @@ -1,10 +1,14 @@ //! tracking changes during compilation -use std::{ffi::OsStr, fs, path::Path}; +use std::{ffi::OsStr, fmt::Debug, fs, path::Path}; -use fontbe::{orchestration::WorkId as BeWorkIdentifier, paths::Paths as BePaths}; +use bitflags::bitflags; +use fontbe::{ + orchestration::{AnyWorkId, WorkId as BeWorkIdentifier}, + paths::Paths as BePaths, +}; -use crate::{Config, Error}; +use crate::{work::AnyWork, workload::Workload, Config, Error}; use fontdrasil::types::GlyphName; use fontir::{ orchestration::WorkId as FeWorkIdentifier, @@ -17,8 +21,20 @@ use ufo2fontir::source::DesignSpaceIrSource; use indexmap::IndexSet; use regex::Regex; -//FIXME: clarify the role of this type. -/// Tracks changes during incremental compilation and... what, exactly? +bitflags! { + #[derive(Clone, Copy, Debug)] + pub struct BuildFlags: u32 { + /// We need to generate the direct output artifacts + const BUILD_OUTPUTS = 0b0001; + /// We need to rebuild anything that depends on us + const BUILD_DEPENDENTS = 0b0010; + const BUILD_ALL = Self::BUILD_OUTPUTS.bits() | Self::BUILD_DEPENDENTS.bits(); + } +} + +/// Figures out what changed and helps create work to build updated versions +/// +/// Uses [Source] to abstract over whether source in .glyphs, .designspace, etc. pub struct ChangeDetector { glyph_name_filter: Option, ir_paths: IrPaths, @@ -29,13 +45,18 @@ pub struct ChangeDetector { emit_ir: bool, } +impl Debug for ChangeDetector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ChangeDetector") + } +} + impl ChangeDetector { pub fn new( config: Config, ir_paths: IrPaths, prev_inputs: Input, ) -> Result { - // What sources are we dealing with? let mut ir_source = ir_source(&config.args.source)?; let mut current_inputs = ir_source.inputs().map_err(Error::FontIrError)?; let be_paths = BePaths::new(ir_paths.build_dir()); @@ -88,19 +109,97 @@ impl ChangeDetector { &self.be_paths } - pub fn init_static_metadata_ir_change(&self) -> bool { + fn target_exists(&self, work_id: &AnyWorkId) -> bool { + match work_id { + AnyWorkId::Fe(work_id) => self.ir_paths.target_file(work_id).is_file(), + AnyWorkId::Be(work_id) => self.be_paths.target_file(work_id).is_file(), + } + } + + fn input_changed(&self, work_id: &AnyWorkId) -> bool { + match work_id { + AnyWorkId::Fe(FeWorkIdentifier::StaticMetadata) => { + self.current_inputs.static_metadata != self.prev_inputs.static_metadata + } + AnyWorkId::Fe(FeWorkIdentifier::GlobalMetrics) => { + self.current_inputs.global_metrics != self.prev_inputs.global_metrics + } + AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(glyph_name)) => { + self.current_inputs.glyphs.get(glyph_name) + != self.prev_inputs.glyphs.get(glyph_name) + } + AnyWorkId::Be(BeWorkIdentifier::GvarFragment(glyph_name)) => { + self.current_inputs.glyphs.get(glyph_name) + != self.prev_inputs.glyphs.get(glyph_name) + } + _ => panic!("input_changed does not yet support {work_id:?}"), + } + } + + fn output_exists(&self, work: &AnyWork) -> bool { + self.target_exists(&work.id()) + && work + .also_completes() + .iter() + .all(|id| self.target_exists(id)) + } + + /// Not all work ... works ... with this method; notably muts support input_changed. + pub(crate) fn simple_should_run(&self, work: &AnyWork) -> bool { + let work_id = work.id(); + !self.output_exists(work) || self.input_changed(&work_id) + } + + /// Simple work has simple, static, dependencies. Anything that depends on all-of-type + /// (e.g. all glyph ir) is not (yet) amenable to this path. + pub fn add_simple_work(&self, workload: &mut Workload, work: AnyWork) { + let output_exists = self.output_exists(&work); + let input_changed = self.input_changed(&work.id()); + let run = input_changed || !output_exists; + workload.add(work, run); + } + + pub fn create_workload(&mut self) -> Result { + let mut workload = Workload::new(self); + + // Create work roughly in the order it would typically occur + // Work is eligible to run as soon as all dependencies are complete + // so this is NOT the definitive execution order + + let source = self.ir_source.as_ref(); + + // Source => IR + self.add_simple_work( + &mut workload, + source + .create_static_metadata_work(&self.current_inputs)? + .into(), + ); + self.add_simple_work( + &mut workload, + source + .create_global_metric_work(&self.current_inputs)? + .into(), + ); + + Ok(workload) + } + + // TODO: could this be solved based on work id and also completes? + + pub fn static_metadata_ir_change(&self) -> bool { self.current_inputs.static_metadata != self.prev_inputs.static_metadata || !self .ir_paths - .target_file(&FeWorkIdentifier::InitStaticMetadata) + .target_file(&FeWorkIdentifier::StaticMetadata) .is_file() } - pub fn final_static_metadata_ir_change(&self) -> bool { + pub fn glyph_order_ir_change(&self) -> bool { self.current_inputs.static_metadata != self.prev_inputs.static_metadata || !self .ir_paths - .target_file(&FeWorkIdentifier::FinalizeStaticMetadata) + .target_file(&FeWorkIdentifier::GlyphOrder) .is_file() } @@ -113,12 +212,16 @@ impl ChangeDetector { } pub fn feature_ir_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.glyph_order_ir_change() || self.current_inputs.features != self.prev_inputs.features || !self .ir_paths .target_file(&FeWorkIdentifier::Features) .is_file() + || !self + .ir_paths + .target_file(&FeWorkIdentifier::Kerning) + .is_file() } pub fn feature_be_change(&self) -> bool { @@ -130,7 +233,8 @@ impl ChangeDetector { } pub fn kerning_ir_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.static_metadata_ir_change() + || self.glyph_order_ir_change() || self.current_inputs.features != self.prev_inputs.features || !self .ir_paths @@ -139,29 +243,30 @@ impl ChangeDetector { } pub fn avar_be_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.static_metadata_ir_change() || !self.be_paths.target_file(&BeWorkIdentifier::Avar).is_file() } pub fn stat_be_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.static_metadata_ir_change() || !self.be_paths.target_file(&BeWorkIdentifier::Stat).is_file() } pub fn fvar_be_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.static_metadata_ir_change() || !self.be_paths.target_file(&BeWorkIdentifier::Fvar).is_file() } pub fn post_be_change(&self) -> bool { - self.final_static_metadata_ir_change() + self.static_metadata_ir_change() + || self.glyph_order_ir_change() || !self.be_paths.target_file(&BeWorkIdentifier::Post).is_file() } pub fn glyphs_changed(&self) -> IndexSet { let glyph_iter = self.current_inputs.glyphs.iter(); - if self.init_static_metadata_ir_change() { + if self.static_metadata_ir_change() || self.glyph_order_ir_change() { return glyph_iter.map(|(name, _)| name).cloned().collect(); } glyph_iter diff --git a/fontc/src/error.rs b/fontc/src/error.rs index 1bf94610..2776301d 100644 --- a/fontc/src/error.rs +++ b/fontc/src/error.rs @@ -21,4 +21,6 @@ pub enum Error { TasksFailed(Vec<(AnyWorkId, String)>), #[error("Invalid regex")] BadRegex(#[from] regex::Error), + #[error("Unable to proceed")] + UnableToProceed, } diff --git a/fontc/src/lib.rs b/fontc/src/lib.rs index 2ada6389..43ef30ea 100644 --- a/fontc/src/lib.rs +++ b/fontc/src/lib.rs @@ -12,11 +12,9 @@ pub use change_detector::ChangeDetector; pub use config::Config; pub use error::Error; -use work::ReadAccess; -use workload::{Job, Workload}; +use workload::Workload; use std::{ - collections::HashSet, fs, io, path::{Path, PathBuf}, }; @@ -32,18 +30,13 @@ use fontbe::{ head::create_head_work, metrics_and_limits::create_metric_and_limit_work, name::create_name_work, - orchestration::{AnyWorkId, WorkId as BeWorkIdentifier}, os2::create_os2_work, post::create_post_work, stat::create_stat_work, }; -use fontdrasil::{orchestration::Access, types::GlyphName}; -use fontir::{ - glyph::create_finalize_static_metadata_work, - orchestration::{Context as FeContext, WorkId as FeWorkIdentifier}, - source::DeleteWork, -}; +use fontdrasil::types::GlyphName; +use fontir::{glyph::create_glyph_order_work, source::DeleteWork}; use fontbe::orchestration::Context as BeContext; use fontbe::paths::Paths as BePaths; @@ -82,723 +75,212 @@ pub fn init_paths(args: &Args) -> Result<(IrPaths, BePaths), Error> { } pub fn write_font_file(args: &Args, be_context: &BeContext) -> Result<(), Error> { - // if IR is off the font didn't get written yet, otherwise it's done already - let font_file = be_context.paths.target_file(&BeWorkIdentifier::Font); + // if IR is off the font didn't get written yet (nothing did), otherwise it's done already + let font_file = be_context.font_file(); if !args.emit_ir { - fs::write(font_file, be_context.get_font().get()).map_err(Error::IoError)?; + fs::write(font_file, be_context.font.get().get()).map_err(Error::IoError)?; } else if !font_file.exists() { return Err(Error::FileExpected(font_file)); } Ok(()) } -fn add_init_static_metadata_ir_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = change_detector - .ir_source() - .create_static_metadata_work(change_detector.current_inputs())?; - if change_detector.init_static_metadata_ir_change() { - let id: AnyWorkId = work.id().into(); - let write_access = Access::one(id.clone()); - workload.insert( - id, - Job { - work: work.into(), - dependencies: HashSet::new(), - read_access: ReadAccess::Dependencies, - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - } - Ok(()) -} - -fn add_finalize_static_metadata_ir_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_finalize_static_metadata_work(); - if change_detector.final_static_metadata_ir_change() { - let mut dependencies: HashSet<_> = change_detector - .glyphs_changed() - .iter() - .map(|gn| FeWorkIdentifier::Glyph(gn.clone()).into()) - .collect(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - - // Finalize may create new glyphs so allow read/write to *all* glyphs - let read_access = Access::custom(|an_id: &AnyWorkId| { - matches!( - an_id, - AnyWorkId::Fe(FeWorkIdentifier::Glyph(..)) - | AnyWorkId::Fe(FeWorkIdentifier::InitStaticMetadata) - ) - }); - let write_access = Access::custom(|an_id: &AnyWorkId| { - matches!( - an_id, - AnyWorkId::Fe(FeWorkIdentifier::Glyph(..)) - | AnyWorkId::Fe(FeWorkIdentifier::FinalizeStaticMetadata) - ) - }); +fn add_glyph_order_ir_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_glyph_order_work().into(); + workload.add(work, workload.change_detector.glyph_order_ir_change()); - workload.insert( - work.id().into(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Custom(read_access), - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - } Ok(()) } -fn add_global_metric_ir_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = change_detector +fn add_feature_ir_job(workload: &mut Workload) -> Result<(), Error> { + let work = workload + .change_detector .ir_source() - .create_global_metric_work(change_detector.current_inputs())?; - if change_detector.global_metrics_ir_change() { - let id: AnyWorkId = work.id().into(); - let write_access = Access::one(id.clone()); - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - workload.insert( - id, - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - } + .create_feature_ir_work(workload.change_detector.current_inputs())? + .into(); + workload.add(work, workload.change_detector.feature_ir_change()); Ok(()) } -fn add_feature_ir_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = change_detector - .ir_source() - .create_feature_ir_work(change_detector.current_inputs())?; - if change_detector.feature_ir_change() { - let id: AnyWorkId = work.id().into(); - let write_access = Access::one(id.clone()); - workload.insert( - id, - Job { - work: work.into(), - dependencies: HashSet::new(), - read_access: ReadAccess::Dependencies, - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - } - Ok(()) -} - -fn add_feature_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { +fn add_feature_be_job(workload: &mut Workload) -> Result<(), Error> { let work = FeatureWork::create(); - if change_detector.feature_be_change() && change_detector.glyph_name_filter().is_none() { - let id: AnyWorkId = work.id().into(); - - // fea-rs compiles, or will compile, several tables - // we want to capture them individually - let write_access = Access::Set(HashSet::from([ - BeWorkIdentifier::Gsub.into(), - BeWorkIdentifier::Gpos.into(), - ])); - workload.insert( - id, - Job { - work: work.into(), - dependencies: HashSet::from([ - FeWorkIdentifier::FinalizeStaticMetadata.into(), - FeWorkIdentifier::Features.into(), - ]), - read_access: ReadAccess::Dependencies, - write_access, - }, - ); - } else { - // Features are extremely prone to not making sense when glyphs are filtered - if change_detector.glyph_name_filter().is_some() { - warn!("Not processing BE Features because a glyph name filter is active"); - } - workload.mark_success(work.id()); - workload.mark_success(BeWorkIdentifier::Gpos); - workload.mark_success(BeWorkIdentifier::Gsub); - } + // Features are extremely prone to not making sense when glyphs are filtered + if workload.change_detector.feature_be_change() + && workload.change_detector.glyph_name_filter().is_some() + { + warn!("Not processing BE Features because a glyph name filter is active"); + } + workload.add( + work.into(), + workload.change_detector.feature_be_change() + && workload.change_detector.glyph_name_filter().is_none(), + ); Ok(()) } -fn add_kerning_ir_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = change_detector +fn add_kerning_ir_job(workload: &mut Workload) -> Result<(), Error> { + let work = workload + .change_detector .ir_source() - .create_kerning_ir_work(change_detector.current_inputs())?; - if change_detector.kerning_ir_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - let write_access = Access::one(id.clone()); - workload.insert( - id, - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - } + .create_kerning_ir_work(workload.change_detector.current_inputs())?; + workload.add(work.into(), workload.change_detector.kerning_ir_change()); Ok(()) } -fn add_glyph_ir_jobs( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { +fn add_glyph_ir_jobs(workload: &mut Workload) -> Result<(), Error> { // Destroy IR for deleted glyphs. No dependencies. - for glyph_name in change_detector.glyphs_deleted().iter() { + for glyph_name in workload.change_detector.glyphs_deleted().iter() { let work = DeleteWork::create(glyph_name.clone()); - workload.insert( - work.id().into(), - Job { - work: work.into(), - dependencies: HashSet::new(), - read_access: ReadAccess::Dependencies, - write_access: Access::none(), - }, - ); + workload.add(work.into(), true); } // Generate IR for changed glyphs - let glyphs_changed = change_detector.glyphs_changed(); - let glyph_work = change_detector + let glyphs_changed = workload.change_detector.glyphs_changed(); + let glyph_work = workload + .change_detector .ir_source() - .create_glyph_ir_work(&glyphs_changed, change_detector.current_inputs())?; + .create_glyph_ir_work(&glyphs_changed, workload.change_detector.current_inputs())?; for work in glyph_work { - let id = work.id(); - let work = work.into(); - let dependencies = HashSet::from([FeWorkIdentifier::InitStaticMetadata.into()]); - - let id: AnyWorkId = id.into(); - let write_access = Access::one(id.clone()); - workload.insert( - id, - Job { - work, - dependencies, - read_access: ReadAccess::Dependencies, - write_access, - }, - ); + workload.add(work.into(), true); } Ok(()) } -fn add_glyf_loca_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let glyphs_changed = change_detector.glyphs_changed(); +fn add_glyph_be_jobs(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); + for glyph_name in glyphs_changed { + add_glyph_be_job(workload, glyph_name); + } + Ok(()) +} + +fn add_glyf_loca_be_job(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); // If no glyph has changed there isn't a lot of merging to do - let work = create_glyf_loca_work(); - if !glyphs_changed.is_empty() { - let mut dependencies: HashSet<_> = glyphs_changed - .iter() - .map(|gn| BeWorkIdentifier::GlyfFragment(gn.clone()).into()) - .collect(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - // Write the merged glyphs and write individual glyphs that are updated, such as composites with bboxes - let write_access = Access::custom(|id| { - matches!( - id, - AnyWorkId::Be(BeWorkIdentifier::Glyf) - | AnyWorkId::Be(BeWorkIdentifier::Loca) - | AnyWorkId::Be(BeWorkIdentifier::LocaFormat) - | AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(..)) - ) - }); + let work = create_glyf_loca_work().into(); + workload.add(work, !glyphs_changed.is_empty()); - let id: AnyWorkId = work.id().into(); - workload.insert( - id, - Job { - work: work.into(), - dependencies, - // We need to read all glyphs, even unchanged ones, plus static metadata - read_access: ReadAccess::custom(|id| { - matches!( - id, - AnyWorkId::Be(BeWorkIdentifier::Glyf) - | AnyWorkId::Be(BeWorkIdentifier::Loca) - | AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(..)) - ) - }), - write_access, - }, - ); - } else { - workload.mark_success(work.id()); - workload.mark_success(BeWorkIdentifier::Loca); - workload.mark_success(BeWorkIdentifier::LocaFormat); - } Ok(()) } -fn add_avar_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_avar_work(); - if change_detector.avar_be_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_avar_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_avar_work().into(); + workload.add(work, workload.change_detector.avar_be_change()); Ok(()) } -fn add_stat_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_stat_work(); - if change_detector.stat_be_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_stat_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_stat_work().into(); + workload.add(work, workload.change_detector.stat_be_change()); Ok(()) } -fn add_fvar_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_fvar_work(); - if change_detector.fvar_be_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_fvar_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_fvar_work().into(); + workload.add(work, workload.change_detector.fvar_be_change()); Ok(()) } -fn add_gvar_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let glyphs_changed = change_detector.glyphs_changed(); +fn add_gvar_be_job(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); // If no glyph has changed there isn't a lot of merging to do - let work = create_gvar_work(); - if !glyphs_changed.is_empty() { - let mut dependencies: HashSet<_> = glyphs_changed - .iter() - .map(|gn| BeWorkIdentifier::GvarFragment(gn.clone()).into()) - .collect(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - // We need to read all gvar fragments, even unchanged ones, plus static metadata - read_access: ReadAccess::custom(|id| { - matches!(id, AnyWorkId::Be(BeWorkIdentifier::GvarFragment(..))) - }), - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } + let work = create_gvar_work().into(); + workload.add(work, !glyphs_changed.is_empty()); + Ok(()) } -fn add_cmap_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let glyphs_changed = change_detector.glyphs_changed(); +fn add_cmap_be_job(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); // If no glyph has changed there isn't a lot of merging to do - let work = create_cmap_work(); - if !glyphs_changed.is_empty() { - let mut dependencies: HashSet<_> = glyphs_changed - .iter() - .map(|gn| FeWorkIdentifier::Glyph(gn.clone()).into()) - .collect(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - // We need to read all glyph IR, even unchanged ones, plus static metadata - read_access: ReadAccess::custom(|id| { - matches!(id, AnyWorkId::Fe(FeWorkIdentifier::Glyph(..))) - }), - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } + let work = create_cmap_work().into(); + workload.add(work, !glyphs_changed.is_empty()); + Ok(()) } -fn add_post_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_post_work(); - if change_detector.post_be_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_post_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_post_work().into(); + workload.add(work, workload.change_detector.post_be_change()); Ok(()) } -fn add_head_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_head_work(); - if change_detector.final_static_metadata_ir_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - dependencies.insert(BeWorkIdentifier::Glyf.into()); - dependencies.insert(BeWorkIdentifier::LocaFormat.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_head_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_head_work().into(); + workload.add(work, workload.change_detector.glyph_order_ir_change()); Ok(()) } -fn add_name_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_name_work(); - if change_detector.init_static_metadata_ir_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::InitStaticMetadata.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_name_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_name_work().into(); + workload.add(work, workload.change_detector.static_metadata_ir_change()); Ok(()) } -fn add_os2_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let work = create_os2_work(); - if change_detector.init_static_metadata_ir_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(BeWorkIdentifier::Features.into()); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - dependencies.insert(FeWorkIdentifier::GlobalMetrics.into()); - dependencies.insert(BeWorkIdentifier::Hhea.into()); - dependencies.insert(BeWorkIdentifier::Hmtx.into()); - dependencies.insert(BeWorkIdentifier::Gpos.into()); - dependencies.insert(BeWorkIdentifier::Gsub.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - // We want to read all the glyphs, which must be done if hmtx is done, for codepoints - read_access: ReadAccess::custom(|id| { - matches!( - id, - AnyWorkId::Fe(FeWorkIdentifier::Glyph(..)) - | AnyWorkId::Be(BeWorkIdentifier::Hhea) - | AnyWorkId::Be(BeWorkIdentifier::Hmtx) - | AnyWorkId::Be(BeWorkIdentifier::Gpos) - | AnyWorkId::Be(BeWorkIdentifier::Gsub) - ) - }), - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } +fn add_os2_be_job(workload: &mut Workload) -> Result<(), Error> { + let work = create_os2_work().into(); + workload.add(work, workload.change_detector.static_metadata_ir_change()); Ok(()) } -fn add_metric_and_limits_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let glyphs_changed = change_detector.glyphs_changed(); +fn add_metric_and_limits_job(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); // If no glyph has changed there isn't a lot to do - let work = create_metric_and_limit_work(); - if !glyphs_changed.is_empty() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::GlobalMetrics.into()); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - // https://github.com/googlefonts/fontmake-rs/issues/285: we need bboxes and those are done for glyf - // since we depend on glyf we needn't block on any individual glyphs - dependencies.insert(BeWorkIdentifier::Glyf.into()); - // We want to update head with glyph extents so await the first pass at head - dependencies.insert(BeWorkIdentifier::Head.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id, - Job { - work: work.into(), - dependencies, - // We need to read all FE and BE glyphs, even unchanged ones, plus static metadata - read_access: ReadAccess::custom(|id| { - matches!( - id, - AnyWorkId::Fe(FeWorkIdentifier::Glyph(..)) - | AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(..)) - | AnyWorkId::Be(BeWorkIdentifier::Maxp) - | AnyWorkId::Be(BeWorkIdentifier::Head) - ) - }), - write_access: Access::custom(|id| { - matches!( - id, - AnyWorkId::Be(BeWorkIdentifier::Hmtx) - | AnyWorkId::Be(BeWorkIdentifier::Hhea) - | AnyWorkId::Be(BeWorkIdentifier::Maxp) - | AnyWorkId::Be(BeWorkIdentifier::Head) - ) - }), - }, - ); - } else { - workload.mark_success(BeWorkIdentifier::Hhea); - workload.mark_success(BeWorkIdentifier::Hmtx); - workload.mark_success(BeWorkIdentifier::Maxp); - } + let work = create_metric_and_limit_work().into(); + workload.add(work, !glyphs_changed.is_empty()); Ok(()) } -fn add_font_be_job( - change_detector: &mut ChangeDetector, - workload: &mut Workload, -) -> Result<(), Error> { - let glyphs_changed = change_detector.glyphs_changed(); +fn add_font_be_job(workload: &mut Workload) -> Result<(), Error> { + let glyphs_changed = workload.change_detector.glyphs_changed(); // If glyphs or features changed we better do the thing let work = create_font_work(); - if !glyphs_changed.is_empty() || change_detector.feature_be_change() { - let mut dependencies = HashSet::new(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - dependencies.insert(BeWorkIdentifier::Features.into()); - dependencies.insert(BeWorkIdentifier::Avar.into()); - dependencies.insert(BeWorkIdentifier::Cmap.into()); - dependencies.insert(BeWorkIdentifier::Fvar.into()); - dependencies.insert(BeWorkIdentifier::Glyf.into()); - dependencies.insert(BeWorkIdentifier::Gpos.into()); - dependencies.insert(BeWorkIdentifier::Gsub.into()); - dependencies.insert(BeWorkIdentifier::Gvar.into()); - dependencies.insert(BeWorkIdentifier::Head.into()); - dependencies.insert(BeWorkIdentifier::Hhea.into()); - dependencies.insert(BeWorkIdentifier::Hmtx.into()); - dependencies.insert(BeWorkIdentifier::Loca.into()); - dependencies.insert(BeWorkIdentifier::LocaFormat.into()); - dependencies.insert(BeWorkIdentifier::Maxp.into()); - dependencies.insert(BeWorkIdentifier::Name.into()); - dependencies.insert(BeWorkIdentifier::Os2.into()); - dependencies.insert(BeWorkIdentifier::Post.into()); - dependencies.insert(BeWorkIdentifier::Stat.into()); - - let id: AnyWorkId = work.id().into(); - workload.insert( - id.clone(), - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access: Access::one(id), - }, - ); - } else { - workload.mark_success(work.id()); - } + workload.add( + work.into(), + !glyphs_changed.is_empty() || workload.change_detector.feature_be_change(), + ); 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); - - // To build a glyph we need it's components, plus static metadata - let mut dependencies: HashSet<_> = glyph_ir - .sources() - .values() - .flat_map(|s| &s.components) - .map(|c| AnyWorkId::Fe(FeWorkIdentifier::Glyph(c.base.clone()))) - .collect(); - dependencies.insert(FeWorkIdentifier::FinalizeStaticMetadata.into()); - - let work = create_glyf_work(glyph_name.clone()); - let id: AnyWorkId = work.id().into(); - let gvar_id = AnyWorkId::Be(BeWorkIdentifier::GvarFragment(glyph_name.clone())); - - // this job should already be a dependency of glyf, gvar, and hmtx; if not terrible things will happen - if !workload.is_dependency(&BeWorkIdentifier::Glyf.into(), &id) { - panic!("BE glyph '{glyph_name}' is being built but not participating in glyf",); - } - if !workload.is_dependency(&BeWorkIdentifier::Gvar.into(), &gvar_id) { - panic!("BE glyph '{glyph_name}' is being built but not participating in gvar",); - } - - let write_access = Access::Set(HashSet::from([id.clone(), gvar_id])); - workload.insert( - id, - Job { - work: work.into(), - dependencies, - read_access: ReadAccess::Dependencies, - write_access, - }, - ); +fn add_glyph_be_job(workload: &mut Workload, glyph_name: GlyphName) { + let work = create_glyf_work(glyph_name).into(); + let should_run = workload.change_detector.simple_should_run(&work); + workload.add(work, should_run); } //FIXME: I should be a method on ChangeDetector pub fn create_workload(change_detector: &mut ChangeDetector) -> Result { - let mut workload = Workload::new(); + let mut workload = change_detector.create_workload()?; // FE: f(source) => IR - add_init_static_metadata_ir_job(change_detector, &mut workload)?; - add_global_metric_ir_job(change_detector, &mut workload)?; - add_feature_ir_job(change_detector, &mut workload)?; - add_kerning_ir_job(change_detector, &mut workload)?; - add_glyph_ir_jobs(change_detector, &mut workload)?; - add_finalize_static_metadata_ir_job(change_detector, &mut workload)?; + add_feature_ir_job(&mut workload)?; + add_kerning_ir_job(&mut workload)?; + add_glyph_ir_jobs(&mut workload)?; + add_glyph_order_ir_job(&mut workload)?; // BE: f(IR, maybe other BE work) => binary - add_feature_be_job(change_detector, &mut workload)?; - add_glyf_loca_be_job(change_detector, &mut workload)?; - add_avar_be_job(change_detector, &mut workload)?; - add_stat_be_job(change_detector, &mut workload)?; - add_cmap_be_job(change_detector, &mut workload)?; - add_fvar_be_job(change_detector, &mut workload)?; - add_gvar_be_job(change_detector, &mut workload)?; - add_head_be_job(change_detector, &mut workload)?; - add_metric_and_limits_job(change_detector, &mut workload)?; - add_name_be_job(change_detector, &mut workload)?; - add_os2_be_job(change_detector, &mut workload)?; - add_post_be_job(change_detector, &mut workload)?; + add_feature_be_job(&mut workload)?; + add_glyph_be_jobs(&mut workload)?; + add_glyf_loca_be_job(&mut workload)?; + add_avar_be_job(&mut workload)?; + add_stat_be_job(&mut workload)?; + add_cmap_be_job(&mut workload)?; + add_fvar_be_job(&mut workload)?; + add_gvar_be_job(&mut workload)?; + add_head_be_job(&mut workload)?; + add_metric_and_limits_job(&mut workload)?; + add_name_be_job(&mut workload)?; + add_os2_be_job(&mut workload)?; + add_post_be_job(&mut workload)?; // Make a damn font - add_font_be_job(change_detector, &mut workload)?; + add_font_be_job(&mut workload)?; Ok(workload) } @@ -828,13 +310,12 @@ mod tests { use chrono::{Duration, TimeZone, Utc}; use fontbe::orchestration::{ - loca_format_from_file, AnyWorkId, Context as BeContext, LocaFormat, - WorkId as BeWorkIdentifier, + AnyWorkId, Context as BeContext, Glyph as BeGlyph, LocaFormat, WorkId as BeWorkIdentifier, }; use fontdrasil::types::GlyphName; use fontir::{ ir::{self, KernParticipant}, - orchestration::{Context as FeContext, WorkId as FeWorkIdentifier}, + orchestration::{Context as FeContext, Persistable, WorkId as FeWorkIdentifier}, }; use indexmap::IndexSet; use kurbo::{Point, Rect}; @@ -851,7 +332,7 @@ mod tests { raw::{ tables::{ cmap::{Cmap, CmapSubtable}, - glyf::{self, CompositeGlyph, CurvePoint, Glyf, SimpleGlyph}, + glyf::{self, CompositeGlyph, CurvePoint, Glyf}, hmtx::Hmtx, loca::Loca, }, @@ -861,13 +342,13 @@ mod tests { GlyphId, Tag, }; use tempfile::{tempdir, TempDir}; - use write_fonts::dump_table; + use write_fonts::{dump_table, tables::glyf::Bbox}; use super::*; struct TestCompile { build_dir: PathBuf, - work_completed: HashSet, + work_executed: HashSet, glyphs_changed: IndexSet, glyphs_deleted: IndexSet, fe_context: FeContext, @@ -883,7 +364,7 @@ mod tests { let build_dir = change_detector.be_paths().build_dir().to_path_buf(); TestCompile { build_dir, - work_completed: HashSet::new(), + work_executed: HashSet::new(), glyphs_changed: change_detector.glyphs_changed(), glyphs_deleted: change_detector.glyphs_deleted(), fe_context, @@ -893,7 +374,8 @@ mod tests { fn get_glyph_index(&self, name: &str) -> u32 { self.fe_context - .get_final_static_metadata() + .glyph_order + .get() .glyph_id(&name.into()) .unwrap() } @@ -912,7 +394,9 @@ mod tests { impl Glyphs { fn new(build_dir: &Path) -> Self { Glyphs { - loca_format: loca_format_from_file(&build_dir.join("loca.format")), + loca_format: LocaFormat::read( + &mut File::open(build_dir.join("loca.format")).unwrap(), + ), raw_glyf: read_file(&build_dir.join("glyf.table")), raw_loca: read_file(&build_dir.join("loca.table")), } @@ -978,29 +462,7 @@ mod tests { let mut change_detector = ChangeDetector::new(config.clone(), ir_paths.clone(), prev_inputs).unwrap(); - let mut workload = Workload::new(); - - add_init_static_metadata_ir_job(&mut change_detector, &mut workload).unwrap(); - add_global_metric_ir_job(&mut change_detector, &mut workload).unwrap(); - add_finalize_static_metadata_ir_job(&mut change_detector, &mut workload).unwrap(); - add_glyph_ir_jobs(&mut change_detector, &mut workload).unwrap(); - add_feature_ir_job(&mut change_detector, &mut workload).unwrap(); - add_kerning_ir_job(&mut change_detector, &mut workload).unwrap(); - add_feature_be_job(&mut change_detector, &mut workload).unwrap(); - - add_glyf_loca_be_job(&mut change_detector, &mut workload).unwrap(); - add_avar_be_job(&mut change_detector, &mut workload).unwrap(); - add_stat_be_job(&mut change_detector, &mut workload).unwrap(); - add_cmap_be_job(&mut change_detector, &mut workload).unwrap(); - add_fvar_be_job(&mut change_detector, &mut workload).unwrap(); - add_gvar_be_job(&mut change_detector, &mut workload).unwrap(); - add_head_be_job(&mut change_detector, &mut workload).unwrap(); - add_metric_and_limits_job(&mut change_detector, &mut workload).unwrap(); - add_name_be_job(&mut change_detector, &mut workload).unwrap(); - add_os2_be_job(&mut change_detector, &mut workload).unwrap(); - add_post_be_job(&mut change_detector, &mut workload).unwrap(); - - add_font_be_job(&mut change_detector, &mut workload).unwrap(); + let mut workload = create_workload(&mut change_detector).unwrap(); // Try to do the work // As we currently don't stress dependencies just run one by one @@ -1009,18 +471,18 @@ mod tests { let fe_root = FeContext::new_root( config.args.flags(), ir_paths, - change_detector.current_inputs().clone(), + workload.change_detector.current_inputs().clone(), ); let be_root = BeContext::new_root(config.args.flags(), be_paths, &fe_root.read_only()); let mut result = TestCompile::new( - &change_detector, + workload.change_detector, fe_root.read_only(), be_root.copy_read_only(), ); let completed = workload.run_for_test(&fe_root, &be_root); change_detector.finish_successfully().unwrap(); - result.work_completed = completed; + result.work_executed = completed; write_font_file(&config.args, &be_root).unwrap(); @@ -1033,15 +495,16 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, "wght_var.designspace")); - let mut completed = result.work_completed.iter().cloned().collect::>(); + let mut completed = result.work_executed.iter().cloned().collect::>(); completed.sort(); assert_eq!( vec![ - AnyWorkId::Fe(FeWorkIdentifier::InitStaticMetadata), + AnyWorkId::Fe(FeWorkIdentifier::StaticMetadata), FeWorkIdentifier::GlobalMetrics.into(), FeWorkIdentifier::Glyph("bar".into()).into(), FeWorkIdentifier::Glyph("plus".into()).into(), - FeWorkIdentifier::FinalizeStaticMetadata.into(), + FeWorkIdentifier::PreliminaryGlyphOrder.into(), + FeWorkIdentifier::GlyphOrder.into(), FeWorkIdentifier::Features.into(), FeWorkIdentifier::Kerning.into(), BeWorkIdentifier::Features.into(), @@ -1085,7 +548,7 @@ mod tests { assert_eq!(IndexSet::new(), result.glyphs_deleted); let result = compile(Args::for_test(build_dir, "wght_var.designspace")); - assert_eq!(HashSet::new(), result.work_completed); + assert_eq!(HashSet::new(), result.work_executed); assert_eq!(IndexSet::new(), result.glyphs_changed); assert_eq!(IndexSet::new(), result.glyphs_deleted); } @@ -1097,12 +560,14 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, "wght_var.designspace")); - assert!(result.work_completed.len() > 1); + assert!(result.work_executed.len() > 1); - fs::remove_file(build_dir.join("glyph_ir/bar.yml")).unwrap(); + let glyph_ir_file = build_dir.join("glyph_ir/bar.yml"); + fs::remove_file(&glyph_ir_file).unwrap(); let result = compile(Args::for_test(build_dir, "wght_var.designspace")); - let mut completed = result.work_completed.iter().cloned().collect::>(); + assert!(glyph_ir_file.exists(), "{glyph_ir_file:?}"); + let mut completed = result.work_executed.iter().cloned().collect::>(); completed.sort(); assert_eq!( vec![ @@ -1110,15 +575,40 @@ mod tests { BeWorkIdentifier::Cmap.into(), BeWorkIdentifier::Font.into(), BeWorkIdentifier::Glyf.into(), - BeWorkIdentifier::GlyfFragment("bar".into()).into(), BeWorkIdentifier::Gvar.into(), - BeWorkIdentifier::GvarFragment("bar".into()).into(), BeWorkIdentifier::Hhea.into(), BeWorkIdentifier::Hmtx.into(), BeWorkIdentifier::Loca.into(), BeWorkIdentifier::LocaFormat.into(), BeWorkIdentifier::Maxp.into(), ], + completed, + "{completed:#?}" + ); + } + + #[test] + fn second_compile_only_kerning() { + let temp_dir = tempdir().unwrap(); + let build_dir = temp_dir.path(); + + let result = compile(Args::for_test(build_dir, "glyphs3/WghtVar.glyphs")); + assert!(result.work_executed.len() > 1); + + fs::remove_file(build_dir.join("kerning.yml")).unwrap(); + + let result = compile(Args::for_test(build_dir, "glyphs3/WghtVar.glyphs")); + let mut completed = result.work_executed.iter().cloned().collect::>(); + completed.sort(); + assert_eq!( + vec![ + AnyWorkId::Fe(FeWorkIdentifier::Features), + FeWorkIdentifier::Kerning.into(), + BeWorkIdentifier::Features.into(), + BeWorkIdentifier::Font.into(), + BeWorkIdentifier::Gpos.into(), + BeWorkIdentifier::Gsub.into(), + ], completed ); } @@ -1220,7 +710,7 @@ mod tests { let result = compile(Args::for_test(&build_dir, source.to_str().unwrap())); // Features and things downstream of features should rebuild - let mut completed = result.work_completed.iter().cloned().collect::>(); + let mut completed = result.work_executed.iter().cloned().collect::>(); completed.sort(); assert_eq!( vec![ @@ -1247,7 +737,8 @@ mod tests { let glyph = result .fe_context - .get_glyph_ir(&"contour_and_component".into()); + .glyphs + .get(&FeWorkIdentifier::Glyph("contour_and_component".into())); assert_eq!(1, glyph.sources().len()); (*glyph).clone() @@ -1260,8 +751,10 @@ mod tests { buf } - fn glyph_glyf_bytes(build_dir: &Path, name: &str) -> Vec { - read_file(&build_dir.join(format!("glyphs/{name}.glyf"))) + fn read_be_glyph(build_dir: &Path, name: &str) -> BeGlyph { + let raw_glyph = read_file(&build_dir.join(format!("glyphs/{name}.glyf"))); + let read: &mut dyn Read = &mut raw_glyph.as_slice(); + BeGlyph::read(read) } #[test] @@ -1271,7 +764,10 @@ mod tests { assert!(glyph.default_instance().contours.is_empty(), "{glyph:?}"); assert_eq!(2, glyph.default_instance().components.len(), "{glyph:?}"); - let raw_glyph = glyph_glyf_bytes(temp_dir.path(), glyph.name.as_str()); + let BeGlyph::Composite(_, glyph) = read_be_glyph(temp_dir.path(), glyph.name.as_str()) else { + panic!("Expected a simple glyph"); + }; + let raw_glyph = dump_table(&glyph).unwrap(); 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()); @@ -1284,9 +780,10 @@ mod tests { assert!(glyph.default_instance().components.is_empty(), "{glyph:?}"); assert_eq!(2, glyph.default_instance().contours.len(), "{glyph:?}"); - let raw_glyph = glyph_glyf_bytes(temp_dir.path(), glyph.name.as_str()); - let glyph = SimpleGlyph::read(FontData::new(&raw_glyph)).unwrap(); - assert_eq!(2, glyph.number_of_contours()); + let BeGlyph::Simple(_, glyph) = read_be_glyph(temp_dir.path(), glyph.name.as_str()) else { + panic!("Expected a simple glyph"); + }; + assert_eq!(2, glyph.contours().len()); } #[test] @@ -1295,13 +792,23 @@ mod tests { let build_dir = temp_dir.path(); compile(Args::for_test(build_dir, "static.designspace")); - let raw_glyph = glyph_glyf_bytes(build_dir, "bar"); - let glyph = SimpleGlyph::read(FontData::new(&raw_glyph)).unwrap(); - assert_eq!(1, glyph.number_of_contours()); - assert_eq!(4, glyph.num_points()); + let BeGlyph::Simple(_, glyph) = read_be_glyph(build_dir, "bar") else { + panic!("Expected a simple glyph"); + }; + + assert_eq!(1, glyph.contours().len()); assert_eq!( - [222, -241, 295, 760], - [glyph.x_min(), glyph.y_min(), glyph.x_max(), glyph.y_max()] + 4_usize, + glyph.contours().iter().map(|c| c.iter().count()).sum() + ); + assert_eq!( + Bbox { + x_min: 222, + y_min: -241, + x_max: 295, + y_max: 760, + }, + glyph.bbox, ); } @@ -1465,7 +972,7 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, "glyphs2/Component.glyphs")); - let raw_cmap = dump_table(&*result.be_context.get_cmap()).unwrap(); + let raw_cmap = dump_table(&result.be_context.cmap.get().0).unwrap(); let font_data = FontData::new(&raw_cmap); let cmap = Cmap::read(font_data).unwrap(); @@ -1517,7 +1024,7 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, "glyphs2/NotDef.glyphs")); - let raw_hmtx = result.be_context.get_hmtx(); + let raw_hmtx = result.be_context.hmtx.get(); let hmtx = Hmtx::read_with_args(FontData::new(raw_hmtx.get()), &(1, 1)).unwrap(); assert_eq!( vec![(600, 250)], @@ -1535,19 +1042,21 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, "glyphs2/Mono.glyphs")); - let hhea = result.be_context.get_hhea(); + let arc_hhea = result.be_context.hhea.get(); + let hhea = &arc_hhea.0; assert_eq!(1, hhea.number_of_long_metrics); assert_eq!(175, hhea.min_left_side_bearing.to_i16()); assert_eq!(50, hhea.min_right_side_bearing.to_i16()); assert_eq!(375, hhea.x_max_extent.to_i16()); assert_eq!(425, hhea.advance_width_max.to_u16()); - let maxp = result.be_context.get_maxp(); + let arc_maxp = result.be_context.maxp.get(); + let maxp = &arc_maxp.0; assert_eq!(3, maxp.num_glyphs); assert_eq!(Some(4), maxp.max_points); assert_eq!(Some(1), maxp.max_contours); - let raw_hmtx = result.be_context.get_hmtx(); + let raw_hmtx = result.be_context.hmtx.get(); let hmtx = Hmtx::read_with_args( FontData::new(raw_hmtx.get()), &(hhea.number_of_long_metrics, maxp.num_glyphs), @@ -2079,7 +1588,7 @@ mod tests { let build_dir = temp_dir.path(); let result = compile(Args::for_test(build_dir, source)); - let kerning = result.fe_context.get_kerning(); + let kerning = result.fe_context.kerning.get(); let mut groups: Vec<_> = kerning .groups diff --git a/fontc/src/main.rs b/fontc/src/main.rs index 96869fd1..5b499867 100644 --- a/fontc/src/main.rs +++ b/fontc/src/main.rs @@ -33,7 +33,7 @@ fn main() -> Result<(), Error> { let fe_root = FeContext::new_root( config.args.flags(), ir_paths, - change_detector.current_inputs().clone(), + workload.current_inputs().clone(), ); let be_root = BeContext::new_root(config.args.flags(), be_paths, &fe_root); workload.exec(&fe_root, &be_root)?; diff --git a/fontc/src/work.rs b/fontc/src/work.rs index 40d0f6b2..0b55e902 100644 --- a/fontc/src/work.rs +++ b/fontc/src/work.rs @@ -2,7 +2,7 @@ //! //! Basically enums that can be a FeWhatever or a BeWhatever. -use std::{collections::HashSet, fmt::Display}; +use std::fmt::Display; use fontbe::{ error::Error as BeError, @@ -42,6 +42,7 @@ impl Display for AnyWorkError { } // Work of any type, FE, BE, ... some future pass, w/e +#[derive(Debug)] pub enum AnyWork { Fe(Box), Be(Box), @@ -60,13 +61,30 @@ impl From> for AnyWork { } impl AnyWork { + pub fn id(&self) -> AnyWorkId { + match self { + AnyWork::Be(work) => work.id(), + AnyWork::Fe(work) => work.id().into(), + } + } + + pub fn read_access(&self) -> AnyAccess { + match self { + AnyWork::Be(work) => work.read_access().into(), + AnyWork::Fe(work) => work.read_access().into(), + } + } + + pub fn write_access(&self) -> AnyAccess { + match self { + AnyWork::Be(work) => work.write_access().into(), + AnyWork::Fe(work) => work.write_access().into(), + } + } + pub fn also_completes(&self) -> Vec { match self { - AnyWork::Be(work) => work - .also_completes() - .into_iter() - .map(|id| id.into()) - .collect(), + AnyWork::Be(work) => work.also_completes().into_iter().collect(), AnyWork::Fe(work) => work .also_completes() .into_iter() @@ -83,20 +101,55 @@ impl AnyWork { } } -pub enum AnyContext { - Fe(FeContext), - Be(BeContext), +#[derive(Debug, Clone)] +pub enum AnyAccess { + Be(Access), + Fe(Access), } -pub enum ReadAccess { - Dependencies, - Custom(Access), +impl From> for AnyAccess { + fn from(value: Access) -> Self { + AnyAccess::Be(value) + } } -impl ReadAccess { - pub fn custom(func: impl Fn(&AnyWorkId) -> bool + Send + Sync + 'static) -> Self { - Self::Custom(Access::custom(func)) +impl From> for AnyAccess { + fn from(value: Access) -> Self { + AnyAccess::Fe(value) + } +} + +impl AnyAccess { + pub fn check(&self, id: &AnyWorkId) -> bool { + match self { + AnyAccess::Be(access) => access.check(id), + AnyAccess::Fe(access) => { + let AnyWorkId::Fe(id) = id else { + return false; + }; + access.check(id) + } + } + } + + pub fn unwrap_be(&self) -> Access { + match self { + AnyAccess::Fe(..) => panic!("Not BE access"), + AnyAccess::Be(access) => access.clone(), + } } + + pub fn unwrap_fe(&self) -> Access { + match self { + AnyAccess::Fe(access) => access.clone(), + AnyAccess::Be(..) => panic!("Not FE access"), + } + } +} + +pub enum AnyContext { + Fe(FeContext), + Be(BeContext), } impl AnyContext { @@ -104,16 +157,13 @@ impl AnyContext { fe_root: &FeContext, be_root: &BeContext, work_id: &AnyWorkId, - dependencies: HashSet, - read_access: ReadAccess, - write_access: Access, + read_access: AnyAccess, + write_access: AnyAccess, ) -> AnyContext { - let read_access = match read_access { - ReadAccess::Dependencies => Access::custom(move |id| dependencies.contains(id)), - ReadAccess::Custom(access_fn) => access_fn, - }; match work_id { - AnyWorkId::Be(..) => AnyContext::Be(be_root.copy_for_work(read_access, write_access)), + AnyWorkId::Be(..) => AnyContext::Be( + be_root.copy_for_work(read_access.unwrap_be(), write_access.unwrap_be()), + ), AnyWorkId::Fe(..) => AnyContext::Fe(fe_root.copy_for_work( Access::custom(move |id: &WorkId| read_access.check(&AnyWorkId::Fe(id.clone()))), Access::custom(move |id: &WorkId| write_access.check(&AnyWorkId::Fe(id.clone()))), diff --git a/fontc/src/workload.rs b/fontc/src/workload.rs index d3a1f40d..101c574a 100644 --- a/fontc/src/workload.rs +++ b/fontc/src/workload.rs @@ -3,20 +3,23 @@ use std::collections::{HashMap, HashSet}; use crossbeam_channel::{Receiver, TryRecvError}; -use fontbe::orchestration::{AnyWorkId, Context as BeContext, WorkId as BeWorkIdentifier}; +use fontbe::orchestration::{AnyWorkId, Context as BeContext}; use fontdrasil::orchestration::Access; -use fontir::orchestration::{Context as FeContext, WorkId as FeWorkIdentifier}; -use log::debug; +use fontir::{ + orchestration::{Context as FeContext, WorkId as FeWorkIdentifier}, + source::Input, +}; +use log::{debug, trace}; use crate::{ - work::{AnyContext, AnyWork, AnyWorkError, ReadAccess}, - Error, + work::{AnyAccess, AnyContext, AnyWork, AnyWorkError}, + ChangeDetector, Error, }; /// A set of interdependent jobs to execute. -#[derive(Default)] -pub struct Workload { - pre_success: usize, +#[derive(Debug)] +pub struct Workload<'a> { + pub(crate) change_detector: &'a ChangeDetector, job_count: usize, success: HashSet, error: Vec<(AnyWorkId, String)>, @@ -27,15 +30,21 @@ pub struct Workload { } /// A unit of executable work plus the identifiers of work that it depends on +/// +/// Exists to allow us to modify dependencies, such as adding new ones. +#[derive(Debug)] pub(crate) struct Job { - // The actual task - pub(crate) work: AnyWork, - // Things that must happen before we execute. Our task can read these things. - pub(crate) dependencies: HashSet, - // Things our job needs read access to. Usually all our dependencies. - pub(crate) read_access: ReadAccess, + pub(crate) id: AnyWorkId, + // The actual task. Exec takes work and sets the running flag. + pub(crate) work: Option, + // Things our job needs read access to. Job won't run if anything it can read is pending. + pub(crate) read_access: AnyAccess, // Things our job needs write access to - pub(crate) write_access: Access, + pub(crate) write_access: AnyAccess, + // Does this job actually need to execute? + pub(crate) run: bool, + // is this job running right now? + pub(crate) running: bool, } enum RecvType { @@ -43,38 +52,71 @@ enum RecvType { NonBlocking, } -impl Workload { - pub fn new() -> Workload { - Self::default() +impl<'a> Workload<'a> { + pub fn new(change_detector: &'a ChangeDetector) -> Workload { + Workload { + change_detector, + job_count: 0, + success: Default::default(), + error: Default::default(), + also_completes: Default::default(), + jobs_pending: Default::default(), + } } - pub(crate) fn insert(&mut self, id: AnyWorkId, job: Job) { - let also_completes = job.work.also_completes(); - if !also_completes.is_empty() { - self.also_completes.insert(id.clone(), also_completes); - } - self.jobs_pending.insert(id, job); - self.job_count += 1; + /// True if job might read what other produces + fn might_read(&self, job: &Job, other: &Job) -> bool { + let result = job.read_access.check(&other.id) + || self + .also_completes + .get(&other.id) + .map(|also| also.iter().any(|other_id| job.read_access.check(other_id))) + .unwrap_or_default(); + result + } + + pub fn current_inputs(&self) -> &Input { + self.change_detector.current_inputs() } - pub(crate) fn is_dependency(&mut self, id: &AnyWorkId, dep: &AnyWorkId) -> bool { - self.jobs_pending - .get(id) - .map(|job| job.dependencies.contains(dep)) - .unwrap_or(false) + // TODO flags would be clearer than multiple bools + pub(crate) fn add(&mut self, work: AnyWork, should_run: bool) { + let id = work.id(); + let read_access = work.read_access(); + let write_access = work.write_access(); + self.insert( + id.clone(), + Job { + id, + work: Some(work), + read_access, + write_access, + run: should_run, + running: false, + }, + ); } - pub(crate) fn mark_success(&mut self, id: impl Into) { - if self.success.insert(id.into()) { - self.pre_success += 1; + pub(crate) fn insert(&mut self, id: AnyWorkId, job: Job) { + let also_completes = job + .work + .as_ref() + .expect("{id:?} submitted without work") + .also_completes(); + + self.job_count += 1 + also_completes.len(); + self.jobs_pending.insert(id.clone(), job); + + if !also_completes.is_empty() { + self.also_completes.insert(id, also_completes); } } fn mark_also_completed(&mut self, success: &AnyWorkId) { let Some(also_completed) = self.also_completes.get(success) else { return }; for id in also_completed { - if self.success.insert(id.clone()) { - self.pre_success += 1; + if !self.success.insert(id.clone()) { + panic!("Multiple completions of {id:?}"); } } } @@ -84,60 +126,65 @@ impl Workload { self.mark_also_completed(&success); - match success { - // When a glyph finishes IR, register BE work for it - AnyWorkId::Fe(FeWorkIdentifier::Glyph(glyph_name)) => { - super::add_glyph_be_job(self, fe_root, glyph_name) + // When glyph order finalizes, add BE work for any new glyphs + if let AnyWorkId::Fe(FeWorkIdentifier::GlyphOrder) = success { + let preliminary_glyph_order = fe_root.preliminary_glyph_order.get(); + for glyph_name in fe_root + .glyph_order + .get() + .difference(&preliminary_glyph_order) + { + debug!("Generating a BE job for {glyph_name}"); + super::add_glyph_be_job(self, glyph_name.clone()); } + } + } - // When static metadata finalizes, add BE work for any new glyphs - // If anything progresses before we've done this we have a graph bug - AnyWorkId::Fe(FeWorkIdentifier::FinalizeStaticMetadata) => { - for glyph_name in fe_root.get_final_static_metadata().glyph_order.iter() { - let glyph_work_id = - AnyWorkId::Be(BeWorkIdentifier::GlyfFragment(glyph_name.clone())); - let gvar_work_id = - AnyWorkId::Be(BeWorkIdentifier::GvarFragment(glyph_name.clone())); - if self.jobs_pending.contains_key(&glyph_work_id) { - continue; - } - - debug!("Adding dependencies on {glyph_name}"); - - // It would be lovely if our new glyph was in glyf, and gvar - // loca hides with glyf. hmtx blocks on glyf. - for (merge_work_id, new_dep) in [ - (BeWorkIdentifier::Glyf, &glyph_work_id), - (BeWorkIdentifier::Gvar, &gvar_work_id), - ] { - self.jobs_pending - .get_mut(&AnyWorkId::Be(merge_work_id)) - .unwrap() - .dependencies - .insert(new_dep.clone()); - } - - super::add_glyph_be_job(self, fe_root, glyph_name.clone()); - } - } + fn can_run_scan(&self, job: &Job) -> bool { + // Job can only run if *no* incomplete jobs exist whose output it could read + !self + .jobs_pending + .iter() + .filter(|(other_id, _)| !self.success.contains(other_id)) + .any(|(_, other_job)| self.might_read(job, other_job)) + } - _ => (), + fn can_run(&self, job: &Job) -> bool { + // Custom access requires a scan across pending jobs, hence avoidance is nice + match &job.read_access { + AnyAccess::Fe(access) => match access { + Access::None => true, + Access::One(id) => !self.jobs_pending.contains_key(&id.into()), + Access::Set(ids) => !ids + .iter() + .any(|id| self.jobs_pending.contains_key(&id.into())), + Access::Custom(..) => self.can_run_scan(job), + Access::All => self.can_run_scan(job), + }, + AnyAccess::Be(access) => match access { + Access::None => true, + Access::One(id) => !self.jobs_pending.contains_key(id), + Access::Set(ids) => !ids.iter().any(|id| self.jobs_pending.contains_key(id)), + Access::Custom(..) => self.can_run_scan(job), + Access::All => self.can_run_scan(job), + }, } } pub fn launchable(&self) -> Vec { - self.jobs_pending + let launchable = self + .jobs_pending .iter() .filter_map(|(id, job)| { - let can_start = job.dependencies.is_subset(&self.success); - if !can_start && log::log_enabled!(log::Level::Trace) { - let mut unfulfilled_deps: Vec<_> = - job.dependencies.difference(&self.success).collect(); - unfulfilled_deps.sort(); - }; - can_start.then(|| id.clone()) + if !job.running && self.can_run(job) { + Some(id.clone()) + } else { + None + } }) - .collect() + .collect(); + trace!("Launchable: {launchable:?}"); + launchable } pub fn exec(mut self, fe_root: &FeContext, be_root: &BeContext) -> Result<(), Error> { @@ -148,28 +195,45 @@ impl Workload { // Whenever a task completes see if it was the last incomplete dependency of other task(s) // and spawn them if it was // TODO timeout and die it if takes too long to make forward progress or we're spinning w/o progress - while self.success.len() < self.job_count + self.pre_success { + while self.success.len() < self.job_count { // Spawn anything that is currently executable (has no unfulfilled dependencies) - for id in self.launchable() { - log::trace!("Start {:?}", id); - let job = self.jobs_pending.remove(&id).unwrap(); - let work = job.work; - let work_context = AnyContext::for_work( - fe_root, - be_root, - &id, - job.dependencies, - job.read_access, - job.write_access, - ); + let launchable = self.launchable(); + if launchable.is_empty() && !self.jobs_pending.values().any(|j| j.running) { + return Err(Error::UnableToProceed); + } + // Launch anything that needs launching + for id in launchable { + let job = self.jobs_pending.get_mut(&id).unwrap(); + log::trace!("Start {:?}", id); let send = send.clone(); - scope.spawn(move |_| { - let result = work.exec(work_context); - if let Err(e) = send.send((id.clone(), result)) { - log::error!("Unable to write {:?} to completion channel: {}", id, e); - } - }) + job.running = true; + let work = job + .work + .take() + .expect("{id:?} ready to run but has no work?!"); + if job.run { + let work_context = AnyContext::for_work( + fe_root, + be_root, + &id, + job.read_access.clone(), + job.write_access.clone(), + ); + + scope.spawn(move |_| { + let result = work.exec(work_context); + if let Err(e) = send.send((id.clone(), result)) { + log::error!( + "Unable to write {:?} to completion channel: {}", + id, + e + ); + } + }) + } else if let Err(e) = send.send((id.clone(), Ok(()))) { + log::error!("Unable to write nop {:?} to completion channel: {}", id, e); + } } // Block for things to phone home to say they are done @@ -185,11 +249,11 @@ impl Workload { // If ^ exited due to error the scope awaited any live tasks; capture their results self.read_completions(&recv, RecvType::NonBlocking)?; - if self.error.is_empty() && self.success.len() != self.job_count + self.pre_success { + if self.error.is_empty() && self.success.len() != self.job_count { panic!( "No errors but only {}/{} succeeded?!", self.success.len(), - self.job_count + self.pre_success + self.job_count ); } @@ -232,9 +296,12 @@ impl Workload { } { panic!("Repeat signals for completion of {completed_id:#?}"); } + // When a job marks another as also completed the original may never have been in pending + self.jobs_pending.remove(&completed_id); + log::debug!( "{}/{} complete, most recently {:?}", - self.error.len() + self.success.len() - self.pre_success, + self.error.len() + self.success.len(), self.job_count, completed_id ); @@ -250,9 +317,27 @@ impl Workload { Ok(successes) } + #[cfg(test)] + fn unfulfilled_deps(&self, job: &Job) -> Vec { + let mut unfulfilled = self + .jobs_pending + .iter() + .filter_map(|(id, other_job)| { + if self.might_read(job, other_job) { + Some(id) + } else { + None + } + }) + .cloned() + .collect::>(); + unfulfilled.sort(); + unfulfilled + } + /// Run all pending jobs. /// - /// Returns the set of ids for tasks that are finished as a result of this run. + /// Returns the set of ids for tasks that executed as a result of this run. #[cfg(test)] pub fn run_for_test(&mut self, fe_root: &FeContext, be_root: &BeContext) -> HashSet { let pre_success = self.success.clone(); @@ -260,21 +345,21 @@ impl Workload { let launchable = self.launchable(); if launchable.is_empty() { log::error!("Completed:"); - for id in self.success.iter() { + let mut success: Vec<_> = self.success.iter().collect(); + success.sort(); + for id in success { log::error!(" {id:?}"); } log::error!("Unable to proceed with:"); for (id, job) in self.jobs_pending.iter() { - let unfulfilled = job - .dependencies - .iter() - .filter(|id| !self.success.contains(id)) - .collect::>(); - log::error!( - " {:?}, happens-after {:?}, unfulfilled {unfulfilled:?}", - id, - job.dependencies - ); + let unfulfilled = self.unfulfilled_deps(job); + log::error!(" {id:?}, has unfulfilled dependencies: {unfulfilled:?}"); + for id in unfulfilled.iter() { + log::error!( + " {id:?}, has unfulfilled dependencies: {:?}", + self.unfulfilled_deps(self.jobs_pending.get(id).unwrap()) + ); + } } assert!( !launchable.is_empty(), @@ -284,24 +369,20 @@ impl Workload { let id = &launchable[0]; let job = self.jobs_pending.remove(id).unwrap(); - let context = AnyContext::for_work( - fe_root, - be_root, - id, - job.dependencies, - job.read_access, - job.write_access, - ); - log::debug!("Exec {:?}", id); - job.work - .exec(context) - .unwrap_or_else(|e| panic!("{id:?} failed: {e:?}")); - assert!( - self.success.insert(id.clone()), - "We just did {id:?} a second time?" - ); - - self.handle_success(fe_root, id.clone()); + if job.run { + let context = + AnyContext::for_work(fe_root, be_root, id, job.read_access, job.write_access); + log::debug!("Exec {:?}", id); + job.work + .expect("{id:?} should have work!") + .exec(context) + .unwrap_or_else(|e| panic!("{id:?} failed: {e:?}")); + assert!( + self.success.insert(id.clone()), + "We just did {id:?} a second time?" + ); + self.handle_success(fe_root, id.clone()); + } } self.success.difference(&pre_success).cloned().collect() } diff --git a/fontdrasil/src/orchestration.rs b/fontdrasil/src/orchestration.rs index e6123373..b62730c9 100644 --- a/fontdrasil/src/orchestration.rs +++ b/fontdrasil/src/orchestration.rs @@ -9,9 +9,14 @@ use std::{ pub const MISSING_DATA: &str = "Missing data, dependency management failed us?"; +/// A type that affords identity. +/// +/// Frequently copied, used in hashmap/sets and printed to logs, hence the trait list. +pub trait Identifier: Debug + Clone + Eq + Hash {} + /// A rule that represents whether access to something is permitted. #[derive(Clone)] -pub enum Access { +pub enum Access { /// No access is permitted None, /// Any access is permitted @@ -24,6 +29,18 @@ pub enum Access { Custom(Arc bool + Send + Sync>), } +impl Debug for Access { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::All => write!(f, "All"), + Self::One(arg0) => f.debug_tuple("One").field(arg0).finish(), + Self::Set(arg0) => f.debug_tuple("Set").field(arg0).finish(), + Self::Custom(..) => write!(f, "Custom"), + } + } +} + /// A unit of work safe to run in parallel /// /// Naively you'd think we'd just return FnOnce + Send but that didn't want to compile @@ -32,7 +49,10 @@ pub enum Access { /// Data produced by work is written into a Context (type parameter C). The identifier /// type for work, I, is used to identify the task itself as well as the slots this work /// might wish to access in the Context. -pub trait Work { +pub trait Work: Debug +where + I: Identifier, +{ /// The identifier for this work fn id(&self) -> I; @@ -46,6 +66,28 @@ pub trait Work { Vec::new() } + /// What this work needs to be able to read; our dependencies + /// + /// Anything we can read should be completed before we execute. + /// Where possible avoid [Access::Custom]; it has to be rechecked whenever the task set changes. + /// + /// The default is no access. + fn read_access(&self) -> Access { + Access::None + } + + /// What this work needs to be able to write. + /// + /// Defaults to our own id plus anything we also complete. + fn write_access(&self) -> Access { + let mut also = self.also_completes(); + if also.is_empty() { + return Access::One(self.id()); + } + also.push(self.id()); + Access::Set(also.into_iter().collect()) + } + fn exec(&self, context: &C) -> Result<(), E>; } @@ -53,7 +95,8 @@ pub trait Work { /// /// Not meant to prevent malicious access, merely to detect mistakes /// because the result of mistaken concurrent access can be confusing to track down. -pub struct AccessControlList { +#[derive(Clone)] +pub struct AccessControlList { // Returns true if you can write to the provided id write_access: Access, @@ -61,7 +104,16 @@ pub struct AccessControlList { read_access: Access, } -impl AccessControlList { +impl Default for AccessControlList { + fn default() -> Self { + Self { + write_access: Access::None, + read_access: Access::None, + } + } +} + +impl AccessControlList { pub fn read_only() -> AccessControlList { AccessControlList { write_access: Access::none(), @@ -77,7 +129,7 @@ impl AccessControlList { } } -impl Access { +impl Access { /// Create a new access rule with custom logic pub fn custom bool + Send + Sync + 'static>(func: F) -> Self { Access::Custom(Arc::new(func)) @@ -124,7 +176,7 @@ impl Display for AccessCheck { } } -fn assert_access_many( +fn assert_access_many( demand: AccessCheck, access: &Access, ids: &[I], @@ -139,14 +191,14 @@ fn assert_access_many( } } -fn assert_access_one(access: &Access, id: &I, desc: &str) { +fn assert_access_one(access: &Access, id: &I, desc: &str) { let allow = access.check(id); if !allow { panic!("Illegal {desc} of {id:?}"); } } -impl AccessControlList { +impl AccessControlList { pub fn assert_read_access_to_all(&self, ids: &[I]) { assert_access_many(AccessCheck::All, &self.read_access, ids, "read"); } diff --git a/fontir/src/glyph.rs b/fontir/src/glyph.rs index 074c3360..5c5a2ce9 100644 --- a/fontir/src/glyph.rs +++ b/fontir/src/glyph.rs @@ -3,10 +3,15 @@ //! Notably includes splitting glyphs with contours and components into one new glyph with //! the contours and one updated glyph with no contours that references the new gyph as a component. -use std::collections::{HashSet, VecDeque}; +use std::{ + collections::{HashSet, VecDeque}, + sync::Arc, +}; -use fontdrasil::{orchestration::Work, types::GlyphName}; -use indexmap::IndexSet; +use fontdrasil::{ + orchestration::{Access, Work}, + types::GlyphName, +}; use kurbo::Affine; use log::{debug, log_enabled, trace}; use ordered_float::OrderedFloat; @@ -15,15 +20,16 @@ use write_fonts::pens::{write_to_pen, BezPathPen, ReverseContourPen}; use crate::{ coords::NormalizedLocation, error::WorkError, - ir::{Component, Glyph, GlyphBuilder}, + ir::{Component, Glyph, GlyphBuilder, GlyphOrder}, orchestration::{Context, Flags, IrWork, WorkId}, }; -pub fn create_finalize_static_metadata_work() -> Box { - Box::new(FinalizeStaticMetadataWork {}) +pub fn create_glyph_order_work() -> Box { + Box::new(GlyphOrderWork {}) } -struct FinalizeStaticMetadataWork {} +#[derive(Debug)] +struct GlyphOrderWork {} /// Glyph should split if it has components *and* contours. /// @@ -64,7 +70,7 @@ fn has_consistent_2x2_transforms(glyph: &Glyph) -> bool { component_seqs.len() <= 1 } -fn name_for_derivative(base_name: &GlyphName, names_in_use: &IndexSet) -> GlyphName { +fn name_for_derivative(base_name: &GlyphName, names_in_use: &GlyphOrder) -> GlyphName { let mut i = 0; let base_name = base_name.as_str(); loop { @@ -79,10 +85,7 @@ fn name_for_derivative(base_name: &GlyphName, names_in_use: &IndexSet /// Returns a tuple of (simple glyph, composite glyph). /// /// The former contains all the contours, the latter contains all the components. -fn split_glyph( - glyph_order: &IndexSet, - original: &Glyph, -) -> Result<(Glyph, Glyph), WorkError> { +fn split_glyph(glyph_order: &GlyphOrder, original: &Glyph) -> Result<(Glyph, Glyph), WorkError> { // Make a simple glyph by erasing the components from it let mut simple_glyph: GlyphBuilder = original.into(); simple_glyph.sources.iter_mut().for_each(|(_, inst)| { @@ -229,7 +232,7 @@ fn convert_components_to_contours(context: &Context, original: &Glyph) -> Result continue; } - let referenced_glyph = context.get_glyph_ir(&component.base); + let referenced_glyph = context.glyphs.get(&WorkId::Glyph(component.base.clone())); frontier.extend( components(&referenced_glyph, component.transform) .iter() @@ -279,13 +282,13 @@ fn convert_components_to_contours(context: &Context, original: &Glyph) -> Result simple.name, original.name ); - context.set_glyph_ir(simple); + context.glyphs.set(simple); Ok(()) } fn move_contours_to_new_component( context: &Context, - new_glyph_order: &mut IndexSet, + new_glyph_order: &mut GlyphOrder, glyph: &Glyph, ) -> Result<(), WorkError> { debug!( @@ -299,8 +302,8 @@ fn move_contours_to_new_component( debug_assert!(simple.name != glyph.name); new_glyph_order.insert(simple.name.clone()); - context.set_glyph_ir(simple); - context.set_glyph_ir(composite); + context.glyphs.set(simple); + context.glyphs.set(composite); Ok(()) } @@ -326,7 +329,7 @@ fn flatten_glyph(context: &Context, glyph: &Glyph) -> Result<(), WorkError> { let mut frontier = VecDeque::new(); frontier.extend(inst.components.split_off(0)); while let Some(component) = frontier.pop_front() { - let ref_glyph = context.get_glyph_ir(&component.base); + let ref_glyph = context.glyphs.get(&WorkId::Glyph(component.base.clone())); let ref_inst = ref_glyph.sources().get(loc).ok_or_else(|| { WorkError::GlyphUndefAtNormalizedLocation { glyph_name: ref_glyph.name.clone(), @@ -351,29 +354,45 @@ fn flatten_glyph(context: &Context, glyph: &Glyph) -> Result<(), WorkError> { glyph.name, glyph.default_instance().components ); - context.set_glyph_ir(glyph); + context.glyphs.set(glyph); Ok(()) } -impl Work for FinalizeStaticMetadataWork { +impl Work for GlyphOrderWork { fn id(&self) -> WorkId { - WorkId::FinalizeStaticMetadata + WorkId::GlyphOrder + } + + fn read_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!( + id, + WorkId::Glyph(..) | WorkId::StaticMetadata | WorkId::PreliminaryGlyphOrder + ) + })) + } + + fn write_access(&self) -> Access { + Access::Custom(Arc::new(|id| { + matches!(id, WorkId::Glyph(..) | WorkId::GlyphOrder) + })) } fn exec(&self, context: &Context) -> Result<(), WorkError> { // We should now have access to *all* the glyph IR // Some of it may need to be massaged to produce BE glyphs // In particular, glyphs with both paths and components need to push the path into a component - let current_metadata = context.get_init_static_metadata(); - let mut new_glyph_order = current_metadata.glyph_order.clone(); + let arc_current = context.preliminary_glyph_order.get(); + let current_glyph_order = &*arc_current; + let mut new_glyph_order = current_glyph_order.clone(); // Glyphs with paths and components, and glyphs whose component 2x2 transforms vary over designspace // are not directly supported in fonts. To resolve we must do one of: // 1) need to push their paths to a new glyph that is a component // 2) collapse such glyphs into a simple (contour-only) glyph // fontmake (Python) prefers option 2. - for glyph_name in current_metadata.glyph_order.iter() { - let glyph = context.get_glyph_ir(glyph_name); + for glyph_name in current_glyph_order.iter() { + let glyph = context.glyphs.get(&WorkId::Glyph(glyph_name.clone())); let inconsistent_components = !has_consistent_2x2_transforms(&glyph); if inconsistent_components || has_components_and_contours(&glyph) { if inconsistent_components { @@ -396,31 +415,27 @@ impl Work for FinalizeStaticMetadataWork { if context.flags.contains(Flags::FLATTEN_COMPONENTS) { for glyph_name in new_glyph_order.iter() { - let glyph = context.get_glyph_ir(glyph_name); + let glyph = context.glyphs.get(&WorkId::Glyph(glyph_name.clone())); flatten_glyph(context, &glyph)?; } } // We now have the final static metadata // If the glyph order changed try not to forget about it - if current_metadata.glyph_order != new_glyph_order { + if *current_glyph_order != new_glyph_order { if log_enabled!(log::Level::Trace) { - let mut new_glyphs: Vec<_> = new_glyph_order - .difference(¤t_metadata.glyph_order) - .collect(); + let mut new_glyphs: Vec<_> = + new_glyph_order.difference(current_glyph_order).collect(); new_glyphs.sort(); trace!( "Added {} additional glyphs: {new_glyphs:?}", new_glyphs.len() ) } - let mut updated_metadata = (*current_metadata).clone(); - updated_metadata.glyph_order = new_glyph_order; - context.set_final_static_metadata(updated_metadata); } else { - trace!("No new glyphs; final static metadata is unchanged"); - context.set_final_static_metadata((*current_metadata).clone()); + trace!("No new glyphs, final glyph order == preliminary glyph order"); } + context.glyph_order.set(new_glyph_order); Ok(()) } } @@ -431,15 +446,14 @@ mod tests { use font_types::Tag; use fontdrasil::{orchestration::Access, types::GlyphName}; - use indexmap::IndexSet; use kurbo::{Affine, BezPath}; use write_fonts::pens::{write_to_pen, BezPathPen, ReverseContourPen}; use crate::{ coords::{NormalizedCoord, NormalizedLocation}, glyph::has_consistent_2x2_transforms, - ir::{Component, Glyph, GlyphBuilder, GlyphInstance}, - orchestration::{Context, Flags}, + ir::{Component, Glyph, GlyphBuilder, GlyphInstance, GlyphOrder}, + orchestration::{Context, Flags, WorkId}, paths::Paths, source::Input, }; @@ -507,9 +521,9 @@ mod tests { impl DeepComponent { fn write_to(&self, context: &Context) { - context.set_glyph_ir(self.simple_glyph.clone()); - context.set_glyph_ir(self.shallow_component.clone()); - context.set_glyph_ir(self.deep_component.clone()); + context.glyphs.set(self.simple_glyph.clone()); + context.glyphs.set(self.shallow_component.clone()); + context.glyphs.set(self.deep_component.clone()); } } @@ -562,7 +576,7 @@ mod tests { #[test] fn names_for_derivatives() { - let mut names = IndexSet::new(); + let mut names = GlyphOrder::new(); assert_eq!( GlyphName::from("duck.0"), name_for_derivative(&"duck".into(), &names) @@ -628,7 +642,7 @@ mod tests { #[test] fn split_a_glyph() { let split_me = contour_and_component_weight_glyph("glyphname"); - let (simple, composite) = split_glyph(&IndexSet::new(), &split_me).unwrap(); + let (simple, composite) = split_glyph(&GlyphOrder::new(), &split_me).unwrap(); let expected_locs = split_me.sources().keys().collect::>(); assert_eq!( @@ -656,10 +670,10 @@ mod tests { let coalesce_me = contour_and_component_weight_glyph("coalesce_me"); let context = test_context(); - context.set_glyph_ir(contour_glyph("component")); + context.glyphs.set(contour_glyph("component")); convert_components_to_contours(&context, &coalesce_me).unwrap(); - let simple = context.get_glyph_ir(&coalesce_me.name); + let simple = context.glyphs.get(&WorkId::Glyph(coalesce_me.name)); assert_simple(&simple); // Our sample is unimaginative; both weights are identical @@ -709,7 +723,7 @@ mod tests { let nested_components = nested_components.try_into().unwrap(); convert_components_to_contours(&context, &nested_components).unwrap(); - let simple = context.get_glyph_ir(&nested_components.name); + let simple = context.glyphs.get(&WorkId::Glyph(nested_components.name)); assert_simple(&simple); assert_eq!(1, simple.sources().len()); let inst = simple.default_instance(); @@ -757,11 +771,11 @@ mod tests { .unwrap(); let context = test_context(); - context.set_glyph_ir(reuse_me); + context.glyphs.set(reuse_me); let glyph = glyph.try_into().unwrap(); convert_components_to_contours(&context, &glyph).unwrap(); - let simple = context.get_glyph_ir(&glyph.name); + let simple = context.glyphs.get(&WorkId::Glyph(glyph.name)); assert_simple(&simple); assert_eq!(1, simple.sources().len()); let inst = simple.sources().values().next().unwrap(); @@ -819,10 +833,10 @@ mod tests { }); let context = test_context(); - context.set_glyph_ir(contour_glyph("component")); + context.glyphs.set(contour_glyph("component")); convert_components_to_contours(&context, &glyph).unwrap(); - let simple = context.get_glyph_ir(&glyph.name); + let simple = context.glyphs.get(&WorkId::Glyph(glyph.name)); assert_simple(&simple); } @@ -832,10 +846,10 @@ mod tests { let glyph = adjust_transform_for_each_instance(&glyph, |i| Affine::scale(i as f64)); let context = test_context(); - context.set_glyph_ir(contour_glyph("component")); + context.glyphs.set(contour_glyph("component")); convert_components_to_contours(&context, &glyph).unwrap(); - let simple = context.get_glyph_ir(&glyph.name); + let simple = context.glyphs.get(&WorkId::Glyph(glyph.name)); assert_simple(&simple); } @@ -848,12 +862,13 @@ mod tests { } fn assert_is_flattened_component(context: &Context, glyph_name: GlyphName) { - let glyph = context.get_glyph_ir(&glyph_name); + let glyph = context.glyphs.get(&WorkId::Glyph(glyph_name)); for (loc, inst) in glyph.sources().iter() { assert!(!inst.components.is_empty()); for component in inst.components.iter() { assert!(context - .get_glyph_ir(&component.base) + .glyphs + .get(&WorkId::Glyph(component.base.clone())) .sources() .get(loc) .unwrap() diff --git a/fontir/src/ir.rs b/fontir/src/ir.rs index 33db2bd8..0e00a874 100644 --- a/fontir/src/ir.rs +++ b/fontir/src/ir.rs @@ -3,9 +3,10 @@ use crate::{ coords::{CoordConverter, NormalizedCoord, NormalizedLocation, UserCoord, UserLocation}, error::{PathConversionError, VariationModelError, WorkError}, + orchestration::{IdAware, Persistable, WorkId}, serde::{ - GlobalMetricsSerdeRepr, GlyphSerdeRepr, KerningSerdeRepr, MiscSerdeRepr, - StaticMetadataSerdeRepr, + GlobalMetricsSerdeRepr, GlyphOrderSerdeRepr, GlyphSerdeRepr, KerningSerdeRepr, + MiscSerdeRepr, StaticMetadataSerdeRepr, }, variations::VariationModel, }; @@ -15,12 +16,13 @@ use font_types::Tag; use fontdrasil::types::{GlyphName, GroupName}; use indexmap::IndexSet; use kurbo::{Affine, BezPath, PathEl, Point}; -use log::{trace, warn}; +use log::warn; use ordered_float::OrderedFloat; use serde::{Deserialize, Serialize}; use std::{ - collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + collections::{hash_map::RandomState, BTreeMap, BTreeSet, HashMap, HashSet}, fmt::Debug, + io::Read, path::PathBuf, }; use write_fonts::tables::os2::SelectionFlags; @@ -49,11 +51,6 @@ pub struct StaticMetadata { /// Named locations in variation space pub named_instances: Vec, - /// The name of every glyph, in the order it will be emitted - /// - /// - pub glyph_order: IndexSet, - /// A model of how variation space is split into regions that have deltas. /// /// This copy includes all locations used in the entire font. That is, every @@ -98,6 +95,75 @@ pub struct MiscMetadata { pub created: Option>, } +/// The name of every glyph, in the order it will be emitted +/// +/// +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(from = "GlyphOrderSerdeRepr", into = "GlyphOrderSerdeRepr")] +pub struct GlyphOrder(IndexSet); + +impl Extend for GlyphOrder { + fn extend>(&mut self, iter: T) { + self.0.extend(iter) + } +} + +impl FromIterator for GlyphOrder { + fn from_iter>(iter: T) -> Self { + GlyphOrder(iter.into_iter().collect::>()) + } +} + +impl GlyphOrder { + pub fn new() -> Self { + GlyphOrder(IndexSet::new()) + } + + pub fn glyph_id(&self, name: &GlyphName) -> Option { + self.0.get_index_of(name).map(|i| i as u32) + } + + pub fn glyph_name(&self, index: usize) -> Option<&GlyphName> { + self.0.get_index(index) + } + + pub fn contains(&self, name: &GlyphName) -> bool { + self.0.contains(name) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + pub fn difference<'a>( + &'a self, + other: &'a GlyphOrder, + ) -> indexmap::set::Difference<'a, GlyphName, RandomState> { + self.0.difference(&other.0) + } + + pub fn insert(&mut self, name: GlyphName) -> bool { + self.0.insert(name) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl IntoIterator for GlyphOrder { + type Item = GlyphName; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + /// In logical (reading) order type KernPair = (KernParticipant, KernParticipant); type KernValues = BTreeMap>; @@ -136,7 +202,6 @@ impl StaticMetadata { names: HashMap, axes: Vec, named_instances: Vec, - mut glyph_order: IndexSet, glyph_locations: HashSet, ) -> Result { // Point axes are less exciting than ranged ones @@ -169,22 +234,12 @@ impl StaticMetadata { .map(|a| (a.tag, NormalizedCoord::new(0.0))) .collect(); - // cmap has strong beliefs wrt .notdef coming first - let notdef: GlyphName = ".notdef".into(); - if let Some(idx) = glyph_order.get_index_of(¬def) { - if idx > 0 { - trace!("Migrate .notdef from {idx} to 0"); - glyph_order.move_index(idx, 0); - } - } - Ok(StaticMetadata { units_per_em, names: key_to_name, axes, variable_axes, named_instances, - glyph_order, variation_model, axes_default, variable_axes_default, @@ -205,10 +260,6 @@ impl StaticMetadata { }) } - pub fn glyph_id(&self, name: &GlyphName) -> Option { - self.glyph_order.get_index_of(name).map(|i| i as u32) - } - /// The default on all known axes. pub fn default_location(&self) -> &NormalizedLocation { &self.axes_default @@ -707,7 +758,7 @@ pub struct NamedInstance { /// /// In time will split gpos/gsub, have different features for different /// locations, etc. -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum Features { Empty, File { @@ -799,6 +850,72 @@ impl Glyph { } } +impl IdAware for Glyph { + fn id(&self) -> WorkId { + WorkId::Glyph(self.name.clone()) + } +} + +impl Persistable for Glyph { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + +impl Persistable for StaticMetadata { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + +impl Persistable for GlyphOrder { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + +impl Persistable for GlobalMetrics { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + +impl Persistable for Features { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + +impl Persistable for Kerning { + fn read(from: &mut dyn Read) -> Self { + serde_yaml::from_reader(from).unwrap() + } + + fn write(&self, to: &mut dyn std::io::Write) { + serde_yaml::to_writer(to, self).unwrap(); + } +} + /// A variable definition of a single glyph. /// /// If defined in many locations, presumed to vary continuously diff --git a/fontir/src/orchestration.rs b/fontir/src/orchestration.rs index 19328d21..6643627a 100644 --- a/fontir/src/orchestration.rs +++ b/fontir/src/orchestration.rs @@ -4,20 +4,18 @@ use std::{ collections::HashMap, fmt::Debug, fs::File, - io::{BufReader, BufWriter}, - path::Path, + hash::Hash, + io::{BufReader, BufWriter, Read, Write}, sync::Arc, }; +use crate::{error::WorkError, ir, paths::Paths, source::Input}; use bitflags::bitflags; use fontdrasil::{ - orchestration::{Access, AccessControlList, Work, MISSING_DATA}, + orchestration::{Access, AccessControlList, Identifier, Work}, types::GlyphName, }; use parking_lot::RwLock; -use serde::{de::DeserializeOwned, Serialize}; - -use crate::{error::WorkError, ir, paths::Paths, source::Input}; bitflags! { #[derive(Clone, Copy, Debug)] @@ -34,83 +32,321 @@ bitflags! { } } -pub type ContextItem = Arc>>>; +impl Default for Flags { + /// Match the way gftools configures fontmake by default + fn default() -> Self { + Flags::EMIT_IR | Flags::PREFER_SIMPLE_GLYPHS + } +} -/// Generates fn $getter_name(&self) -> Arc<$value_type>. +/// Clones are cheap and reference the same wrapped item courtesy of Arc /// -/// Assumes we are in an impl block for a Context and that -/// self.$lock_name is a ContextItem<$ir_type> -/// -/// -#[macro_export] -macro_rules! context_accessors { - ($getter_name:ident, $setter_name:ident, $has_name:ident, $lock_name:ident, $value_type:ty, $id:expr, $restore_fn:ident, $prepersist_fn:ident) => { - pub fn $getter_name(&self) -> Arc<$value_type> { - let id = $id; - self.acl.assert_read_access(&id.clone().into()); - { - let rl = self.$lock_name.read(); - if rl.is_some() { - return rl.as_ref().unwrap().clone(); - } - } - set_cached(&self.$lock_name, $restore_fn(&self.paths.target_file(&id))); - let rl = self.$lock_name.read(); - rl.as_ref().expect(MISSING_DATA).clone() +/// Courtesy of Arc this is Clone even if T isn't +#[derive(Default)] +pub struct ContextItem +where + I: Identifier, +{ + id: I, + acl: Arc>, + persistent_storage: Arc

, + value: Arc>>>, +} + +impl ContextItem +where + I: Identifier, + P: PersistentStorage, + T: Persistable, +{ + pub fn new(id: I, acl: Arc>, persistent_storage: Arc

) -> Self { + ContextItem { + id, + acl, + persistent_storage, + value: Default::default(), } + } + + pub fn clone_with_acl(&self, acl: Arc>) -> Self { + ContextItem { + id: self.id.clone(), + acl, + persistent_storage: self.persistent_storage.clone(), + value: self.value.clone(), + } + } - pub fn $has_name(&self) -> bool { - let id = $id; - self.acl.assert_read_access(&id.clone().into()); - { - let rl = self.$lock_name.read(); - if rl.is_some() { - return true; - } + /// Read item that you are sure must exist. Panic if not. + /// + /// Intended for use in [Work] to access items present in + /// [Work::read_access]. If these are missing something is horribly + /// wrong and we should kerplode. + pub fn get(&self) -> Arc { + if let Some(in_memory) = self.try_get() { + return in_memory; + } + + // it's *not* in memory but perhaps it's written down? + if self.persistent_storage.active() { + if let Some(mut reader) = self.persistent_storage.reader(&self.id) { + let restored = T::read(&mut reader); + *self.value.write() = Some(Arc::from(restored)); } - self.paths.target_file(&id).is_file() } - pub fn $setter_name(&self, value: $value_type) { - let id = $id; - self.acl.assert_write_access(&id.clone().into()); - if self.flags.contains(Flags::EMIT_IR) { - let buf = $prepersist_fn(&value); - self.persist(&self.paths.target_file(&id), &buf); + // if we still don't have an answer just give up + self.try_get() + .unwrap_or_else(|| panic!("{:?} is not available", self.id)) + } + + /// Read an item that might not exist + pub fn try_get(&self) -> Option> { + self.acl.assert_read_access(&self.id); + self.value.read().as_ref().cloned() + } + + /// Update the value of the item whether or not it has changed. + /// + /// The change will be logged and anything relying on change detection, such as + /// conditional execution of dependent tasks, will fire. + /// + /// [ContextItem::set] is preferable where possible. + /// + /// This exists largely because write types in fontations do not always implement PartialEq. + /// TODO: should they? + pub fn set_unconditionally(&self, value: T) { + self.acl.assert_write_access(&self.id); + + if self.persistent_storage.active() { + let mut writer = self.persistent_storage.writer(&self.id); + value.write(&mut writer); + } + + *self.value.write() = Some(Arc::from(value)); + } +} + +impl ContextItem +where + I: Identifier, + T: PartialEq + Persistable, + P: PersistentStorage, +{ + /// Update the value if it has changed. + /// + /// Change logging and dependent task execution will only fire if the value changed. + pub fn set(&self, value: T) { + self.acl.assert_write_access(&self.id); + + // nop? + if self + .value + .read() + .as_ref() + .map(|arc| **arc == value) + .unwrap_or(false) + { + return; + } + + self.set_unconditionally(value); + } +} + +/// Clones are cheap and reference the same wrapped item courtesy of Arc +/// +/// Courtesy of Arc this is Clone even if T isn't +#[derive(Default)] +pub struct ContextMap +where + I: Identifier, + T: IdAware, + P: PersistentStorage, +{ + acl: Arc>, + persistent_storage: Arc

, + value: Arc>>>, +} + +impl ContextMap +where + I: Identifier, + T: IdAware + Persistable, + P: PersistentStorage, +{ + pub fn new(acl: Arc>, persistent_storage: Arc

) -> Self { + ContextMap { + acl, + persistent_storage, + value: Default::default(), + } + } + + pub fn clone_with_acl(&self, acl: Arc>) -> Self { + ContextMap { + acl, + persistent_storage: self.persistent_storage.clone(), + value: self.value.clone(), + } + } + + /// Read an item that might not exist + pub fn try_get(&self, id: &I) -> Option> { + self.acl.assert_read_access(id); + self.value.read().get(id).cloned() + } + + /// Read item that you are sure must exist. Panic if not. + /// + /// Intended for use in [Work] to access items present in + /// [Work::read_access]. If these are missing something is horribly + /// wrong and we should kerplode. + pub fn get(&self, id: &I) -> Arc { + if let Some(in_memory) = self.try_get(id) { + return in_memory; + } + + // it's *not* in memory but perhaps it's written down? + if self.persistent_storage.active() { + if let Some(mut reader) = self.persistent_storage.reader(id) { + let restored = T::read(&mut reader); + self.value.write().insert(id.clone(), Arc::from(restored)); } - set_cached(&self.$lock_name, value); } - }; + + // if we still don't have an answer just give up + self.try_get(id) + .unwrap_or_else(|| panic!("{:?} is not available", id)) + } +} + +impl ContextMap +where + I: Identifier, + T: IdAware + Persistable, + Ir: PersistentStorage, +{ + pub fn set_unconditionally(&self, value: T) { + let key = value.id(); + self.acl.assert_write_access(&key); + + if self.persistent_storage.active() { + let mut writer = self.persistent_storage.writer(&key); + value.write(&mut writer); + } + + self.value.write().insert(key, Arc::from(value)); + } } -impl Default for Flags { - /// Match the way gftools configures fontmake by default - fn default() -> Self { - Flags::EMIT_IR | Flags::PREFER_SIMPLE_GLYPHS +impl ContextMap +where + I: Identifier, + T: IdAware + PartialEq + Persistable, + Ir: PersistentStorage, +{ + pub fn set(&self, value: T) { + let key = value.id(); + self.acl.assert_write_access(&key); + + // nop? + if self + .value + .read() + .get(&key) + .map(|arc| **arc == value) + .unwrap_or(false) + { + return; + } + + self.set_unconditionally(value); } } +pub trait IdAware { + fn id(&self) -> I; +} + +pub trait Persistable { + fn read(from: &mut dyn Read) -> Self; + fn write(&self, to: &mut dyn Write); +} + +/// Reads and writes to somewhere that lives longer than processes. +/// +/// This enables the compiler to restore state from prior executions which is +/// crucial to incremental operation. +pub trait PersistentStorage { + fn active(&self) -> bool; + /// None if there is nothing written down for id + fn reader(&self, id: &I) -> Option>; + fn writer(&self, id: &I) -> Box; +} + // Unique identifier of work. If there are no fields work is unique. // Meant to be small and cheap to copy around. #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum WorkId { /// Build the initial static metadata. - InitStaticMetadata, + /// + /// Contains all names, axes, etc. Does NOT contain the final glyph order. + StaticMetadata, /// Build potentially variable font-wide metrics. GlobalMetrics, Glyph(GlyphName), GlyphIrDelete(GlyphName), - /// Update static metadata based on what we learned from IR + /// Glyph order from source, prior to adjustment /// - /// Notably, IR glyphs with both components and paths may split into multiple - /// BE glyphs so the glyph order may change. - FinalizeStaticMetadata, + /// Typically whatever2ir would populate this and then + /// it would be used to produce GlyphOrder. + /// + /// Most things should use GlyphOrder not this. + PreliminaryGlyphOrder, + /// The final glyph order. Most things that need glyph order should rely on this. + GlyphOrder, Features, Kerning, } +impl Identifier for WorkId {} + pub type IrWork = dyn Work + Send; +pub struct IrPersistentStorage { + active: bool, + pub(crate) paths: Paths, +} + +impl PersistentStorage for IrPersistentStorage { + fn active(&self) -> bool { + self.active + } + + fn reader(&self, id: &WorkId) -> Option> { + let file = self.paths.target_file(id); + if !file.exists() { + return None; + } + let raw_file = File::open(file.clone()) + .map_err(|e| panic!("Unable to write {file:?} {e}")) + .unwrap(); + Some(Box::from(BufReader::new(raw_file))) + } + + fn writer(&self, id: &WorkId) -> Box { + let file = self.paths.target_file(id); + let raw_file = File::create(file.clone()) + .map_err(|e| panic!("Unable to write {file:?} {e}")) + .unwrap(); + Box::from(BufWriter::new(raw_file)) + } +} + +type FeContextItem = ContextItem; +type FeContextMap = ContextMap; + /// Read/write access to data for async work. /// /// Intent is a root orchestrator creates a context and share copies with restricted @@ -119,22 +355,21 @@ pub type IrWork = dyn Work + Send; pub struct Context { pub flags: Flags, - pub(crate) paths: Arc, + pub(crate) persistent_storage: Arc, // The input we're working on. Note that change detection may mean we only process // a subset of the full input. pub input: Arc, - acl: AccessControlList, - // work results we've completed or restored from disk // We create individual caches so we can return typed results from get fns - init_static_metadata: ContextItem, - final_static_metadata: ContextItem, - global_metrics: ContextItem, - glyph_ir: Arc>>>, - feature_ir: ContextItem, - kerning: ContextItem, + pub static_metadata: FeContextItem, + pub preliminary_glyph_order: FeContextItem, + pub glyph_order: FeContextItem, + pub global_metrics: FeContextItem, + pub glyphs: FeContextMap, + pub features: FeContextItem, + pub kerning: FeContextItem, } pub fn set_cached(lock: &Arc>>>, value: T) { @@ -144,32 +379,54 @@ pub fn set_cached(lock: &Arc>>>, value: T) { impl Context { fn copy(&self, acl: AccessControlList) -> Context { + let acl = Arc::from(acl); Context { flags: self.flags, - paths: self.paths.clone(), + persistent_storage: self.persistent_storage.clone(), input: self.input.clone(), - acl, - init_static_metadata: self.init_static_metadata.clone(), - final_static_metadata: self.final_static_metadata.clone(), - global_metrics: self.global_metrics.clone(), - glyph_ir: self.glyph_ir.clone(), - feature_ir: self.feature_ir.clone(), - kerning: self.kerning.clone(), + static_metadata: self.static_metadata.clone_with_acl(acl.clone()), + preliminary_glyph_order: self.preliminary_glyph_order.clone_with_acl(acl.clone()), + glyph_order: self.glyph_order.clone_with_acl(acl.clone()), + global_metrics: self.global_metrics.clone_with_acl(acl.clone()), + glyphs: self.glyphs.clone_with_acl(acl.clone()), + features: self.features.clone_with_acl(acl.clone()), + kerning: self.kerning.clone_with_acl(acl), } } pub fn new_root(flags: Flags, paths: Paths, input: Input) -> Context { + let acl = Arc::from(AccessControlList::read_only()); + let persistent_storage = Arc::from(IrPersistentStorage { + active: flags.contains(Flags::EMIT_IR), + paths, + }); Context { flags, - paths: Arc::from(paths), + persistent_storage: persistent_storage.clone(), input: Arc::from(input), - acl: AccessControlList::read_only(), - init_static_metadata: Arc::from(RwLock::new(None)), - final_static_metadata: Arc::from(RwLock::new(None)), - global_metrics: Arc::from(RwLock::new(None)), - glyph_ir: Arc::from(RwLock::new(HashMap::new())), - feature_ir: Arc::from(RwLock::new(None)), - kerning: Arc::from(RwLock::new(None)), + static_metadata: ContextItem::new( + WorkId::StaticMetadata, + acl.clone(), + persistent_storage.clone(), + ), + preliminary_glyph_order: ContextItem::new( + WorkId::PreliminaryGlyphOrder, + acl.clone(), + persistent_storage.clone(), + ), + glyph_order: ContextItem::new( + WorkId::GlyphOrder, + acl.clone(), + persistent_storage.clone(), + ), + global_metrics: ContextItem::new( + WorkId::GlobalMetrics, + acl.clone(), + persistent_storage.clone(), + ), + glyphs: ContextMap::new(acl.clone(), persistent_storage.clone()), + features: ContextItem::new(WorkId::Features, acl.clone(), persistent_storage.clone()), + kerning: ContextItem::new(WorkId::Kerning, acl, persistent_storage), } } @@ -184,77 +441,4 @@ impl Context { pub fn read_only(&self) -> Context { self.copy(AccessControlList::read_only()) } - - fn maybe_persist(&self, file: &Path, content: &V) - where - V: ?Sized + Serialize + Debug, - { - if !self.flags.contains(Flags::EMIT_IR) { - return; - } - self.persist(file, content); - } - - fn persist(&self, file: &Path, content: &V) - where - V: ?Sized + Serialize + Debug, - { - let raw_file = File::create(file) - .map_err(|e| panic!("Unable to write {file:?} {e}")) - .unwrap(); - let buf_io = BufWriter::new(raw_file); - serde_yaml::to_writer(buf_io, &content) - .map_err(|e| panic!("Unable to serialize\n{content:#?}\nto {file:?}: {e}")) - .unwrap(); - } - - fn set_cached_glyph(&self, ir: ir::Glyph) { - let mut wl = self.glyph_ir.write(); - wl.insert(ir.name.clone(), Arc::from(ir)); - } - - pub fn get_glyph_ir(&self, glyph_name: &GlyphName) -> Arc { - let id = WorkId::Glyph(glyph_name.clone()); - self.acl.assert_read_access(&id); - { - let rl = self.glyph_ir.read(); - if let Some(glyph) = rl.get(glyph_name) { - return glyph.clone(); - } - } - self.set_cached_glyph(restore(&self.paths.target_file(&id))); - let rl = self.glyph_ir.read(); - rl.get(glyph_name).expect(MISSING_DATA).clone() - } - - pub fn set_glyph_ir(&self, ir: ir::Glyph) { - let id = WorkId::Glyph(ir.name.clone()); - self.acl.assert_write_access(&id); - self.maybe_persist(&self.paths.target_file(&id), &ir); - self.set_cached_glyph(ir); - } - - context_accessors! { get_init_static_metadata, set_init_static_metadata, has_init_static_metadata, init_static_metadata, ir::StaticMetadata, WorkId::InitStaticMetadata, restore, nop } - context_accessors! { get_final_static_metadata, set_final_static_metadata, has_final_static_metadata, final_static_metadata, ir::StaticMetadata, WorkId::FinalizeStaticMetadata, restore, nop } - context_accessors! { get_global_metrics, set_global_metrics, has_global_metrics, global_metrics, ir::GlobalMetrics, WorkId::GlobalMetrics, restore, nop } - context_accessors! { get_features, set_features, has_feature_ir, feature_ir, ir::Features, WorkId::Features, restore, nop } - context_accessors! { get_kerning, set_kerning, has_kerning, kerning, ir::Kerning, WorkId::Kerning, restore, nop } -} - -fn nop(v: &T) -> &T { - v -} - -fn restore(file: &Path) -> V -where - V: ?Sized + DeserializeOwned, -{ - let raw_file = File::open(file) - .map_err(|e| panic!("Unable to read {file:?} {e}")) - .unwrap(); - let buf_io = BufReader::new(raw_file); - match serde_yaml::from_reader(buf_io) { - Ok(v) => v, - Err(e) => panic!("Unable to deserialize {file:?} {e}"), - } } diff --git a/fontir/src/paths.rs b/fontir/src/paths.rs index 4a8d014d..5130a814 100644 --- a/fontir/src/paths.rs +++ b/fontir/src/paths.rs @@ -43,8 +43,9 @@ impl Paths { pub fn target_file(&self, id: &WorkId) -> PathBuf { match id { - WorkId::InitStaticMetadata => self.build_dir.join("static_metadata.preliminary.yml"), - WorkId::FinalizeStaticMetadata => self.build_dir.join("static_metadata.yml"), + WorkId::StaticMetadata => self.build_dir.join("static_metadata.yml"), + WorkId::PreliminaryGlyphOrder => self.build_dir.join("glyph_order.preliminary.yml"), + WorkId::GlyphOrder => self.build_dir.join("glyph_order.yml"), WorkId::GlobalMetrics => self.build_dir.join("global_metrics.yml"), WorkId::Glyph(name) => self.glyph_ir_file(name.as_str()), WorkId::GlyphIrDelete(name) => { diff --git a/fontir/src/serde.rs b/fontir/src/serde.rs index f1b8d792..849cc846 100644 --- a/fontir/src/serde.rs +++ b/fontir/src/serde.rs @@ -13,8 +13,8 @@ use write_fonts::tables::os2::SelectionFlags; use crate::{ coords::{CoordConverter, DesignCoord, NormalizedLocation, UserCoord}, ir::{ - Axis, GlobalMetric, GlobalMetrics, Glyph, GlyphBuilder, GlyphInstance, KernParticipant, - Kerning, MiscMetadata, NameKey, NamedInstance, StaticMetadata, + Axis, GlobalMetric, GlobalMetrics, Glyph, GlyphBuilder, GlyphInstance, GlyphOrder, + KernParticipant, Kerning, MiscMetadata, NameKey, NamedInstance, StaticMetadata, }, stateset::{FileState, MemoryState, State, StateIdentifier, StateSet}, }; @@ -60,7 +60,6 @@ pub struct StaticMetadataSerdeRepr { pub glyph_locations: Vec, pub names: HashMap, pub misc: MiscSerdeRepr, - pub glyph_order: Vec, } impl From for StaticMetadata { @@ -70,7 +69,6 @@ impl From for StaticMetadata { from.names, from.axes, from.named_instances, - from.glyph_order.into_iter().map(|s| s.into()).collect(), from.glyph_locations.into_iter().collect(), ) .unwrap() @@ -87,15 +85,25 @@ impl From for StaticMetadataSerdeRepr { glyph_locations, names: from.names, misc: from.misc.into(), - glyph_order: from - .glyph_order - .into_iter() - .map(|n| n.as_str().to_string()) - .collect(), } } } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GlyphOrderSerdeRepr(Vec); + +impl From for GlyphOrder { + fn from(value: GlyphOrderSerdeRepr) -> Self { + value.0.into_iter().map(|v| v.into()).collect() + } +} + +impl From for GlyphOrderSerdeRepr { + fn from(value: GlyphOrder) -> Self { + GlyphOrderSerdeRepr(value.into_iter().map(|v| v.to_string()).collect()) + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct KerningSerdeRepr { pub groups: Vec, diff --git a/fontir/src/source.rs b/fontir/src/source.rs index 6d919acb..847f78d6 100644 --- a/fontir/src/source.rs +++ b/fontir/src/source.rs @@ -15,6 +15,7 @@ use crate::{ }; /// Destroy a file, such as the IR for a deleted glyph +#[derive(Debug)] pub struct DeleteWork { glyph_name: GlyphName, } @@ -31,7 +32,7 @@ impl Work for DeleteWork { } fn exec(&self, context: &Context) -> Result<(), WorkError> { - let path = context.paths.target_file(&self.id()); + let path = context.persistent_storage.paths.target_file(&self.id()); debug!("Delete {:#?}", path); if path.exists() { fs::remove_file(&path).map_err(WorkError::IoError)? diff --git a/glyphs2fontir/src/source.rs b/glyphs2fontir/src/source.rs index 04613156..0faf74ce 100644 --- a/glyphs2fontir/src/source.rs +++ b/glyphs2fontir/src/source.rs @@ -1,12 +1,12 @@ use chrono::{TimeZone, Utc}; use font_types::{NameId, Tag}; -use fontdrasil::orchestration::Work; +use fontdrasil::orchestration::{Access, Work}; use fontdrasil::types::{GlyphName, GroupName}; use fontir::coords::NormalizedCoord; use fontir::error::{Error, WorkError}; use fontir::ir::{ - self, GlobalMetric, GlobalMetrics, GlyphInstance, KernParticipant, Kerning, NameBuilder, - NameKey, NamedInstance, StaticMetadata, DEFAULT_VENDOR_ID, + self, GlobalMetric, GlobalMetrics, GlyphInstance, GlyphOrder, KernParticipant, Kerning, + NameBuilder, NameKey, NamedInstance, StaticMetadata, DEFAULT_VENDOR_ID, }; use fontir::orchestration::{Context, IrWork, WorkId}; use fontir::source::{Input, Source}; @@ -277,6 +277,7 @@ fn names(font: &Font) -> HashMap { builder.into_inner() } +#[derive(Debug)] struct StaticMetadataWork { font_info: Arc, glyph_names: Arc>, @@ -284,7 +285,11 @@ struct StaticMetadataWork { impl Work for StaticMetadataWork { fn id(&self) -> WorkId { - WorkId::InitStaticMetadata + WorkId::StaticMetadata + } + + fn also_completes(&self) -> Vec { + vec![WorkId::PreliminaryGlyphOrder] } fn exec(&self, context: &Context) -> Result<(), WorkError> { @@ -321,12 +326,6 @@ impl Work for StaticMetadataWork { .iter() .map(|m| font_info.locations.get(&m.axes_values).cloned().unwrap()) .collect(); - let glyph_order = font - .glyph_order - .iter() - .map(|s| s.into()) - .filter(|gn| self.glyph_names.contains(gn)) - .collect(); let selection_flags = match font.use_typo_metrics.unwrap_or_default() { true => SelectionFlags::USE_TYPO_METRICS, @@ -348,7 +347,6 @@ impl Work for StaticMetadataWork { names(font), axes, named_instances, - glyph_order, glyph_locations, ) .map_err(WorkError::VariationModelError)?; @@ -376,11 +374,20 @@ impl Work for StaticMetadataWork { }) .or(static_metadata.misc.created); - context.set_init_static_metadata(static_metadata); + context.static_metadata.set(static_metadata); + + let glyph_order = font + .glyph_order + .iter() + .map(|s| s.into()) + .filter(|gn| self.glyph_names.contains(gn)) + .collect(); + context.preliminary_glyph_order.set(glyph_order); Ok(()) } } +#[derive(Debug)] struct GlobalMetricWork { font_info: Arc, } @@ -390,6 +397,10 @@ impl Work for GlobalMetricWork { WorkId::GlobalMetrics } + fn read_access(&self) -> Access { + Access::One(WorkId::StaticMetadata) + } + fn exec(&self, context: &Context) -> Result<(), WorkError> { let font_info = self.font_info.as_ref(); let font = &font_info.font; @@ -401,7 +412,7 @@ impl Work for GlobalMetricWork { .unwrap_or("") ); - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); let mut metrics = GlobalMetrics::new( static_metadata.default_location().clone(), static_metadata.units_per_em, @@ -456,11 +467,12 @@ impl Work for GlobalMetricWork { ); } - context.set_global_metrics(metrics); + context.global_metrics.set(metrics); Ok(()) } } +#[derive(Debug)] struct FeatureWork { font_info: Arc, } @@ -475,7 +487,7 @@ impl Work for FeatureWork { let font_info = self.font_info.as_ref(); let font = &font_info.font; - context.set_features(to_ir_features(&font.features)?); + context.features.set(to_ir_features(&font.features)?); Ok(()) } } @@ -506,13 +518,14 @@ fn is_kerning_class(name: &str) -> bool { name.starts_with("@MMK_") } +#[derive(Debug)] struct KerningWork { font_info: Arc, } /// See fn kern_participant( - glyph_order: &IndexSet, + glyph_order: &GlyphOrder, groups: &HashMap>, side: KernSide, raw_side: &str, @@ -554,10 +567,14 @@ impl Work for KerningWork { WorkId::Kerning } + fn read_access(&self) -> Access { + Access::One(WorkId::GlyphOrder) + } + fn exec(&self, context: &Context) -> Result<(), WorkError> { trace!("Generate IR for kerning"); - let static_metadata = context.get_final_static_metadata(); - let glyph_order = &static_metadata.glyph_order; + let arc_glyph_order = context.glyph_order.get(); + let glyph_order = arc_glyph_order.as_ref(); let font_info = self.font_info.as_ref(); let font = &font_info.font; @@ -623,11 +640,12 @@ impl Work for KerningWork { .insert(pos, (value as f32).into()); }); - context.set_kerning(kerning); + context.kerning.set(kerning); Ok(()) } } +#[derive(Debug)] struct GlyphIrWork { glyph_name: GlyphName, font_info: Arc, @@ -654,12 +672,16 @@ impl Work for GlyphIrWork { WorkId::Glyph(self.glyph_name.clone()) } + fn read_access(&self) -> Access { + Access::One(WorkId::StaticMetadata) + } + fn exec(&self, context: &Context) -> Result<(), WorkError> { trace!("Generate IR for '{}'", self.glyph_name.as_str()); let font_info = self.font_info.as_ref(); let font = &font_info.font; - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); let axes = &static_metadata.axes; let glyph = font @@ -731,7 +753,7 @@ impl Work for GlyphIrWork { check_pos(&self.glyph_name, positions, axis, &max)?; } - context.set_glyph_ir(ir_glyph.try_into()?); + context.glyphs.set(ir_glyph.try_into()?); Ok(()) } } @@ -752,7 +774,7 @@ mod tests { UserLocation, }, error::WorkError, - ir::{self, GlobalMetricsInstance, NameKey}, + ir::{self, GlobalMetricsInstance, GlyphOrder, NameKey}, orchestration::{Context, Flags, WorkId}, paths::Paths, source::Source, @@ -845,8 +867,13 @@ mod tests { #[test] fn static_metadata_ir() { let (source, context) = context_for(glyphs3_dir().join("WghtVar.glyphs")); - let task_context = - context.copy_for_work(Access::none(), Access::one(WorkId::InitStaticMetadata)); + let task_context = context.copy_for_work( + Access::none(), + Access::Set(HashSet::from([ + WorkId::StaticMetadata, + WorkId::PreliminaryGlyphOrder, + ])), + ); source .create_static_metadata_work(&context.input) .unwrap() @@ -856,13 +883,14 @@ mod tests { assert_eq!( vec![Tag::new(b"wght")], context - .get_init_static_metadata() + .static_metadata + .get() .axes .iter() .map(|a| a.tag) .collect::>() ); - let expected: IndexSet = vec![ + let expected: GlyphOrder = vec![ "space", "exclam", "hyphen", @@ -873,15 +901,20 @@ mod tests { .iter() .map(|s| (*s).into()) .collect(); - assert_eq!(expected, context.get_init_static_metadata().glyph_order); + assert_eq!(expected, *context.preliminary_glyph_order.get()); } #[test] fn static_metadata_ir_multi_axis() { // Caused index out of bounds due to transposed master and value indices let (source, context) = context_for(glyphs2_dir().join("BadIndexing.glyphs")); - let task_context = - context.copy_for_work(Access::none(), Access::one(WorkId::InitStaticMetadata)); + let task_context = context.copy_for_work( + Access::none(), + Access::Set(HashSet::from([ + WorkId::StaticMetadata, + WorkId::PreliminaryGlyphOrder, + ])), + ); source .create_static_metadata_work(&context.input) .unwrap() @@ -892,14 +925,19 @@ mod tests { #[test] fn loads_axis_mappings_from_glyphs2() { let (source, context) = context_for(glyphs2_dir().join("OpszWghtVar_AxisMappings.glyphs")); - let task_context = - context.copy_for_work(Access::none(), Access::one(WorkId::InitStaticMetadata)); + let task_context = context.copy_for_work( + Access::none(), + Access::Set(HashSet::from([ + WorkId::StaticMetadata, + WorkId::PreliminaryGlyphOrder, + ])), + ); source .create_static_metadata_work(&context.input) .unwrap() .exec(&task_context) .unwrap(); - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); // Did you load the mappings? DID YOU?! assert_eq!( @@ -947,8 +985,13 @@ mod tests { fn build_static_metadata(glyphs_file: PathBuf) -> (impl Source, Context) { let _ = env_logger::builder().is_test(true).try_init(); let (source, context) = context_for(glyphs_file); - let task_context = - context.copy_for_work(Access::none(), Access::one(WorkId::InitStaticMetadata)); + let task_context = context.copy_for_work( + Access::none(), + Access::Set(HashSet::from([ + WorkId::StaticMetadata, + WorkId::PreliminaryGlyphOrder, + ])), + ); source .create_static_metadata_work(&context.input) .unwrap() @@ -960,7 +1003,7 @@ mod tests { fn build_global_metrics(glyphs_file: PathBuf) -> (impl Source, Context) { let (source, context) = build_static_metadata(glyphs_file); let task_context = context.copy_for_work( - Access::one(WorkId::InitStaticMetadata), + Access::one(WorkId::StaticMetadata), Access::one(WorkId::GlobalMetrics), ); source @@ -983,7 +1026,7 @@ mod tests { .unwrap(); for work in work_items.iter() { let task_context = context.copy_for_work( - Access::one(WorkId::InitStaticMetadata), + Access::one(WorkId::StaticMetadata), Access::one(WorkId::Glyph(glyph_name.clone())), ); work.exec(&task_context)?; @@ -999,7 +1042,7 @@ mod tests { build_static_metadata(glyphs2_dir().join("OpszWghtVar_AxisMappings.glyphs")); build_glyphs(&source, &context, &[&glyph_name]).unwrap(); // we dont' care about geometry - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); let axes = static_metadata.axes.iter().map(|a| (a.tag, a)).collect(); let mut expected_locations = HashSet::new(); @@ -1018,7 +1061,8 @@ mod tests { expected_locations.insert(loc); } let actual_locations = context - .get_glyph_ir(&glyph_name) + .glyphs + .get(&WorkId::Glyph(glyph_name)) .sources() .keys() .map(|c| c.to_user(&axes)) @@ -1050,7 +1094,8 @@ mod tests { expected_locations.insert(loc); } let actual_locations = context - .get_glyph_ir(&glyph_name) + .glyphs + .get(&WorkId::Glyph(glyph_name)) .sources() .keys() .cloned() @@ -1076,7 +1121,7 @@ mod tests { #[test] fn read_axis_location() { let (_, context) = build_static_metadata(glyphs3_dir().join("WghtVar_AxisLocation.glyphs")); - let wght = &context.get_init_static_metadata().axes; + let wght = &context.static_metadata.get().axes; assert_eq!(1, wght.len()); let wght = &wght[0]; @@ -1100,7 +1145,7 @@ mod tests { fn captures_single_codepoints() { let (source, context) = build_static_metadata(glyphs2_dir().join("WghtVar.glyphs")); build_glyphs(&source, &context, &[&"hyphen".into()]).unwrap(); - let glyph = context.get_glyph_ir(&"hyphen".into()); + let glyph = context.glyphs.get(&WorkId::Glyph("hyphen".into())); assert_eq!(HashSet::from([0x002d]), glyph.codepoints); } @@ -1109,7 +1154,7 @@ mod tests { let (source, context) = build_static_metadata(glyphs3_dir().join("Unicode-UnquotedDec.glyphs")); build_glyphs(&source, &context, &[&"name".into()]).unwrap(); - let glyph = context.get_glyph_ir(&"name".into()); + let glyph = context.glyphs.get(&WorkId::Glyph("name".into())); assert_eq!(HashSet::from([182]), glyph.codepoints); } @@ -1118,7 +1163,7 @@ mod tests { let (source, context) = build_static_metadata(glyphs3_dir().join("Unicode-UnquotedDecSequence.glyphs")); build_glyphs(&source, &context, &[&"name".into()]).unwrap(); - let glyph = context.get_glyph_ir(&"name".into()); + let glyph = context.glyphs.get(&WorkId::Glyph("name".into())); assert_eq!(HashSet::from([1619, 1764]), glyph.codepoints); } @@ -1126,7 +1171,7 @@ mod tests { #[test] fn loads_minimal() { let (_, context) = build_static_metadata(glyphs2_dir().join("NotDef.glyphs")); - assert_eq!(1000, context.get_init_static_metadata().units_per_em); + assert_eq!(1000, context.static_metadata.get().units_per_em); } #[test] @@ -1246,9 +1291,10 @@ mod tests { #[test] fn captures_global_metrics() { let (_, context) = build_global_metrics(glyphs3_dir().join("WghtVar.glyphs")); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let default_metrics = context - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); assert_eq!( GlobalMetricsInstance { @@ -1285,14 +1331,14 @@ mod tests { let (_, context) = build_static_metadata(glyphs3_dir().join("TheBestNames.glyphs")); assert_eq!( Tag::new(b"RODS"), - context.get_init_static_metadata().misc.vendor_id + context.static_metadata.get().misc.vendor_id ); } #[test] fn default_underline_settings() { let (_, context) = build_static_metadata(glyphs3_dir().join("Oswald-O.glyphs")); - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); assert_eq!( (1000, 50.0, -100.0), ( diff --git a/glyphs2fontir/src/toir.rs b/glyphs2fontir/src/toir.rs index 8b198709..e7275962 100644 --- a/glyphs2fontir/src/toir.rs +++ b/glyphs2fontir/src/toir.rs @@ -220,6 +220,7 @@ fn ir_axes(font: &Font) -> Result, Error> { } /// A [Font] with some prework to convert to IR predone. +#[derive(Debug)] pub struct FontInfo { pub font: Font, /// Index by master id diff --git a/ufo2fontir/src/source.rs b/ufo2fontir/src/source.rs index 4f2baa9e..6dde49fa 100644 --- a/ufo2fontir/src/source.rs +++ b/ufo2fontir/src/source.rs @@ -8,15 +8,15 @@ use std::{ use chrono::{DateTime, TimeZone, Utc}; use font_types::{InvalidTag, NameId, Tag}; use fontdrasil::{ - orchestration::Work, + orchestration::{Access, Work}, types::{GlyphName, GroupName}, }; use fontir::{ coords::{DesignLocation, NormalizedLocation, UserCoord}, error::{Error, WorkError}, ir::{ - Features, GlobalMetric, GlobalMetrics, KernParticipant, Kerning, NameBuilder, NameKey, - NamedInstance, StaticMetadata, DEFAULT_VENDOR_ID, + Features, GlobalMetric, GlobalMetrics, GlyphOrder, KernParticipant, Kerning, NameBuilder, + NameKey, NamedInstance, StaticMetadata, DEFAULT_VENDOR_ID, }, orchestration::{Context, IrWork, WorkId}, source::{Input, Source}, @@ -409,22 +409,26 @@ impl Source for DesignSpaceIrSource { } } +#[derive(Debug)] struct StaticMetadataWork { designspace_file: PathBuf, designspace: Arc, glyph_names: Arc>, } +#[derive(Debug)] struct GlobalMetricsWork { designspace_file: PathBuf, designspace: Arc, } +#[derive(Debug)] struct FeatureWork { designspace_file: PathBuf, fea_files: Arc>, } +#[derive(Debug)] struct KerningWork { designspace_file: PathBuf, designspace: Arc, @@ -467,10 +471,10 @@ fn glyph_order( source: &norad::designspace::Source, designspace_dir: &Path, glyph_names: &HashSet, -) -> Result, WorkError> { +) -> Result { // The UFO at the default master *may* elect to specify a glyph order // That glyph order *may* deign to overlap with the actual glyph set - let mut glyph_order = IndexSet::new(); + let mut glyph_order = GlyphOrder::new(); let lib_plist = load_plist(&designspace_dir.join(&source.filename), "lib.plist")?; if let Some(plist::Value::Array(ufo_order)) = lib_plist.get("public.glyphOrder") { let mut pending_add: HashSet<_> = glyph_names.clone(); @@ -659,7 +663,11 @@ fn try_parse_date(raw_date: Option<&String>) -> Option> { impl Work for StaticMetadataWork { fn id(&self) -> WorkId { - WorkId::InitStaticMetadata + WorkId::StaticMetadata + } + + fn also_completes(&self) -> Vec { + vec![WorkId::PreliminaryGlyphOrder] } fn exec(&self, context: &Context) -> Result<(), WorkError> { @@ -723,15 +731,9 @@ impl Work for StaticMetadataWork { StyleMapStyle::BoldItalic => SelectionFlags::BOLD | SelectionFlags::ITALIC, }; - let mut static_metadata = StaticMetadata::new( - units_per_em, - names, - axes, - named_instances, - glyph_order, - glyph_locations, - ) - .map_err(WorkError::VariationModelError)?; + let mut static_metadata = + StaticMetadata::new(units_per_em, names, axes, named_instances, glyph_locations) + .map_err(WorkError::VariationModelError)?; static_metadata.misc.selection_flags = selection_flags; if let Some(vendor_id) = &font_info_at_default.open_type_os2_vendor_id { static_metadata.misc.vendor_id = @@ -770,7 +772,8 @@ impl Work for StaticMetadataWork { try_parse_date(font_info_at_default.open_type_head_created.as_ref()) .or(static_metadata.misc.created); - context.set_init_static_metadata(static_metadata); + context.preliminary_glyph_order.set(glyph_order); + context.static_metadata.set(static_metadata); Ok(()) } } @@ -785,9 +788,13 @@ impl Work for GlobalMetricsWork { WorkId::GlobalMetrics } + fn read_access(&self) -> Access { + Access::One(WorkId::StaticMetadata) + } + fn exec(&self, context: &Context) -> Result<(), WorkError> { debug!("Global metrics for {:#?}", self.designspace_file); - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); let designspace_dir = self.designspace_file.parent().unwrap(); let font_infos = font_infos(designspace_dir, &self.designspace)?; @@ -870,7 +877,7 @@ impl Work for GlobalMetricsWork { } trace!("{:#?}", metrics); - context.set_global_metrics(metrics); + context.global_metrics.set(metrics); Ok(()) } } @@ -908,9 +915,11 @@ impl Work for FeatureWork { fea_file.clone(), ) })?; - context.set_features(Features::from_file(fea_file, Some(include_dir))); + context + .features + .set(Features::from_file(fea_file, Some(include_dir))); } else { - context.set_features(Features::empty()); + context.features.set(Features::empty()); } Ok(()) @@ -919,7 +928,7 @@ impl Work for FeatureWork { fn kerning_groups_for( designspace_dir: &Path, - glyph_order: &IndexSet, + glyph_order: &GlyphOrder, source: &norad::designspace::Source, ) -> Result>, WorkError> { let ufo_dir = designspace_dir.join(&source.filename); @@ -974,12 +983,17 @@ impl Work for KerningWork { WorkId::Kerning } + fn read_access(&self) -> Access { + Access::Set(HashSet::from([WorkId::StaticMetadata, WorkId::GlyphOrder])) + } + /// See fn exec(&self, context: &Context) -> Result<(), WorkError> { debug!("Kerning for {:#?}", self.designspace_file); let designspace_dir = self.designspace_file.parent().unwrap(); - let static_metadata = context.get_final_static_metadata(); + let glyph_order = context.glyph_order.get(); + let static_metadata = context.static_metadata.get(); let master_locations = master_locations(&static_metadata.axes, &self.designspace.sources); let mut kerning = Kerning::default(); @@ -992,11 +1006,7 @@ impl Work for KerningWork { // Based on discussion on https://github.com/googlefonts/ufo2ft/pull/635, take the groups of // the default master as the authoritative source on groups - kerning.groups = kerning_groups_for( - designspace_dir, - &static_metadata.glyph_order, - default_master, - )?; + kerning.groups = kerning_groups_for(designspace_dir, glyph_order.as_ref(), default_master)?; // Group names are side-specific (public.kern1/2) but the set of glyph names isn't // so include side in the key to avoid matching the wrong side @@ -1022,9 +1032,7 @@ impl Work for KerningWork { .enumerate() .filter(|(idx, source)| !is_glyph_only(source) && *idx != default_master_idx) { - for (name, entries) in - kerning_groups_for(designspace_dir, &static_metadata.glyph_order, source)? - { + for (name, entries) in kerning_groups_for(designspace_dir, &glyph_order, source)? { let Some(real_name) = reverse_groups.get(&(KernSide::of(&name), &entries)) else { warn!("{name} exists only in {} and will be ignored", source.name); continue; @@ -1069,7 +1077,7 @@ impl Work for KerningWork { Some(KernParticipant::Group((**group_name).clone())) } else { let glyph_name = GlyphName::from(name); - if !static_metadata.glyph_order.contains(&glyph_name) { + if !glyph_order.contains(&glyph_name) { warn!("'{name}' refers to a non-existent glyph; ignored"); return None; } @@ -1095,11 +1103,12 @@ impl Work for KerningWork { } } - context.set_kerning(kerning); + context.kerning.set(kerning); Ok(()) } } +#[derive(Debug)] struct GlyphIrWork { glyph_name: GlyphName, glif_files: HashMap>, @@ -1110,13 +1119,17 @@ impl Work for GlyphIrWork { WorkId::Glyph(self.glyph_name.clone()) } + fn read_access(&self) -> Access { + Access::One(WorkId::StaticMetadata) + } + fn exec(&self, context: &Context) -> Result<(), WorkError> { trace!( "Generate glyph IR for {:?} from {:#?}", self.glyph_name, self.glif_files ); - let static_metadata = context.get_init_static_metadata(); + let static_metadata = context.static_metadata.get(); // Migrate glif_files into internal coordinates let axes_by_name = static_metadata.axes.iter().map(|a| (a.tag, a)).collect(); @@ -1130,7 +1143,7 @@ impl Work for GlyphIrWork { } let glyph_ir = to_ir_glyph(self.glyph_name.clone(), &glif_files)?; - context.set_glyph_ir(glyph_ir); + context.glyphs.set(glyph_ir); Ok(()) } } @@ -1143,15 +1156,14 @@ mod tests { }; use font_types::Tag; - use fontdrasil::{orchestration::Access, types::GlyphName}; + use fontdrasil::orchestration::Access; use fontir::{ coords::{DesignCoord, DesignLocation, NormalizedCoord, NormalizedLocation, UserCoord}, - ir::{GlobalMetricsInstance, NameKey}, + ir::{GlobalMetricsInstance, GlyphOrder, NameKey}, orchestration::{Context, Flags, WorkId}, paths::Paths, source::{Input, Source}, }; - use indexmap::IndexSet; use norad::designspace; use pretty_assertions::assert_eq; @@ -1224,8 +1236,13 @@ mod tests { Paths::new(Path::new("/nothing/should/write/here")), input, ); - let task_context = - context.copy_for_work(Access::none(), Access::one(WorkId::InitStaticMetadata)); + let task_context = context.copy_for_work( + Access::none(), + Access::Set(HashSet::from([ + WorkId::StaticMetadata, + WorkId::PreliminaryGlyphOrder, + ])), + ); source .create_static_metadata_work(&context.input) .unwrap() @@ -1234,10 +1251,20 @@ mod tests { (source, context) } + fn build_glyph_order(context: &Context) { + let task_context = context.copy_for_work( + Access::one(WorkId::PreliminaryGlyphOrder), + Access::one(WorkId::GlyphOrder), + ); + task_context + .glyph_order + .set((*task_context.preliminary_glyph_order.get()).clone()); + } + fn build_global_metrics(name: &str) -> (impl Source, Context) { let (source, context) = build_static_metadata(name); let task_context = context.copy_for_work( - Access::one(WorkId::InitStaticMetadata), + Access::one(WorkId::StaticMetadata), Access::one(WorkId::GlobalMetrics), ); source @@ -1250,16 +1277,10 @@ mod tests { fn build_kerning(name: &str) -> (impl Source, Context) { let (source, context) = build_static_metadata(name); - - context - .copy_for_work( - Access::one(WorkId::InitStaticMetadata), - Access::one(WorkId::FinalizeStaticMetadata), - ) - .set_final_static_metadata((*context.get_init_static_metadata()).clone()); + build_glyph_order(&context); let task_context = context.copy_for_work( - Access::one(WorkId::FinalizeStaticMetadata), + Access::Set(HashSet::from([WorkId::StaticMetadata, WorkId::GlyphOrder])), Access::one(WorkId::Kerning), ); source @@ -1378,7 +1399,7 @@ mod tests { ) .unwrap(); // lib.plist specifies plus, so plus goes first and then the rest in alphabetical order - let expected: IndexSet = vec!["plus", "an-imaginary-one", "bar"] + let expected: GlyphOrder = vec!["plus", "an-imaginary-one", "bar"] .iter() .map(|s| (*s).into()) .collect(); @@ -1450,9 +1471,10 @@ mod tests { #[test] fn captures_global_metrics_from_ints() { let (_, context) = build_global_metrics("static.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let default_metrics = context - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); assert_eq!( (720.0, 510.0), @@ -1466,9 +1488,10 @@ mod tests { #[test] fn captures_global_metrics_from_floats() { let (_, context) = build_global_metrics("float.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let default_metrics = context - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); assert_eq!( (755.25, -174.5), @@ -1484,16 +1507,17 @@ mod tests { let (_, context) = build_static_metadata("fontinfo.designspace"); assert_eq!( Tag::new(b"RODS"), - context.get_init_static_metadata().misc.vendor_id + context.static_metadata.get().misc.vendor_id ); } #[test] fn captures_global_metrics() { let (_, context) = build_global_metrics("fontinfo.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let default_metrics = context - .get_global_metrics() + .global_metrics + .get() .at(static_metadata.default_location()); assert_eq!( GlobalMetricsInstance { @@ -1534,7 +1558,7 @@ mod tests { #[test] fn glyph_locations() { let (_, context) = build_static_metadata("wght_var.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let wght = static_metadata.variable_axes.first().unwrap(); assert_eq!( @@ -1554,10 +1578,11 @@ mod tests { #[test] fn no_metrics_for_glyph_only_sources() { let (_, context) = build_global_metrics("wght_var.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); let wght = static_metadata.variable_axes.first().unwrap(); let mut metric_locations = context - .get_global_metrics() + .global_metrics + .get() .iter() .map(|(loc, ..)| loc) .collect::>() @@ -1579,7 +1604,7 @@ mod tests { #[test] fn default_underline_settings() { let (_, context) = build_static_metadata("wght_var.designspace"); - let static_metadata = &context.get_init_static_metadata(); + let static_metadata = &context.static_metadata.get(); assert_eq!( (1000, 50.0, -75.0), ( @@ -1593,7 +1618,7 @@ mod tests { #[test] fn groups_renamed_to_match_master() { let (_, context) = build_kerning("wght_var.designspace"); - let kerning = context.get_kerning(); + let kerning = context.kerning.get(); let mut groups: Vec<_> = kerning .groups