mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Robustness: VectorCraft never crashes — no panics in shipped code
Shipped code no longer unwraps, expects or panics: every one of the 124 unwrap()/expect()/panic!/unreachable! sites outside tests now handles the failure (Result/Option, ?, let-else, defaults) and reports errors to the user. - Lints: workspace clippy denies unwrap_used, expect_used, panic, todo, unimplemented and unreachable; clippy.toml allows them in tests, and integration tests, examples and testkit opt out explicitly. - Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls the document back and returns EngineError::Internal; a panicking tool is reset; the MCP server answers with an internal error and keeps serving; the UI frame and each control request are guarded. - APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp return Result (JPEG/WebP errors used to write empty files), FontDb::face returns Option, Region::top returns Option. - Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG and PDF through import → render → export (30k cases locally, no panics). - Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section, README pointer and a PR template checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
0f241aac9f
commit
3e323bfa2a
@@ -231,7 +231,7 @@ fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(EngineError::Other("Flatten Transparency: select objects to flatten".into()));
|
||||
}
|
||||
let src = s.doc()?.doc.clone();
|
||||
let plan = plan(&src, &roots, &o, true);
|
||||
let plan = plan(&src, &roots, &o, true)?;
|
||||
let options = serde_json::to_value(&o).unwrap_or_default();
|
||||
if plan.flat.is_empty() && !plan.kept.iter().any(|id| src.node(*id).is_some_and(|n| kept_work(n, &o))) {
|
||||
return Ok(json!({ "ids": roots.iter().map(|i| i.0).collect::<Vec<_>>(), "rasterized": 0, "vector": 0, "options": options }));
|
||||
@@ -635,8 +635,8 @@ impl FlattenReport {
|
||||
}
|
||||
|
||||
/// What flattening `roots` of `doc` with `o` would do.
|
||||
pub fn report(doc: &Document, roots: &[NodeId], o: &FlattenOptions) -> FlattenReport {
|
||||
let plan = plan(doc, roots, o, false);
|
||||
pub fn report(doc: &Document, roots: &[NodeId], o: &FlattenOptions) -> Result<FlattenReport> {
|
||||
let plan = plan(doc, roots, o, false)?;
|
||||
let mut r = FlattenReport { transparent: plan.transparent, ..Default::default() };
|
||||
// Every object under `id` (but a compound path's own pieces), visible ones only.
|
||||
fn visit(n: &Node, f: &mut dyn FnMut(&Node)) {
|
||||
@@ -685,7 +685,7 @@ pub fn report(doc: &Document, roots: &[NodeId], o: &FlattenOptions) -> FlattenRe
|
||||
});
|
||||
}
|
||||
r.affected.sort_by_key(|id| doc.index_path(*id));
|
||||
r
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -704,7 +704,7 @@ impl Session {
|
||||
let layers: Vec<u64> = self.doc()?.doc.layers.iter().map(|l| l.id.0).collect();
|
||||
target_roots(self, &json!({ "ids": layers }))?
|
||||
};
|
||||
let r = report(&self.doc()?.doc, &roots, &o);
|
||||
let r = report(&self.doc()?.doc, &roots, &o)?;
|
||||
Ok((o, r))
|
||||
}
|
||||
}
|
||||
@@ -778,8 +778,8 @@ struct Raster {
|
||||
}
|
||||
|
||||
/// The flattening of `roots` with `o`; `render` renders the images (the preview only needs where
|
||||
/// they go).
|
||||
fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render: bool) -> Plan {
|
||||
/// they go). Fails only when an image can't be encoded.
|
||||
fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render: bool) -> Result<Plan> {
|
||||
let tmp = isolated_doc(src, roots.iter().filter_map(|id| src.node(*id).cloned()).collect());
|
||||
// Geometry effects first: what is left on the art are raster effects.
|
||||
let baked = effects::bake_document(&tmp).unwrap_or(tmp);
|
||||
@@ -792,19 +792,24 @@ fn plan(src: &Document, roots: &[NodeId], o: &FlattenOptions, render: bool) -> P
|
||||
let mut out = Plan { flat: vec![], kept: vec![], transparent: roots.iter().zip(&see_through).filter(|(_, t)| **t).map(|(id, _)| *id).collect() };
|
||||
for g in overlapping(&reaches) {
|
||||
let ids: Vec<NodeId> = g.iter().map(|&i| roots[i]).collect();
|
||||
let flat = g.iter().any(|&i| see_through[i]).then(|| {
|
||||
let art: Vec<Node> = g.iter().map(|&i| art[i].clone()).collect();
|
||||
let group = g.iter().any(|&i| see_through[i]).then(|| {
|
||||
let plain: Vec<&Node> = g.iter().map(|&i| &plain[i]).collect();
|
||||
let (regions, area) = flatten_group(&plain, o, cmyk)?;
|
||||
let raster = area.as_ref().filter(|_| render).map(|a| render_raster(&baked, &art, a.rect, a.ppi, a.clip.clone(), o));
|
||||
Some(Flat { roots: ids.clone(), regions, area, raster })
|
||||
flatten_group(&plain, o, cmyk)
|
||||
});
|
||||
match flat.flatten() {
|
||||
Some(f) => out.flat.push(f),
|
||||
None => out.kept.extend(ids),
|
||||
}
|
||||
let Some((regions, area)) = group.flatten() else {
|
||||
out.kept.extend(ids);
|
||||
continue;
|
||||
};
|
||||
let raster = match area.as_ref().filter(|_| render) {
|
||||
Some(a) => {
|
||||
let art: Vec<Node> = g.iter().map(|&i| art[i].clone()).collect();
|
||||
Some(render_raster(&baked, &art, a.rect, a.ppi, a.clip.clone(), o)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
out.flat.push(Flat { roots: ids, regions, area, raster });
|
||||
}
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The regions and the image area replacing one group of overlapping objects (`plain`: the art as
|
||||
@@ -902,8 +907,8 @@ fn region_fill(elems: &[Elem], sources: &[usize], o: &FlattenOptions, cmyk: bool
|
||||
|
||||
/// `art` rendered over `rect` at `ppi`: over white with the white the art doesn't cover taken out
|
||||
/// again (so blend modes see the white page), or over nothing with Preserve Alpha. The image and
|
||||
/// its `clip` reach a pixel further.
|
||||
fn render_raster(doc: &Document, art: &[Node], rect: Rect, ppi: f64, clip: Option<PathData>, o: &FlattenOptions) -> Raster {
|
||||
/// its `clip` reach a pixel further. Fails when the image can't be encoded.
|
||||
fn render_raster(doc: &Document, art: &[Node], rect: Rect, ppi: f64, clip: Option<PathData>, o: &FlattenOptions) -> Result<Raster> {
|
||||
let mut scale = ppi / 72.0;
|
||||
scale = scale.min((MAX_PIXELS / (rect.width() * rect.height()).max(1.0)).sqrt());
|
||||
let px = 1.0 / scale;
|
||||
@@ -952,7 +957,8 @@ fn render_raster(doc: &Document, art: &[Node], rect: Rect, ppi: f64, clip: Optio
|
||||
}
|
||||
}
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
Raster { png: img.to_png(), width: img.width, height: img.height, xf, clip }
|
||||
let png = img.to_png().map_err(|e| EngineError::Other(format!("Flatten Transparency: {e}")))?;
|
||||
Ok(Raster { png, width: img.width, height: img.height, xf, clip })
|
||||
}
|
||||
|
||||
/// `n` at full opacity with Normal blending throughout (objects, fills and strokes).
|
||||
|
||||
Reference in New Issue
Block a user