Type: colour emoji fonts with bitmap glyphs draw and export

Squashed from #3561 onto main.
This commit is contained in:
bflatastic
2026-10-23 00:44:31 +07:00
parent b50fbc08cf
commit 26b9cf84ad
25 changed files with 940 additions and 61 deletions
+41
View File
@@ -1811,6 +1811,12 @@ impl Renderer {
ctx.set_stroke(kurbo::Stroke::new(1.0));
ctx.set_paint(peniko::Color::BLACK);
ctx.stroke_path(&p);
// Colour bitmap glyphs: their boxes, as images show in outline mode.
for (_, image, to_text) in &g.images {
let mut b = image.rect().to_path(0.1);
b.apply_affine(t.xf * *to_text);
self.hairline(ctx, f, &b, [0, 0, 0, 255]);
}
self.draw_inlines(ctx, f, t, g);
return;
}
@@ -1852,6 +1858,11 @@ impl Renderer {
}
}
for (i, run) in t.runs.iter().enumerate() {
// Colour bitmap glyphs paint themselves: the run's fill and stroke don't.
for (_, image, to_text) in g.images.iter().filter(|(r, ..)| *r == i) {
overprint(ctx, false);
self.draw_glyph_image(ctx, f, image, t.xf * *to_text);
}
let path = g.run(i);
let path = path.as_ref();
if path.elements().is_empty() {
@@ -1908,6 +1919,26 @@ impl Renderer {
}
}
/// 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) {
let Some((key, pm)) = blob_pixmap(&image.blob, true) else { return };
let (key, pm) = ink::ink_image(&key, &pm, f.ink, Some(&image.blob), true);
let unit = f.view * xf * Affine::scale_non_uniform(image.width as f64, image.height as f64);
let pm = image_mip(&key, pm, unit);
let sx = image.width as f64 / pm.width().max(1) as f64;
let sy = image.height as f64 / pm.height().max(1) as f64;
let quality = if f.opts.smooth_images { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low };
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm),
sampler: peniko::ImageSampler { quality, ..Default::default() },
});
ctx.set_transform(f.view * xf);
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
ctx.fill_rect(&image.rect());
ctx.reset_paint_transform();
}
fn draw_image(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject) {
self.draw_image_in(ctx, f, im, None);
}
@@ -2336,6 +2367,8 @@ struct TextGeom {
alternate_glyphs: std::sync::OnceLock<BezPath>,
/// Inline graphics placed by the layout.
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)>,
}
impl TextGeom {
@@ -2384,6 +2417,7 @@ impl TextGeom {
+ self.alternate_glyphs.get().map_or(0, lru::path_bytes)
+ 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)>()
}
}
@@ -2464,8 +2498,12 @@ 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![];
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));
}
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);
@@ -2504,6 +2542,7 @@ fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeom {
alternate_candidates,
alternate_glyphs: std::sync::OnceLock::new(),
inlines: layout.inlines,
images,
}
}
@@ -2565,6 +2604,8 @@ mod tests_adjust;
#[cfg(test)]
mod tests_alternate;
#[cfg(test)]
mod tests_bitmapglyphs;
#[cfg(test)]
mod tests_blend;
#[cfg(test)]
mod tests_changed;
+47
View File
@@ -0,0 +1,47 @@
//! Colour bitmap glyphs (emoji): drawn as their images, which the type's fill doesn't paint.
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, COLOUR_LARGE, colour_bitmap_family, colour_bitmap_font};
use super::*;
/// 100 pt [`COLOUR_CHAR`] in the colour bitmap test font (`sbix` or `CBDT`), filled blue, its
/// baseline at (20, 120), on a white 160×160 page.
fn render(sbix: bool, opts: RenderOptions) -> Rendered {
let _adding = crate::tests_fontchange::adding_fonts();
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(sbix).unwrap());
let mut d = Document::new(160.0, 160.0);
let st = CharStyle {
font_family: colour_bitmap_family(sbix).into(),
size: 100.0,
fill: Paint::solid(Color::rgb(0.0, 0.0, 1.0)),
..Default::default()
};
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &COLOUR_CHAR.to_string(), st))));
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 })
}
#[test]
fn a_bitmap_glyph_draws_its_largest_strike_not_the_fill() {
for sbix in [true, false] {
let img = render(sbix, RenderOptions::default());
// Inside the em square the image covers (the CBDT one from 75 above the baseline to 25
// below, the sbix one from 87.2 above to 12.8 below), away from its edges.
assert_eq!(img.pixel(70, 80), COLOUR_LARGE, "sbix {sbix}");
let blue = (0..160).flat_map(|y| (0..160).map(move |x| (x, y))).any(|(x, y)| img.pixel(x, y)[2] > 200 && img.pixel(x, y)[0] < 50);
assert!(!blue, "the fill doesn't paint the glyph (sbix {sbix})");
}
}
#[test]
fn outline_mode_draws_a_bitmap_glyph_as_its_box() {
let img = render(false, RenderOptions { outline: true, ..Default::default() });
assert_eq!(img.pixel(70, 80), [255, 255, 255, 255], "no pixels inside");
// The box's left edge at x 32.5 (the bearing of 2 px of 6.25 pt), from y 45 to 145.
assert!((31..=34).any(|x| img.pixel(x, 100)[0] < 128), "the box's edge shows");
}
+1 -1
View File
@@ -20,7 +20,7 @@ pub(crate) fn fonts_steady() -> RwLockReadGuard<'static, ()> {
}
/// No test holding [`fonts_steady`] runs until the guard is dropped.
fn adding_fonts() -> RwLockWriteGuard<'static, ()> {
pub(crate) fn adding_fonts() -> RwLockWriteGuard<'static, ()> {
FONTS.write().unwrap_or_else(PoisonError::into_inner)
}