diff --git a/crates/render/src/blobs.rs b/crates/render/src/blobs.rs index 4ff88e36b..88d721638 100644 --- a/crates/render/src/blobs.rs +++ b/crates/render/src/blobs.rs @@ -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, diff --git a/crates/render/src/encode/tga.rs b/crates/render/src/encode/tga.rs index abac01dba..031b5f1b6 100644 --- a/crates/render/src/encode/tga.rs +++ b/crates/render/src/encode/tga.rs @@ -34,7 +34,8 @@ pub fn encode(rgba: &[u8], width: u32, height: u32, o: &TgaOptions) -> Result().0; diff --git a/crates/render/src/fx/renderer.rs b/crates/render/src/fx/renderer.rs index 9512d89d6..987499710 100644 --- a/crates/render/src/fx/renderer.rs +++ b/crates/render/src/fx/renderer.rs @@ -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, 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 diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 81a91d8ea..e1ded30c8 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -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). diff --git a/crates/render/src/tests/objectfx.rs b/crates/render/src/tests/objectfx.rs index 85f7505dd..edc044f0a 100644 --- a/crates/render/src/tests/objectfx.rs +++ b/crates/render/src/tests/objectfx.rs @@ -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); +}