mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 01:28:22 +00:00
With image mips on (the canvas, navigator and layer thumbnails), an image was colorized, separated into the frame's ink plane and shown through Separations Preview's plates at its full size, and only then shrunk to the mip drawn. The 141-megapixel image of #4155 made 1.6 GB of planes a frame, more than the image store holds, so every frame made them again. The mip is now picked first and everything else is made from it, under the keys `kept_mip` and `nearest_kept` already look up. A CMYK image's own inks are halved with its pixels (`ink::key_inks`, `images::half_inks`), so it still paints them rather than inks separated from its displayed colours. Colour bitmap glyphs follow the same order. On that file the app's first frame went from 9.2 s to 1.6 s and the sharp frame after a pan from 3.9 s to 8 ms (M3 Pro, one run each). Fine detail now shows its true average: 1-pixel black and white lines show mid grey, where separating first showed them 44 levels darker.
328 lines
17 KiB
Rust
328 lines
17 KiB
Rust
use super::{
|
|
Affine, AppearanceItem, Arc, BezPath, BlendMode, FillRule, Frame, MAX_INLINE_DEPTH, MISSING_INLINE_LOGGED, Node, NodeKind, PlacedEntry,
|
|
PlacedKey, Rect, RenderContext, Renderer, SUBSTITUTED, Shape, TextGeom, TextObject, blend_mode, blob_pixmap, depends_on_document, fx, holds_type,
|
|
image_mip, ink, kurbo, lru, peniko, place, proof, stain_bits, text_geom, text_geom_snapped, vello_cpu,
|
|
};
|
|
use crate::paint;
|
|
|
|
impl Renderer {
|
|
pub(super) fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
|
|
let t = &*f.doc.inline_resolved(t);
|
|
// Type drawn as `fx_owner` (the node `draw_arc` is drawing, or type art that `fx_arts`
|
|
// keeps, see `draw_fx_art`) takes its glyph geometry from `texts`, as on the fast path,
|
|
// except in frames that snap type to pixels.
|
|
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned();
|
|
let (g, kept) = match (f.text_snap(t), owner) {
|
|
(None, Some(a)) => self.text_geom_of(&a, t),
|
|
(snap, _) => (Arc::new(text_geom_snapped(t, snap)), false),
|
|
};
|
|
self.draw_text_geom(ctx, f, n, t, &g, kept);
|
|
}
|
|
|
|
/// Draw the inline graphics of type `t` (laid out in `g`): each symbol's art through the
|
|
/// normal node path, placed by the layout (inside the text's own transparency group, so its
|
|
/// opacity and blend mode apply). A missing symbol draws nothing.
|
|
fn draw_inlines(&mut self, ctx: &mut RenderContext, f: &Frame, t: &TextObject, g: &TextGeom) {
|
|
if g.inlines.is_empty() && !t.runs.iter().any(|r| r.inline.is_some()) {
|
|
return;
|
|
}
|
|
if self.inline_depth >= MAX_INLINE_DEPTH {
|
|
return;
|
|
}
|
|
for r in t.runs.iter().filter_map(|r| r.inline.as_ref()) {
|
|
if !f.doc.symbols.iter().any(|s| s.name == r.symbol) && !MISSING_INLINE_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
|
log::warn!("inline graphic shows symbol {:?}, which the document doesn't have: it draws nothing", r.symbol);
|
|
}
|
|
}
|
|
self.inline_depth += 1;
|
|
for ig in &g.inlines {
|
|
let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue };
|
|
let Some(sym) = f.doc.symbols.iter().find(|s| s.name == art.symbol) else { continue };
|
|
// Strokes scale with the art (it is sized to the type, as in the SVG `<use>`).
|
|
let (node, kept) = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None);
|
|
self.draw_placed_art(ctx, f, &node, kept);
|
|
}
|
|
self.inline_depth -= 1;
|
|
}
|
|
|
|
/// Symbol art `art` as an instance or an inline graphic draws it ([`place`]), and whether it is
|
|
/// kept. Art that is a group, a layer or a live object without an appearance of its own is kept
|
|
/// between frames under the art's `Arc`, `xf`, `scale_strokes` and the stain of instance
|
|
/// `inst`: it and its members keep their `Arc`s from frame to frame, and the caches keyed by a
|
|
/// node's address (geometry, stroke outlines, glyphs, live-effect geometry, evaluated live
|
|
/// objects) hit for them. Other art is placed again in every frame, and so is art that holds
|
|
/// symbol instances, type with inline graphics, images, placed documents or raster effects
|
|
/// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is
|
|
/// placed again when the fonts change.
|
|
pub(super) fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc<Node>, bool) {
|
|
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
|
|
// An instance with appearance changes of its own isn't kept: the key doesn't tell them apart.
|
|
let overridden = inst.is_some_and(|i| !i.extras().symbol_overrides.is_empty());
|
|
if !kept_kind || overridden || fx::has_object_fx(art) {
|
|
return (place(art, xf, scale_strokes, inst), false);
|
|
}
|
|
let stain = stain_bits(inst.and_then(vectorcraft_brush::stain));
|
|
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain);
|
|
if let Some(e) = self.placed_arts.get(&key, self.stamp)
|
|
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
|
|
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
|
|
&& e.slice.is_none()
|
|
{
|
|
return match &e.placed {
|
|
Some(placed) => (placed.clone(), true),
|
|
None => (place(art, xf, scale_strokes, inst), false),
|
|
};
|
|
}
|
|
let placed = place(art, xf, scale_strokes, inst);
|
|
let inserted = self.keep_placed(key, art, &placed, None);
|
|
(placed, inserted)
|
|
}
|
|
|
|
/// Symbol art `art` placed for instance `inst` (placed by `xf`) through 9-slice map `map`
|
|
/// ([`vectorcraft_doc::NineSliceMap::place`], stained by the instance), and whether it is kept,
|
|
/// when the frame's symbol budget (`used`, see [`vectorcraft_doc::charge_symbol_art`]) has room
|
|
/// for it. The art is kept between frames as [`Self::placed_art`] keeps it, under the same key
|
|
/// and only with the same map. Kept art is charged by its own object count, the art of its
|
|
/// opacity masks included ([`Node::count_with_masks`]), and is not placed
|
|
/// again; other art is charged by [`vectorcraft_doc::NineSliceMap::most_placed`] before it is
|
|
/// placed.
|
|
pub(super) fn sliced_art(
|
|
&mut self,
|
|
doc: &vectorcraft_doc::Document,
|
|
art: &Arc<Node>,
|
|
map: &vectorcraft_doc::NineSliceMap,
|
|
xf: Affine,
|
|
inst: &Node,
|
|
used: &mut u64,
|
|
) -> Option<(Arc<Node>, bool)> {
|
|
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
|
|
let stain = stain_bits(vectorcraft_brush::stain(inst));
|
|
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), false, stain);
|
|
// As in `placed_art`, an instance with appearance changes of its own isn't kept.
|
|
let overridden = !inst.extras().symbol_overrides.is_empty();
|
|
let mut keeps = kept_kind && !overridden && !fx::has_object_fx(art);
|
|
if keeps
|
|
&& let Some(e) = self.placed_arts.get(&key, self.stamp)
|
|
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
|
|
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
|
|
&& e.slice.as_ref() == Some(map)
|
|
{
|
|
match e.placed.clone() {
|
|
Some(placed) => {
|
|
let count = placed.count_with_masks() as u64;
|
|
return vectorcraft_doc::charge_symbol_art(used, count, || ((placed, true), count));
|
|
}
|
|
None => keeps = false,
|
|
}
|
|
}
|
|
let stained = crate::brush_fx::instance_art(art, inst);
|
|
let most = map.most_placed(&stained, doc);
|
|
let placed = vectorcraft_doc::charge_symbol_art(used, most, || {
|
|
let placed = Arc::new(map.place(&stained, doc));
|
|
let count = placed.count_with_masks() as u64;
|
|
(placed, count)
|
|
})?;
|
|
let kept = keeps && self.keep_placed(key, art, &placed, Some(*map));
|
|
Some((placed, kept))
|
|
}
|
|
|
|
/// Keep `placed`, symbol art `art` as placed under `key` (through 9-slice map `slice`, when it
|
|
/// has one), unless `art` depends on the document ([`depends_on_document`]): then the entry
|
|
/// records that it is placed again in every frame. Whether `placed` is kept.
|
|
fn keep_placed(&mut self, key: PlacedKey, art: &Arc<Node>, placed: &Arc<Node>, slice: Option<vectorcraft_doc::NineSliceMap>) -> bool {
|
|
let kept = (!depends_on_document(art)).then(|| placed.clone());
|
|
let keeps = kept.is_some();
|
|
let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
|
|
// The bytes count the key and the entry, the art's allocation that the weak reference
|
|
// keeps, and the kept art.
|
|
let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes);
|
|
let inserted = self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts, slice }, bytes, self.stamp);
|
|
keeps && inserted
|
|
}
|
|
|
|
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
|
|
/// Text `t` that is not the node's own, such as the copy whose inline graphics
|
|
/// `Document::inline_resolved` sized again, is laid out without the cache. Also returns
|
|
/// whether the geometry is kept in the cache.
|
|
pub(super) fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> (Arc<TextGeom>, bool) {
|
|
if !matches!(&a.kind, NodeKind::Text(own) if std::ptr::eq(&**own, t)) {
|
|
return (Arc::new(text_geom(t)), false);
|
|
}
|
|
let fonts = vectorcraft_text::FontDb::global().generation();
|
|
if fonts != self.text_fonts {
|
|
self.texts.clear();
|
|
self.char_strokes.clear();
|
|
self.text_fonts = fonts;
|
|
}
|
|
let key = Arc::as_ptr(a) as usize;
|
|
if let Some((node, g)) = self.texts.get(&key, self.stamp)
|
|
&& Arc::ptr_eq(node, a)
|
|
{
|
|
return (g.clone(), true);
|
|
}
|
|
let g = Arc::new(text_geom(t));
|
|
let kept = self.texts.insert(key, (a.clone(), g.clone()), g.bytes(), self.stamp);
|
|
(g, kept)
|
|
}
|
|
|
|
/// Draw type `n` (`t`, laid out in `g`). The outlines of its character strokes are cached
|
|
/// when `g` is kept in `texts` (`kept`).
|
|
pub(super) fn draw_text_geom(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &Arc<TextGeom>, kept: bool) {
|
|
let xf = f.view * t.xf;
|
|
if f.opts.outline {
|
|
let mut p = g.all.clone();
|
|
p.apply_affine(xf);
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
ctx.set_stroke(kurbo::Stroke::new(self.hairline));
|
|
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]);
|
|
}
|
|
// 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;
|
|
}
|
|
let tb = t.xf.transform_rect_bbox(g.bounds);
|
|
// Overprinting characters darken per draw, like overprinting fills and strokes
|
|
// ([`proof`]); the object blend mode is Normal here or applied by an enclosing layer.
|
|
let overprints = proof::overprints(f.opts);
|
|
let overprint = |ctx: &mut RenderContext, on: bool| {
|
|
if overprints {
|
|
ctx.set_blend_mode(if on { blend_mode(vectorcraft_color::BlendMode::Darken) } else { BlendMode::default() });
|
|
}
|
|
};
|
|
// The object's own fills and strokes paint the whole outline: those below the Characters
|
|
// row under the characters' own fill and stroke, the others over them.
|
|
let (below, above) = n.appearance.split_contents();
|
|
let all = (!n.appearance.items.is_empty()).then(|| {
|
|
let mut all = g.all.clone();
|
|
all.apply_affine(t.xf);
|
|
all
|
|
});
|
|
if let Some(all) = &all {
|
|
self.draw_text_items(ctx, f, n, below, all, tb);
|
|
}
|
|
if f.opts.highlight_substitutions {
|
|
for (on, path) in [
|
|
(f.opts.highlight_substituted_fonts, Some(&g.substituted_fonts)),
|
|
(f.doc.setup.highlight_substituted_glyphs, Some(&g.substituted_glyphs)),
|
|
// Measured only when shown: it lays the text out again.
|
|
(f.opts.highlight_alternate_glyphs, f.opts.highlight_alternate_glyphs.then(|| g.alternate_glyphs(t))),
|
|
] {
|
|
let Some(path) = path else { continue };
|
|
if on && !path.elements().is_empty() {
|
|
ctx.set_transform(xf);
|
|
ctx.set_fill_rule(peniko::Fill::NonZero);
|
|
ctx.set_paint(SUBSTITUTED);
|
|
ctx.fill_path(path);
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
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() {
|
|
continue;
|
|
}
|
|
ctx.set_transform(xf);
|
|
ctx.set_fill_rule(peniko::Fill::NonZero);
|
|
if paint::set_fill_paint(ctx, &run.style.fill, g.bounds, f) {
|
|
self.fold_alpha(ctx, f.ink, &run.style.fill);
|
|
overprint(ctx, run.style.overprint_fill);
|
|
ctx.fill_path(path);
|
|
}
|
|
if run.style.has_stroke() {
|
|
// Character strokes are drawn in text space, with their cap, join and dashes.
|
|
overprint(ctx, run.style.overprint_stroke);
|
|
let chars = kept.then_some((g, i));
|
|
self.draw_stroke(ctx, f, t.xf, path, FillRule::NonZero, &run.style.stroke_layer(), chars, g.bounds);
|
|
}
|
|
}
|
|
overprint(ctx, false);
|
|
self.draw_inlines(ctx, f, t, g);
|
|
if let Some(all) = &all {
|
|
self.draw_text_items(ctx, f, n, above, all, tb);
|
|
}
|
|
}
|
|
|
|
/// Paint some of type `n`'s own fills and strokes (`items`) on its glyph outlines `all`
|
|
/// (document space; `tb` their bounds). Fills composite with their own opacity and blend mode;
|
|
/// strokes take every stroke option, as on paths.
|
|
fn draw_text_items(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, items: &[AppearanceItem], all: &BezPath, tb: Rect) {
|
|
for item in items.iter().filter(|i| i.visible() && !i.paint().is_none()) {
|
|
match item {
|
|
AppearanceItem::Fill(fl) => {
|
|
let layered = fl.opacity < 1.0 || fl.blend != vectorcraft_color::BlendMode::Normal;
|
|
if layered {
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
ctx.push_layer(None, Some(blend_mode(fl.blend)), Some(fl.opacity), None, None);
|
|
}
|
|
ctx.set_transform(f.view);
|
|
if paint::set_fill_paint(ctx, &fl.paint, tb, f) {
|
|
ctx.set_fill_rule(peniko::Fill::NonZero);
|
|
ctx.fill_path(all);
|
|
}
|
|
if layered {
|
|
ctx.pop_layer();
|
|
}
|
|
}
|
|
AppearanceItem::Stroke(st) if st.width > 0.0 => {
|
|
if st.brush.is_none() || !self.draw_brush(ctx, f, n, all, st) {
|
|
self.draw_stroke(ctx, f, Affine::IDENTITY, all, FillRule::NonZero, st, None, tb);
|
|
}
|
|
}
|
|
AppearanceItem::Stroke(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
let Some((key, pm)) = blob_pixmap(&image.blob, true) else { return };
|
|
// Shrunk before it is inked, as images are (`draw_image_in`).
|
|
let unit = f.view * xf * Affine::scale_non_uniform(image.width as f64, image.height as f64);
|
|
let (key, pm) = image_mip(key, pm, unit);
|
|
let (key, pm) = ink::ink_image(&key, &pm, f.ink, Some(&image.blob), true);
|
|
let pm = match f.plates {
|
|
Some(lut) => proof::plate_image(lut, &key, &pm, Some(&image.blob), true).1,
|
|
None => pm,
|
|
};
|
|
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();
|
|
}
|
|
}
|