mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Raster export: refuse sizes the renderer can't produce instead of crashing
PNG/JPEG/WebP export renders the artboard at the requested scale with no size check. The CPU rasteriser addresses at most 65535 pixels a side (and panics at that edge), so a large artboard at a print resolution, e.g. the maximum 16383 pt artboard at 300 ppi (~68 000 px), or a screenshot at scale 16, panicked inside vello_common: the desktop app or the MCP server exited and unsaved work was lost. `vectorcraft_render::raster_size` checks the pixel size first (at most 32 768 px a side and 268 megapixels, like Rasterize already refuses oversized images) and the export paths return an error that names the size: `document.export` / `exportSelection` / `serialize`, the MCP `export` and `screenshot` tools, File ▸ Export and the control protocol's `ui.render`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
604c20ed90
commit
152228d76d
@@ -134,7 +134,9 @@ fn encode_doc_with(doc: &vectorcraft_doc::Document, format: &str, scale: f64, ar
|
||||
"pdf" => super::rasterfx::export_pdf(doc, &vectorcraft_pdf::PdfOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?,
|
||||
"png" | "jpg" | "jpeg" | "webp" => {
|
||||
let r = doc.artboards.get(artboard).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let img = vectorcraft_render::Renderer::new().render_region(doc, r, scale.clamp(0.01, 64.0), format != "png" && format != "webp");
|
||||
let scale = scale.clamp(0.01, 64.0);
|
||||
vectorcraft_render::raster_size(r, scale).map_err(EngineError::Other)?;
|
||||
let img = vectorcraft_render::Renderer::new().render_region(doc, r, scale, format != "png" && format != "webp");
|
||||
match format {
|
||||
"png" => img.to_png(),
|
||||
"webp" => img.to_webp(),
|
||||
|
||||
@@ -183,3 +183,26 @@ fn template_opens_as_untitled() {
|
||||
assert_eq!(st.doc.layers[0].children().unwrap().len(), 1);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_export_too_large_is_an_error_not_a_crash() {
|
||||
// The largest artboard (16383 pt) at 300 ppi is ~68 000 px a side: more than the rasteriser can
|
||||
// address. The export used to panic inside the renderer (crashing the app or the MCP server).
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 16383, "height": 16383})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 100})).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("vc-raster-limit-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for format in ["png", "jpg", "webp"] {
|
||||
let path = dir.join(format!("big.{format}"));
|
||||
let e = s.execute("document.export", &json!({"path": path.to_string_lossy(), "scale": 300.0 / 72.0})).unwrap_err().to_string();
|
||||
assert!(e.contains("pixels"), "{format}: {e}");
|
||||
assert!(!path.exists(), "{format}");
|
||||
assert!(s.execute("document.serialize", &json!({"format": format, "scale": 300.0 / 72.0})).is_err(), "{format}");
|
||||
}
|
||||
// A size within the limits still exports.
|
||||
let path = dir.join("ok.png");
|
||||
s.execute("document.export", &json!({"path": path.to_string_lossy(), "scale": 0.25})).unwrap();
|
||||
assert!(std::fs::read(&path).unwrap().starts_with(b"\x89PNG"));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user