diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93f12d89..579152b8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: rust: - - 1.42.0 + - 1.51.0 - stable steps: - name: Checkout diff --git a/Cargo.toml b/Cargo.toml index 80ae1a07..99d8b7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ glyph-names = [] gvar-alloc = ["std"] [dev-dependencies] -base64 = "0.13" +base64 = "0.22.1" pico-args = "0.5" +tiny-skia-path = "0.11.4" xmlwriter = "0.1" diff --git a/README.md b/README.md index 4918b34c..8ac296b2 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ There are roughly three types of TrueType tables: | `bloc` table | ✓ | ✓ | | | `CBDT` table | ~ (no 8, 9) | ✓ | | | `CBLC` table | ✓ | ✓ | | -| `COLR` table | ~ (only v0) | ✓ | | -| `CPAL` table | ~ (only v0) | ✓ | | +| `COLR` table | ✓ | ✓ | | +| `CPAL` table | ✓ | ✓ | | | `CFF ` table | ✓ | ✓ | ~ (no `seac` support) | | `CFF2` table | ✓ | ✓ | | | `cmap` table | ~ (no 8) | ✓ | ~ (no 2,8,10,14; Unicode-only) | diff --git a/examples/font2svg.rs b/examples/font2svg.rs index ce765519..6fd756b3 100644 --- a/examples/font2svg.rs +++ b/examples/font2svg.rs @@ -1,7 +1,11 @@ +use base64::engine::general_purpose::STANDARD; + use std::io::Write; use std::path::PathBuf; use ttf_parser as ttf; +use ttf_parser::colr::{ClipBox, Paint}; +use ttf_parser::{RgbaColor, Transform}; const FONT_SIZE: f64 = 128.0; const COLUMNS: u32 = 100; @@ -103,14 +107,16 @@ fn process(args: Args) -> Result<(), Box> { } } + let num_glyphs = face.number_of_glyphs(); + let units_per_em = face.units_per_em(); let scale = FONT_SIZE / units_per_em as f64; let cell_size = face.height() as f64 * FONT_SIZE / units_per_em as f64; - let rows = (face.number_of_glyphs() as f64 / COLUMNS as f64).ceil() as u32; + let rows = (num_glyphs as f64 / COLUMNS as f64).ceil() as u32; let mut svg = xmlwriter::XmlWriter::with_capacity( - face.number_of_glyphs() as usize * 512, + num_glyphs as usize * 512, xmlwriter::Options::default(), ); svg.start_element("svg"); @@ -127,12 +133,14 @@ fn process(args: Args) -> Result<(), Box> { ), ); - draw_grid(face.number_of_glyphs(), cell_size, &mut svg); + draw_grid(num_glyphs, cell_size, &mut svg); let mut path_buf = String::with_capacity(256); let mut row = 0; let mut column = 0; - for id in 0..face.number_of_glyphs() { + let mut gradient_index = 1; + let mut clip_path_index = 1; + for id in 0..num_glyphs { let gid = ttf::GlyphId(id); let x = column as f64 * cell_size; let y = row as f64 * cell_size; @@ -145,20 +153,20 @@ fn process(args: Args) -> Result<(), Box> { svg.write_text_fmt(format_args!("{}", &id)); svg.end_element(); - if face.tables().colr.is_some() { - if face.is_color_glyph(gid) { - color_glyph( - x, - y, - &face, - args.colr_palette, - gid, - cell_size, - scale, - &mut svg, - &mut path_buf, - ); - } + if face.is_color_glyph(gid) { + color_glyph( + x, + y, + &face, + args.colr_palette, + gid, + cell_size, + scale, + &mut gradient_index, + &mut clip_path_index, + &mut svg, + &mut path_buf, + ); } else if let Some(img) = face.glyph_raster_image(gid, std::u16::MAX) { svg.start_element("image"); svg.write_attribute("x", &(x + 2.0 + img.x as f64)); @@ -168,7 +176,7 @@ fn process(args: Args) -> Result<(), Box> { svg.write_attribute_raw("xlink:href", |buf| { buf.extend_from_slice(b"data:image/png;base64, "); - let mut enc = base64::write::EncoderWriter::new(buf, base64::STANDARD); + let mut enc = base64::write::EncoderWriter::new(buf, &STANDARD); enc.write_all(img.data).unwrap(); enc.finish().unwrap(); }); @@ -182,7 +190,7 @@ fn process(args: Args) -> Result<(), Box> { svg.write_attribute_raw("xlink:href", |buf| { buf.extend_from_slice(b"data:image/svg+xml;base64, "); - let mut enc = base64::write::EncoderWriter::new(buf, base64::STANDARD); + let mut enc = base64::write::EncoderWriter::new(buf, &STANDARD); enc.write_all(img.data).unwrap(); enc.finish().unwrap(); }); @@ -319,14 +327,173 @@ fn glyph_to_path( } } +// NOTE: this is not a feature-full implementation and just a demo. struct GlyphPainter<'a> { face: &'a ttf::Face<'a>, svg: &'a mut xmlwriter::XmlWriter, path_buf: &'a mut String, + gradient_index: usize, + clip_path_index: usize, + palette_index: u16, + transform: ttf::Transform, + outline_transform: ttf::Transform, + transforms_stack: Vec, +} + +impl<'a> GlyphPainter<'a> { + fn write_gradient_stops(&mut self, stops: ttf::colr::GradientStopsIter) { + for stop in stops { + self.svg.start_element("stop"); + self.svg.write_attribute("offset", &stop.stop_offset); + self.svg.write_color_attribute("stop-color", stop.color); + let opacity = f32::from(stop.color.alpha) / 255.0; + self.svg.write_attribute("stop-opacity", &opacity); + self.svg.end_element(); + } + } + + fn paint_solid(&mut self, color: ttf::RgbaColor) { + self.svg.start_element("path"); + self.svg.write_color_attribute("fill", color); + let opacity = f32::from(color.alpha) / 255.0; + self.svg.write_attribute("fill-opacity", &opacity); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_linear_gradient(&mut self, gradient: ttf::colr::LinearGradient<'a>) { + let gradient_id = format!("lg{}", self.gradient_index); + self.gradient_index += 1; + + let gradient_transform = paint_transform(self.outline_transform, self.transform); + + // TODO: We ignore x2, y2. Have to apply them somehow. + // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode + // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will + // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and + // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf + // we will see the actual spreadMode. We need to account for that somehow. + self.svg.start_element("linearGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("x1", &gradient.x0); + self.svg.write_attribute("y1", &gradient.y0); + self.svg.write_attribute("x2", &gradient.x1); + self.svg.write_attribute("y2", &gradient.y1); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", gradient_transform); + self.write_gradient_stops(gradient.stops( + self.palette_index, + #[cfg(feature = "variable-fonts")] + self.face.variation_coordinates(), + )); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_radial_gradient(&mut self, gradient: ttf::colr::RadialGradient<'a>) { + let gradient_id = format!("rg{}", self.gradient_index); + self.gradient_index += 1; + + self.svg.start_element("radialGradient"); + self.svg.write_attribute("id", &gradient_id); + self.svg.write_attribute("cx", &gradient.x1); + self.svg.write_attribute("cy", &gradient.y1); + self.svg.write_attribute("r", &gradient.r1); + self.svg.write_attribute("fr", &gradient.r0); + self.svg.write_attribute("fx", &gradient.x0); + self.svg.write_attribute("fy", &gradient.y0); + self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); + self.svg.write_spread_method_attribute(gradient.extend); + self.svg + .write_transform_attribute("gradientTransform", self.transform); + self.write_gradient_stops(gradient.stops( + self.palette_index, + #[cfg(feature = "variable-fonts")] + self.face.variation_coordinates(), + )); + self.svg.end_element(); + + self.svg.start_element("path"); + self.svg + .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", self.path_buf); + self.svg.end_element(); + } + + fn paint_sweep_gradient(&mut self, _: ttf::colr::SweepGradient<'a>) { + println!("Warning: sweep gradients are not supported.") + } +} + +fn paint_transform(outline_transform: Transform, transform: Transform) -> Transform { + let outline_transform = tiny_skia_path::Transform::from_row( + outline_transform.a, + outline_transform.b, + outline_transform.c, + outline_transform.d, + outline_transform.e, + outline_transform.f, + ); + + let gradient_transform = tiny_skia_path::Transform::from_row( + transform.a, + transform.b, + transform.c, + transform.d, + transform.e, + transform.f, + ); + + let gradient_transform = outline_transform + .invert() + .unwrap() + .pre_concat(gradient_transform); + + ttf_parser::Transform { + a: gradient_transform.sx, + b: gradient_transform.ky, + c: gradient_transform.kx, + d: gradient_transform.sy, + e: gradient_transform.tx, + f: gradient_transform.ty, + } +} + +impl GlyphPainter<'_> { + fn clip_with_path(&mut self, path: &str) { + let clip_id = format!("cp{}", self.clip_path_index); + self.clip_path_index += 1; + + self.svg.start_element("clipPath"); + self.svg.write_attribute("id", &clip_id); + self.svg.start_element("path"); + self.svg + .write_transform_attribute("transform", self.outline_transform); + self.svg.write_attribute("d", &path); + self.svg.end_element(); + self.svg.end_element(); + + self.svg.start_element("g"); + self.svg + .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); + } } -impl ttf::colr::Painter for GlyphPainter<'_> { - fn outline(&mut self, glyph_id: ttf::GlyphId) { +impl<'a> ttf::colr::Painter<'a> for GlyphPainter<'a> { + fn outline_glyph(&mut self, glyph_id: ttf::GlyphId) { self.path_buf.clear(); let mut builder = Builder(self.path_buf); match self.face.outline_glyph(glyph_id, &mut builder) { @@ -334,21 +501,110 @@ impl ttf::colr::Painter for GlyphPainter<'_> { None => return, }; builder.finish(); + + // We have to write outline using the current transform. + self.outline_transform = self.transform; } - fn paint_foreground(&mut self) { - // The caller must provide this color. We simply fallback to black. - self.paint_color(ttf::RgbaColor::new(0, 0, 0, 255)); + fn push_layer(&mut self, mode: ttf::colr::CompositeMode) { + self.svg.start_element("g"); + + use ttf::colr::CompositeMode; + // TODO: Need to figure out how to represent the other blend modes + // in SVG. + let mode = match mode { + CompositeMode::SourceOver => "normal", + CompositeMode::Screen => "screen", + CompositeMode::Overlay => "overlay", + CompositeMode::Darken => "darken", + CompositeMode::Lighten => "lighten", + CompositeMode::ColorDodge => "color-dodge", + CompositeMode::ColorBurn => "color-burn", + CompositeMode::HardLight => "hard-light", + CompositeMode::SoftLight => "soft-light", + CompositeMode::Difference => "difference", + CompositeMode::Exclusion => "exclusion", + CompositeMode::Multiply => "multiply", + CompositeMode::Hue => "hue", + CompositeMode::Saturation => "saturation", + CompositeMode::Color => "color", + CompositeMode::Luminosity => "luminosity", + _ => { + println!("Warning: unsupported blend mode: {:?}", mode); + "normal" + } + }; + self.svg.write_attribute_fmt( + "style", + format_args!("mix-blend-mode: {}; isolation: isolate", mode), + ); } - fn paint_color(&mut self, color: ttf::RgbaColor) { - self.svg.start_element("path"); - self.svg.write_color_attribute("fill", color); - let opacity = f32::from(color.alpha) / 255.0; - self.svg.write_attribute("fill-opacity", &opacity); - self.svg.write_attribute("d", self.path_buf); + fn pop_layer(&mut self) { + self.svg.end_element(); // g + } + + fn push_translate(&mut self, tx: f32, ty: f32) { + self.push_transform(ttf::Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty)); + } + + fn push_scale(&mut self, sx: f32, sy: f32) { + self.push_transform(ttf::Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0)); + } + + fn push_rotate(&mut self, angle: f32) { + let cc = (angle * std::f32::consts::PI).cos(); + let ss = (angle * std::f32::consts::PI).sin(); + self.push_transform(ttf::Transform::new(cc, ss, -ss, cc, 0.0, 0.0)); + } + + fn push_skew(&mut self, skew_x: f32, skew_y: f32) { + let x = (-skew_x * std::f32::consts::PI).tan(); + let y = (skew_y * std::f32::consts::PI).tan(); + self.push_transform(ttf::Transform::new(1.0, y, x, 1.0, 0.0, 0.0)); + } + + fn push_transform(&mut self, transform: ttf::Transform) { + self.transforms_stack.push(self.transform); + self.transform = ttf::Transform::combine(self.transform, transform); + } + + fn paint(&mut self, paint: Paint<'a>) { + match paint { + Paint::Solid(color) => self.paint_solid(color), + Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), + Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), + Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), + } + } + + fn pop_transform(&mut self) { + if let Some(ts) = self.transforms_stack.pop() { + self.transform = ts + } + } + + fn push_clip(&mut self) { + self.clip_with_path(&self.path_buf.clone()); + } + + fn pop_clip(&mut self) { self.svg.end_element(); } + + fn push_clip_box(&mut self, clipbox: ClipBox) { + let x_min = clipbox.x_min; + let x_max = clipbox.x_max; + let y_min = clipbox.y_min; + let y_max = clipbox.y_max; + + let clip_path = format!( + "M {} {} L {} {} L {} {} L {} {} Z", + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ); + + self.clip_with_path(&clip_path); + } } fn color_glyph( @@ -359,6 +615,8 @@ fn color_glyph( glyph_id: ttf::GlyphId, cell_size: f64, scale: f64, + gradient_index: &mut usize, + clip_path_index: &mut usize, svg: &mut xmlwriter::XmlWriter, path_buf: &mut String, ) { @@ -372,14 +630,29 @@ fn color_glyph( face, svg, path_buf, + gradient_index: *gradient_index, + clip_path_index: *clip_path_index, + palette_index, + transform: ttf::Transform::default(), + outline_transform: ttf::Transform::default(), + transforms_stack: vec![ttf::Transform::default()], }; - face.paint_color_glyph(glyph_id, palette_index, &mut painter); + face.paint_color_glyph( + glyph_id, + palette_index, + RgbaColor::new(0, 0, 0, 255), + &mut painter, + ); + *gradient_index = painter.gradient_index; + *clip_path_index = painter.clip_path_index; svg.end_element(); } trait XmlWriterExt { fn write_color_attribute(&mut self, name: &str, ts: ttf::RgbaColor); + fn write_transform_attribute(&mut self, name: &str, ts: ttf::Transform); + fn write_spread_method_attribute(&mut self, method: ttf::colr::GradientExtend); } impl XmlWriterExt for xmlwriter::XmlWriter { @@ -389,4 +662,29 @@ impl XmlWriterExt for xmlwriter::XmlWriter { format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), ); } + + fn write_transform_attribute(&mut self, name: &str, ts: ttf::Transform) { + if ts.is_default() { + return; + } + + self.write_attribute_fmt( + name, + format_args!( + "matrix({} {} {} {} {} {})", + ts.a, ts.b, ts.c, ts.d, ts.e, ts.f + ), + ); + } + + fn write_spread_method_attribute(&mut self, extend: ttf::colr::GradientExtend) { + self.write_attribute( + "spreadMethod", + match extend { + ttf::colr::GradientExtend::Pad => &"pad", + ttf::colr::GradientExtend::Repeat => &"repeat", + ttf::colr::GradientExtend::Reflect => &"reflect", + }, + ); + } } diff --git a/src/delta_set.rs b/src/delta_set.rs new file mode 100644 index 00000000..d7a40b19 --- /dev/null +++ b/src/delta_set.rs @@ -0,0 +1,53 @@ +use core::convert::TryFrom; + +use crate::parser::Stream; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct DeltaSetIndexMap<'a> { + data: &'a [u8], +} + +impl<'a> DeltaSetIndexMap<'a> { + #[inline] + pub(crate) fn new(data: &'a [u8]) -> Self { + DeltaSetIndexMap { data } + } + + #[inline] + pub(crate) fn map(&self, mut index: u32) -> Option<(u16, u16)> { + let mut s = Stream::new(self.data); + let format = s.read::()?; + let entry_format = s.read::()?; + let map_count = if format == 0 { + s.read::()? as u32 + } else { + s.read::()? + }; + + if map_count == 0 { + return None; + } + + // 'If a given glyph ID is greater than mapCount-1, then the last entry is used.' + if index >= map_count { + index = map_count - 1; + } + + let entry_size = ((entry_format >> 4) & 3) + 1; + let inner_index_bit_count = u32::from((entry_format & 0xF) + 1); + + s.advance(usize::try_from(entry_size).ok()? * usize::try_from(index).ok()?); + + let mut n = 0u32; + for b in s.read_bytes(usize::from(entry_size))? { + n = (n << 8) + u32::from(*b); + } + + let outer_index = n >> inner_index_bit_count; + let inner_index = n & ((1 << inner_index_bit_count) - 1); + Some(( + u16::try_from(outer_index).ok()?, + u16::try_from(inner_index).ok()?, + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index 939e0199..0a48d108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,8 @@ macro_rules! try_opt_or { #[cfg(feature = "apple-layout")] mod aat; +#[cfg(feature = "variable-fonts")] +mod delta_set; #[cfg(feature = "opentype-layout")] mod ggg; mod language; @@ -359,18 +361,23 @@ impl Rect { } } -#[derive(Clone, Copy, Debug)] -pub(crate) struct BBox { - x_min: f32, - y_min: f32, - x_max: f32, - y_max: f32, +/// A rectangle described by the left-lower and upper-right points. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RectF { + /// The horizontal minimum of the rect. + pub x_min: f32, + /// The vertical minimum of the rect. + pub y_min: f32, + /// The horizontal maximum of the rect. + pub x_max: f32, + /// The vertical maximum of the rect. + pub y_max: f32, } -impl BBox { +impl RectF { #[inline] fn new() -> Self { - BBox { + RectF { x_min: core::f32::MAX, y_min: core::f32::MAX, x_max: core::f32::MIN, @@ -405,28 +412,37 @@ impl BBox { } } +/// An affine transform. #[derive(Clone, Copy, PartialEq)] -pub(crate) struct Transform { +pub struct Transform { + /// The 'a' component of the transform. pub a: f32, + /// The 'b' component of the transform. pub b: f32, + /// The 'c' component of the transform. pub c: f32, + /// The 'd' component of the transform. pub d: f32, + /// The 'e' component of the transform. pub e: f32, + /// The 'f' component of the transform. pub f: f32, } impl Transform { + /// Creates a new transform with the specified components. #[inline] pub fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self { Transform { a, b, c, d, e, f } } - #[cfg(feature = "variable-fonts")] + /// Creates a new translation transform. #[inline] pub fn new_translate(tx: f32, ty: f32) -> Self { Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty) } + /// Combines two transforms with each other. #[inline] pub fn combine(ts1: Self, ts2: Self) -> Self { Transform { @@ -447,6 +463,7 @@ impl Transform { *y = self.b * tx + self.d * ty + self.f; } + /// Checks whether a transform is the identity transform. #[inline] pub fn is_default(&self) -> bool { // A direct float comparison is fine in our case. @@ -504,6 +521,10 @@ impl RgbaColor { alpha, } } + + pub(crate) fn apply_alpha(&mut self, alpha: f32) { + self.alpha = (((f32::from(self.alpha) / 255.0) * alpha) * 255.0) as u8; + } } /// A trait for glyph outline construction. @@ -658,15 +679,25 @@ impl FromData for TableRecord { } #[cfg(feature = "variable-fonts")] -const MAX_VAR_COORDS: usize = 32; +const MAX_VAR_COORDS: usize = 64; #[cfg(feature = "variable-fonts")] -#[derive(Clone, Default)] +#[derive(Clone)] struct VarCoords { data: [NormalizedCoordinate; MAX_VAR_COORDS], len: u8, } +#[cfg(feature = "variable-fonts")] +impl Default for VarCoords { + fn default() -> Self { + Self { + data: [NormalizedCoordinate::default(); MAX_VAR_COORDS], + len: u8::default(), + } + } +} + #[cfg(feature = "variable-fonts")] impl VarCoords { #[inline] @@ -2172,9 +2203,17 @@ impl<'a> Face<'a> { &self, glyph_id: GlyphId, palette: u16, - painter: &mut dyn colr::Painter, + foreground_color: RgbaColor, + painter: &mut dyn colr::Painter<'a>, ) -> Option<()> { - self.tables.colr?.paint(glyph_id, palette, painter) + self.tables.colr?.paint( + glyph_id, + palette, + painter, + #[cfg(feature = "variable-fonts")] + self.coords(), + foreground_color, + ) } /// Returns an iterator over variation axes. @@ -2186,11 +2225,11 @@ impl<'a> Face<'a> { /// Sets a variation axis coordinate. /// - /// This is the only mutable method in the library. + /// This is one of the two only mutable methods in the library. /// We can simplify the API a lot by storing the variable coordinates /// in the face object itself. /// - /// Since coordinates are stored on the stack, we allow only 32 of them. + /// Since coordinates are stored on the stack, we allow only 64 of them. /// /// Returns `None` when face is not variable or doesn't have such axis. #[cfg(feature = "variable-fonts")] @@ -2270,30 +2309,6 @@ impl<'a> Face<'a> { } } -struct DefaultTableProvider<'a> { - data: &'a [u8], - tables: LazyArrayIter16<'a, TableRecord>, -} - -impl<'a> Iterator for DefaultTableProvider<'a> { - type Item = Result<(Tag, Option<&'a [u8]>), FaceParsingError>; - - #[inline] - fn next(&mut self) -> Option { - self.tables.next().map(|table| { - Ok((table.tag, { - let offset = usize::num_from(table.offset); - let length = usize::num_from(table.length); - let end = offset - .checked_add(length) - .ok_or(FaceParsingError::MalformedFont)?; - let range = offset..end; - self.data.get(range) - })) - }) - } -} - impl core::fmt::Debug for Face<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Face()") diff --git a/src/parser.rs b/src/parser.rs index dfebbeef..d6526f0a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -127,6 +127,12 @@ impl F2DOT14 { pub fn to_f32(self) -> f32 { f32::from(self.0) / 16384.0 } + + #[cfg(feature = "variable-fonts")] + #[inline] + pub fn apply_float_delta(&self, delta: f32) -> f32 { + self.to_f32() + (delta as f64 * (1.0 / 16384.0)) as f32 + } } impl FromData for F2DOT14 { @@ -152,6 +158,14 @@ impl FromData for Fixed { } } +impl Fixed { + #[cfg(feature = "variable-fonts")] + #[inline] + pub(crate) fn apply_float_delta(&self, delta: f32) -> f32 { + self.0 + (delta as f64 * (1.0 / 65536.0)) as f32 + } +} + /// A safe u32 to usize casting. /// /// Rust doesn't implement `From for usize`, @@ -781,11 +795,6 @@ impl<'a> Stream<'a> { pub trait Offset { /// Converts the offset to `usize`. fn to_usize(&self) -> usize; - - /// Checks that offset is null. - fn is_null(&self) -> bool { - self.to_usize() == 0 - } } /// A type-safe u16 offset. @@ -822,6 +831,40 @@ impl FromData for Option { } } +/// A type-safe u24 offset. +#[derive(Clone, Copy, Debug)] +pub struct Offset24(pub u32); + +impl Offset for Offset24 { + #[inline] + fn to_usize(&self) -> usize { + usize::num_from(self.0) + } +} + +impl FromData for Offset24 { + const SIZE: usize = 3; + + #[inline] + fn parse(data: &[u8]) -> Option { + U24::parse(data).map(|n| Offset24(n.0)) + } +} + +impl FromData for Option { + const SIZE: usize = Offset24::SIZE; + + #[inline] + fn parse(data: &[u8]) -> Option { + let offset = Offset24::parse(data)?; + if offset.0 != 0 { + Some(Some(offset)) + } else { + Some(None) + } + } +} + /// A type-safe u32 offset. #[derive(Clone, Copy, Debug)] pub struct Offset32(pub u32); diff --git a/src/tables/cff/cff1.rs b/src/tables/cff/cff1.rs index af93acc0..5a2c9985 100644 --- a/src/tables/cff/cff1.rs +++ b/src/tables/cff/cff1.rs @@ -20,7 +20,7 @@ use super::index::{parse_index, skip_index, Index}; use super::std_names::STANDARD_NAMES; use super::{calc_subroutine_bias, conv_subroutine_index, Builder, CFFError, IsEven, StringId}; use crate::parser::{LazyArray16, NumFrom, Stream, TryNumFrom}; -use crate::{BBox, DummyOutline, GlyphId, OutlineBuilder, Rect}; +use crate::{DummyOutline, GlyphId, OutlineBuilder, Rect, RectF}; // Limits according to the Adobe Technical Note #5176, chapter 4 DICT Data. const MAX_OPERANDS_LEN: usize = 48; @@ -376,7 +376,7 @@ fn parse_char_string( let mut inner_builder = Builder { builder, - bbox: BBox::new(), + bbox: RectF::new(), }; let stack = ArgumentsStack { diff --git a/src/tables/cff/cff2.rs b/src/tables/cff/cff2.rs index c3a301db..acb9b90b 100644 --- a/src/tables/cff/cff2.rs +++ b/src/tables/cff/cff2.rs @@ -13,7 +13,7 @@ use super::index::{parse_index, Index}; use super::{calc_subroutine_bias, conv_subroutine_index, Builder, CFFError}; use crate::parser::{NumFrom, Stream, TryNumFrom}; use crate::var_store::*; -use crate::{BBox, GlyphId, NormalizedCoordinate, OutlineBuilder, Rect}; +use crate::{GlyphId, NormalizedCoordinate, OutlineBuilder, Rect, RectF}; // https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#7-top-dict-data // 'Operators in DICT may be preceded by up to a maximum of 513 operands.' @@ -249,7 +249,7 @@ fn parse_char_string( let mut inner_builder = Builder { builder, - bbox: BBox::new(), + bbox: RectF::new(), }; let stack = ArgumentsStack { diff --git a/src/tables/cff/mod.rs b/src/tables/cff/mod.rs index 89825bd5..a88738c8 100644 --- a/src/tables/cff/mod.rs +++ b/src/tables/cff/mod.rs @@ -13,7 +13,7 @@ mod std_names; use core::convert::TryFrom; use crate::parser::{FromData, TryNumFrom}; -use crate::{BBox, OutlineBuilder}; +use crate::{OutlineBuilder, RectF}; /// A list of errors that can occur during a CFF glyph outlining. #[allow(missing_docs)] @@ -44,7 +44,7 @@ pub enum CFFError { pub(crate) struct Builder<'a> { builder: &'a mut dyn OutlineBuilder, - bbox: BBox, + bbox: RectF, } impl<'a> Builder<'a> { diff --git a/src/tables/colr.rs b/src/tables/colr.rs index efd772f0..a2e8dcdc 100644 --- a/src/tables/colr.rs +++ b/src/tables/colr.rs @@ -1,8 +1,17 @@ //! A [Color Table]( //! https://docs.microsoft.com/en-us/typography/opentype/spec/colr) implementation. -use crate::cpal; -use crate::parser::{FromData, LazyArray16, Offset, Offset32, Stream}; +// NOTE: Parts of the implementation have been inspired by +// [skrifa](https://github.com/googlefonts/fontations/tree/main/skrifa). + +#[cfg(feature = "variable-fonts")] +use crate::delta_set::DeltaSetIndexMap; +use crate::parser::{FromData, LazyArray16, Offset, Offset24, Offset32, Stream, F2DOT14}; +#[cfg(feature = "variable-fonts")] +use crate::var_store::ItemVariationStore; +#[cfg(feature = "variable-fonts")] +use crate::NormalizedCoordinate; +use crate::{cpal, Fixed, LazyArray32, RectF, Transform}; use crate::{GlyphId, RgbaColor}; /// A [base glyph]( @@ -14,6 +23,119 @@ struct BaseGlyphRecord { num_layers: u16, } +/// A [ClipBox](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +pub type ClipBox = RectF; + +/// A paint. +#[derive(Clone, Debug)] +pub enum Paint<'a> { + /// A paint with a solid color. + Solid(RgbaColor), + /// A paint with a linear gradient. + LinearGradient(LinearGradient<'a>), + /// A paint with a radial gradient. + RadialGradient(RadialGradient<'a>), + /// A paint with a sweep gradient. + SweepGradient(SweepGradient<'a>), +} + +/// A [clip record]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +#[derive(Clone, Copy, Debug)] +struct ClipRecord { + /// The first glyph ID for the range covered by this record. + pub start_glyph_id: GlyphId, + /// The last glyph ID, *inclusive*, for the range covered by this record. + pub end_glyph_id: GlyphId, + /// The offset to the clip box. + pub clip_box_offset: Offset24, +} + +impl FromData for ClipRecord { + const SIZE: usize = 7; + + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(ClipRecord { + start_glyph_id: s.read::()?, + end_glyph_id: s.read::()?, + clip_box_offset: s.read::()?, + }) + } +} + +impl ClipRecord { + /// Returns the glyphs range. + pub fn glyphs_range(&self) -> core::ops::RangeInclusive { + self.start_glyph_id..=self.end_glyph_id + } +} + +/// A [clip list]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +#[derive(Clone, Copy, Debug, Default)] +struct ClipList<'a> { + data: &'a [u8], + records: LazyArray32<'a, ClipRecord>, +} + +impl<'a> ClipList<'a> { + pub fn get( + &self, + index: u32, + #[cfg(feature = "variable-fonts")] variation_data: &VariationData, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + ) -> Option { + let record = self.records.get(index)?; + let offset = record.clip_box_offset.to_usize(); + self.data.get(offset..).and_then(|data| { + let mut s = Stream::new(data); + let format = s.read::()?; + + #[cfg(not(feature = "variable-fonts"))] + let deltas = [0.0, 0.0, 0.0, 0.0]; + #[cfg(feature = "variable-fonts")] + let deltas = if format == 2 { + let mut var_s = s.clone(); + var_s.advance(8); + let var_index_base = var_s.read::()?; + + variation_data.read_deltas::<4>(var_index_base, coords) + } else { + [0.0, 0.0, 0.0, 0.0] + }; + + Some(ClipBox { + x_min: s.read::()? as f32 + deltas[0], + y_min: s.read::()? as f32 + deltas[1], + x_max: s.read::()? as f32 + deltas[2], + y_max: s.read::()? as f32 + deltas[3], + }) + }) + } + + /// Returns a ClipBox by glyph ID. + #[inline] + pub fn find( + &self, + glyph_id: GlyphId, + #[cfg(feature = "variable-fonts")] variation_data: &VariationData, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + ) -> Option { + let index = self + .records + .into_iter() + .position(|v| v.glyphs_range().contains(&glyph_id))?; + self.get( + index as u32, + #[cfg(feature = "variable-fonts")] + variation_data, + #[cfg(feature = "variable-fonts")] + coords, + ) + } +} + impl FromData for BaseGlyphRecord { const SIZE: usize = 6; @@ -47,16 +169,545 @@ impl FromData for LayerRecord { } } +/// A [BaseGlyphPaintRecord]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +#[derive(Clone, Copy, Debug)] +struct BaseGlyphPaintRecord { + glyph_id: GlyphId, + paint_table_offset: Offset32, +} + +impl FromData for BaseGlyphPaintRecord { + const SIZE: usize = 6; + + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(Self { + glyph_id: s.read::()?, + paint_table_offset: s.read::()?, + }) + } +} + +/// A [gradient extend]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum GradientExtend { + /// The `Pad` gradient extend mode. + Pad, + /// The `Repeat` gradient extend mode. + Repeat, + /// The `Reflect` gradient extend mode. + Reflect, +} + +impl FromData for GradientExtend { + const SIZE: usize = 1; + + fn parse(data: &[u8]) -> Option { + match data[0] { + 0 => Some(Self::Pad), + 1 => Some(Self::Repeat), + 2 => Some(Self::Reflect), + _ => None, + } + } +} + +/// A [color stop]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#color-references-colorstop-and-colorline). +#[derive(Clone, Copy, Debug)] +struct ColorStopRaw { + stop_offset: F2DOT14, + palette_index: u16, + alpha: F2DOT14, +} + +impl FromData for ColorStopRaw { + const SIZE: usize = 6; + + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(Self { + stop_offset: s.read::()?, + palette_index: s.read::()?, + alpha: s.read::()?, + }) + } +} + +/// A [var color stop]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#color-references-colorstop-and-colorline). +#[cfg(feature = "variable-fonts")] +#[derive(Clone, Copy, Debug)] +struct VarColorStopRaw { + stop_offset: F2DOT14, + palette_index: u16, + alpha: F2DOT14, + var_index_base: u32, +} + +#[cfg(feature = "variable-fonts")] +impl FromData for VarColorStopRaw { + const SIZE: usize = 10; + + fn parse(data: &[u8]) -> Option { + let mut s = Stream::new(data); + Some(Self { + stop_offset: s.read::()?, + palette_index: s.read::()?, + alpha: s.read::()?, + var_index_base: s.read::()?, + }) + } +} + +#[derive(Clone)] +struct NonVarColorLine<'a> { + extend: GradientExtend, + colors: LazyArray16<'a, ColorStopRaw>, + palettes: cpal::Table<'a>, + foreground_color: RgbaColor, +} + +impl NonVarColorLine<'_> { + // TODO: Color stops should be sorted, but hard to do without allocations + fn get(&self, palette: u16, index: u16) -> Option { + let info = self.colors.get(index)?; + + let mut color = if info.palette_index == u16::MAX { + self.foreground_color + } else { + self.palettes.get(palette, info.palette_index)? + }; + + color.apply_alpha(info.alpha.to_f32()); + Some(ColorStop { + stop_offset: info.stop_offset.to_f32(), + color, + }) + } +} + +#[cfg(feature = "variable-fonts")] +impl VarColorLine<'_> { + // TODO: Color stops should be sorted, but hard to do without allocations + fn get( + &self, + palette: u16, + index: u16, + #[cfg(feature = "variable-fonts")] variation_data: VariationData, + #[cfg(feature = "variable-fonts")] coordinates: &[NormalizedCoordinate], + ) -> Option { + let info = self.colors.get(index)?; + + let mut color = if info.palette_index == u16::MAX { + self.foreground_color + } else { + self.palettes.get(palette, info.palette_index)? + }; + + let deltas = variation_data.read_deltas::<2>(info.var_index_base, coordinates); + let stop_offset = info.stop_offset.apply_float_delta(deltas[0]); + color.apply_alpha(info.alpha.apply_float_delta(deltas[1])); + + Some(ColorStop { stop_offset, color }) + } +} + +#[cfg(feature = "variable-fonts")] +#[derive(Clone)] +struct VarColorLine<'a> { + extend: GradientExtend, + colors: LazyArray16<'a, VarColorStopRaw>, + palettes: cpal::Table<'a>, + foreground_color: RgbaColor, +} + +#[derive(Clone)] +enum ColorLine<'a> { + #[cfg(feature = "variable-fonts")] + VarColorLine(VarColorLine<'a>), + NonVarColorLine(NonVarColorLine<'a>), +} + +/// A [gradient extend]( +/// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#baseglyphlist-layerlist-and-cliplist). +#[derive(Clone, Copy, Debug)] +pub struct ColorStop { + /// The offset of the color stop. + pub stop_offset: f32, + /// The color of the color stop. + pub color: RgbaColor, +} + +/// A [linear gradient](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#formats-4-and-5-paintlineargradient-paintvarlineargradient) +#[derive(Clone)] +pub struct LinearGradient<'a> { + /// The `x0` value. + pub x0: f32, + /// The `y0` value. + pub y0: f32, + /// The `x1` value. + pub x1: f32, + /// The `y1` value. + pub y1: f32, + /// The `x2` value. + pub x2: f32, + /// The `y2` value. + pub y2: f32, + /// The extend. + pub extend: GradientExtend, + #[cfg(feature = "variable-fonts")] + variation_data: VariationData<'a>, + color_line: ColorLine<'a>, +} + +impl<'a> core::fmt::Debug for LinearGradient<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LinearGradient") + .field("x0", &self.x0) + .field("y0", &self.y0) + .field("x1", &self.x1) + .field("y1", &self.y1) + .field("x2", &self.x2) + .field("y2", &self.y2) + .field("extend", &self.extend) + .field( + "stops", + &self.stops( + 0, + #[cfg(feature = "variable-fonts")] + &[], + ), + ) + .finish() + } +} + +impl<'a> LinearGradient<'a> { + /// Returns an iterator over the stops of the linear gradient. Stops need to be sorted + /// manually by the caller. + pub fn stops<'b>( + &'b self, + palette: u16, + #[cfg(feature = "variable-fonts")] coords: &'b [NormalizedCoordinate], + ) -> GradientStopsIter<'a, 'b> { + GradientStopsIter { + color_line: &self.color_line, + palette, + index: 0, + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data, + #[cfg(feature = "variable-fonts")] + coords, + } + } +} + +/// A [radial gradient](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#formats-6-and-7-paintradialgradient-paintvarradialgradient) +#[derive(Clone)] +pub struct RadialGradient<'a> { + /// The `x0` value. + pub x0: f32, + /// The `y0` value. + pub y0: f32, + /// The `r0` value. + pub r0: f32, + /// The `r1` value. + pub r1: f32, + /// The `x1` value. + pub x1: f32, + /// The `y1` value. + pub y1: f32, + /// The extend. + pub extend: GradientExtend, + #[cfg(feature = "variable-fonts")] + variation_data: VariationData<'a>, + color_line: ColorLine<'a>, +} + +impl<'a> core::fmt::Debug for RadialGradient<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("RadialGradient") + .field("x0", &self.x0) + .field("y0", &self.y0) + .field("r0", &self.r0) + .field("r1", &self.r1) + .field("x1", &self.x1) + .field("y1", &self.y1) + .field("extend", &self.extend) + .field( + "stops", + &self.stops( + 0, + #[cfg(feature = "variable-fonts")] + &[], + ), + ) + .finish() + } +} + +impl<'a> RadialGradient<'a> { + /// Returns an iterator over the stops of the radial gradient. Stops need to be sorted + /// manually by the caller. + pub fn stops<'b>( + &'b self, + palette: u16, + #[cfg(feature = "variable-fonts")] coords: &'a [NormalizedCoordinate], + ) -> GradientStopsIter<'a, 'b> { + GradientStopsIter { + color_line: &self.color_line, + palette, + index: 0, + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data, + #[cfg(feature = "variable-fonts")] + coords, + } + } +} + +/// A [sweep gradient](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#formats-8-and-9-paintsweepgradient-paintvarsweepgradient) +#[derive(Clone)] +pub struct SweepGradient<'a> { + /// The x of the center. + pub center_x: f32, + /// The y of the center. + pub center_y: f32, + /// The start angle. + pub start_angle: f32, + /// The end angle. + pub end_angle: f32, + /// The extend. + pub extend: GradientExtend, + #[cfg(feature = "variable-fonts")] + variation_data: VariationData<'a>, + color_line: ColorLine<'a>, +} + +impl<'a> core::fmt::Debug for SweepGradient<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SweepGradient") + .field("center_x", &self.center_x) + .field("center_y", &self.center_y) + .field("start_angle", &self.start_angle) + .field("end_angle", &self.end_angle) + .field("extend", &self.extend) + .field( + "stops", + &self.stops( + 0, + #[cfg(feature = "variable-fonts")] + &[], + ), + ) + .finish() + } +} + +impl<'a> SweepGradient<'a> { + // TODO: Make API nicer so that variable coordinates don't + // need to be passed by the caller (same for radial and linear gradient) + /// Returns an iterator over the stops of the sweep gradient. Stops need to be sorted + /// manually by the caller. + pub fn stops<'b>( + &'b self, + palette: u16, + #[cfg(feature = "variable-fonts")] coords: &'a [NormalizedCoordinate], + ) -> GradientStopsIter<'a, 'b> { + GradientStopsIter { + color_line: &self.color_line, + palette, + index: 0, + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data, + #[cfg(feature = "variable-fonts")] + coords, + } + } +} + +/// An iterator over stops of a gradient. +#[derive(Clone, Copy)] +pub struct GradientStopsIter<'a, 'b> { + color_line: &'b ColorLine<'a>, + palette: u16, + index: u16, + #[cfg(feature = "variable-fonts")] + variation_data: VariationData<'a>, + #[cfg(feature = "variable-fonts")] + coords: &'b [NormalizedCoordinate], +} + +impl Iterator for GradientStopsIter<'_, '_> { + type Item = ColorStop; + + fn next(&mut self) -> Option { + let len = match self.color_line { + #[cfg(feature = "variable-fonts")] + ColorLine::VarColorLine(vcl) => vcl.colors.len(), + ColorLine::NonVarColorLine(nvcl) => nvcl.colors.len(), + }; + + if self.index == len { + return None; + } + + let index = self.index; + self.index = self.index.checked_add(1)?; + + match self.color_line { + #[cfg(feature = "variable-fonts")] + ColorLine::VarColorLine(vcl) => { + vcl.get(self.palette, index, self.variation_data, self.coords) + } + ColorLine::NonVarColorLine(nvcl) => nvcl.get(self.palette, index), + } + } +} + +impl core::fmt::Debug for GradientStopsIter<'_, '_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(*self).finish() + } +} + +/// A [composite mode](https://learn.microsoft.com/en-us/typography/opentype/spec/colr#format-32-paintcomposite) +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum CompositeMode { + /// The composite mode 'Clear'. + Clear, + /// The composite mode 'Source'. + Source, + /// The composite mode 'Destination'. + Destination, + /// The composite mode 'SourceOver'. + SourceOver, + /// The composite mode 'DestinationOver'. + DestinationOver, + /// The composite mode 'SourceIn'. + SourceIn, + /// The composite mode 'DestinationIn'. + DestinationIn, + /// The composite mode 'SourceOut'. + SourceOut, + /// The composite mode 'DestinationOut'. + DestinationOut, + /// The composite mode 'SourceAtop'. + SourceAtop, + /// The composite mode 'DestinationAtop'. + DestinationAtop, + /// The composite mode 'Xor'. + Xor, + /// The composite mode 'Plus'. + Plus, + /// The composite mode 'Screen'. + Screen, + /// The composite mode 'Overlay'. + Overlay, + /// The composite mode 'Darken'. + Darken, + /// The composite mode 'Lighten'. + Lighten, + /// The composite mode 'ColorDodge'. + ColorDodge, + /// The composite mode 'ColorBurn'. + ColorBurn, + /// The composite mode 'HardLight'. + HardLight, + /// The composite mode 'SoftLight'. + SoftLight, + /// The composite mode 'Difference'. + Difference, + /// The composite mode 'Exclusion'. + Exclusion, + /// The composite mode 'Multiply'. + Multiply, + /// The composite mode 'Hue'. + Hue, + /// The composite mode 'Saturation'. + Saturation, + /// The composite mode 'Color'. + Color, + /// The composite mode 'Luminosity'. + Luminosity, +} + +impl FromData for CompositeMode { + const SIZE: usize = 1; + + fn parse(data: &[u8]) -> Option { + match data[0] { + 0 => Some(Self::Clear), + 1 => Some(Self::Source), + 2 => Some(Self::Destination), + 3 => Some(Self::SourceOver), + 4 => Some(Self::DestinationOver), + 5 => Some(Self::SourceIn), + 6 => Some(Self::DestinationIn), + 7 => Some(Self::SourceOut), + 8 => Some(Self::DestinationOut), + 9 => Some(Self::SourceAtop), + 10 => Some(Self::DestinationAtop), + 11 => Some(Self::Xor), + 12 => Some(Self::Plus), + 13 => Some(Self::Screen), + 14 => Some(Self::Overlay), + 15 => Some(Self::Darken), + 16 => Some(Self::Lighten), + 17 => Some(Self::ColorDodge), + 18 => Some(Self::ColorBurn), + 19 => Some(Self::HardLight), + 20 => Some(Self::SoftLight), + 21 => Some(Self::Difference), + 22 => Some(Self::Exclusion), + 23 => Some(Self::Multiply), + 24 => Some(Self::Hue), + 25 => Some(Self::Saturation), + 26 => Some(Self::Color), + 27 => Some(Self::Luminosity), + _ => None, + } + } +} + /// A trait for color glyph painting. /// /// See [COLR](https://learn.microsoft.com/en-us/typography/opentype/spec/colr) for details. -pub trait Painter { - /// Outlines a glyph and stores it until the next paint command. - fn outline(&mut self, glyph_id: GlyphId); - /// Paints the current glyph outline using the application provided text foreground color. - fn paint_foreground(&mut self); - /// Paints the current glyph outline using the provided color. - fn paint_color(&mut self, color: RgbaColor); +pub trait Painter<'a> { + /// Outline a glyph and store it. + fn outline_glyph(&mut self, glyph_id: GlyphId); + /// Paint the stored outline using the provided color. + fn paint(&mut self, paint: Paint<'a>); + + /// Push a new clip path using the currently stored outline. + fn push_clip(&mut self); + + /// Push a new clip path using the clip box. + fn push_clip_box(&mut self, clipbox: ClipBox); + /// Pop the last clip path. + fn pop_clip(&mut self); + + /// Push a new layer with the given composite mode. + fn push_layer(&mut self, mode: CompositeMode); + /// Pop the last layer. + fn pop_layer(&mut self); + + // TODO: Unify transforms into one callback. + /// Push a translation transform. + fn push_translate(&mut self, tx: f32, ty: f32); + /// Push a scaling transform. + fn push_scale(&mut self, sx: f32, sy: f32); + /// Push a rotation transform. + fn push_rotate(&mut self, angle: f32); + /// Push a skewing transform. + fn push_skew(&mut self, skew_x: f32, skew_y: f32); + /// Push a transform. + fn push_transform(&mut self, transform: Transform); + /// Pop the last transform. + fn pop_transform(&mut self); } /// A [Color Table]( @@ -66,8 +717,22 @@ pub trait Painter { #[derive(Clone, Copy, Debug)] pub struct Table<'a> { pub(crate) palettes: cpal::Table<'a>, + data: &'a [u8], + version: u8, + // v0 base_glyphs: LazyArray16<'a, BaseGlyphRecord>, layers: LazyArray16<'a, LayerRecord>, + // v1 + base_glyph_paints_offset: Offset32, + base_glyph_paints: LazyArray32<'a, BaseGlyphPaintRecord>, + layer_paint_offsets_offset: Offset32, + layer_paint_offsets: LazyArray32<'a, Offset32>, + clip_list_offsets_offset: Offset32, + clip_list: ClipList<'a>, + #[cfg(feature = "variable-fonts")] + var_index_map: Option>, + #[cfg(feature = "variable-fonts")] + item_variation_store: Option>, } impl<'a> Table<'a> { @@ -76,7 +741,7 @@ impl<'a> Table<'a> { let mut s = Stream::new(data); let version = s.read::()?; - if version != 0 { + if version > 1 { return None; } @@ -91,27 +756,177 @@ impl<'a> Table<'a> { let layers = Stream::new_at(data, layers_offset.to_usize())? .read_array16::(num_layers)?; - Some(Self { + let mut table = Self { + version: version as u8, + data, palettes, base_glyphs, layers, - }) + base_glyph_paints_offset: Offset32(0), // the actual value doesn't matter + base_glyph_paints: LazyArray32::default(), + layer_paint_offsets_offset: Offset32(0), + layer_paint_offsets: LazyArray32::default(), + clip_list_offsets_offset: Offset32(0), + clip_list: ClipList::default(), + #[cfg(feature = "variable-fonts")] + item_variation_store: None, + #[cfg(feature = "variable-fonts")] + var_index_map: None, + }; + + if version == 0 { + return Some(table); + } + + table.base_glyph_paints_offset = s.read::()?; + let layer_list_offset = s.read::>()?; + let clip_list_offset = s.read::>()?; + #[cfg(feature = "variable-fonts")] + let var_index_map_offset = s.read::>()?; + #[cfg(feature = "variable-fonts")] + let item_variation_offset = s.read::>()?; + + { + let mut s = Stream::new_at(data, table.base_glyph_paints_offset.to_usize())?; + let count = s.read::()?; + table.base_glyph_paints = s.read_array32::(count)?; + } + + if let Some(offset) = layer_list_offset { + table.layer_paint_offsets_offset = offset; + let mut s = Stream::new_at(data, offset.to_usize())?; + let count = s.read::()?; + table.layer_paint_offsets = s.read_array32::(count)?; + } + + if let Some(offset) = clip_list_offset { + table.clip_list_offsets_offset = offset; + let clip_data = data.get(offset.to_usize()..)?; + let mut s = Stream::new(clip_data); + s.skip::(); // Format + let count = s.read::()?; + table.clip_list = ClipList { + data: clip_data, + records: s.read_array32::(count)?, + }; + } + + #[cfg(feature = "variable-fonts")] + { + if let Some(offset) = item_variation_offset { + let item_var_data = data.get(offset.to_usize()..)?; + let s = Stream::new(item_var_data); + let var_store = ItemVariationStore::parse(s)?; + table.item_variation_store = Some(var_store); + } + } + + #[cfg(feature = "variable-fonts")] + { + if let Some(offset) = var_index_map_offset { + let var_index_map_data = data.get(offset.to_usize()..)?; + let var_index_map = DeltaSetIndexMap::new(var_index_map_data); + table.var_index_map = Some(var_index_map); + } + } + + Some(table) } - fn get(&self, glyph_id: GlyphId) -> Option { + /// Returns `true` if the current table has version 0. + /// + /// A simple table can only emit `outline_glyph` and `paint` + /// [`Painter`] methods. + pub fn is_simple(&self) -> bool { + self.version == 0 + } + + fn get_v0(&self, glyph_id: GlyphId) -> Option { self.base_glyphs .binary_search_by(|base| base.glyph_id.cmp(&glyph_id)) .map(|v| v.1) } + fn get_v1(&self, glyph_id: GlyphId) -> Option { + self.base_glyph_paints + .binary_search_by(|base| base.glyph_id.cmp(&glyph_id)) + .map(|v| v.1) + } + + #[cfg(feature = "variable-fonts")] + fn variation_data(&self) -> VariationData<'a> { + VariationData { + variation_store: self.item_variation_store, + delta_map: self.var_index_map, + } + } + /// Whether the table contains a definition for the given glyph. pub fn contains(&self, glyph_id: GlyphId) -> bool { - self.get(glyph_id).is_some() + self.get_v1(glyph_id).is_some() || self.get_v0(glyph_id).is_some() } + // This method should only be called from outside, not from within `colr.rs`. + // From inside, you always should call paint_impl, so that the recursion stack can + // be passed on and any kind of recursion can be prevented. /// Paints the color glyph. - pub fn paint(&self, glyph_id: GlyphId, palette: u16, painter: &mut dyn Painter) -> Option<()> { - let base = self.get(glyph_id)?; + pub fn paint( + &self, + glyph_id: GlyphId, + palette: u16, + painter: &mut dyn Painter<'a>, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + foreground_color: RgbaColor, + ) -> Option<()> { + let mut recursion_stack = RecursionStack { + stack: [0; 64], + len: 0, + }; + + self.paint_impl( + glyph_id, + palette, + painter, + &mut recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ) + } + + fn paint_impl( + &self, + glyph_id: GlyphId, + palette: u16, + painter: &mut dyn Painter<'a>, + recusion_stack: &mut RecursionStack, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + foreground_color: RgbaColor, + ) -> Option<()> { + if let Some(base) = self.get_v1(glyph_id) { + self.paint_v1( + base, + palette, + painter, + recusion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ) + } else if let Some(base) = self.get_v0(glyph_id) { + self.paint_v0(base, palette, painter, foreground_color) + } else { + None + } + } + + fn paint_v0( + &self, + base: BaseGlyphRecord, + palette: u16, + painter: &mut dyn Painter, + foreground_color: RgbaColor, + ) -> Option<()> { let start = base.first_layer_index; let end = start.checked_add(base.num_layers)?; let layers = self.layers.slice(start..end)?; @@ -119,15 +934,974 @@ impl<'a> Table<'a> { for layer in layers { if layer.palette_index == 0xFFFF { // A special case. - painter.outline(layer.glyph_id); - painter.paint_foreground(); + painter.outline_glyph(layer.glyph_id); + painter.paint(Paint::Solid(foreground_color)); } else { let color = self.palettes.get(palette, layer.palette_index)?; - painter.outline(layer.glyph_id); - painter.paint_color(color); + painter.outline_glyph(layer.glyph_id); + painter.paint(Paint::Solid(color)); } } Some(()) } + + fn paint_v1( + &self, + base: BaseGlyphPaintRecord, + palette: u16, + painter: &mut dyn Painter<'a>, + recursion_stack: &mut RecursionStack, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + foreground_color: RgbaColor, + ) -> Option<()> { + let clip_box = self.clip_list.find( + base.glyph_id, + #[cfg(feature = "variable-fonts")] + &self.variation_data(), + #[cfg(feature = "variable-fonts")] + coords, + ); + if let Some(clip_box) = clip_box { + painter.push_clip_box(clip_box); + } + + self.parse_paint( + self.base_glyph_paints_offset.to_usize() + base.paint_table_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + + if clip_box.is_some() { + painter.pop_clip(); + } + + Some(()) + } + + fn parse_paint( + &self, + offset: usize, + palette: u16, + painter: &mut dyn Painter<'a>, + recursion_stack: &mut RecursionStack, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + foreground_color: RgbaColor, + ) -> Option<()> { + let mut s = Stream::new_at(self.data, offset)?; + let format = s.read::()?; + + // Cycle detected + if recursion_stack.contains(offset) { + return None; + } + + recursion_stack.push(offset).ok()?; + let result = self.parse_paint_impl( + offset, + palette, + painter, + recursion_stack, + &mut s, + format, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + recursion_stack.pop(); + + result + } + + fn parse_paint_impl( + &self, + offset: usize, + palette: u16, + painter: &mut dyn Painter<'a>, + recursion_stack: &mut RecursionStack, + s: &mut Stream, + format: u8, + #[cfg(feature = "variable-fonts")] coords: &[NormalizedCoordinate], + foreground_color: RgbaColor, + ) -> Option<()> { + match format { + 1 => { + // PaintColrLayers + let layers_count = s.read::()?; + let first_layer_index = s.read::()?; + + for i in 0..layers_count { + let index = first_layer_index.checked_add(u32::from(i))?; + let paint_offset = self.layer_paint_offsets.get(index)?; + self.parse_paint( + self.layer_paint_offsets_offset.to_usize() + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + } + } + 2 => { + // PaintSolid + let palette_index = s.read::()?; + let alpha = s.read::()?; + + let mut color = if palette_index == u16::MAX { + foreground_color + } else { + self.palettes.get(palette, palette_index)? + }; + + color.apply_alpha(alpha.to_f32()); + painter.paint(Paint::Solid(color)); + } + #[cfg(feature = "variable-fonts")] + 3 => { + // PaintVarSolid + let palette_index = s.read::()?; + let alpha = s.read::()?; + let var_index_base = s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<1>(var_index_base, coords); + + let mut color = if palette_index == u16::MAX { + foreground_color + } else { + self.palettes.get(palette, palette_index)? + }; + + color.apply_alpha(alpha.apply_float_delta(deltas[0])); + painter.paint(Paint::Solid(color)); + } + 4 => { + // PaintLinearGradient + let color_line_offset = s.read::()?; + let color_line = + self.parse_color_line(offset + color_line_offset.to_usize(), foreground_color)?; + + painter.paint(Paint::LinearGradient(LinearGradient { + x0: s.read::()? as f32, + y0: s.read::()? as f32, + x1: s.read::()? as f32, + y1: s.read::()? as f32, + x2: s.read::()? as f32, + y2: s.read::()? as f32, + extend: color_line.extend, + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data(), + color_line: ColorLine::NonVarColorLine(color_line), + })) + } + #[cfg(feature = "variable-fonts")] + 5 => { + // PaintVarLinearGradient + let var_color_line_offset = s.read::()?; + let color_line = self.parse_var_color_line( + offset + var_color_line_offset.to_usize(), + foreground_color, + )?; + let mut var_s = s.clone(); + var_s.advance(12); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<6>(var_index_base, coords); + + painter.paint(Paint::LinearGradient(LinearGradient { + x0: s.read::()? as f32 + deltas[0], + y0: s.read::()? as f32 + deltas[1], + x1: s.read::()? as f32 + deltas[2], + y1: s.read::()? as f32 + deltas[3], + x2: s.read::()? as f32 + deltas[4], + y2: s.read::()? as f32 + deltas[5], + extend: color_line.extend, + variation_data: self.variation_data(), + color_line: ColorLine::VarColorLine(color_line), + })) + } + 6 => { + // PaintRadialGradient + let color_line_offset = s.read::()?; + let color_line = + self.parse_color_line(offset + color_line_offset.to_usize(), foreground_color)?; + painter.paint(Paint::RadialGradient(RadialGradient { + x0: s.read::()? as f32, + y0: s.read::()? as f32, + r0: s.read::()? as f32, + x1: s.read::()? as f32, + y1: s.read::()? as f32, + r1: s.read::()? as f32, + extend: color_line.extend, + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data(), + color_line: ColorLine::NonVarColorLine(color_line), + })) + } + #[cfg(feature = "variable-fonts")] + 7 => { + // PaintVarRadialGradient + let color_line_offset = s.read::()?; + let color_line = self.parse_var_color_line( + offset + color_line_offset.to_usize(), + foreground_color, + )?; + + let mut var_s = s.clone(); + var_s.advance(12); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<6>(var_index_base, coords); + + painter.paint(Paint::RadialGradient(RadialGradient { + x0: s.read::()? as f32 + deltas[0], + y0: s.read::()? as f32 + deltas[1], + r0: s.read::()? as f32 + deltas[2], + x1: s.read::()? as f32 + deltas[3], + y1: s.read::()? as f32 + deltas[4], + r1: s.read::()? as f32 + deltas[5], + extend: color_line.extend, + variation_data: self.variation_data(), + color_line: ColorLine::VarColorLine(color_line), + })) + } + 8 => { + // PaintSweepGradient + let color_line_offset = s.read::()?; + let color_line = + self.parse_color_line(offset + color_line_offset.to_usize(), foreground_color)?; + painter.paint(Paint::SweepGradient(SweepGradient { + center_x: s.read::()? as f32, + center_y: s.read::()? as f32, + start_angle: s.read::()?.to_f32(), + end_angle: s.read::()?.to_f32(), + extend: color_line.extend, + color_line: ColorLine::NonVarColorLine(color_line), + #[cfg(feature = "variable-fonts")] + variation_data: self.variation_data(), + })) + } + #[cfg(feature = "variable-fonts")] + 9 => { + // PaintVarSweepGradient + let color_line_offset = s.read::()?; + let color_line = self.parse_var_color_line( + offset + color_line_offset.to_usize(), + foreground_color, + )?; + + let mut var_s = s.clone(); + var_s.advance(8); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<4>(var_index_base, coords); + + painter.paint(Paint::SweepGradient(SweepGradient { + center_x: s.read::()? as f32 + deltas[0], + center_y: s.read::()? as f32 + deltas[1], + start_angle: s.read::()?.apply_float_delta(deltas[2]), + end_angle: s.read::()?.apply_float_delta(deltas[3]), + extend: color_line.extend, + color_line: ColorLine::VarColorLine(color_line), + variation_data: self.variation_data(), + })) + } + 10 => { + // PaintGlyph + let paint_offset = s.read::()?; + let glyph_id = s.read::()?; + painter.outline_glyph(glyph_id); + painter.push_clip(); + + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + + painter.pop_clip(); + } + 11 => { + // PaintColrGlyph + let glyph_id = s.read::()?; + self.paint_impl( + glyph_id, + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + } + 12 => { + // PaintTransform + let paint_offset = s.read::()?; + let ts_offset = s.read::()?; + let mut s = Stream::new_at(self.data, offset + ts_offset.to_usize())?; + let ts = Transform { + a: s.read::().map(|n| n.0)?, + b: s.read::().map(|n| n.0)?, + c: s.read::().map(|n| n.0)?, + d: s.read::().map(|n| n.0)?, + e: s.read::().map(|n| n.0)?, + f: s.read::().map(|n| n.0)?, + }; + + painter.push_transform(ts); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 13 => { + // PaintVarTransform + let paint_offset = s.read::()?; + let ts_offset = s.read::()?; + let mut s = Stream::new_at(self.data, offset + ts_offset.to_usize())?; + + let mut var_s = s.clone(); + var_s.advance(24); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<6>(var_index_base, coords); + + let ts = Transform { + a: s.read::()?.apply_float_delta(deltas[0]), + b: s.read::()?.apply_float_delta(deltas[1]), + c: s.read::()?.apply_float_delta(deltas[2]), + d: s.read::()?.apply_float_delta(deltas[3]), + e: s.read::()?.apply_float_delta(deltas[4]), + f: s.read::()?.apply_float_delta(deltas[5]), + }; + + painter.push_transform(ts); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 14 => { + // PaintTranslate + let paint_offset = s.read::()?; + let tx = f32::from(s.read::()?); + let ty = f32::from(s.read::()?); + + painter.push_translate(tx, ty); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 15 => { + // PaintVarTranslate + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(4); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<2>(var_index_base, coords); + + let tx = f32::from(s.read::()?) + deltas[0]; + let ty = f32::from(s.read::()?) + deltas[1]; + + painter.push_translate(tx, ty); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 16 => { + // PaintScale + let paint_offset = s.read::()?; + let sx = s.read::()?.to_f32(); + let sy = s.read::()?.to_f32(); + + painter.push_scale(sx, sy); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 17 => { + // PaintVarScale + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(4); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<2>(var_index_base, coords); + + let sx = s.read::()?.apply_float_delta(deltas[0]); + let sy = s.read::()?.apply_float_delta(deltas[1]); + + painter.push_scale(sx, sy); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 18 => { + // PaintScaleAroundCenter + let paint_offset = s.read::()?; + let sx = s.read::()?.to_f32(); + let sy = s.read::()?.to_f32(); + let center_x = f32::from(s.read::()?); + let center_y = f32::from(s.read::()?); + + painter.push_translate(center_x, center_y); + painter.push_scale(sx, sy); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 19 => { + // PaintVarScaleAroundCenter + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(8); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<4>(var_index_base, coords); + + let sx = s.read::()?.apply_float_delta(deltas[0]); + let sy = s.read::()?.apply_float_delta(deltas[1]); + let center_x = f32::from(s.read::()?) + deltas[2]; + let center_y = f32::from(s.read::()?) + deltas[3]; + + painter.push_translate(center_x, center_y); + painter.push_scale(sx, sy); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + 20 => { + // PaintScaleUniform + let paint_offset = s.read::()?; + let scale = s.read::()?.to_f32(); + + painter.push_scale(scale, scale); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 21 => { + // PaintVarScaleUniform + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(2); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<1>(var_index_base, coords); + + let scale = s.read::()?.apply_float_delta(deltas[0]); + + painter.push_scale(scale, scale); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 22 => { + // PaintScaleUniformAroundCenter + let paint_offset = s.read::()?; + let scale = s.read::()?.to_f32(); + let center_x = f32::from(s.read::()?); + let center_y = f32::from(s.read::()?); + + painter.push_translate(center_x, center_y); + painter.push_scale(scale, scale); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 23 => { + // PaintVarScaleUniformAroundCenter + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(6); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<3>(var_index_base, coords); + + let scale = s.read::()?.apply_float_delta(deltas[0]); + let center_x = f32::from(s.read::()?) + deltas[1]; + let center_y = f32::from(s.read::()?) + deltas[2]; + + painter.push_translate(center_x, center_y); + painter.push_scale(scale, scale); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + 24 => { + // PaintRotate + let paint_offset = s.read::()?; + let angle = s.read::()?.to_f32(); + + painter.push_rotate(angle); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 25 => { + // PaintVarRotate + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(2); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<1>(var_index_base, coords); + + let angle = s.read::()?.apply_float_delta(deltas[0]); + + painter.push_rotate(angle); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 26 => { + // PaintRotateAroundCenter + let paint_offset = s.read::()?; + let angle = s.read::()?.to_f32(); + let center_x = f32::from(s.read::()?); + let center_y = f32::from(s.read::()?); + + painter.push_translate(center_x, center_y); + painter.push_rotate(angle); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 27 => { + // PaintVarRotateAroundCenter + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(6); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<3>(var_index_base, coords); + + let angle = s.read::()?.apply_float_delta(deltas[0]); + let center_x = f32::from(s.read::()?) + deltas[1]; + let center_y = f32::from(s.read::()?) + deltas[2]; + + painter.push_translate(center_x, center_y); + painter.push_rotate(angle); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + 28 => { + // PaintSkew + let paint_offset = s.read::()?; + let skew_x = s.read::()?.to_f32(); + let skew_y = s.read::()?.to_f32(); + + painter.push_skew(skew_x, skew_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 29 => { + // PaintVarSkew + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(4); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<2>(var_index_base, coords); + + let skew_x = s.read::()?.apply_float_delta(deltas[0]); + let skew_y = s.read::()?.apply_float_delta(deltas[1]); + + painter.push_skew(skew_x, skew_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + } + 30 => { + // PaintSkewAroundCenter + let paint_offset = s.read::()?; + let skew_x = s.read::()?.to_f32(); + let skew_y = s.read::()?.to_f32(); + let center_x = f32::from(s.read::()?); + let center_y = f32::from(s.read::()?); + + painter.push_translate(center_x, center_y); + painter.push_skew(skew_x, skew_y); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + #[cfg(feature = "variable-fonts")] + 31 => { + // PaintVarSkewAroundCenter + let paint_offset = s.read::()?; + + let mut var_s = s.clone(); + var_s.advance(8); + let var_index_base = var_s.read::()?; + + let deltas = self + .variation_data() + .read_deltas::<4>(var_index_base, coords); + + let skew_x = s.read::()?.apply_float_delta(deltas[0]); + let skew_y = s.read::()?.apply_float_delta(deltas[1]); + let center_x = f32::from(s.read::()?) + deltas[2]; + let center_y = f32::from(s.read::()?) + deltas[3]; + + painter.push_translate(center_x, center_y); + painter.push_skew(skew_x, skew_y); + painter.push_translate(-center_x, -center_y); + self.parse_paint( + offset + paint_offset.to_usize(), + palette, + painter, + recursion_stack, + coords, + foreground_color, + ); + painter.pop_transform(); + painter.pop_transform(); + painter.pop_transform(); + } + 32 => { + // PaintComposite + let source_paint_offset = s.read::()?; + let composite_mode = s.read::()?; + let backdrop_paint_offset = s.read::()?; + + painter.push_layer(CompositeMode::SourceOver); + self.parse_paint( + offset + backdrop_paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.push_layer(composite_mode); + self.parse_paint( + offset + source_paint_offset.to_usize(), + palette, + painter, + recursion_stack, + #[cfg(feature = "variable-fonts")] + coords, + foreground_color, + ); + painter.pop_layer(); + painter.pop_layer(); + } + _ => {} + } + + Some(()) + } + + fn parse_color_line( + &self, + offset: usize, + foreground_color: RgbaColor, + ) -> Option> { + let mut s = Stream::new_at(self.data, offset)?; + let extend = s.read::()?; + let count = s.read::()?; + let colors = s.read_array16::(count)?; + Some(NonVarColorLine { + extend, + colors, + foreground_color, + palettes: self.palettes, + }) + } + + #[cfg(feature = "variable-fonts")] + fn parse_var_color_line( + &self, + offset: usize, + foreground_color: RgbaColor, + ) -> Option> { + let mut s = Stream::new_at(self.data, offset)?; + let extend = s.read::()?; + let count = s.read::()?; + let colors = s.read_array16::(count)?; + Some(VarColorLine { + extend, + colors, + foreground_color, + palettes: self.palettes, + }) + } +} + +struct RecursionStack { + // The limit of 64 is chosen arbitrarily and not from the spec. But we have to stop somewhere... + stack: [usize; 64], + len: usize, +} + +impl RecursionStack { + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + #[inline] + pub fn push(&mut self, offset: usize) -> Result<(), ()> { + if self.len == self.stack.len() { + Err(()) + } else { + self.stack[self.len] = offset; + self.len += 1; + Ok(()) + } + } + + #[inline] + pub fn contains(&self, offset: usize) -> bool { + if let Some(offsets) = self.stack.get(..self.len) { + return offsets.contains(&offset); + } + + false + } + + #[inline] + pub fn pop(&mut self) { + debug_assert!(!self.is_empty()); + self.len -= 1; + } +} + +#[cfg(feature = "variable-fonts")] +#[derive(Clone, Copy, Debug, Default)] +struct VariationData<'a> { + variation_store: Option>, + delta_map: Option>, +} + +#[cfg(feature = "variable-fonts")] +impl VariationData<'_> { + // Inspired from `fontations`. + fn read_deltas( + &self, + var_index_base: u32, + coordinates: &[NormalizedCoordinate], + ) -> [f32; N] { + const NO_VARIATION_DELTAS: u32 = 0xFFFFFFFF; + let mut deltas = [0.0; N]; + + if coordinates.is_empty() + || self.variation_store.is_none() + || var_index_base == NO_VARIATION_DELTAS + { + return deltas; + } + + let variation_store = self.variation_store.as_ref().unwrap(); + + for i in 0..N { + deltas[i] = self + .delta_map + .and_then(|d| d.map(var_index_base + i as u32)) + .and_then(|d| variation_store.parse_delta(d.0, d.1, coordinates)) + .unwrap_or(0.0); + } + + deltas + } } diff --git a/src/tables/cpal.rs b/src/tables/cpal.rs index 007e2558..bed754dd 100644 --- a/src/tables/cpal.rs +++ b/src/tables/cpal.rs @@ -20,7 +20,7 @@ impl<'a> Table<'a> { let mut s = Stream::new(data); let version = s.read::()?; - if version != 0 { + if version > 1 { return None; } diff --git a/src/tables/glyf.rs b/src/tables/glyf.rs index 4a70f871..e38c1ca7 100644 --- a/src/tables/glyf.rs +++ b/src/tables/glyf.rs @@ -4,7 +4,7 @@ use core::num::NonZeroU16; use crate::parser::{LazyArray16, NumFrom, Stream, F2DOT14}; -use crate::{loca, BBox, GlyphId, OutlineBuilder, Rect, Transform}; +use crate::{loca, GlyphId, OutlineBuilder, Rect, RectF, Transform}; pub(crate) struct Builder<'a> { pub builder: &'a mut dyn OutlineBuilder, @@ -12,7 +12,7 @@ pub(crate) struct Builder<'a> { is_default_ts: bool, // `bool` is faster than `Option` or `is_default`. // We have to always calculate the bbox, because `gvar` doesn't store one // and in case of a malformed bbox in `glyf`. - pub bbox: BBox, + pub bbox: RectF, first_on_curve: Option, first_off_curve: Option, last_off_curve: Option, @@ -20,7 +20,7 @@ pub(crate) struct Builder<'a> { impl<'a> Builder<'a> { #[inline] - pub fn new(transform: Transform, bbox: BBox, builder: &'a mut dyn OutlineBuilder) -> Self { + pub fn new(transform: Transform, bbox: RectF, builder: &'a mut dyn OutlineBuilder) -> Self { Builder { builder, transform, @@ -594,7 +594,7 @@ impl<'a> Table<'a> { /// Outlines a glyph. #[inline] pub fn outline(&self, glyph_id: GlyphId, builder: &mut dyn OutlineBuilder) -> Option { - let mut b = Builder::new(Transform::default(), BBox::new(), builder); + let mut b = Builder::new(Transform::default(), RectF::new(), builder); let glyph_data = self.get(glyph_id)?; outline_impl(self.loca_table, self.data, glyph_data, 0, &mut b)? } diff --git a/src/tables/gvar.rs b/src/tables/gvar.rs index 3894d955..8df3b7f8 100644 --- a/src/tables/gvar.rs +++ b/src/tables/gvar.rs @@ -13,7 +13,7 @@ use core::num::NonZeroU16; use crate::glyf; use crate::parser::{LazyArray16, Offset, Offset16, Offset32, Stream, F2DOT14}; -use crate::{BBox, GlyphId, NormalizedCoordinate, OutlineBuilder, Rect, Transform}; +use crate::{GlyphId, NormalizedCoordinate, OutlineBuilder, Rect, RectF, Transform}; /// 'The TrueType rasterizer dynamically generates 'phantom' points for each glyph /// that represent horizontal and vertical advance widths and side bearings, @@ -1739,7 +1739,7 @@ impl<'a> Table<'a> { glyph_id: GlyphId, builder: &mut dyn OutlineBuilder, ) -> Option { - let mut b = glyf::Builder::new(Transform::default(), BBox::new(), builder); + let mut b = glyf::Builder::new(Transform::default(), RectF::new(), builder); let glyph_data = glyf_table.get(glyph_id)?; outline_var_impl( glyf_table, diff --git a/src/tables/hvar.rs b/src/tables/hvar.rs index 9f1a8047..96b87ffd 100644 --- a/src/tables/hvar.rs +++ b/src/tables/hvar.rs @@ -1,58 +1,11 @@ //! A [Horizontal/Vertical Metrics Variations Table]( //! https://docs.microsoft.com/en-us/typography/opentype/spec/hvar) implementation. -use core::convert::TryFrom; - +use crate::delta_set::DeltaSetIndexMap; use crate::parser::{Offset, Offset32, Stream}; use crate::var_store::ItemVariationStore; use crate::{GlyphId, NormalizedCoordinate}; -struct DeltaSetIndexMap<'a> { - data: &'a [u8], -} - -impl<'a> DeltaSetIndexMap<'a> { - #[inline] - fn new(data: &'a [u8]) -> Self { - DeltaSetIndexMap { data } - } - - #[inline] - fn map(&self, glyph_id: GlyphId) -> Option<(u16, u16)> { - let mut idx = glyph_id.0; - - let mut s = Stream::new(self.data); - let entry_format = s.read::()?; - let map_count = s.read::()?; - - if map_count == 0 { - return None; - } - - // 'If a given glyph ID is greater than mapCount-1, then the last entry is used.' - if idx >= map_count { - idx = map_count - 1; - } - - let entry_size = ((entry_format >> 4) & 3) + 1; - let inner_index_bit_count = u32::from((entry_format & 0xF) + 1); - - s.advance(usize::from(entry_size) * usize::from(idx)); - - let mut n = 0u32; - for b in s.read_bytes(usize::from(entry_size))? { - n = (n << 8) + u32::from(*b); - } - - let outer_index = n >> inner_index_bit_count; - let inner_index = n & ((1 << inner_index_bit_count) - 1); - Some(( - u16::try_from(outer_index).ok()?, - u16::try_from(inner_index).ok()?, - )) - } -} - /// A [Horizontal/Vertical Metrics Variations Table]( /// https://docs.microsoft.com/en-us/typography/opentype/spec/hvar). #[derive(Clone, Copy)] @@ -93,7 +46,7 @@ impl<'a> Table<'a> { coordinates: &[NormalizedCoordinate], ) -> Option { let (outer_idx, inner_idx) = if let Some(offset) = self.advance_width_mapping_offset { - DeltaSetIndexMap::new(self.data.get(offset.to_usize()..)?).map(glyph_id)? + DeltaSetIndexMap::new(self.data.get(offset.to_usize()..)?).map(glyph_id.0 as u32)? } else { // 'If there is no delta-set index mapping table for advance widths, // then glyph IDs implicitly provide the indices: @@ -114,7 +67,7 @@ impl<'a> Table<'a> { coordinates: &[NormalizedCoordinate], ) -> Option { let set_data = self.data.get(self.lsb_mapping_offset?.to_usize()..)?; - let (outer_idx, inner_idx) = DeltaSetIndexMap::new(set_data).map(glyph_id)?; + let (outer_idx, inner_idx) = DeltaSetIndexMap::new(set_data).map(glyph_id.0 as u32)?; self.variation_store .parse_delta(outer_idx, inner_idx, coordinates) } diff --git a/src/tables/post.rs b/src/tables/post.rs index ed696dc3..2b487a11 100644 --- a/src/tables/post.rs +++ b/src/tables/post.rs @@ -325,7 +325,6 @@ pub struct Table<'a> { pub underline_metrics: LineMetrics, /// Flag that indicates that the font is monospaced. pub is_monospaced: bool, - glyph_indexes: LazyArray16<'a, u16>, names_data: &'a [u8], } diff --git a/src/var_store.rs b/src/var_store.rs index a37fcb25..0697b9ca 100644 --- a/src/var_store.rs +++ b/src/var_store.rs @@ -5,7 +5,7 @@ use crate::parser::{FromData, LazyArray16, NumFrom, Stream}; use crate::NormalizedCoordinate; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub(crate) struct ItemVariationStore<'a> { data: &'a [u8], data_offsets: LazyArray16<'a, u32>, @@ -80,7 +80,7 @@ impl<'a> ItemVariationStore<'a> { let offset = self.data_offsets.get(outer_index)?; let mut s = Stream::new_at(self.data, usize::num_from(offset))?; let item_count = s.read::()?; - let short_delta_count = s.read::()?; + let word_delta_count = s.read::()?; let region_index_count = s.read::()?; let region_indices = s.read_array16::(region_index_count)?; @@ -88,20 +88,38 @@ impl<'a> ItemVariationStore<'a> { return None; } - let delta_set_len = usize::from(short_delta_count) + usize::from(region_index_count); - s.advance(usize::from(inner_index).checked_mul(delta_set_len)?); + let has_long_words = (word_delta_count & 0x8000) != 0; + let word_delta_count = word_delta_count & 0x7FFF; + + let delta_set_len = if has_long_words { + 4 * word_delta_count + 2 * region_index_count.checked_sub(word_delta_count)? + } else { + 2 * region_index_count + }; + s.advance(usize::from(inner_index).checked_mul(usize::from(delta_set_len))?); let mut delta = 0.0; let mut i = 0; - while i < short_delta_count { + while i < word_delta_count { let idx = region_indices.get(i)?; - delta += f32::from(s.read::()?) * self.regions.evaluate_region(idx, coordinates); + let num = if has_long_words { + // TODO: use f64? + s.read::()? as f32 + } else { + f32::from(s.read::()?) + }; + delta += num * self.regions.evaluate_region(idx, coordinates); i += 1; } while i < region_index_count { let idx = region_indices.get(i)?; - delta += f32::from(s.read::()?) * self.regions.evaluate_region(idx, coordinates); + let num = if has_long_words { + f32::from(s.read::()?) + } else { + f32::from(s.read::()?) + }; + delta += num * self.regions.evaluate_region(idx, coordinates); i += 1; } @@ -109,7 +127,7 @@ impl<'a> ItemVariationStore<'a> { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct VariationRegionList<'a> { axis_count: u16, regions: LazyArray16<'a, RegionAxisCoordinatesRecord>, @@ -137,7 +155,7 @@ impl<'a> VariationRegionList<'a> { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] struct RegionAxisCoordinatesRecord { start_coord: i16, peak_coord: i16, diff --git a/tests/fonts/colr_1.ttf b/tests/fonts/colr_1.ttf new file mode 100644 index 00000000..d23b3a23 Binary files /dev/null and b/tests/fonts/colr_1.ttf differ diff --git a/tests/fonts/colr_1_LICENSE b/tests/fonts/colr_1_LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/tests/fonts/colr_1_LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/tests/fonts/colr_1_variable.ttf b/tests/fonts/colr_1_variable.ttf new file mode 100644 index 00000000..c3846e2b Binary files /dev/null and b/tests/fonts/colr_1_variable.ttf differ diff --git a/tests/tables/colr.rs b/tests/tables/colr.rs index de0c4d71..a003e254 100644 --- a/tests/tables/colr.rs +++ b/tests/tables/colr.rs @@ -1,5 +1,5 @@ use crate::{convert, Unit::*}; -use ttf_parser::colr::{self, Painter}; +use ttf_parser::colr::{self, ClipBox, CompositeMode, GradientExtend, Paint, Painter}; use ttf_parser::{cpal, GlyphId, RgbaColor}; #[test] @@ -35,7 +35,7 @@ fn basic() { let colr = colr::Table::parse(cpal, &colr_data).unwrap(); let paint = |id| { let mut painter = VecPainter(vec![]); - colr.paint(GlyphId(id), 0, &mut painter).map(|_| painter.0) + colr.paint(GlyphId(id), 0, &mut painter, &[], RgbaColor::new(0, 0, 0, 255)).map(|_| painter.0) }; let a = RgbaColor::new(20, 15, 10, 25); @@ -56,49 +56,474 @@ fn basic() { assert!(!colr.contains(GlyphId(6))); assert!(colr.contains(GlyphId(7))); + let a = CustomPaint::Solid(a); + let b = CustomPaint::Solid(b); + let c = CustomPaint::Solid(c); + assert_eq!(paint(1), None); - assert_eq!(paint(2).unwrap(), vec![ - Command::Outline(12), - Command::PaintColor(c), - Command::Outline(13), - Command::PaintColor(a), - ]); + assert_eq!( + paint(2).unwrap(), vec![ + Command::OutlineGlyph(GlyphId(12)), + Command::Paint(c.clone()), + Command::OutlineGlyph(GlyphId(13)), + Command::Paint(a.clone())] + ); assert_eq!(paint(3).unwrap(), vec![ - Command::Outline(10), - Command::PaintColor(c), - Command::Outline(11), - Command::PaintColor(b), - Command::Outline(12), - Command::PaintColor(c), + Command::OutlineGlyph(GlyphId(10)), + Command::Paint(c.clone()), + Command::OutlineGlyph(GlyphId(11)), + Command::Paint(b.clone()), + Command::OutlineGlyph(GlyphId(12)), + Command::Paint(c.clone()), ]); assert_eq!(paint(7).unwrap(), vec![ - Command::Outline(11), - Command::PaintColor(b), + Command::OutlineGlyph(GlyphId(11)), + Command::Paint(b.clone()), ]); } -#[derive(Clone, Copy, PartialEq, Debug)] +#[derive(Clone, Debug, PartialEq)] +struct CustomStop(f32, RgbaColor); + +#[derive(Clone, Debug, PartialEq)] +enum CustomPaint { + Solid(RgbaColor), + LinearGradient(f32, f32, f32, f32, f32, f32, GradientExtend, Vec), + RadialGradient(f32, f32, f32, f32, f32, f32, GradientExtend, Vec), + SweepGradient(f32, f32, f32, f32, GradientExtend, Vec), +} + +#[derive(Clone, Debug, PartialEq)] enum Command { - Outline(u16), - Foreground, - PaintColor(RgbaColor), + OutlineGlyph(GlyphId), + Paint(CustomPaint), + PushLayer(CompositeMode), + PopLayer, + Translate(f32, f32), + Scale(f32, f32), + Rotate(f32), + Skew(f32, f32), + Transform(ttf_parser::Transform), + PopTransform, + PushClip, + PushClipBox(ClipBox), + PopClip, } struct VecPainter(Vec); -impl Painter for VecPainter { - fn outline(&mut self, glyph_id: GlyphId) { - self.0.push(Command::Outline(glyph_id.0)); +impl<'a> Painter<'a> for VecPainter { + fn outline_glyph(&mut self, glyph_id: GlyphId) { + self.0.push(Command::OutlineGlyph(glyph_id)); + } + + fn paint(&mut self, paint: Paint<'a>) { + let custom_paint = match paint { + Paint::Solid(color) => CustomPaint::Solid(color), + Paint::LinearGradient(lg) => CustomPaint::LinearGradient(lg.x0, lg.y0, + lg.x1, lg.y1, + lg.x2, lg.y2, + lg.extend, lg.stops(0, &[]).map(|stop| CustomStop(stop.stop_offset, stop.color)).collect()), + Paint::RadialGradient(rg) => CustomPaint::RadialGradient(rg.x0, rg.y0, + rg.r0, rg.r1, + rg.x1, rg.y1, + rg.extend, rg.stops(0, &[]).map(|stop| CustomStop(stop.stop_offset, stop.color)).collect()), + Paint::SweepGradient(sg) => CustomPaint::SweepGradient(sg.center_x, sg.center_y, + sg.start_angle, sg.end_angle, + sg.extend, sg.stops(0, &[]).map(|stop| CustomStop(stop.stop_offset, stop.color)).collect()), + }; + + self.0.push(Command::Paint(custom_paint)); + } + + fn push_layer(&mut self, mode: colr::CompositeMode) { + self.0.push(Command::PushLayer(mode)); + } + + fn pop_layer(&mut self) { + self.0.push(Command::PopLayer) + } + + fn push_translate(&mut self, tx: f32, ty: f32) { + self.0.push(Command::Translate(tx, ty)) + } + + fn push_scale(&mut self, sx: f32, sy: f32) { + self.0.push(Command::Scale(sx, sy)) + } + + fn push_rotate(&mut self, angle: f32) { + self.0.push(Command::Rotate(angle)) + } + + fn push_skew(&mut self, skew_x: f32, skew_y: f32) { + self.0.push(Command::Skew(skew_x, skew_y)) + } + + fn push_transform(&mut self, transform: ttf_parser::Transform) { + self.0.push(Command::Transform(transform)) + } + + fn pop_transform(&mut self) { + self.0.push(Command::PopTransform) } - fn paint_foreground(&mut self) { - self.0.push(Command::Foreground); + fn push_clip(&mut self) { + self.0.push(Command::PushClip) } - fn paint_color(&mut self, color: RgbaColor) { - self.0.push(Command::PaintColor(color)); + fn push_clip_box(&mut self, clipbox: ClipBox) { + self.0.push(Command::PushClipBox(clipbox)) + } + + fn pop_clip(&mut self) { + self.0.push(Command::PopClip) + } +} + +// A static and variable COLRv1 test font from Google Fonts: +// https://github.com/googlefonts/color-fonts +static COLR1_STATIC: &[u8] = include_bytes!("../fonts/colr_1.ttf"); +static COLR1_VARIABLE: &[u8] = include_bytes!("../fonts/colr_1_variable.ttf"); + +mod colr1_static { + use ttf_parser::{Face, GlyphId, RgbaColor}; + use ttf_parser::colr::ClipBox; + use ttf_parser::colr::CompositeMode::*; + use ttf_parser::colr::GradientExtend::*; + use crate::colr::{COLR1_STATIC, Command, CustomStop, VecPainter}; + use crate::colr::Command::*; + use crate::colr::CustomPaint::*; + + #[test] + fn linear_gradient() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(9), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushClipBox(ClipBox { x_min: 100.0, y_min: 250.0, x_max: 900.0, y_max: 950.0 }), + OutlineGlyph(GlyphId(9)), + PushClip, + Paint(LinearGradient(100.0, 250.0, 900.0, 250.0, 100.0, 300.0, Repeat, vec![ + CustomStop(0.2000122, RgbaColor { red: 255, green: 0, blue: 0, alpha: 255 }), + CustomStop(0.7999878, RgbaColor { red: 0, green: 0, blue: 255, alpha: 255 })])), + PopClip, + PopClip] + ) + } + + #[test] + fn sweep_gradient() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(13), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushClipBox(ClipBox { x_min: 0.0, y_min: 0.0, x_max: 1000.0, y_max: 1000.0 }), + OutlineGlyph(GlyphId(176)), + PushClip, + Paint(SweepGradient(500.0, 600.0, -0.666687, 0.666687, Pad, vec![ + CustomStop(0.25, RgbaColor { red: 250, green: 240, blue: 230, alpha: 255 }), + CustomStop(0.416687, RgbaColor { red: 0, green: 0, blue: 255, alpha: 255 }), + CustomStop(0.583313, RgbaColor { red: 255, green: 0, blue: 0, alpha: 255 }), + CustomStop(0.75, RgbaColor { red: 47, green: 79, blue: 79, alpha: 255 })])), + PopClip, + PopClip] + ) + } + + #[test] + fn scale_around_center() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(84), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushLayer(SourceOver), + OutlineGlyph(GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 0, green: 0, blue: 255, alpha: 127 })), + PopClip, + PushLayer(DestinationOver), + Translate(500.0, 500.0), + Scale(0.5, 1.5), + Translate(-500.0, -500.0), + OutlineGlyph( + GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 255, green: 165, blue: 0, alpha: 178 })), + PopClip, + PopTransform, + PopTransform, + PopTransform, + PopLayer, + PopLayer] + ) + } + + #[test] + fn scale() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(86), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Scale(0.5, 1.5))) + } + + #[test] + fn radial_gradient() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(93), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushClipBox(ClipBox { x_min: 0.0, y_min: 0.0, x_max: 1000.0, y_max: 1000.0 }), + OutlineGlyph(GlyphId(2)), + PushClip, + Paint(RadialGradient(166.0, 768.0, 0.0, 256.0, 166.0, 768.0, Pad, vec![ + CustomStop(0.0, RgbaColor { red: 0, green: 128, blue: 0, alpha: 255 }), + CustomStop(0.5, RgbaColor { red: 255, green: 255, blue: 255, alpha: 255 }), + CustomStop(1.0, RgbaColor { red: 255, green: 0, blue: 0, alpha: 255 })])), + PopClip, + PopClip] + ) + } + + #[test] + fn rotate() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(99), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Rotate(0.055541992))) + } + + #[test] + fn rotate_around_center() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(101), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushLayer(SourceOver), + OutlineGlyph(GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 0, green: 0, blue: 255, alpha: 127 })), + PopClip, + PushLayer(DestinationOver), + Translate(500.0, 500.0), + Rotate(0.13891602), + Translate(-500.0, -500.0), + OutlineGlyph(GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 255, green: 165, blue: 0, alpha: 178 })), + PopClip, + PopTransform, + PopTransform, + PopTransform, + PopLayer, + PopLayer, + ] + ) + } + + #[test] + fn skew() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(103), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Skew(0.13891602, 0.0))); + } + + #[test] + fn skew_around_center() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(104), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushLayer(SourceOver), + OutlineGlyph(GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 0, green: 0, blue: 255, alpha: 127 })), + PopClip, + PushLayer(DestinationOver), + Translate(500.0, 500.0), + Skew(0.13891602, 0.0), + Translate(-500.0, -500.0), + OutlineGlyph(GlyphId(3)), + PushClip, + Paint(Solid(RgbaColor { red: 255, green: 165, blue: 0, alpha: 178 })), + PopClip, + PopTransform, + PopTransform, + PopTransform, + PopLayer, + PopLayer]) + } + + #[test] + fn transform() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(109), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + + assert!(vec_painter.0.contains(&Transform(ttf_parser::Transform { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + e: 125.0, + f: 125.0 + } + ))); + } + + #[test] + fn translate() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(114), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + + assert!(vec_painter.0.contains(&Translate(0.0, 100.0))); + } + + #[test] + fn composite() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(131), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + + assert!(vec_painter.0.contains(&Command::PushLayer(Xor))); + } + + #[test] + fn cyclic_dependency() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(179), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + } +} + + +mod colr1_variable { + use ttf_parser::{Face, GlyphId, RgbaColor, Tag}; + use ttf_parser::colr::ClipBox; + use ttf_parser::colr::GradientExtend::*; + use crate::colr::{COLR1_STATIC, COLR1_VARIABLE, CustomStop, VecPainter}; + use crate::colr::Command::*; + use crate::colr::CustomPaint::*; + + #[test] + fn sweep_gradient() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"SWPS"), 45.0); + face.set_variation(Tag::from_bytes(b"SWPE"), 58.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(13), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Paint(SweepGradient(500.0, 600.0, -0.416687, 0.9888916, Pad, vec![ + CustomStop(0.25, RgbaColor { red: 250, green: 240, blue: 230, alpha: 255 }), + CustomStop(0.416687, RgbaColor { red: 0, green: 0, blue: 255, alpha: 255 }), + CustomStop(0.583313, RgbaColor { red: 255, green: 0, blue: 0, alpha: 255 }), + CustomStop(0.75, RgbaColor { red: 47, green: 79, blue: 79, alpha: 255 })])) + )); + } + + #[test] + fn scale_around_center() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"SCSX"), 1.1); + face.set_variation(Tag::from_bytes(b"SCSY"), -0.9); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(84), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Scale(1.599942, 0.60009766))) + } + + #[test] + fn scale() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"SCSX"), 1.1); + face.set_variation(Tag::from_bytes(b"SCSY"), -0.9); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(86), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Scale(1.599942, 0.60009766))) + } + + #[test] + fn radial_gradient() { + let face = Face::parse(COLR1_STATIC, 0).unwrap(); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(93), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert_eq!(vec_painter.0, vec![ + PushClipBox(ClipBox { x_min: 0.0, y_min: 0.0, x_max: 1000.0, y_max: 1000.0 }), + OutlineGlyph(GlyphId(2)), + PushClip, + Paint(RadialGradient(166.0, 768.0, 0.0, 256.0, 166.0, 768.0, Pad, vec![ + CustomStop(0.0, RgbaColor { red: 0, green: 128, blue: 0, alpha: 255 }), + CustomStop(0.5, RgbaColor { red: 255, green: 255, blue: 255, alpha: 255 }), + CustomStop(1.0, RgbaColor { red: 255, green: 0, blue: 0, alpha: 255 })])), + PopClip, + PopClip] + ) + } + + #[test] + fn rotate() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"ROTA"), 150.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(99), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Rotate(0.87341005))) + } + + #[test] + fn rotate_around_center() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"ROTA"), 150.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(101), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Rotate(0.9336252))) + } + + #[test] + fn skew() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"SKXA"), 46.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(103), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Skew(0.3944702, 0.0))); + } + + #[test] + fn skew_around_center() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"SKXA"), 46.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(104), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + assert!(vec_painter.0.contains(&Skew(0.3944702, 0.0))); + } + + #[test] + fn transform() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"TRDX"), 150.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(109), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + + assert!(vec_painter.0.contains(&Transform(ttf_parser::Transform { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + e: 274.9939, + f: 125.0 + } + ))); + } + + #[test] + fn translate() { + let mut face = Face::parse(COLR1_VARIABLE, 0).unwrap(); + face.set_variation(Tag::from_bytes(b"TLDX"), 100.0); + let mut vec_painter = VecPainter(vec![]); + face.paint_color_glyph(GlyphId(114), 0, RgbaColor::new(0, 0, 0, 255), &mut vec_painter); + + assert!(vec_painter.0.contains(&Translate(99.975586, 100.0))); } }