Render: decode print-size rasters, and say when an image is missing from an export
The renderer decoded images with the decoder's default 512 MB allocation limit, so a 300 ppi roll (6614 x 59055 px, 1.56 GB as RGBA) that opens and exports to PDF fine drew nothing in a raster export - a fully transparent PNG, exit code 0, no warning. It now decodes with the allowance opening has (2 GB, 512 MB on the web), shrinks an image past a pixmap's 65535 px side to fit instead of dropping it, and counts any image it still can't decode: a raster export that drew fewer images than it should says so in its warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e01b9a20d9
commit
d084af162f
|
|
@ -382,7 +382,13 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
|||
/// [`encode_all`] of `doc` as it is.
|
||||
fn encode_all_of(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
let f = super::writable(C, Some(format), None)?;
|
||||
let failed = vectorcraft_render::decode_failures();
|
||||
let mut enc = encode_files(doc, f, p)?;
|
||||
// An image the renderer couldn't decode draws nothing: say so rather than write a picture
|
||||
// that silently lacks it.
|
||||
if f.raster && vectorcraft_render::decode_failures() > failed {
|
||||
enc.warnings.push("an image couldn't be decoded and is missing from the picture".into());
|
||||
}
|
||||
// Formats that draw type: missing fonts come out in the fallback font (PDF and SVG say so
|
||||
// themselves: SVG only when it outlines or embeds fonts).
|
||||
if (f.raster || matches!(f.id, "eps" | "emf" | "wmf"))
|
||||
|
|
|
|||
|
|
@ -163,3 +163,20 @@ fn an_image_opens_at_the_size_its_resolution_declares() {
|
|||
assert!((art.width() - w).abs() < 1e-6 && (art.height() - h).abs() < 1e-6, "{what}: the image fills it, {art:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_raster_export_says_when_an_image_could_not_be_decoded() {
|
||||
// A PNG whose header reads (so it places at its size) but whose pixel data is cut short:
|
||||
// the renderer can't decode it and draws nothing, which the export must say.
|
||||
let mut s = session(72.0, 36.0, 1);
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(64, 32, image::Rgba([200, 30, 30, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let cut = png_chunk(&png, b"IDAT").map(|d| d.as_ptr() as usize - png.as_ptr() as usize).unwrap() + 4;
|
||||
png.truncate(cut);
|
||||
s.execute("file.place", &json!({"name": "cut.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap();
|
||||
let r = s.execute("document.export", &json!({"format": "png", "background": "transparent"})).unwrap();
|
||||
let warnings = r["warnings"].as_array().cloned().unwrap_or_default();
|
||||
assert!(warnings.iter().any(|w| w.as_str().is_some_and(|w| w.contains("couldn't be decoded"))), "{r}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ mod ink;
|
|||
mod live;
|
||||
mod lru;
|
||||
mod paint;
|
||||
pub use paint::decode_failures;
|
||||
mod pattern;
|
||||
pub mod placed_document;
|
||||
pub mod proof;
|
||||
|
|
|
|||
|
|
@ -52,14 +52,53 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
|
|||
}
|
||||
}
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap.
|
||||
/// The most memory decoding one image may take: print rasters run to gigabytes (a 300 ppi roll,
|
||||
/// 6614 x 59055 px, is 1.56 GB as RGBA), past the decoder's 512 MB default. The web keeps it.
|
||||
const MAX_DECODE_ALLOC: u64 = if cfg!(target_arch = "wasm32") { 512 << 20 } else { 2 << 30 };
|
||||
|
||||
/// Images that couldn't be decoded (and so drew nothing), counted for exports to report.
|
||||
static DECODE_FAILURES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// How many images have failed to decode so far (process-wide): an export compares the count
|
||||
/// before and after to say it left images out.
|
||||
pub fn decode_failures() -> u64 {
|
||||
DECODE_FAILURES.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// `bytes` decoded within [`MAX_DECODE_ALLOC`]; a failure is counted in [`decode_failures`].
|
||||
fn decode(bytes: &[u8]) -> Option<image::DynamicImage> {
|
||||
let img = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok().and_then(|mut r| {
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(MAX_DECODE_ALLOC);
|
||||
r.limits(limits);
|
||||
r.decode().ok()
|
||||
});
|
||||
if img.is_none() {
|
||||
DECODE_FAILURES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
/// `img` shrunk, keeping its proportions, to fit a pixmap's sides (at most `u16::MAX` pixels).
|
||||
fn fit_pixmap(img: image::DynamicImage) -> image::DynamicImage {
|
||||
let (w, h) = (img.width(), img.height());
|
||||
let max = u16::MAX as u32;
|
||||
if w <= max && h <= max {
|
||||
return img;
|
||||
}
|
||||
let s = max as f64 / w.max(h) as f64;
|
||||
let (nw, nh) = (((w as f64 * s).round() as u32).clamp(1, max), ((h as f64 * s).round() as u32).clamp(1, max));
|
||||
img.resize_exact(nw, nh, image::imageops::FilterType::Triangle)
|
||||
}
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap (shrunk to fit a pixmap's sides).
|
||||
pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
|
||||
pixmap_of(image::load_from_memory(bytes).ok()?.to_rgba8())
|
||||
pixmap_of(fit_pixmap(decode(bytes)?).to_rgba8())
|
||||
}
|
||||
|
||||
/// [`decode_pixmap`] shrunk to `w`×`h` pixels when the image is larger (never enlarged).
|
||||
pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixmap> {
|
||||
let img = image::load_from_memory(bytes).ok()?;
|
||||
let img = decode(bytes)?;
|
||||
let img = if img.width() > w as u32 || img.height() > h as u32 {
|
||||
img.resize_exact(w as u32, h as u32, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue