From 648aedbe37745b99a8ef0e232a5bd88c2d1cc26a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 05:45:32 -0800 Subject: [PATCH] Type: a COLR glyph whose radial gradient draws no circle draws nothing A radial gradient whose circles both have the same negative radius (as a variable font's instance can give) draws no circle. A glyph painted with only that gradient was read as no colour glyph, so layout drew its outline instead. It is now colour art that draws nothing. --- crates/text/src/colr.rs | 12 ++++++++++-- crates/text/src/test_fonts.rs | 6 ++++++ crates/text/src/tests/colr.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/text/src/colr.rs b/crates/text/src/colr.rs index 80aa3132cf..281ef8a65f 100644 --- a/crates/text/src/colr.rs +++ b/crates/text/src/colr.rs @@ -148,6 +148,9 @@ struct Tree<'a> { painted: Option, /// It paints with the foreground colour (palette index 0xFFFF). foreground_used: bool, + /// A fill draws nothing (a radial gradient none of whose circles has a positive radius), so a + /// glyph with nothing else is colour art that draws nothing, not its outline. + drew_nothing: bool, } impl<'a> Tree<'a> { @@ -166,6 +169,7 @@ impl<'a> Tree<'a> { too_large: false, painted: None, foreground_used: false, + drew_nothing: false, } } @@ -254,7 +258,7 @@ impl<'a> Tree<'a> { self.close(); } let root = self.groups.pop()?; - if root.children.is_empty() { + if root.children.is_empty() && !self.drew_nothing { return None; } Some(Arc::new(Node::group(NO_ID, root.children))) @@ -314,7 +318,11 @@ impl<'a> Tree<'a> { self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); return None; } - let (c0, r0, c1, r1, mut stops) = non_negative_radii(point(*c0), f64::from(*r0), point(*c1), f64::from(*r1), line, *extend)?; + let Some((c0, r0, c1, r1, mut stops)) = non_negative_radii(point(*c0), f64::from(*r0), point(*c1), f64::from(*r1), line, *extend) + else { + self.drew_nothing = true; + return None; + }; let concentric = (c0 - c1).hypot() < 1e-6; if (r0 > 1e-6 && !concentric) || r1 <= r0 || (c0 - c1).hypot() >= r1 { self.unsupported.get_or_insert("a radial gradient from a circle"); diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index bdb818d3f3..7abae3f0eb 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -1939,6 +1939,12 @@ pub fn colr_variable_font() -> Option> { with_colr(&variable_font()?, family_names(COLR_VARIABLE_FAMILY, &axis_names)?, &colr) } +/// [`variable_font`] (one weight axis, 400 to 700) as `family` with `colr` and [`colr_font`]'s +/// palette. `None` when it can't be built. +pub fn colr_variable_font_with(family: &str, colr: &write_fonts::tables::colr::Colr) -> Option> { + with_colr(&variable_font()?, family_names(family, &[])?, colr) +} + /// A `name` table for Regular of `family`, with `more` records. fn family_names(family: &str, more: &[(u16, &str)]) -> Option> { let ps = family.replace(' ', ""); diff --git a/crates/text/src/tests/colr.rs b/crates/text/src/tests/colr.rs index 26e5fd4e6a..c6c4696f16 100644 --- a/crates/text/src/tests/colr.rs +++ b/crates/text/src/tests/colr.rs @@ -467,6 +467,38 @@ fn an_image_of_a_radial_gradient_with_a_stop_past_its_circle_of_radius_0_draws_f } } +/// A radial gradient whose circles both have the same negative radius at a variable font's +/// instance draws no circle, so a glyph painted with only that gradient is colour art that draws +/// nothing, not its outline. The radii are 10 at the default weight and -10 at 700. +#[test] +fn a_radial_gradient_with_no_circle_drawn_leaves_the_glyph_empty() { + use write_fonts::tables::colr::{BaseGlyphList, BaseGlyphPaint, Colr, Extend, VarColorLine, VarColorStop}; + use write_fonts::tables::variations::{ItemVariationData, ItemVariationStore, RegionAxisCoordinates, VariationRegion, VariationRegionList}; + use write_fonts::types::{F2Dot14, FWord, GlyphId16, UfWord}; + let bytes = colr_font(false).unwrap(); + let read = skrifa::FontRef::new(&bytes).unwrap(); + let gid = |ch| GlyphId16::new(u16::try_from(read.charmap().map(ch).unwrap().to_u32()).unwrap()); + let (one, fixed) = (F2Dot14::from_f32(1.0), 0xFFFF_FFFF); + let line = + VarColorLine::new(Extend::Pad, 2, vec![VarColorStop::new(F2Dot14::from_f32(0.0), 0, one, fixed), VarColorStop::new(one, 1, one, fixed)]); + let w = FWord::new; + let radial = ColrPaint::var_radial_gradient(line, w(290), w(330), UfWord::new(10), w(290), w(330), UfWord::new(10), 0); + let mut colr = Colr::new(0, None, None, 0); + colr.base_glyph_list = Some(BaseGlyphList::new(1, vec![BaseGlyphPaint::new(gid(COLOUR_CHAR), ColrPaint::glyph(radial, gid('O')))])).into(); + // One 16-bit delta for each of x0, y0, r0, x1, y1 and r1, at full weight. + let deltas: Vec = [0i16, 0, -20, 0, 0, -20].iter().flat_map(|d| d.to_be_bytes()).collect(); + let region = VariationRegion::new(vec![RegionAxisCoordinates::new(F2Dot14::from_f32(0.0), one, one)]); + let data = ItemVariationData::new(6, 1, vec![0], deltas); + colr.item_variation_store = Some(ItemVariationStore::new(VariationRegionList::new(1, vec![region]), vec![Some(data)])).into(); + let font = crate::test_fonts::colr_variable_font_with("Colrtest No Circle", &colr).unwrap(); + let font = skrifa::FontRef::new(&font).unwrap(); + let bold = font.axes().location([("wght", 700.0)]); + let Some(ColourContent::Art(root)) = colour_glyph(&font, (&bold).into(), 1000.0, u32::from(gid(COLOUR_CHAR).to_u16()), FOREGROUND).0 else { + panic!("colour art") + }; + assert!(children(&root).is_empty(), "{:?}", children(&root).iter().map(|c| c.kind_label()).collect::>()); +} + /// A COLRv1 color line through `stops` (offset, palette index) that extends by `extend`. fn color_line(extend: write_fonts::tables::colr::Extend, stops: &[(f32, u16)]) -> write_fonts::tables::colr::ColorLine { use write_fonts::tables::colr::{ColorLine, ColorStop};