mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge pull request #6019 from mockydevs/fix/eps-memory-cap
EPS import: an image's transient data stops counting against the memory budget
This commit is contained in:
@@ -451,6 +451,10 @@ impl Interp<'_> {
|
||||
continue;
|
||||
}
|
||||
let want = step.min(n - o.len());
|
||||
// The bytes of this read's own copy of a procedure's string: counted while it
|
||||
// lives, released once it is appended (the string itself is the program's, and
|
||||
// stays counted as long as the program can hold it).
|
||||
let mut copied = 0;
|
||||
let chunk = match src {
|
||||
Source::File(f) => self.read(f, want)?,
|
||||
// A string gives its bytes over and over.
|
||||
@@ -462,11 +466,14 @@ impl Interp<'_> {
|
||||
self.call(p.clone())?;
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.alloc(s.len())?;
|
||||
copied = s.len();
|
||||
s
|
||||
}
|
||||
};
|
||||
*e = chunk.is_empty();
|
||||
o.extend_from_slice(chunk.get(..chunk.len().min(n - o.len())).unwrap_or_default());
|
||||
drop(chunk);
|
||||
self.unalloc(copied);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
@@ -549,12 +556,16 @@ impl Interp<'_> {
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
// Interleaved data is read into this decoder's own buffers, split into the two images'
|
||||
// samples: those stop counting once both are decoded (their pixels still count).
|
||||
let mut charged = 0;
|
||||
if interleave != 3.0 {
|
||||
let src = image.sources.first().cloned().ok_or(PsError::Ps("undefined", "DataSource".into()))?;
|
||||
let (pixels, samples) = if interleave == 2.0 {
|
||||
let (mrow, drow) = (stencil.row_bytes(1), image.row_bytes(n));
|
||||
let total = mrow.saturating_mul(stencil.h).saturating_add(drow.saturating_mul(image.h));
|
||||
self.alloc(total)?;
|
||||
charged = total;
|
||||
let raw = self.source_bytes(&src, total.min(MAX_DATA))?;
|
||||
by_rows(&raw, (mrow, stencil.h), (drow, image.h))
|
||||
} else {
|
||||
@@ -562,15 +573,21 @@ impl Interp<'_> {
|
||||
stencil.bpc = image.bpc;
|
||||
(stencil.w, stencil.h) = (image.w, image.h);
|
||||
let row = (image.w * (n + 1) * usize::from(image.bpc)).div_ceil(8);
|
||||
self.alloc(row.saturating_mul(image.h))?;
|
||||
let raw = self.source_bytes(&src, row.saturating_mul(image.h).min(MAX_DATA))?;
|
||||
charged = row.saturating_mul(image.h);
|
||||
self.alloc(charged)?;
|
||||
let raw = self.source_bytes(&src, charged.min(MAX_DATA))?;
|
||||
by_samples(&raw, image.w, image.h, n, image.bpc)
|
||||
};
|
||||
stencil.sources = vec![Source::Str(pixels)];
|
||||
image.sources = vec![Source::Str(samples)];
|
||||
}
|
||||
let Some(mut img) = self.decode_image(image)? else { return Ok(None) };
|
||||
let Some(m) = self.decode_image(stencil)? else { return Ok(Some(img)) };
|
||||
let decoded = match self.decode_image(image) {
|
||||
Ok(Some(img)) => self.decode_image(stencil).map(|m| Some((img, m))),
|
||||
other => other.map(|_| None),
|
||||
};
|
||||
self.unalloc(charged);
|
||||
let Some((mut img, m)) = decoded? else { return Ok(None) };
|
||||
let Some(m) = m else { return Ok(Some(img)) };
|
||||
let (w, h, mw, mh) = (img.w as usize, img.h as usize, m.w as usize, m.h as usize);
|
||||
for (i, px) in img.rgba.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let (x, y) = (i % w, i / w);
|
||||
@@ -689,7 +706,11 @@ 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"))?;
|
||||
self.alloc(plane.saturating_mul(sources.len()))?;
|
||||
// The sample planes are this decoder's own and live only while the image is decoded: they
|
||||
// stop counting once it is (the pixels made from them still count). What the data source
|
||||
// procedures allocate is the program's and stays counted.
|
||||
let plane_bytes = plane.saturating_mul(sources.len());
|
||||
self.alloc(plane_bytes)?;
|
||||
let mut planes = self.read_sources(sources, row * s.h, if multi { row } else { row * s.h })?;
|
||||
for p in &mut planes {
|
||||
p.resize(row * s.h, 0);
|
||||
@@ -766,6 +787,8 @@ impl Interp<'_> {
|
||||
rgba.extend([rgb[0], rgb[1], rgb[2], if masked { 0 } else { 255 }]);
|
||||
}
|
||||
}
|
||||
drop(planes);
|
||||
self.unalloc(plane_bytes);
|
||||
Ok(Some(Pixels { w: s.w as u32, h: s.h as u32, rgba, inks, matrix: s.matrix }))
|
||||
}
|
||||
|
||||
|
||||
@@ -367,6 +367,12 @@ impl<'a> Interp<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop counting `bytes` the program no longer holds: buffers an operator made for itself and
|
||||
/// freed before it returned. Never for what the program's own procedures allocated.
|
||||
pub(crate) fn unalloc(&mut self, bytes: usize) {
|
||||
self.allocated = self.allocated.saturating_sub(bytes);
|
||||
}
|
||||
|
||||
fn tick(&mut self) -> Res {
|
||||
self.spend(1)
|
||||
}
|
||||
|
||||
@@ -1436,6 +1436,31 @@ 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 decoder's copies of 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);
|
||||
}
|
||||
|
||||
/// What a data source procedure allocates stays counted, whatever image it fills: a procedure
|
||||
/// that keeps each new string (in an array here) still runs into the memory budget, however small
|
||||
/// the images it fills are.
|
||||
#[test]
|
||||
fn strings_a_data_source_keeps_stay_charged() {
|
||||
let body = "/keep 1100 array def /i 0 def\n\
|
||||
1100 { 1 1 8 [1 0 0 1 0 0] { keep i 1048576 string put keep i get /i i 1 add def } image } repeat\n";
|
||||
let r = import(&eps(body)).unwrap();
|
||||
assert!(r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// #3568: a single sample plane over the data limit is refused.
|
||||
#[test]
|
||||
fn an_oversized_image_plane_is_refused() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; 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-07 · **Change:** minor (EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; 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
|
||||
@@ -320,6 +320,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | minor | EPS import: the buffers an image decoder makes for itself (its sample planes, its copies of the strings a procedure data source hands back, and the split buffers of an interleaved masked image) stop counting against the PostScript reader's 1 GiB memory budget once the image is decoded, as they are freed then; the pixels kept, and anything the program's own procedures allocate, 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-07 | minor | EPS import: a sampled function (type 0) of two or more inputs took the nearest sample, so a function-based shading (type 1) over one came in as flat blocks; it is now interpolated between its samples (multilinearly), and with `Order 3` by cubics through them (Catmull-Rom, kept in the function's range), as the PostScript Language Reference (3rd ed., §3.10.1, type 0 functions) defines. An axial or radial shading whose function is a linearly interpolated sampled function of one input has a stop at each sample, not 33 evenly spaced ones. Tests `sampled_functions_interpolate_linearly_or_by_cubics`, `mesh_and_function_shadings_become_gradient_meshes`; a 2 × 2 function-based shading is 1.2% from Ghostscript's render (it was 45.9%); scores unchanged |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document keeps its paragraphs' tab stops (positions and alignments; leaders aren't read), at most 1,024 a paragraph and 2^20 a story, past which the default stops apply with a warning. A user's `.ai` (used locally, not committed) whose price lists came in on the default stops now lays them out as its page has them (its type 0.8% from its page, from 1.5%; what differs is hyphenation in Hungarian and type in a font that isn't installed). Scores and estimates unchanged. |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document: auto leading stays auto at the paragraph's share (it was the first two lines' distance for every line, so 10 pt lines under a 15 pt heading came in 18 pt apart), the paragraph's hyphenation options are read, and area type of one frame whose last lines the file sets in too short a frame for them here may get a frame taller by its lines' deepest descent plus 0.05 pt, when that shows more of them. A user's `.ai` (used locally, not committed) whose type differed from its page by 6.6%, so that it opened from its PDF part, now opens from its layers (4.9%; type with tab stops in right-aligned paragraphs still differs, a separate issue) (#6137). Scores and estimates unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user