From 055e64c091f99529c3147402d741b696fcfbc509 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 02:10:40 +0000 Subject: [PATCH] address review: count EMF+ object bytes against the canvas byte budget --- crates/metafile/src/canvas.rs | 11 ++++++++++- crates/metafile/src/emfplus.rs | 7 ++++++- crates/metafile/src/tests.rs | 9 +++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index d6a1afa27..4cc4777d7 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -313,6 +313,8 @@ pub(crate) struct Canvas { saved: Vec, /// Bytes held by the items so far (path ops and pixels), capped at [`MAX_BYTES`]. used: usize, + /// Bytes retained outside the canvas (EMF+ objects) that count against the same cap. + held: usize, objects: Vec>, pen: Pen, brush: Brush, @@ -345,6 +347,7 @@ impl Canvas { viewport_set: false, saved: Vec::new(), used: 0, + held: 0, objects: Vec::new(), pen: Pen { style: 0, width: 0.0, color: [0, 0, 0, 255] }, brush: Brush::new(BS_SOLID, [255, 255, 255, 255]), @@ -390,6 +393,12 @@ impl Canvas { } } + /// Records the bytes another part of the player retains, so the items and they stay within + /// [`MAX_BYTES`] together. + pub fn hold(&mut self, bytes: usize) { + self.held = bytes; + } + fn push_raw(&mut self, mut raw: Raw) { if self.clip.as_ref().is_some_and(Vec::is_empty) { return; @@ -407,7 +416,7 @@ impl Canvas { Raw::Bitmap { img, .. } => img.px.len(), Raw::Text { text: t, .. } => t.text.len() + t.dx.len() * 4 + t.font.family.len() + 128, } + region_cost; - if self.items.len() >= MAX_ITEMS || self.used.saturating_add(cost) > MAX_BYTES { + if self.items.len() >= MAX_ITEMS || self.used.saturating_add(self.held).saturating_add(cost) > MAX_BYTES { self.full = true; } else { self.used += cost; diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index da27d346c..5bff5a3d1 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -289,7 +289,12 @@ impl Plus { return None; } match typ { - 0x4008 => self.object(flags, r, cv.used()), + 0x4008 => { + let done = self.object(flags, r, cv.used()); + // The retained objects share the canvas's byte budget. + cv.hold(self.object_bytes); + done + } 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), diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 1b750ffcf..fa2b48617 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -2104,3 +2104,12 @@ fn full_canvas_does_not_sample_gradient_tiles() { 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); } + +#[test] +fn canvas_budget_counts_bytes_held_elsewhere() { + let mut cv = Canvas::new(); + cv.hold(MAX_BYTES - 100); + cv.bitmap([0.0, 0.0, 1.0, 1.0], Rgba { w: 1, h: 1, bottom_up: false, px: vec![0u8; 200] }); + assert!(cv.full()); + assert!(cv.items.is_empty()); +}