diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index c6441bcae..d6a1afa27 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -369,6 +369,11 @@ impl Canvas { } /// True once the item cap is hit; the readers stop there and keep what they have. + /// Bytes the canvas has retained so far. + pub fn used(&self) -> usize { + self.used + } + pub fn full(&self) -> bool { self.full } @@ -551,6 +556,10 @@ impl Canvas { /// of at most [`SHADE_RES`] cells on the longer side, laid out so that its one repeat covers /// the shape. pub fn shade_shape(&mut self, ops: &[Op], shade: &dyn Fn(P) -> [u8; 4]) { + // A full canvas keeps nothing more, so do not sample a tile for it. + if self.full { + return; + } let dev: Vec = ops.iter().map(|o| self.map_op(*o)).collect(); let Some([l, t, r, b]) = crate::cliprect::ops_bbox(&dev) else { return }; let side = (r - l).max(b - t); diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index cf5fd97e9..6125799e7 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -289,7 +289,7 @@ impl Plus { return None; } match typ { - 0x4008 => self.object(flags, r), + 0x4008 => self.object(flags, r, cv.used()), 0x4009 | 0x4013 | 0x401A..=0x401C | 0x4036 => self.record_image_text_region(cv, typ, flags, r), 0x4025..=0x4035 => self.record_state(cv, typ, flags, r), 0x4010..=0x4012 | 0x4016..=0x4019 => self.record_curve(cv, typ, flags, r), @@ -559,7 +559,7 @@ impl Plus { Some(()) } - fn object(&mut self, flags: u16, r: Bytes) -> Option<()> { + fn object(&mut self, flags: u16, r: Bytes, canvas_used: usize) -> Option<()> { let id = usize::from(flags & 0xFF); let kind = (flags >> 8) & 0x7F; if flags & 0x8000 != 0 { @@ -575,7 +575,7 @@ impl Plus { p.data.extend_from_slice(&chunk[..chunk.len().min(room)]); if p.data.len() >= p.total { let obj = parse_object(p.kind, Bytes(&p.data)); - self.store(p.id, obj); + self.store(p.id, obj, canvas_used); } else { self.partial = Some(p); } @@ -589,22 +589,23 @@ impl Plus { let room = p.total.saturating_sub(p.data.len()); p.data.extend_from_slice(&r.0[..r.0.len().min(room)]); let obj = parse_object(kind, Bytes(&p.data)); - self.store(id, obj); + self.store(id, obj, canvas_used); return Some(()); } let obj = parse_object(kind, r); - self.store(id, obj); + self.store(id, obj, canvas_used); Some(()) } - fn store(&mut self, id: usize, obj: Option) { + /// `canvas_used` is what the canvas already retains: objects and items share one budget. + fn store(&mut self, id: usize, obj: Option, canvas_used: usize) { let Some(slot) = self.objects.get_mut(id) else { return }; if let Some(old) = slot.take() { self.object_bytes = self.object_bytes.saturating_sub(Self::obj_cost(&old)); } if let Some(new_obj) = obj { let cost = Self::obj_cost(&new_obj); - if self.object_bytes.saturating_add(cost) <= crate::MAX_BYTES { + if self.object_bytes.saturating_add(cost).saturating_add(canvas_used) <= crate::MAX_BYTES { self.object_bytes += cost; *slot = Some(new_obj); } @@ -615,7 +616,7 @@ impl Plus { match obj { Obj::Image(img) => match img.as_ref() { ImageData::Bitmap(rgba) => rgba.px.len(), - ImageData::Meta(pic) => pic.items.len().saturating_mul(128), + ImageData::Meta(pic) => pic.items.iter().map(item_cost).sum(), }, Obj::Brush(Fill::Texture { img, .. }) => img.px.len(), Obj::Path(ops) => ops.len().saturating_mul(std::mem::size_of::()), @@ -1276,6 +1277,18 @@ fn metafile(r: Bytes) -> Option { pic.filter(|p| p.width > 0.0 && p.height > 0.0) } +/// Bytes an item of a nested picture owns: its path segments, clip rectangles, pixels and text. +fn item_cost(item: &crate::Item) -> usize { + use crate::Item; + let region = |r: &Option>| r.as_ref().map_or(0, |v| v.len() * 16); + 128 + match item { + Item::Path { segs, region: r, .. } => segs.len() * std::mem::size_of::() + region(r), + Item::Bitmap { rgba, region: r, .. } => rgba.len() + region(r), + Item::Pattern { segs, tile, region: r, .. } => segs.len() * std::mem::size_of::() + tile.rgba.len() + region(r), + Item::Text(t) => t.text.len() + region(&t.region), + } +} + fn bitmap(r: Bytes) -> Option { let (w, h, stride) = (r.u32(8)? as i32, r.u32(12)? as i32, r.u32(16)? as i32); let format = r.u32(20)?; diff --git a/crates/metafile/src/place.rs b/crates/metafile/src/place.rs index be00ea22f..13cac6766 100644 --- a/crates/metafile/src/place.rs +++ b/crates/metafile/src/place.rs @@ -58,6 +58,7 @@ impl Picture { let k = (c[0].abs() + c[3].abs()) / 2.0; let mut out = Vec::with_capacity(self.items.len()); let mut total_elements = 0usize; + let region_elements = std::cell::Cell::new(0usize); let mut cached_region: Option<(*const [f32; 4], usize, Option>>)> = None; let mut get_region = |reg: &Option>| -> Option>> { let (ptr, len) = match reg { @@ -71,12 +72,15 @@ impl Picture { return res.clone(); } let res = placed_region(&tf, reg); + // Each distinct clip region is expanded once; its elements count against the placement budget. + let n = res.as_ref().and_then(Option::as_ref).map_or(0, |p| p.elements().len()); + region_elements.set(region_elements.get().saturating_add(n)); cached_region = Some((ptr, len, res.clone())); res }; for (index, item) in self.items.iter().enumerate() { - if total_elements >= MAX_PLACED_ELEMENTS { + if total_elements.saturating_add(region_elements.get()) >= MAX_PLACED_ELEMENTS { break; } match item { diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 29fab81ec..4fdf0ed5f 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -2074,3 +2074,15 @@ fn clip_polygon_work_bound() { let clipped = crate::cliprect::clip_polygon(&poly, &clip); assert!(clipped.is_empty(), "excessive polygon clip product rejected"); } + +#[test] +fn full_canvas_does_not_sample_gradient_tiles() { + let mut cv = Canvas::new(); + let big = || Rgba { w: 1, h: 1, bottom_up: false, px: vec![0u8; MAX_BYTES / 2] }; + for _ in 0..3 { + cv.bitmap([0.0, 0.0, 1.0, 1.0], big()); + } + assert!(cv.full()); + cv.shade_shape(&crate::geom::rect(0.0, 0.0, 10.0, 10.0), &|_| panic!("sampled a full canvas")); + assert_eq!(cv.items.len(), 2); +}