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:
storybored379
2026-10-04 01:25:18 -07:00
co-authored by Claude Opus 5.5
parent 604c20ed90
commit 152228d76d
8 changed files with 90 additions and 2 deletions
+25
View File
@@ -29,6 +29,31 @@ pub use vectorcraft_effects as effects;
pub use vello_cpu;
pub use width::width_outline;
/// Largest raster an export may ask [`Renderer::render_region`] for, per side. The CPU rasteriser
/// addresses at most `u16::MAX` pixels per side (and panics at that edge).
pub const MAX_RASTER_SIDE: u32 = 32_768;
/// Largest raster an export may ask for in total (16384², 1 GiB of RGBA pixels).
pub const MAX_RASTER_PIXELS: u64 = 1 << 28;
/// Pixel size of `region` rendered at `scale` (as [`Renderer::render_region`] rounds it), or an error
/// when it exceeds [`MAX_RASTER_SIDE`] / [`MAX_RASTER_PIXELS`]. Raster exports check this first,
/// because the renderer can only clamp (wrong picture) or fail (abort) at those sizes.
pub fn raster_size(region: Rect, scale: f64) -> Result<(u32, u32), String> {
if !(scale.is_finite() && scale > 0.0) {
return Err(format!("invalid scale {scale}"));
}
let w = (region.width() * scale).round().max(1.0);
let h = (region.height() * scale).round().max(1.0);
let max = f64::from(MAX_RASTER_SIDE);
if !(w <= max && h <= max && w * h <= MAX_RASTER_PIXELS as f64) {
return Err(format!(
"the image would be {w:.0} × {h:.0} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {} megapixels: lower the scale or resolution",
MAX_RASTER_PIXELS / 1_000_000
));
}
Ok((w as u32, h as u32))
}
/// Rendering options.
#[derive(Clone, Debug)]
pub struct RenderOptions {
+13
View File
@@ -26,6 +26,19 @@ fn fills_red_rect() {
assert_eq!(r.pixel(70, 70), [255, 255, 255, 255]);
}
#[test]
fn raster_size_limits() {
let r = Rect::new(0.0, 0.0, 100.0, 50.0);
assert_eq!(raster_size(r, 2.0), Ok((200, 100)));
assert_eq!(raster_size(r, 0.001), Ok((1, 1)));
assert_eq!(raster_size(Rect::new(0.0, 0.0, MAX_RASTER_SIDE as f64, 1.0), 1.0), Ok((MAX_RASTER_SIDE, 1)));
assert!(raster_size(Rect::new(0.0, 0.0, MAX_RASTER_SIDE as f64 + 1.0, 1.0), 1.0).is_err());
assert!(raster_size(Rect::new(0.0, 0.0, 20_000.0, 20_000.0), 1.0).is_err(), "400 megapixels");
assert!(raster_size(r, f64::NAN).is_err());
assert!(raster_size(r, f64::INFINITY).is_err());
assert!(raster_size(r, 0.0).is_err());
}
#[test]
fn stroke_draws_outline_only() {
let mut d = Document::new(100.0, 100.0);