EPS: an image left out at the page limit is encoded again only once it fits

Each later placement of an image or a colorized image that was turned away at the page limit
was decoding and compressing it again. The scene now remembers what the first placement of a
turned-away encoding adds to the page and leaves later placements out with the same warning
while that still doesn't fit. Once the page has room again, after art is taken back, the image
is encoded and placed as usual.
This commit is contained in:
dotCooCoo
2026-11-14 22:16:34 -08:00
parent 260da1cb7b
commit 8ad2ecdc40
2 changed files with 109 additions and 2 deletions
+55 -2
View File
@@ -266,6 +266,14 @@ pub(crate) struct Scene<'a> {
images: HashMap<String, Option<usize>>,
/// The source key and solid paint together identify a colorized encoding.
colorized_images: HashMap<(String, String), Option<usize>>,
/// 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<String, usize>,
/// 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<u64, Vec<usize>>,
@@ -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;
}
+54
View File
@@ -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<u8> = (0..=255u8).cycle().take(1000).collect();