Use shared neutral SVG scene for paste

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 04:25:01 -05:00
1 parent 2769d0b88f
commit bfbb831e54
4 files changed
+74 -105

No files matched your search

Generated
+29 -28
View File
@@ -1245,7 +1245,7 @@ dependencies = [
[[package]]
name = "craft-image-codecs"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43#f7d76eec548a77a79fd2e056e7846a8655ee3f43"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895#2e670cf9377b523112f3fe4b409399772c745895"
dependencies = [
"craft-jpeg",
"exr",
@@ -1275,7 +1275,7 @@ dependencies = [
[[package]]
name = "craft-jpeg"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43#f7d76eec548a77a79fd2e056e7846a8655ee3f43"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895#2e670cf9377b523112f3fe4b409399772c745895"
dependencies = [
"thiserror 2.0.21",
]
@@ -1411,7 +1411,7 @@ dependencies = [
[[package]]
name = "craft-psd"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43#f7d76eec548a77a79fd2e056e7846a8655ee3f43"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895#2e670cf9377b523112f3fe4b409399772c745895"
dependencies = [
"encoding_rs",
"flate2",
@@ -1433,6 +1433,21 @@ name = "craft-sheet"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf"
[[package]]
name = "craft-svg"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895#2e670cf9377b523112f3fe4b409399772c745895"
dependencies = [
"craft-image-codecs",
"craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895)",
"flate2",
"kurbo",
"resvg 0.48.1",
"roxmltree 0.21.2",
"thiserror 2.0.21",
"usvg 0.48.1",
]
[[package]]
name = "craft-svg"
version = "0.1.0"
@@ -1458,21 +1473,6 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-svg"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43#f7d76eec548a77a79fd2e056e7846a8655ee3f43"
dependencies = [
"craft-image-codecs",
"craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43)",
"flate2",
"kurbo",
"resvg 0.48.1",
"roxmltree 0.21.2",
"thiserror 2.0.21",
"usvg 0.48.1",
]
[[package]]
name = "craft-ui"
version = "0.1.0"
@@ -1566,6 +1566,15 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-xml"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895#2e670cf9377b523112f3fe4b409399772c745895"
dependencies = [
"quick-xml 0.41.0",
"thiserror 2.0.21",
]
[[package]]
name = "craft-xml"
version = "0.1.0"
@@ -1612,15 +1621,6 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-xml"
version = "0.1.0"
source = "git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43#f7d76eec548a77a79fd2e056e7846a8655ee3f43"
dependencies = [
"quick-xml 0.41.0",
"thiserror 2.0.21",
]
[[package]]
name = "craft-zip"
version = "0.1.0"
@@ -1894,6 +1894,7 @@ dependencies = [
"craft-indd",
"craft-prepress",
"craft-print",
"craft-svg 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895)",
"craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)",
"craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=f23b0f33f0d00b157476533a43cbbcfd901aa1f1)",
"designcraft-color",
@@ -1999,7 +2000,7 @@ dependencies = [
"craft-file-formats",
"craft-image-codecs",
"craft-psd",
"craft-svg 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=f7d76eec548a77a79fd2e056e7846a8655ee3f43)",
"craft-svg 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895)",
"designcraft-color",
"designcraft-fonts",
"flate2",
+3 -3
View File
@@ -18,7 +18,7 @@ craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435
craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
craft-indd = { git = "https://github.com/storytold/craft-libs", rev = "47bf5650b0d503b97bfb5e841746c4957897d11f", version = "=0.1.0" }
craft-svg = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "f7d76eec548a77a79fd2e056e7846a8655ee3f43", version = "=0.1.0", features = ["tree-render", "tree-text"] }
craft-svg = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "2e670cf9377b523112f3fe4b409399772c745895", version = "=0.1.0", features = ["tree-render", "tree-text"] }
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
craft-xml-dtd = { package = "craft-xml", git = "https://github.com/dylanmtaylor/craft-libs", rev = "f23b0f33f0d00b157476533a43cbbcfd901aa1f1", version = "=0.1.0" }
craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
@@ -26,8 +26,8 @@ craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243
craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" }
craft-csv = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "44d7ac8d942dbd87048f82b3efc3e0d14483cc8d", version = "=0.1.0" }
craft-image-codecs = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "f7d76eec548a77a79fd2e056e7846a8655ee3f43", version = "=0.1.0" }
craft-psd = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "f7d76eec548a77a79fd2e056e7846a8655ee3f43", version = "=0.1.0" }
craft-image-codecs = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "2e670cf9377b523112f3fe4b409399772c745895", version = "=0.1.0" }
craft-psd = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "2e670cf9377b523112f3fe4b409399772c745895", version = "=0.1.0" }
craft-file-formats = { git = "https://github.com/storytold/craft-libs", rev = "8abfa9d0f14bafbfc3449fc552ffeafb9ca6a551", version = "=0.1.0" }
craft-opc = { git = "https://github.com/storytold/craft-libs", rev = "8abfa9d0f14bafbfc3449fc552ffeafb9ca6a551", version = "=0.1.0" }
craft-pptx = { git = "https://github.com/storytold/craft-libs", rev = "f6cab1980", version = "=0.1.0" }
+1
View File
@@ -35,6 +35,7 @@ thiserror = { workspace = true }
log = { workspace = true }
craft-xml = { workspace = true }
craft-xml-dtd = { workspace = true }
craft-svg = { workspace = true }
caseless = { workspace = true }
unicode-segmentation = { workspace = true }
+41 -74
View File
@@ -3,9 +3,9 @@
//! clipboard as SVG markup; each path becomes a DesignCraft path with its fill and stroke, groups
//! stay groups, and text comes in as outlines.
use craft_svg::tree::document::{Document as SvgDocument, LineCap, LineJoin, Node as SvgNode, Paint as SvgPaint, Segment};
use designcraft_doc::{Content, Fill, Item, ItemId, Selection, Shape, Stroke, StrokeType};
use designcraft_geom::{Affine, BezPath, PathData, Point, Rect};
use designcraft_images::usvg;
use serde_json::{Value, json};
use super::{CommandSpec, Result, Session, bad, cmd, has_doc, spread_param, str_param};
@@ -27,7 +27,7 @@ pub fn is_svg_markup(text: &str) -> bool {
designcraft_images::is_svg(text.as_bytes())
}
/// Cap conversion nesting too; the shared image parser bounds XML before usvg runs.
/// Cap conversion nesting too; the shared SVG scene builder enforces this limit.
const MAX_DEPTH: usize = 64;
/// A converted SVG node, in points.
@@ -68,20 +68,15 @@ struct Notes {
too_deep: bool,
}
fn paint(p: &usvg::Paint, opacity: f32, notes: &mut Notes) -> Option<Paint> {
let rgb = match p {
usvg::Paint::Color(c) => [c.red, c.green, c.blue],
// Gradients and patterns come in as their first colour (InDesign keeps Illustrator
// gradients; DesignCraft's swatch gradients can't be made from SVG stops yet).
usvg::Paint::LinearGradient(g) => {
fn paint(p: &SvgPaint, notes: &mut Notes) -> Option<Paint> {
let (rgb, opacity) = match p {
SvgPaint::Color { rgb, opacity } => (*rgb, *opacity),
// Gradients and patterns are retained as their first color where available.
SvgPaint::Gradient { first_rgb, opacity } => {
notes.gradients += 1;
g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue])?
(*first_rgb.as_ref()?, *opacity)
}
usvg::Paint::RadialGradient(g) => {
notes.gradients += 1;
g.stops().first().map(|s| [s.color().red, s.color().green, s.color().blue])?
}
usvg::Paint::Pattern(_) => {
SvgPaint::Pattern => {
notes.gradients += 1;
return None;
}
@@ -94,56 +89,43 @@ fn round(v: f64) -> f64 {
(v * 10_000.0).round() / 10_000.0
}
/// `t` (SVG px space) then px → pt.
fn to_affine(t: usvg::Transform) -> Affine {
let pt = designcraft_images::PT_PER_PX;
Affine::scale(pt) * Affine::new([f64::from(t.sx), f64::from(t.ky), f64::from(t.kx), f64::from(t.sy), f64::from(t.tx), f64::from(t.ty)])
}
fn bezpath(data: &usvg::tiny_skia_path::Path, xf: Affine) -> BezPath {
use usvg::tiny_skia_path::PathSegment as S;
let p = |q: usvg::tiny_skia_path::Point| xf * Point::new(f64::from(q.x), f64::from(q.y));
fn bezpath(segments: &[Segment]) -> BezPath {
let p = |q: [f64; 2]| Point::new(q[0] * designcraft_images::PT_PER_PX, q[1] * designcraft_images::PT_PER_PX);
let mut bp = BezPath::new();
for seg in data.segments() {
for seg in segments {
match seg {
S::MoveTo(a) => bp.move_to(p(a)),
S::LineTo(a) => bp.line_to(p(a)),
S::QuadTo(a, b) => bp.quad_to(p(a), p(b)),
S::CubicTo(a, b, c) => bp.curve_to(p(a), p(b), p(c)),
S::Close => bp.close_path(),
Segment::MoveTo(a) => bp.move_to(p(*a)),
Segment::LineTo(a) => bp.line_to(p(*a)),
Segment::QuadTo(a, b) => bp.quad_to(p(*a), p(*b)),
Segment::CubicTo(a, b, c) => bp.curve_to(p(*a), p(*b), p(*c)),
Segment::Close => bp.close_path(),
}
}
bp
}
fn convert(g: &usvg::Group, depth: usize, notes: &mut Notes, out: &mut Vec<Art>) {
for node in g.children() {
fn convert(nodes: &[SvgNode], notes: &mut Notes, out: &mut Vec<Art>) {
for node in nodes {
match node {
usvg::Node::Path(path) => {
if !path.is_visible() || path.data().is_empty() {
continue;
}
let xf = to_affine(path.abs_transform());
let fill = path.fill().and_then(|f| paint(f.paint(), f.opacity().get(), notes));
let stroke = path.stroke().and_then(|s| {
let c = paint(s.paint(), s.opacity().get(), notes)?;
// A stroke scales with the art: its width in points.
let scale = xf.determinant().abs().sqrt();
let width = round(f64::from(s.width().get()) * scale);
let dashes = s.dasharray().map(|d| d.iter().map(|v| round(f64::from(*v) * scale)).collect::<Vec<_>>());
SvgNode::Path(path) => {
let fill = path.fill.as_ref().and_then(|p| paint(p, notes));
let stroke = path.stroke.as_ref().and_then(|s| {
let c = paint(&s.paint, notes)?;
let width = round(s.width * designcraft_images::PT_PER_PX);
let dashes = s.dash_array.as_ref().map(|d| d.iter().map(|v| round(v * designcraft_images::PT_PER_PX)).collect::<Vec<_>>());
let style = StrokeStyle {
width,
cap: match s.linecap() {
usvg::LineCap::Butt => designcraft_doc::Cap::Butt,
usvg::LineCap::Round => designcraft_doc::Cap::Round,
usvg::LineCap::Square => designcraft_doc::Cap::Projecting,
cap: match s.cap {
LineCap::Butt => designcraft_doc::Cap::Butt,
LineCap::Round => designcraft_doc::Cap::Round,
LineCap::Square => designcraft_doc::Cap::Projecting,
},
join: match s.linejoin() {
usvg::LineJoin::Round => designcraft_doc::Join::Round,
usvg::LineJoin::Bevel => designcraft_doc::Join::Bevel,
join: match s.join {
LineJoin::Round => designcraft_doc::Join::Round,
LineJoin::Bevel => designcraft_doc::Join::Bevel,
_ => designcraft_doc::Join::Miter,
},
miter: f64::from(s.miterlimit().get()),
miter: s.miter_limit,
dashes,
};
Some((c, style))
@@ -156,38 +138,22 @@ fn convert(g: &usvg::Group, depth: usize, notes: &mut Notes, out: &mut Vec<Art>)
{
notes.mixed_opacity += 1;
}
out.push(Art::Path { path: bezpath(path.data(), xf), fill, stroke, opacity: 1.0 });
out.push(Art::Path { path: bezpath(&path.segments), fill, stroke, opacity: 1.0 });
}
usvg::Node::Group(child) => {
if depth >= MAX_DEPTH {
notes.too_deep = true;
continue;
}
SvgNode::Group { children, opacity } => {
let mut items = Vec::new();
convert(child, depth + 1, notes, &mut items);
convert(children, notes, &mut items);
// usvg wraps nodes in groups for transforms and opacity; only a group of several
// objects stays a group. Its opacity goes to what it holds.
let opacity = child.opacity().get();
if items.len() > 1 {
out.push(Art::Group(items, opacity));
out.push(Art::Group(items, *opacity));
} else {
for mut a in items {
a.fade(opacity);
a.fade(*opacity);
out.push(a);
}
}
}
// Text arrives as outlines, as Illustrator's Create Outlines would give it.
usvg::Node::Text(text) => {
let mut items = Vec::new();
convert(text.flattened(), depth + 1, notes, &mut items);
if items.len() > 1 {
out.push(Art::Group(items, 1.0));
} else {
out.extend(items);
}
}
usvg::Node::Image(_) => notes.images += 1,
}
}
}
@@ -280,9 +246,10 @@ fn paste_svg(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(ID, "the clipboard doesn't hold SVG markup"));
}
let tree = designcraft_images::svg_tree(svg.as_bytes()).ok_or_else(|| bad(ID, "the SVG on the clipboard can't be read"))?;
let mut notes = Notes::default();
let scene = SvgDocument::from_tree(&tree, MAX_DEPTH);
let mut notes = Notes { images: scene.images, too_deep: scene.too_deep, ..Default::default() };
let mut art = Vec::new();
convert(tree.root(), 0, &mut notes, &mut art);
convert(&scene.nodes, &mut notes, &mut art);
let b = bounds(&art).ok_or_else(|| bad(ID, "the SVG on the clipboard has no paths to paste"))?;
let sr = spread_param(p, ID)?;
let layer = s.doc()?.active_layer;