mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user