Merge pull request #5415 from storytold/fix/sec-render-fx-symbol-budget

Render: symbol instances with effects use the symbol budget; CMYK decodes are admitted; checked Targa size
This commit is contained in:
BFlat
2026-11-03 23:54:25 +07:00
committed by GitHub
5 changed files with 57 additions and 3 deletions
+11 -2
View File
@@ -38,8 +38,17 @@ pub(crate) fn blob_pixmap(blob: &vectorcraft_doc::ImageBlob, touch: bool) -> Opt
paint::add_decode_failures(1);
return None;
}
// Decoded outside the lock: other renderers carry on meanwhile.
let pm = match lut.and_then(|lut| ink::cmyk_pixmap(blob, &lut)) {
// Decoded outside the lock: other renderers carry on meanwhile. The inks and the pixels
// shown from them are admitted together, as other images are, before either is made:
// without room the image is read as other images are (which reduces it).
let room = || {
image::ImageReader::new(std::io::Cursor::new(blob.bytes.as_slice()))
.with_guessed_format()
.ok()
.and_then(|r| r.into_dimensions().ok())
.is_some_and(|(w, h)| paint::within_allowance(w, h.saturating_mul(2)))
};
let pm = match lut.filter(|_| room()).and_then(|lut| ink::cmyk_pixmap(blob, &lut)) {
Some(pm) => pm,
None => match paint::decode_pixmap_or_why(&blob.bytes) {
Ok(pm) => pm,
+2 -1
View File
@@ -34,7 +34,8 @@ pub fn encode(rgba: &[u8], width: u32, height: u32, o: &TgaOptions) -> Result<Ve
return Err(format!("{width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution"));
};
let (w, h) = (usize::from(w16), usize::from(h16));
if w == 0 || h == 0 || (w * h).checked_mul(4) != Some(rgba.len()) {
// Checked: on 32-bit targets 65535 × 65535 wraps.
if w == 0 || h == 0 || w.checked_mul(h).and_then(|n| n.checked_mul(4)) != Some(rgba.len()) {
return Err("Targa encoding failed: the pixel buffer doesn't match the image size".into());
}
let px = rgba.as_chunks::<4>().0;
+16
View File
@@ -378,6 +378,9 @@ impl Renderer {
/// What [`Self::draw_object_fx`] draws inside the object's transparency group.
fn draw_object_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, cache: bool) {
if !self.charge_fx_instance(f.doc, a) {
return;
}
let art = self.fx_art(f.doc, a, cache);
// Art the cache did not keep is made again in the next frame.
let kept = cache && self.fx_arts.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &art));
@@ -410,6 +413,19 @@ impl Renderer {
self.made_again = outer;
}
/// Whether the frame has room for the art of `a` when it is a symbol instance, charged to the
/// frame's symbol budget as an instance without effects is (`draw_node`): an instance whose
/// effects reshape its art draws through here, and nested instances would otherwise expand
/// without a limit. True for any other object.
pub(crate) fn charge_fx_instance(&mut self, doc: &vectorcraft_doc::Document, a: &Node) -> bool {
let NodeKind::SymbolInstance { symbol, .. } = &a.kind else { return true };
let count = doc.symbols.iter().find(|s| &s.name == symbol).map_or(0, |s| s.art.count() as u64);
let mut used = self.symbol_nodes;
let room = vectorcraft_doc::charge_symbol_art(&mut used, count, || ((), count)).is_some();
self.symbol_nodes = used;
room
}
/// The art [`Self::draw_object_fx`] draws for `a` (cached by `Arc` identity when `cache`):
/// reshaped by its geometry effects, else type, images and paths as themselves and the others
/// as their evaluated art (groups and layers: [`effects::evaluate_container`], keeping their
+14
View File
@@ -1322,6 +1322,20 @@ fn a_kept_decoded_image_is_drawn_without_decoding_it() {
assert!(!image_cached(&cmyk));
}
/// A CMYK image's inks and the pixels shown from them are admitted against the decode allowance
/// before either is made: past it, the image is read as other images are (reduced), not whole.
#[test]
fn cmyk_images_past_the_allowance_are_not_decoded_whole() {
use vectorcraft_doc::ImageBlob;
use vectorcraft_doc::cmyk::Inks;
let cmyk = ImageBlob::cmyk_tiff(&Inks::new(64, 48, [11, 22, 33, 44].repeat(64 * 48)).unwrap()).unwrap();
let whole = with_allowance(1 << 20, || crate::blobs::blob_pixmap(&cmyk, false)).map(|(_, pm)| (pm.width(), pm.height()));
assert_eq!(whole, Some((64, 48)), "within the allowance, whole");
let other = ImageBlob::cmyk_tiff(&Inks::new(64, 48, [12, 22, 33, 44].repeat(64 * 48)).unwrap()).unwrap();
let small = with_allowance(64 * 48 * 4 - 1, || crate::blobs::blob_pixmap(&other, false)).map(|(_, pm)| (pm.width(), pm.height()));
assert_ne!(small, Some((64, 48)), "past it, not decoded whole");
}
/// An image drawn larger than its pixels samples smoothly, or with `smooth_images` off each screen
/// pixel takes its nearest image pixel (Pixel Preview with File Handling › Display Bitmaps as
/// Anti-aliased Images off, #394).
+14
View File
@@ -884,3 +884,17 @@ fn darkness_adjusts_gradient_stop_colors_before_interpolation() {
}
}
}
/// A symbol instance with object effects is charged to the frame's symbol budget like one
/// without: past the budget it isn't expanded.
#[test]
fn symbol_instances_with_effects_are_charged_to_the_symbol_budget() {
let d = symbol_doc(vec![fx("distort.roughen", json!({}))]);
let inst = d.layers[0].children().unwrap()[0].clone();
let mut r = Renderer::new();
r.symbol_nodes = vectorcraft_doc::MAX_SYMBOL_NODES - 1;
assert!(r.charge_fx_instance(&d, &inst), "one object fits");
assert_eq!(r.symbol_nodes, vectorcraft_doc::MAX_SYMBOL_NODES);
assert!(!r.charge_fx_instance(&d, &inst), "the next does not");
assert_eq!(r.symbol_nodes, vectorcraft_doc::MAX_SYMBOL_NODES);
}