Skip to content

Commit

Permalink
Rough in wiring for glyph compile and merge
Browse files Browse the repository at this point in the history
  • Loading branch information
rsheeter committed Feb 15, 2023
1 parent 418cbd4 commit 68da0bc
Show file tree
Hide file tree
Showing 17 changed files with 690 additions and 67 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[patch.crates-io]
read-fonts = { git = 'https://github.com/googlefonts/fontations' }
write-fonts = { git = 'https://github.com/googlefonts/fontations' }

[workspace]

members = [
Expand Down
6 changes: 4 additions & 2 deletions fontbe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ serde = {version = "1.0", features = ["derive"] }
serde_yaml = "0.9.14"

thiserror = "1.0.37"
kurbo = { version = "0.9.0" }
ordered-float = { version = "3.4.0", features = ["serde"] }
indexmap = "1.9.2"

Expand All @@ -26,11 +27,12 @@ env_logger = "0.9.0"

parking_lot = "0.12.1"

read-fonts = "0.0.5"
write-fonts = "0.0.5"
fea-rs = "0.2.0"
smol_str = "0.1.18"

read-fonts = "0.0.5"
write-fonts = "0.0.5"

[dev-dependencies]
diff = "0.1.12"
ansi_term = "0.12.1"
Expand Down
30 changes: 29 additions & 1 deletion fontbe/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::io;
use std::{fmt::Display, io};

use fea_rs::compile::error::{BinaryCompilationError, CompilerError};
use fontdrasil::types::GlyphName;
use thiserror::Error;
use write_fonts::tables::glyf::BadKurbo;

#[derive(Debug, Error)]
pub enum Error {
Expand All @@ -11,4 +13,30 @@ pub enum Error {
FeaAssembleError(#[from] BinaryCompilationError),
#[error("Fea compilation failure")]
FeaCompileError(#[from] CompilerError),
#[error("'{0}' {1}")]
GlyphError(GlyphName, GlyphProblem),
#[error("'{0}' {1:?}")]
KurboError(GlyphName, BadKurbo),
}

#[derive(Debug)]
pub enum GlyphProblem {
InconsistentComponents,
InconsistentPathElements,
HasComponentsAndPath,
MissingDefault,
}

impl Display for GlyphProblem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
GlyphProblem::HasComponentsAndPath => "has components *and* paths",
GlyphProblem::InconsistentComponents => {
"has different components at different points in designspace"
}
GlyphProblem::InconsistentPathElements => "has interpolation-incompatible paths",
GlyphProblem::MissingDefault => "has no default master",
};
f.write_str(message)
}
}
8 changes: 6 additions & 2 deletions fontbe/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use fea_rs::{
};
use fontir::ir::Features;
use log::{debug, error, trace, warn};
use write_fonts::FontBuilder;

use fontdrasil::orchestration::Work;
use write_fonts::FontBuilder;

use crate::{
error::Error,
Expand Down Expand Up @@ -74,7 +74,11 @@ impl Display for NotSupportedError {
}

impl FeatureWork {
fn compile(&self, features: &Features, glyph_order: GlyphMap) -> Result<FontBuilder, Error> {
fn compile(
&self,
features: &Features,
glyph_order: GlyphMap,
) -> Result<FontBuilder<'static>, Error> {
let root_path = if let Features::File(file) = features {
OsString::from(file)
} else {
Expand Down
229 changes: 229 additions & 0 deletions fontbe/src/glyphs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! 'glyf' Glyph binary compilation

use std::collections::{BTreeSet, HashMap, HashSet};

use fontdrasil::{orchestration::Work, types::GlyphName};
use fontir::{coords::NormalizedLocation, ir};
use kurbo::{Affine, BezPath, PathEl};
use log::{error, info, trace, warn};

use write_fonts::tables::glyf::SimpleGlyph;

use crate::{
error::{Error, GlyphProblem},
orchestration::{BeWork, Context},
};

struct GlyphWork {
glyph_name: GlyphName,
}

pub fn create_glyph_work(glyph_name: GlyphName) -> Box<BeWork> {
Box::new(GlyphWork { glyph_name })
}

impl Work<Context, Error> for GlyphWork {
fn exec(&self, context: &Context) -> Result<(), Error> {
trace!("BE glyph work for {}", self.glyph_name);

let static_metadata = context.ir.get_static_metadata();
let var_model = &static_metadata.variation_model;
let default_location = var_model.default_location();
let ir_glyph = &*context.ir.get_glyph_ir(&self.glyph_name);
let glyph: CheckedGlyph = ir_glyph.try_into()?;

// TODO refine (submodel) var model if glyph locations is a subset of var model locations

// TODO do we want to write to ensure we don't lose interpolability?
match glyph {
CheckedGlyph::Composite {
components,
transforms,
} => {
warn!(
"'{}': composite glyphs not implemented yet; uses {:?} {:?}",
ir_glyph.name, components, transforms
);
}
CheckedGlyph::Contour { contours } => {
if contours.get(default_location).is_none() {
eprintln!("{}", self.glyph_name);
for contour in contours.iter() {
eprintln!(" {:?}", contour.0);
}
}
let path = contours.get(default_location).ok_or_else(|| {
Error::GlyphError(ir_glyph.name.clone(), GlyphProblem::MissingDefault)
})?;
let base_glyph = SimpleGlyph::from_kurbo(path)
.map_err(|e| Error::KurboError(self.glyph_name.clone(), e))?;
info!("'{}' base is {}", ir_glyph.name, path.to_svg());
context.set_glyph(ir_glyph.name.clone(), base_glyph);
}
}

Ok(())
}
}

/// An [ir::Glyph] that has been confirmed to maintain invariants:
///
/// <ul>
/// <li>Components are consistent across the design space</li>
/// <li>Paths are interpolation compatible</li>
/// </ul>
enum CheckedGlyph {
Composite {
components: Vec<GlyphName>,
transforms: HashMap<(GlyphName, NormalizedLocation), Affine>,
},
Contour {
contours: HashMap<NormalizedLocation, BezPath>,
},
}

impl TryFrom<&ir::Glyph> for CheckedGlyph {
type Error = Error;

fn try_from(glyph: &ir::Glyph) -> Result<Self, Self::Error> {
// every instance must have consistent component glyphs
let components: HashSet<BTreeSet<GlyphName>> = glyph
.sources
.values()
.map(|s| s.components.iter().map(|c| c.base.clone()).collect())
.collect();
if components.len() > 1 {
return Err(Error::GlyphError(
glyph.name.clone(),
GlyphProblem::InconsistentComponents,
));
}

// every instance must have consistent path element types
let path_els: HashSet<String> = glyph
.sources
.values()
.map(|s| {
s.contours
.iter()
.map(|c| c.elements().iter().map(path_el_type).collect::<String>())
.collect()
})
.collect();
if path_els.len() > 1 {
return Err(Error::GlyphError(
glyph.name.clone(),
GlyphProblem::InconsistentPathElements,
));
}
let mut components = components.into_iter().next().unwrap_or_default();
let path_els = path_els.into_iter().next().unwrap_or_default();
trace!(
"'{}' consistent: components '{:?}', paths '{}'",
glyph.name,
components,
path_els
);

// TEMPORARY HACKERY; real fix is to hoist the contour into a new glyph
if !components.is_empty() && !path_els.is_empty() {
error!(
"'{}' components discarded due to use of both outline *and* component",
glyph.name
);
components = BTreeSet::new();
}

if !components.is_empty() && !path_els.is_empty() {
return Err(Error::GlyphError(
glyph.name.clone(),
GlyphProblem::HasComponentsAndPath,
));
}

// TEMPORARY HACKERY; real fix is to cu2qu in an interpolation friendly manner
if path_els.contains('C') {
error!("'{}' outline discarded due to use of cubics", glyph.name);
let contours = glyph
.sources
.keys()
.map(|location| (location.clone(), BezPath::new()))
.collect();
return Ok(CheckedGlyph::Contour { contours });
}

// All is well, build the result
Ok(if components.is_empty() {
let contours = glyph
.sources
.iter()
.map(|(location, instance)| {
if instance.contours.len() > 1 {
trace!(
"Merging {} contours to form '{}' at {:?}",
instance.contours.len(),
glyph.name,
location
);
}
let mut path = instance.contours.first().cloned().unwrap_or_default();
for contour in instance.contours.iter().skip(1) {
for el in contour.elements() {
path.push(*el);
}
}
(location.clone(), path)
})
.collect();
CheckedGlyph::Contour { contours }
} else {
// Stable ordering is nice
let mut components: Vec<_> = components.iter().cloned().collect();
components.sort();

let transforms = glyph
.sources
.iter()
.flat_map(|(location, instance)| {
instance
.components
.iter()
.map(|c| ((c.base.clone(), location.clone()), c.transform))
})
.collect();
CheckedGlyph::Composite {
components,
transforms,
}
})
}
}

fn path_el_type(el: &PathEl) -> &'static str {
match el {
PathEl::MoveTo(..) => "M",
PathEl::LineTo(..) => "L",
PathEl::QuadTo(..) => "Q",
PathEl::CurveTo(..) => "C",
PathEl::ClosePath => "Z",
}
}

struct GlyphMergeWork {}

pub fn create_glyph_merge_work() -> Box<BeWork> {
Box::new(GlyphMergeWork {})
}

impl Work<Context, Error> for GlyphMergeWork {
fn exec(&self, context: &Context) -> Result<(), Error> {
let static_metadata = context.ir.get_static_metadata();

error!(
"TODO merge {} glyphs in glyph order => final result",
static_metadata.glyph_order.len()
);

Ok(())
}
}
1 change: 1 addition & 0 deletions fontbe/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod error;
pub mod features;
pub mod glyphs;
pub mod orchestration;
pub mod paths;
Loading

0 comments on commit 68da0bc

Please sign in to comment.