-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Expose and fix broken processing of bbox for components whose transform is more than scale and transform #1050
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,15 +3,18 @@ | |
//! 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::{ | ||
coords::NormalizedLocation, | ||
orchestration::{Access, AccessBuilder, Work}, | ||
types::GlyphName, | ||
}; | ||
use fontir::{ | ||
ir, | ||
ir::{self, GlyphOrder}, | ||
orchestration::{Flags, WorkId as FeWorkId}, | ||
variations::{VariationModel, VariationRegion}, | ||
}; | ||
|
@@ -30,6 +33,7 @@ use write_fonts::{ | |
}, | ||
gvar::{iup::iup_delta_optimize, GlyphDelta}, | ||
}, | ||
types::GlyphId16, | ||
OtRound, | ||
}; | ||
|
||
|
@@ -77,19 +81,10 @@ fn can_reuse_metrics( | |
coeffs == Affine::IDENTITY.as_coeffs() | ||
} | ||
|
||
fn create_component( | ||
context: &Context, | ||
ref_glyph_name: &GlyphName, | ||
fn create_component_ref_gid( | ||
gid: GlyphId16, | ||
transform: &Affine, | ||
) -> Result<(Component, Bbox), GlyphProblem> { | ||
// Obtain glyph id from static metadata | ||
let gid = context | ||
.ir | ||
.glyph_order | ||
.get() | ||
.glyph_id(ref_glyph_name) | ||
.ok_or(GlyphProblem::NotInGlyphOrder)?; | ||
|
||
// No known source does point anchoring so we just turn transform into a 2x2 + offset | ||
let [a, b, c, d, e, f] = transform.as_coeffs(); | ||
let flags = ComponentFlags { | ||
|
@@ -99,8 +94,8 @@ fn create_component( | |
let component = Component::new( | ||
gid, | ||
Anchor::Offset { | ||
x: e as i16, | ||
y: f as i16, | ||
x: e.ot_round(), | ||
y: f.ot_round(), | ||
}, | ||
Transform { | ||
xx: F2Dot14::from_f32(a as f32), | ||
|
@@ -115,6 +110,22 @@ fn create_component( | |
Ok((component, Bbox::default())) | ||
} | ||
|
||
fn create_component_ref_name( | ||
context: &Context, | ||
ref_glyph_name: &GlyphName, | ||
transform: &Affine, | ||
) -> Result<(Component, Bbox), GlyphProblem> { | ||
// Obtain glyph id from static metadata | ||
let gid = context | ||
.ir | ||
.glyph_order | ||
.get() | ||
.glyph_id(ref_glyph_name) | ||
.ok_or(GlyphProblem::NotInGlyphOrder)?; | ||
|
||
create_component_ref_gid(gid, transform) | ||
} | ||
|
||
fn create_composite( | ||
context: &Context, | ||
glyph: &ir::Glyph, | ||
|
@@ -139,7 +150,7 @@ fn create_composite( | |
} | ||
}) | ||
.filter_map(|(ref_glyph_name, transform)| { | ||
create_component(context, ref_glyph_name, transform) | ||
create_component_ref_name(context, ref_glyph_name, transform) | ||
.map_err(|problem| { | ||
errors.push(Error::ComponentError { | ||
glyph: glyph.name.clone(), | ||
|
@@ -731,22 +742,62 @@ fn affine_for(component: &Component) -> Affine { | |
]) | ||
} | ||
|
||
fn bbox2rect(bbox: Bbox) -> Rect { | ||
Rect { | ||
x0: bbox.x_min.into(), | ||
y0: bbox.y_min.into(), | ||
x1: bbox.x_max.into(), | ||
y1: bbox.y_max.into(), | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
struct GlyfLocaWork {} | ||
|
||
pub fn create_glyf_loca_work() -> Box<BeWork> { | ||
Box::new(GlyfLocaWork {}) | ||
} | ||
|
||
/// See <https://github.com/fonttools/fonttools/blob/42c1a52c5facd0edbc9c685787b084af44f6f607/Lib/fontTools/ttLib/tables/_g_l_y_f.py#L1244> | ||
fn bbox_of_composite( | ||
glyph_order: &GlyphOrder, | ||
glyphs: &HashMap<&GlyphName, Arc<Glyph>>, | ||
composite: &CompositeGlyph, | ||
affine: Affine, | ||
) -> Result<Option<Rect>, Error> { | ||
// For simple scale+translate transforms, which seem to be common, we could just transform a bbox | ||
// Let's wait to see if that pops out in a profile and do the simple solution for now | ||
// Because transforms can skew/rotate the control box computed for the simple glyph isn't always reusable | ||
|
||
let mut bbox: Option<Rect> = None; | ||
for component in composite.components() { | ||
// The transform we get here has changed because it got turned into F2Dot14 and i16 parts | ||
// We could go get the "real" transform from IR but ... this seems to match fontmake so far | ||
Comment on lines
+765
to
+766
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the 'real' transform from source or IR doesn't actually matter any more, it's the one that's stored in the glyf table that matters for bbox computation, that's what fonttools TTGlyph.recalcBounds() does (and ufo2ft uses the same method on the compiled TTGlyphs when building the glyf table). |
||
let affine = affine * affine_for(component); | ||
|
||
let ref_glyph_name = glyph_order | ||
.glyph_name(component.glyph.to_u16() as usize) | ||
.unwrap(); | ||
let Some(ref_glyph) = glyphs.get(ref_glyph_name) else { | ||
return Err(Error::MissingGlyphId(ref_glyph_name.clone())); | ||
}; | ||
match &ref_glyph.data { | ||
RawGlyph::Empty => continue, // no impact on our bbox | ||
RawGlyph::Simple(ref_simple) => { | ||
// Update our bbox to include the transformed points | ||
for pt in ref_simple.contours().iter().flat_map(|c| c.iter()) { | ||
let pt = affine * Point::new(pt.x as f64, pt.y as f64); | ||
bbox = Some(if let Some(current) = bbox { | ||
current.union_pt(pt) | ||
} else { | ||
Rect::from_points(pt, pt) | ||
}); | ||
} | ||
} | ||
RawGlyph::Composite(ref_composite) => { | ||
// Chase our components using an updated transform | ||
if let Some(child_bbox) = | ||
bbox_of_composite(glyph_order, glyphs, ref_composite, affine)? | ||
{ | ||
bbox = bbox.map(|bbox| bbox.union(child_bbox)).or(Some(child_bbox)); | ||
} | ||
} | ||
} | ||
} | ||
Ok(bbox) | ||
} | ||
|
||
fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { | ||
let glyph_order = context.ir.glyph_order.get(); | ||
|
||
|
@@ -763,71 +814,13 @@ fn compute_composite_bboxes(context: &Context) -> Result<(), Error> { | |
// 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<GlyphName, Rect> = HashMap::new(); | ||
let mut composites = glyphs | ||
.values() | ||
.filter(|glyph| glyph.is_composite()) | ||
.collect::<Vec<_>>(); | ||
|
||
trace!("Resolve bbox for {} composites", composites.len()); | ||
while !composites.is_empty() { | ||
let pending = composites.len(); | ||
|
||
// Hopefully we can figure out some of those bboxes! | ||
for composite in composites.iter() { | ||
let glyph_name = &composite.name; | ||
let RawGlyph::Composite(composite) = &composite.data else { | ||
unreachable!("we just checked that these are all composites"); | ||
}; | ||
|
||
let mut missing_boxes = false; | ||
let boxes: Vec<_> = composite | ||
.components() | ||
.iter() | ||
.filter_map(|c| { | ||
if missing_boxes { | ||
return None; // can't succeed | ||
} | ||
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.data { | ||
RawGlyph::Composite(..) => None, | ||
RawGlyph::Empty => None, | ||
RawGlyph::Simple(simple_glyph) => Some(bbox2rect(simple_glyph.bbox)), | ||
}) | ||
}); | ||
if bbox.is_none() { | ||
trace!("Can't compute bbox for {glyph_name} because bbox for {ref_glyph_name} isn't ready yet"); | ||
missing_boxes = true; | ||
return None; // maybe next time? | ||
}; | ||
|
||
// The transform we get here has changed because it got turned into F2Dot14 and i16 parts | ||
// We could go get the "real" transform from IR...? | ||
let affine = affine_for(c); | ||
let transformed_box = affine.transform_rect_bbox(bbox.unwrap()); | ||
Some(transformed_box) | ||
}) | ||
.collect(); | ||
if missing_boxes { | ||
trace!("bbox for {glyph_name} not yet resolveable"); | ||
continue; | ||
} | ||
|
||
let bbox = boxes.into_iter().reduce(|acc, e| acc.union(e)).unwrap(); | ||
trace!("bbox for {glyph_name} {bbox:?}"); | ||
bbox_acquired.insert(glyph_name.clone(), bbox); | ||
} | ||
|
||
// Kerplode if we didn't make any progress this spin | ||
composites.retain(|composite| !bbox_acquired.contains_key(&composite.name)); | ||
if pending == composites.len() { | ||
return Err(Error::CompositesStalled( | ||
composites.iter().map(|g| g.name.clone()).collect(), | ||
)); | ||
} | ||
for (glyph_name, glyph) in glyphs.iter().filter_map(|(gn, g)| match &g.data { | ||
RawGlyph::Composite(composite) => Some((*gn, composite)), | ||
RawGlyph::Simple(..) | RawGlyph::Empty => None, | ||
}) { | ||
let bbox = bbox_of_composite(&glyph_order, &glyphs, glyph, Affine::IDENTITY)?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you went for a recursive traversal instead of iterative one. I'm ok with it (fonttools does the same), but I was wondering, even though unlikely, should you prevent from a component cycle triggering an infinite recursion? Python has a max recursion depth after which it raises an exception. Does Rust protect you as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think for glyphs sources at least we already check against component cycles when we propagate anchors, see glyphs-reader's propagate_anchors.rs (depth_sorted_composite_glyphs) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me take confirmation of no cycles as a follow-on. I have the vague notion we might already do it but I'll double-check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
bbox_acquired.insert(glyph_name.clone(), bbox.unwrap_or_default()); | ||
} | ||
|
||
// It'd be a shame to just throw away those nice boxes | ||
|
@@ -1027,4 +1020,18 @@ mod tests { | |
expected_segments | ||
); | ||
} | ||
|
||
// Contributor to https://github.com/googlefonts/fontc/pull/1050 | ||
#[test] | ||
fn component_translation_otrounds() { | ||
let (c, _) = create_component_ref_gid( | ||
GlyphId16::new(42), | ||
&Affine::new([1.0, 0.0, 0.0, 1.0, 0.4, 0.9]), | ||
) | ||
.unwrap(); | ||
let Anchor::Offset { x, y } = c.anchor else { | ||
panic!("Must be an offset"); | ||
}; | ||
assert_eq!((0, 1), (x, y)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
{ | ||
.appVersion = "3219"; | ||
familyName = MatrixComponent; | ||
fontMaster = ( | ||
{ | ||
id = m01; | ||
} | ||
); | ||
glyphs = ( | ||
{ | ||
glyphname = rot60more; | ||
layers = ( | ||
{ | ||
components = ( | ||
{ | ||
name = rot30; | ||
transform = "{0.5, 0.866, -0.866, 0.5, 179.9, -11.6}"; | ||
} | ||
); | ||
layerId = "m01"; | ||
width = 705; | ||
} | ||
); | ||
unicode = 221D; | ||
}, | ||
{ | ||
glyphname = rot30; | ||
layers = ( | ||
{ | ||
components = ( | ||
{ | ||
name = triangle; | ||
|
||
transform = "{0.866, 0.5, -0.5, 0.866, 88.4, -29.9}"; | ||
} | ||
); | ||
layerId = "m01"; | ||
width = 705; | ||
} | ||
); | ||
unicode = 221E; | ||
}, | ||
{ | ||
glyphname = triangle; | ||
layers = ( | ||
{ | ||
layerId = "m01"; | ||
paths = ( | ||
{ | ||
closed = 1; | ||
nodes = ( | ||
"100 100 LINE", | ||
"400 150 LINE", | ||
"100 200 LINE", | ||
); | ||
} | ||
); | ||
width = 474; | ||
} | ||
); | ||
unicode = 0038; | ||
} | ||
); | ||
|
||
unitsPerEm = 1000; | ||
versionMajor = 42; | ||
versionMinor = 42; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you actually try to profile before/after this? I suggest filing an issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, #1061