mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 13:56:54 -06:00
Refuse incomplete SVG/PDF exports beyond the depth limit (#218)
SVG and PDF exports check the visible subtree's depth before bounds or drawing and refuse, with an actionable message, a document nested past 256 levels instead of silently leaving the deep layers out; nothing partial is written, and an inner layer can still be exported. The CLI and MCP report the same error. Fixes #208. Also: on Windows, figcraft and figcraft-cli link with an 8 MB main-thread stack (Windows gives 1 MB where Linux and macOS give 8), so an export at the depth limit no longer overflows it. Co-authored-by: bflatastic <bflatastic@users.noreply.github.com>
This commit is contained in:
@@ -362,7 +362,7 @@ const MAX_EFFECT_MARGIN_PX: f64 = 2048.0;
|
||||
|
||||
/// Draw node `id` onto `pix`, where `parent_xf` maps the parent's space to `pix` pixels.
|
||||
pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, depth: usize) {
|
||||
if depth > 256 || ctx.skip.contains(&id) {
|
||||
if depth > crate::MAX_EXPORT_DEPTH || ctx.skip.contains(&id) {
|
||||
return;
|
||||
}
|
||||
let Some(n) = ctx.doc.node(id) else { return };
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Preflight for vector exports, including subtrees rendered into embedded images.
|
||||
|
||||
use figcraft_doc::{Document, NodeId};
|
||||
|
||||
/// Maximum descendant depth for SVG/PDF export; each selected root is at depth zero.
|
||||
pub const MAX_EXPORT_DEPTH: usize = 256;
|
||||
|
||||
/// Exporting this visible subtree would silently omit content at the traversal limit.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ExportDepthError {
|
||||
pub root: NodeId,
|
||||
pub node: NodeId,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExportDepthError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"cannot export layer {}: layer {} exceeds the maximum export depth of {MAX_EXPORT_DEPTH}; select an inner layer or reduce nesting",
|
||||
self.root, self.node
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ExportDepthError {}
|
||||
|
||||
/// Refuse a visible descendant deeper than [`MAX_EXPORT_DEPTH`] before computing bounds or
|
||||
/// drawing. Hidden subtrees are not exported. Masks, boolean operands and rasterised effects
|
||||
/// still depend on their descendants, so they participate in the same preflight.
|
||||
///
|
||||
/// The walk uses one iterator per ancestor (bounded auxiliary memory), rather than recursion or
|
||||
/// collecting a whole subtree. A cycle also reaches the depth limit and is refused.
|
||||
pub fn check_export_depth(doc: &Document, roots: &[NodeId]) -> Result<(), ExportDepthError> {
|
||||
for root in roots {
|
||||
let mut stack = vec![(std::slice::from_ref(root).iter(), 0)];
|
||||
while let Some((siblings, depth)) = stack.last_mut() {
|
||||
let Some(&id) = siblings.next() else {
|
||||
stack.pop();
|
||||
continue;
|
||||
};
|
||||
let depth = *depth;
|
||||
let Some(node) = doc.node(id) else { continue };
|
||||
if !node.visible {
|
||||
continue;
|
||||
}
|
||||
if depth > MAX_EXPORT_DEPTH {
|
||||
return Err(ExportDepthError { root: *root, node: id });
|
||||
}
|
||||
if !node.children.is_empty() {
|
||||
stack.push((node.children.iter(), depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use figcraft_doc::NodeKind;
|
||||
|
||||
#[test]
|
||||
fn export_depth_preflight_refuses_a_cyclic_subtree() {
|
||||
let mut doc = Document::new("cycle");
|
||||
let root = doc.create(doc.first_page().unwrap(), NodeKind::Group, "root", 0.0, 0.0, 1.0, 1.0).unwrap();
|
||||
doc.get_mut(root).unwrap().children.push(root);
|
||||
assert_eq!(check_export_depth(&doc, &[root]), Err(ExportDepthError { root, node: root }));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod backdrop;
|
||||
pub mod blur;
|
||||
pub mod boolean;
|
||||
pub mod draw;
|
||||
mod export_depth;
|
||||
pub mod fx;
|
||||
pub mod glass;
|
||||
pub mod grain;
|
||||
@@ -21,6 +22,7 @@ pub mod retained;
|
||||
pub mod shadow;
|
||||
|
||||
pub use draw::{node_outline, paint_bounds, stroke_fill, subtree_outline, ts_path};
|
||||
pub use export_depth::{ExportDepthError, MAX_EXPORT_DEPTH, check_export_depth};
|
||||
pub use retained::{Retained, Work};
|
||||
pub use tiny_skia::{FillRule, Pixmap};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user