mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Type: COLR colour fonts draw and export as vector art
Glyphs whose paints documents have no form for (sweep gradients, compositing modes) are drawn as images of the glyph.
This commit is contained in:
@@ -1817,6 +1817,10 @@ impl Renderer {
|
||||
b.apply_affine(t.xf * *to_text);
|
||||
self.hairline(ctx, f, &b, [0, 0, 0, 255]);
|
||||
}
|
||||
// COLR glyphs: their layers' outlines.
|
||||
for (_, art) in &g.colours {
|
||||
self.draw_colour_glyph(ctx, f, art, t.xf);
|
||||
}
|
||||
self.draw_inlines(ctx, f, t, g);
|
||||
return;
|
||||
}
|
||||
@@ -1863,6 +1867,9 @@ impl Renderer {
|
||||
overprint(ctx, false);
|
||||
self.draw_glyph_image(ctx, f, image, t.xf * *to_text);
|
||||
}
|
||||
for (_, art) in g.colours.iter().filter(|(r, _)| *r == i) {
|
||||
self.draw_colour_glyph(ctx, f, art, t.xf);
|
||||
}
|
||||
let path = g.run(i);
|
||||
let path = path.as_ref();
|
||||
if path.elements().is_empty() {
|
||||
@@ -1919,6 +1926,15 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw COLR glyph `art` (its layers in text space) through `xf` (text space → document), as
|
||||
/// art: kept between frames as symbol art is ([`Self::placed_art`]).
|
||||
fn draw_colour_glyph(&mut self, ctx: &mut RenderContext, f: &Frame, art: &Arc<Node>, xf: Affine) {
|
||||
let (node, kept) = self.placed_art(art, xf, true, None);
|
||||
let outer = std::mem::replace(&mut self.made_again, !kept);
|
||||
self.draw_node(ctx, f, &node, true);
|
||||
self.made_again = outer;
|
||||
}
|
||||
|
||||
/// Draw colour bitmap glyph `image` through `xf` (its pixels → document space), its pixels
|
||||
/// decoded once and kept with the images' pixels.
|
||||
fn draw_glyph_image(&mut self, ctx: &mut RenderContext, f: &Frame, image: &vectorcraft_text::GlyphImage, xf: Affine) {
|
||||
@@ -2372,6 +2388,8 @@ struct TextGeom {
|
||||
inlines: Vec<vectorcraft_text::InlineGlyph>,
|
||||
/// Colour bitmap glyphs: (run, image, its pixels → text space), in text order.
|
||||
images: Vec<(usize, Arc<vectorcraft_text::GlyphImage>, Affine)>,
|
||||
/// COLR colour glyphs: (run, their layers placed in text space), in text order.
|
||||
colours: Vec<(usize, Arc<Node>)>,
|
||||
}
|
||||
|
||||
impl TextGeom {
|
||||
@@ -2421,6 +2439,7 @@ impl TextGeom {
|
||||
+ self.spans.len() * size_of::<(usize, usize, usize)>()
|
||||
+ self.inlines.len() * size_of::<vectorcraft_text::InlineGlyph>()
|
||||
+ self.images.len() * size_of::<(usize, Arc<vectorcraft_text::GlyphImage>, Affine)>()
|
||||
+ self.colours.iter().map(|(_, art)| lru::node_bytes(art)).sum::<usize>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2501,12 +2520,15 @@ fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeom {
|
||||
.collect();
|
||||
let (mut substituted_fonts, mut substituted_glyphs, mut alternate_candidates) = (BezPath::new(), BezPath::new(), vec![]);
|
||||
let plain = t.plain_text();
|
||||
let mut images = vec![];
|
||||
let (mut images, mut colours) = (vec![], vec![]);
|
||||
for g in &layout.glyphs {
|
||||
push(&mut all, g.run, &g.outline);
|
||||
if let Some((image, xf)) = g.placed_image() {
|
||||
images.push((g.run, image.clone(), xf));
|
||||
}
|
||||
if let Some(art) = g.placed_colour(Affine::IDENTITY) {
|
||||
colours.push((g.run, Arc::new(art)));
|
||||
}
|
||||
let Some(&(missing, face)) = faces.get(g.run) else { continue };
|
||||
let Some(line) = layout.lines.get(g.line) else { continue };
|
||||
let mut cell = Rect::new(g.origin.x, g.origin.y - line.ascent, g.origin.x + g.advance, g.origin.y + line.descent).to_path(0.1);
|
||||
@@ -2546,6 +2568,7 @@ fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeom {
|
||||
alternate_glyphs: std::sync::OnceLock::new(),
|
||||
inlines: layout.inlines,
|
||||
images,
|
||||
colours,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2619,6 +2642,8 @@ mod tests_clip;
|
||||
#[cfg(test)]
|
||||
mod tests_cmykblend;
|
||||
#[cfg(test)]
|
||||
mod tests_colrglyphs;
|
||||
#[cfg(test)]
|
||||
mod tests_container;
|
||||
#[cfg(test)]
|
||||
mod tests_dither;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! COLR colour glyphs: drawn as their layers (or, for paints documents have no form for, their
|
||||
//! image), which the type's fill doesn't paint, except in the layers in the foreground colour.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::text::{CharStyle, TextObject};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, COLR_SWEEP_CHAR, colr_family, colr_font};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 100 pt `text` in the COLR test font (v1 or v0), filled magenta, its baseline at (20, 120)
|
||||
/// turned by `turn` about there, on a white 160×160 page.
|
||||
fn render(v1: bool, text: char, turn: f64, opts: RenderOptions) -> Rendered {
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
vectorcraft_text::FontDb::global().add_font(colr_font(v1).unwrap());
|
||||
let mut d = Document::new(160.0, 160.0);
|
||||
let st = CharStyle { font_family: colr_family(v1).into(), size: 100.0, fill: Paint::solid(Color::rgb(1.0, 0.0, 1.0)), ..Default::default() };
|
||||
let mut t = TextObject::point(Point::new(20.0, 120.0), &text.to_string(), st);
|
||||
t.xf = Affine::rotate_about(turn, (20.0, 120.0)) * t.xf;
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
Renderer::new().render(&d, 160, 160, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..opts })
|
||||
}
|
||||
|
||||
/// The box of the pixels `like` says yes to.
|
||||
fn ink(img: &Rendered, like: impl Fn([u8; 4]) -> bool) -> Option<(u32, u32, u32, u32)> {
|
||||
let mut b: Option<(u32, u32, u32, u32)> = None;
|
||||
for y in 0..img.height {
|
||||
for x in 0..img.width {
|
||||
if like(img.pixel(x, y)) {
|
||||
b = Some(b.map_or((x, y, x, y), |(x0, y0, x1, y1)| (x0.min(x), y0.min(y), x1.max(x), y1.max(y))));
|
||||
}
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
fn magenta(p: [u8; 4]) -> bool {
|
||||
p[0] > 200 && p[2] > 200 && p[1] < 60
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colr_layers_draw_their_colours_not_the_fill() {
|
||||
let img = render(true, COLOUR_CHAR, 0.0, RenderOptions::default());
|
||||
// The `O` with its linear gradient red → blue from x 0 to 500 units (20 to 70 pt).
|
||||
let reddish = ink(&img, |p| p[0] > 180 && p[2] < 90 && p[1] < 60).expect("red at the gradient's start");
|
||||
let bluish = ink(&img, |p| p[2] > 180 && p[0] < 90 && p[1] < 60).expect("blue at its end");
|
||||
assert!(reddish.0 < bluish.0, "red left of blue: {reddish:?} {bluish:?}");
|
||||
assert!(ink(&img, magenta).is_none(), "the fill doesn't paint the glyph");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_foreground_layer_takes_the_fill() {
|
||||
let img = render(false, COLOUR_CHAR, 0.0, RenderOptions::default());
|
||||
assert!(ink(&img, magenta).is_some(), "the `I` layer is in the foreground colour");
|
||||
assert!(ink(&img, |p| p == [255, 0, 0, 255]).is_some(), "the `O` layer in red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turned_type_turns_its_colour_layers() {
|
||||
let size = |turn| {
|
||||
let (x0, y0, x1, y1) = ink(&render(false, COLOUR_CHAR, turn, RenderOptions::default()), magenta).unwrap();
|
||||
(x1 - x0, y1 - y0)
|
||||
};
|
||||
let (w, h) = size(0.0);
|
||||
assert!(h > w * 3, "the `I` stands: {w}×{h}");
|
||||
let (w, h) = size(std::f64::consts::FRAC_PI_2);
|
||||
assert!(w > h * 3, "turned, it lies: {w}×{h}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_mode_shows_the_layers_outlines() {
|
||||
let filled = render(false, COLOUR_CHAR, 0.0, RenderOptions::default());
|
||||
let outlined = render(false, COLOUR_CHAR, 0.0, RenderOptions { outline: true, ..Default::default() });
|
||||
let (x0, y0, x1, y1) = ink(&filled, |p| p == [255, 0, 0, 255]).unwrap();
|
||||
let dark = |p: [u8; 4]| p[0] < 128 && p[1] < 128 && p[2] < 128;
|
||||
assert!(ink(&outlined, |p| p == [255, 0, 0, 255]).is_none(), "no colours in outline mode");
|
||||
let (ox0, oy0, ox1, oy1) = ink(&outlined, dark).expect("the outlines show");
|
||||
assert!(ox0 <= x0 + 2 && oy0 <= y0 + 2 && ox1 + 2 >= x1 && oy1 + 2 >= y1, "around the layers: {:?} {:?}", (ox0, oy0, ox1, oy1), (x0, y0, x1, y1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sweep_glyph_draws_its_image() {
|
||||
let img = render(true, COLR_SWEEP_CHAR, 0.0, RenderOptions::default());
|
||||
assert!(ink(&img, |p| p[0] > 180 && p[1] < 60 && p[2] < 120).is_some(), "red where the sweep starts");
|
||||
assert!(ink(&img, magenta).is_none());
|
||||
}
|
||||
Reference in New Issue
Block a user