diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs index badf183ed0..17608c7346 100644 --- a/crates/eps/src/scene.rs +++ b/crates/eps/src/scene.rs @@ -266,6 +266,14 @@ pub(crate) struct Scene<'a> { images: HashMap>, /// The source key and solid paint together identify a colorized encoding. colorized_images: HashMap<(String, String), Option>, + /// Image keys whose encoding didn't fit under the page limit → how much its first placement + /// adds to the body. The image is encoded again only once the page has room for that. + too_large_images: HashMap, + /// The same for colorized encodings, by `colorized_images`' keys. + too_large_colorized: HashMap<(String, String), usize>, + /// How many times an image or a colorized image was encoded. + #[cfg(test)] + encodes: usize, /// Freeform gradients' images by [`ImageCode::content_key`] → their indices in `codes`: a field /// encoded the same as one already kept draws from it. fields: HashMap>, @@ -297,6 +305,10 @@ impl<'a> Scene<'a> { brushes: None, images: HashMap::new(), colorized_images: HashMap::new(), + too_large_images: HashMap::new(), + too_large_colorized: HashMap::new(), + #[cfg(test)] + encodes: 0, fields: HashMap::new(), codes: vec![], placements: vec![], @@ -326,6 +338,17 @@ impl<'a> Scene<'a> { (self.page(), uses, cached) } + /// [`Self::run`], with how many times the scene encoded an image or a colorized image. + #[cfg(test)] + pub fn run_counting_encodes(mut self) -> (Page, usize) { + let doc = self.doc; + for l in &doc.layers { + self.node(l, false); + } + let encodes = self.encodes; + (self.page(), encodes) + } + pub fn run(mut self) -> Page { let doc = self.doc; for l in &doc.layers { @@ -1479,9 +1502,24 @@ impl<'a> Scene<'a> { let encoded = match self.images.get(key) { Some(e) => *e, None => { + // An image turned away at the page limit is encoded again only once it fits. + if let Some(&len) = self.too_large_images.get(key) { + if self.size().saturating_add(len).saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } + self.too_large_images.remove(key); + } + #[cfg(test)] + { + self.encodes += 1; + } let code = self.decode_image(blob).and_then(|img| self.encode_image(img.as_raw(), img.width(), img.height())); // An image that can't be placed isn't kept. - if code.as_ref().is_some_and(|c| self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body) { + if let Some(c) = &code + && self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body + { + self.too_large_images.insert(key.to_string(), c.inline_len); self.warn(TOO_LARGE); return; } @@ -1517,9 +1555,24 @@ impl<'a> Scene<'a> { let encoded = match self.colorized_images.get(&cache) { Some(e) => *e, None => { + // An image turned away at the page limit is encoded again only once it fits. + if let Some(&len) = self.too_large_colorized.get(&cache) { + if self.size().saturating_add(len).saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } + self.too_large_colorized.remove(&cache); + } + #[cfg(test)] + { + self.encodes += 1; + } let code = self.encode_colorized(blob, paint); // An image that can't be placed isn't kept. - if code.as_ref().is_some_and(|c| self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body) { + if let Some(c) = &code + && self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body + { + self.too_large_colorized.insert(cache, c.inline_len); self.warn(TOO_LARGE); return; } diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index a1a44a5524..60b5832859 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -1663,6 +1663,60 @@ fn an_image_rejected_by_the_page_limit_keeps_no_unused_code() { } } +/// An image turned away at the page limit is encoded once, not again at each later placement +/// that doesn't fit either. +#[test] +fn an_image_rejected_by_the_page_limit_is_encoded_once() { + for pic in [Pic::Placed, Pic::Colorized] { + for level in Level::ALL { + let case = format!("{pic:?}, {level:?}"); + let d = image_doc_with(|d| { + let mut nodes = vec![red_rect(d)]; + nodes.extend((0..50).map(|k| pic_node(d, pic, f64::from(k), 20.0, 4.0))); + nodes + }); + let o = EpsOptions { level, ..opts(&d) }; + let rect = crate::scene::Scene::new(&image_doc_with(|d| vec![red_rect(d)]), &o).run(); + let (page, encodes) = crate::scene::Scene::new(&d, &o).with_max_body(rect.body.len() + 64).run_counting_encodes(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings); + assert_eq!(encodes, 1, "{case}"); + assert_eq!((&page.setup, &page.body), (&rect.setup, &rect.body), "{case}"); + } + } +} + +/// An image turned away at the page limit inside art that is then taken back is encoded again +/// once the page has room for it, and written where it is drawn. +#[test] +fn an_image_rejected_in_art_taken_back_is_encoded_again_when_it_fits() { + for pic in [Pic::Placed, Pic::Colorized] { + for level in Level::ALL { + let case = format!("{pic:?}, {level:?}"); + let build = |stroked: bool, after: bool| { + image_doc_with(|d| { + let fill = pic_pattern(d, pic); + let mut nodes = vec![many_sided(d, &[fill], stroked)]; + if after { + nodes.push(pic_node(d, pic, 20.0, 20.0, 4.0)); + } + nodes + }) + }; + let o = EpsOptions { level, ..opts(&build(false, false)) }; + let solid = image_doc_with(|d| vec![many_sided(d, &[Paint::solid(Color::rgb(1.0, 0.0, 0.0))], false)]); + // The shape's outline fits under this limit, and its first tile of the image doesn't. + let limit = crate::scene::Scene::new(&solid, &o).run().body.len() + 64; + let (page, encodes) = crate::scene::Scene::new(&build(false, false), &o).with_max_body(limit).run_counting_encodes(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings); + assert_eq!((encodes, page.body.matches("} exec\n").count()), (1, 0), "{case}: {}", page.body); + let (page, encodes) = crate::scene::Scene::new(&build(true, true), &o).with_max_body(limit).run_counting_encodes(); + assert_eq!(encodes, 2, "{case}"); + let alone = crate::scene::Scene::new(&image_doc_with(|d| vec![pic_node(d, pic, 20.0, 20.0, 4.0)]), &o).run(); + assert_eq!((&page.setup, &page.body), (&alone.setup, &alone.body), "{case}"); + } + } +} + #[test] fn run_length_and_ascii85_round_trip() { let cycle: Vec = (0..=255u8).cycle().take(1000).collect();