mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 14:40:24 -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user