Part of #1479: an image too large to decode is reported on Open, Place and in the Links panel

An image whose decode needs more than the 2 GiB allowance (512 MB on the web) is not drawn.
A raster export says so since #1517; on screen it was still blank with no word. Open and
Place now carry a warning for it, and the Links panel's row a note (links.list: note).
This commit is contained in:
bflatastic
2026-10-13 06:34:12 +07:00
parent b3f6edddfe
commit 66add30d65
21 changed files with 79 additions and 7 deletions
+13 -1
View File
@@ -192,7 +192,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
let title = file_name(name);
let mut converted = false;
let mut preview_only = false;
let (mut doc, warnings, restored) = match format.id {
let (mut doc, mut warnings, restored) = match format.id {
// EPS and PostScript .ai: the document our EPS files carry, else the PostScript read.
_ if format.id == "eps" || vectorcraft_pdf::is_postscript(bytes) => {
let editing = vectorcraft_eps::has_native(bytes).then_some((true, || vectorcraft_eps::native(bytes)));
@@ -263,6 +263,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
if !matches!(format.id, "vectorcraft" | "template") || doc.title.is_empty() {
doc.title = title;
}
warnings.extend(doc.images.values().filter_map(|b| too_large_note(&b.bytes)));
Ok(Loaded { doc, format, warnings, restored, converted, preview_only })
}
@@ -394,6 +395,17 @@ pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result<Value> {
/// that default: wasm32 has 4 GB to address and aborts when an allocation fails.
pub(crate) const MAX_RASTER_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
/// What to say of encoded image `bytes` that need more than [`MAX_RASTER_ALLOC`] to decode (its
/// header says so; the pixels aren't read): the renderer, which has the same allowance, draws
/// nothing for them, so Open, Place and the Links panel say it (#1479). `None` for images that
/// decode, and for bytes that aren't an image.
pub(crate) fn too_large_note(bytes: &[u8]) -> Option<String> {
use image::ImageDecoder as _;
let d = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().ok()?.into_decoder().ok()?;
let (w, h) = d.dimensions();
(d.total_bytes() > MAX_RASTER_ALLOC).then(|| format!("an image of {w} × {h} pixels is too large to draw"))
}
/// Decode an image's header (and, for formats stored as PNG, its pixels). CMYK TIFFs are kept as
/// they are, with their ink amounts ([`ImageBlob::cmyk`]); CMYK TIFFs with an alpha channel, which
/// the decoder can't read, become RGBA in the active colour settings' CMYK, as do CMYK Photoshop
+1 -1
View File
@@ -28,7 +28,7 @@ pub mod eps;
mod export;
pub(crate) mod imagemap;
mod load;
pub(crate) use load::MAX_RASTER_ALLOC;
pub(crate) use load::{MAX_RASTER_ALLOC, too_large_note};
mod metafile;
mod native;
pub mod pdf;
+9 -2
View File
@@ -75,7 +75,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Links",
[],
None,
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels)}], missing, modified, embedded}",
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image too large to draw, document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels)}], missing, modified, embedded}",
has_doc,
list
),
@@ -1011,7 +1011,14 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
}
let (link, mut row) = match &n.kind {
NodeKind::Image(im) => {
(im.link.as_ref(), json!({ "format": format_label(&st.doc, im), "pixelWidth": im.width, "pixelHeight": im.height }))
let mut row = json!({ "format": format_label(&st.doc, im), "pixelWidth": im.width, "pixelHeight": im.height });
// Only an image that could be past the decode allowance (at 16 bytes a pixel, the
// most a decoder gives) has its header read for this.
let large = im.width as u64 * im.height as u64 > fileio::MAX_RASTER_ALLOC / 16;
if let Some(note) = st.doc.images.get(&im.key).filter(|_| large).and_then(|b| fileio::too_large_note(&b.bytes)) {
row["note"] = json!(note);
}
(im.link.as_ref(), row)
}
NodeKind::PlacedDocument(p) => (Some(&p.link), document_row(p)),
_ => continue,
+2 -1
View File
@@ -183,7 +183,8 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
let (sx, sy) = pt_per_px(img.ppi);
let natural = Rect::new(0.0, 0.0, img.width as f64 * sx, img.height as f64 * sy);
let link = src.path.map(|path| super::links::link_info(path, &src.bytes));
return Ok(Loaded { name, format, art: Art::Image(img), natural, warnings: vec![], link, board: None });
let warnings = fileio::too_large_note(&img.blob.bytes).into_iter().collect();
return Ok(Loaded { name, format, art: Art::Image(img), natural, warnings, link, board: None });
}
let anchored = format.id == "dxf" && (opts.dxf.fit || !opts.dxf.center);
let (mut doc, warnings) = match format.id {
+32
View File
@@ -17,6 +17,38 @@ fn place_embedded(s: &mut Session, path: &str) -> NodeId {
NodeId(r["ids"][0].as_u64().unwrap())
}
/// A PNG that says it is `w` × `h` 1-bit grey pixels (its header and no rows, which is all that
/// placing and listing read).
fn png_header(w: u32, h: u32) -> Vec<u8> {
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
let ihdr: Vec<u8> = [&b"IHDR"[..], &w.to_be_bytes(), &h.to_be_bytes(), &[1, 0, 0, 0, 0]].concat();
for chunk in [&ihdr[..], b"IDAT\x78\x9c", b"IEND"] {
out.extend((chunk.len() as u32 - 4).to_be_bytes());
out.extend(chunk);
out.extend(crc32fast::hash(chunk).to_be_bytes());
}
out
}
/// An image past what a decode may take isn't drawn: Place and Open say so and the Links panel's
/// row carries the note, where the image used to be blank with no word (#1479).
#[test]
fn an_image_too_large_to_draw_is_reported() {
const NOTE: &str = "an image of 200000 × 200000 pixels is too large to draw";
let dir = Folder::new("huge");
let (big, small) = (dir.file("wall.png"), dir.file("small.png"));
write(&big, &png_header(200_000, 200_000));
write(&small, &png(40, 20, BLUE));
let mut s = session();
assert_eq!(run(&mut s, "file.place", json!({"path": big, "at": [200, 150], "link": false}))["warnings"], json!([NOTE]));
assert_eq!(run(&mut s, "file.place", json!({"path": small, "at": [20, 20], "link": false}))["warnings"], json!([]));
let l = run(&mut s, "links.list", json!({}));
let notes: Vec<&Value> = l["links"].as_array().unwrap().iter().map(|r| &r["note"]).collect();
assert_eq!(notes, [&Value::Null, &json!(NOTE)], "top first: the small image has nothing to say");
// Opened as a document: said too. An image that decodes (16,000 px square) has no note.
assert_eq!(crate::cmd::fileio::load("wall.png", &png_header(200_000, 200_000)).unwrap().warnings, [NOTE]);
assert!(crate::cmd::fileio::load("fine.png", &png_header(16_000, 16_000)).unwrap().warnings.is_empty());
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}