From 95dcbfe59fc5b8d251d90ae0c0844148a0159271 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:11:52 -0500 Subject: [PATCH] EPS import: an image's transient data stops counting against the memory budget The sample planes an image is decoded from, and the strings a procedure data source hands back to fill them, are freed once the image is decoded, so they stop counting against the 1 GiB budget then; the pixels kept still count. A file of many images whose procedure returns one large buffer each time no longer runs out of budget. --- crates/eps/src/import/data/ops.rs | 6 ++++++ crates/eps/src/import/interp.rs | 11 +++++++++++ crates/eps/src/import/tests.rs | 14 ++++++++++++++ docs/file-format-parity.md | 3 ++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/data/ops.rs b/crates/eps/src/import/data/ops.rs index baca4d0ed..9e4cc3a8b 100644 --- a/crates/eps/src/import/data/ops.rs +++ b/crates/eps/src/import/data/ops.rs @@ -657,8 +657,12 @@ impl Interp<'_> { return ps_err("rangecheck", "DataSource"); } let plane = row.checked_mul(s.h).filter(|p| *p <= MAX_DATA).ok_or(PsError::Limit("decoded data is too large"))?; + // The planes and the strings a procedure gives to fill them live only while the image is + // decoded: they stop counting once it is (the pixels made from them still count). + let before = self.allocated(); self.alloc(plane.saturating_mul(sources.len()))?; let mut planes = self.read_sources(sources, row * s.h, if multi { row } else { row * s.h })?; + let transient = self.allocated().saturating_sub(before); for p in &mut planes { p.resize(row * s.h, 0); } @@ -734,6 +738,8 @@ impl Interp<'_> { rgba.extend([rgb[0], rgb[1], rgb[2], if masked { 0 } else { 255 }]); } } + drop(planes); + self.unalloc(transient); Ok(Some(Pixels { w: s.w as u32, h: s.h as u32, rgba, inks, matrix: s.matrix })) } diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 75f7c7d13..da558f174 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -341,6 +341,17 @@ impl<'a> Interp<'a> { Ok(()) } + /// The bytes counted against [`MAX_MEMORY`] so far. + pub(crate) fn allocated(&self) -> usize { + self.allocated + } + + /// Stop counting `bytes` the program no longer holds: buffers that lived only while one + /// operator ran, freed when it returned. + pub(crate) fn unalloc(&mut self, bytes: usize) { + self.allocated = self.allocated.saturating_sub(bytes); + } + fn tick(&mut self) -> Res { self.spend(1) } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 86fa8e35d..40cc4edc7 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1381,6 +1381,20 @@ fn the_planes_of_multi_source_images_are_charged() { assert!(r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings); } +/// The planes an image is decoded from, and the strings a procedure gives to fill them, stop +/// counting against the memory budget once the image is decoded: they are freed then. A procedure +/// that hands back one large buffer for every image (1 MiB here, for a one-pixel image) charged the +/// whole buffer each time, and a file of many images came in as its preview. +#[test] +fn an_images_transient_data_is_not_charged_after_it() { + let body = "/buf 1048576 string def\n1100 { 1 1 8 [1 0 0 1 0 0] { buf } image } repeat\n"; + let r = import(&eps(body)).unwrap(); + assert!(!r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings); + let mut images = 0; + r.document.walk(|n| images += usize::from(matches!(n.kind, NodeKind::Image(_)))); + assert_eq!(images, 1100); +} + /// #3568: a single sample plane over the data limit is refused. #[test] fn an_oversized_image_plane_is_refused() { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index d3e2f0303..038eb8a7b 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (EPS import: an image's transient data stops counting against the memory budget once it is decoded; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -191,6 +191,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | EPS import: the buffers an image is decoded from (its sample planes, and the strings a procedure data source hands back to fill them) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept still count. A user's Illustrator 25.2 EPS with 71 CMYK images (used locally, not committed) used to stop with "the program uses too much memory" and come in as its screen preview; it now opens with its art. The budget itself is unchanged. Scores and estimates unchanged. | | 2026-11-06 | minor | SVG import: filter and mask regions clip (SVG 1.1 §15.5, §14.4). An object whose imported blur, shadow or glow reaches past its filter's region comes in inside a clip group cut to the region (#5876), and a mask's art is cut to the mask's region, in user space or the object's bounding box (#5875). Objects that fit their regions, and our own exports, are unchanged; primitive subregions are still ignored. Tests `import_live::a_blur_is_cut_at_its_filter_region`, `import_live::a_mask_counts_only_inside_its_region` (crate `vectorcraft-svg`); scores unchanged | | 2026-11-06 | minor | SVG import: a Gaussian blur keeps the colour space its filter asks for with `color-interpolation-filters` (initially `linearRGB`, SVG 1.1 §15.7.6): a linear-light blur becomes Gaussian Blur with the new `linear` parameter, which blurs the colours in linear light (black beside white meets at about 188/255 instead of 128), and SVG export writes it back on the `feGaussianBlur` (#5877). Blurs that ask for sRGB, and the blurs our own export writes, are as before. CSS export still writes `blur()`. Tests `pixel::tests::linear_light_blur_mixes_light_not_values` (crate `vectorcraft-effects`), `import_live::a_gaussian_blur_keeps_its_colour_space` (crate `vectorcraft-svg`); scores unchanged | | 2026-11-06 | minor | PDF export and PDF-compatible `.ai` saves write each object's URL (Attributes panel) as a link annotation with a URI action (web and mail URLs only) over the bounds of the object that shows, trimmed by its clipping groups (ISO 32000-1, 12.5.6.5 and 12.6.4.7); hidden objects, layers that don't print and prints have none, and PDF/X files leave them out with a warning (#5785). Own-fixture tests in `tests::links`; PDF import doesn't read links back as URLs yet; scores unchanged |