M4.33: Export As with Use Artboards (All or a range) and art bounds

File > Export > Export As... (file.exportAs) picks any writable format and Use Artboards: All
or a range writes one file per artboard, <file>-<artboard>.<ext> (a PDF keeps them as pages);
off, the export covers the bounds of the visible art (an artless document is refused before the
save dialog). The format's options dialog follows: PNG/JPEG/WebP Options, SVG Options, or Save
PDF for artboards as pages. document.export takes useArtboards for raster formats (true: one
file per chosen artboard, default all) and PDF (false: the art's bounds), and the app's exports
return every file written and the encoder's warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 09:14:42 +07:00
co-authored by Claude Opus 5.5
parent 157724279f
commit 950a941003
20 changed files with 606 additions and 36 deletions
+22 -2
View File
@@ -4,10 +4,10 @@
pub mod png;
use vectorcraft_doc::Document;
use vectorcraft_doc::{Document, Node, NodeKind};
use vectorcraft_geom::Rect;
use crate::{AntiAlias, RenderOptions, Rendered, Renderer};
use crate::{AntiAlias, RenderOptions, Rendered, Renderer, fx};
/// A raster file format.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -109,5 +109,25 @@ pub(crate) fn webp(img: &Rendered) -> Result<Vec<u8>, String> {
Ok(buf)
}
/// Bounds of the art an export draws: visible objects off template layers (guides left out),
/// with their strokes and live effects. `None` when nothing would be drawn.
pub fn art_bounds(doc: &Document) -> Option<Rect> {
doc.layers.iter().fold(None, |acc, l| vectorcraft_geom::union_opt(acc, drawn_bounds(l)))
}
fn drawn_bounds(n: &Node) -> Option<Rect> {
if !n.visible {
return None;
}
match &n.kind {
NodeKind::Layer { template: true, .. } | NodeKind::Path { guide: true, .. } => None,
NodeKind::Layer { children, .. } | NodeKind::Group { children, clip: false } => {
children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, drawn_bounds(c)))
}
_ if fx::has_fx(n) => fx::visual_bounds(n),
_ => n.visual_bounds(),
}
}
#[cfg(test)]
mod tests;
+18
View File
@@ -171,3 +171,21 @@ fn anti_alias_none_is_the_same_on_every_thread_count() {
let worst = st.pixels.iter().zip(&mt.pixels).map(|(a, b)| a.abs_diff(*b)).max().unwrap();
assert!(worst <= 4, "max channel difference {worst}");
}
#[test]
fn art_bounds_cover_visible_art_only() {
let mut d = circle_doc();
let l = d.layers[0].id;
let hidden = {
let id = d.alloc_id();
let mut n =
Node::path(id, shapes::rectangle(Rect::new(100.0, 100.0, 120.0, 120.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
n.visible = false;
n
};
d.insert(Some(l), 1, hidden).unwrap();
let b = art_bounds(&d).unwrap();
assert!((b.x0 - 5.3).abs() < 1e-6 && (b.x1 - 31.1).abs() < 1e-6 && (b.y1 - 26.2).abs() < 1e-6, "{b:?}");
let empty = Document::new(10.0, 10.0);
assert_eq!(art_bounds(&empty), None);
}