//! usvg tree → document conversion. use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; use usvg::roxmltree; use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; use vectorcraft_doc::{ Appearance, AppearanceItem, Dash, Document, FillLayer, ForeignAttribute, ImageBlob, ImageObject, LayerColor, LineCap, LineJoin, MAX_SVG_SCRIPTS, Node, NodeId, NodeKind, PatternDef, SVG_EVENTS, StrokeLayer, Unit, }; use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2, shapes}; use crate::export::Reuse; use crate::{ImportOptions, SvgError, fnv1a}; use css::XNode; use text::TextSlots; pub use depth::MAX_ELEMENT_DEPTH; pub(crate) use depth::xml_too_deep; pub use files::MAX_NESTED_SVG_IMAGES; pub(crate) use fx::BLEND; /// Most objects an imported SVG may expand to: a guard against `` and nested-image /// expansion bombs (#3564). pub const MAX_NODES: usize = if cfg!(target_arch = "wasm32") { 500_000 } else { 2_000_000 }; mod convert; mod importer; mod layers; mod prepass; pub(super) use convert::href; use convert::{aff, made_up, mask_flags, midpoint_stops, only_clips}; pub(super) use importer::PT_PER_IN; use importer::SymbolMade; pub(super) use layers::DEFAULT_FONT_SIZE; pub(crate) use layers::SUBLAYER; use layers::{RootUnits, inkscape_pages, promote_sublayers, root_layer_name}; pub(super) use prepass::Edits; pub use prepass::{MAX_FOREIGN, MAX_FOREIGN_VALUE}; use prepass::{labels, prepass}; mod css; mod css_transform; mod depth; mod files; mod fx; mod spread; mod symbol_units; mod text; mod vector_effect; /// The XML of `svg`. fn parse(svg: &str) -> Result, SvgError> { if crate::entities::expansion_too_large(svg) { return Err(SvgError::Parse("the entities the document declares expand to too much text".into())); } roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }) .map_err(|e| SvgError::Parse(e.to_string())) } /// The document SVG text `svg` holds, read with `opts`, and the warnings about what it couldn't keep. pub(crate) fn import(svg: &str, opts: &ImportOptions, max_nodes: usize) -> Result<(Document, Vec), SvgError> { // Before anything reads the tree: the XML reader, usvg, and what is made of its tree, all by // recursion. let too_deep = || SvgError::TooDeep(depth::MAX_ELEMENT_DEPTH); let scan = depth::prescan(svg); if scan.too_deep { return Err(too_deep()); } let xml = parse(svg)?; let Some(height) = depth::height(&xml, depth::MAX_ELEMENT_DEPTH) else { return Err(too_deep()); }; let (svg, found) = prepass(svg, &scan.markers, &xml, opts); // The prepass's edits are in the text: the XML is read again only when it made any. let xml = match &svg { Cow::Borrowed(_) => xml, Cow::Owned(edited) => parse(edited)?, }; let svg = svg.as_ref(); let units = RootUnits::of(xml.root_element()); let root_name = root_layer_name(xml.root_element()); let mut warnings = found.warnings; let (src, slots) = text::prepare(svg, &xml, units.dpi, &mut warnings); let files = found.files; let tree = { let opt = usvg::Options { dpi: units.dpi as f32, font_size: DEFAULT_FONT_SIZE as f32, image_href_resolver: files.resolver(height), ..usvg::Options::default() }; usvg::Tree::from_str(&src, &opt).map_err(|e| SvgError::Parse(e.to_string()))? }; let size = tree.size(); let k = units.k; let mut doc = Document::new(size.width() as f64 * k, size.height() as f64 * k); doc.units = units.unit; let mut im = Importer { doc, warnings, mask_flags: mask_flags(&xml), links: found.links, slots, patterns: HashMap::new(), midpoints: midpoint_stops(&xml), labels: labels(&xml), hidden: found.hidden, uses: found.uses, symbols: HashMap::new(), blends: fx::blends(&xml, &scan.markers), files, non_scaling: found.non_scaling, data: found.data, events: found.events, foreign: found.foreign, sublayers: found.sublayers, sublayer_groups: HashSet::new(), screen: k, converted_nodes: 1, max_nodes, error: None, nested_svg_images: 0, }; im.doc.svg_scripts = found.scripts; if im.files.nested_text() { im.warn("text inside an SVG image isn't imported".into()); } for note in im.files.left_out() { im.warn(note); } // usvg wraps everything in an id-less group carrying the viewBox transform when needed. let mut top = tree.root(); let mut base = Affine::scale(k) * aff(top.transform()); if let Some(g) = im.wrapper(top) { base *= aff(g.transform()); top = g; } if let Some(boards) = inkscape_pages(&xml, base) { im.doc.artboards = boards; } // Top-level `` elements become layers (as in the reference app), named from their // labels (`data-name`…) else their ids; otherwise all art goes into "Layer 1". A clip path on // one (as on an Inkscape layer, or our clipping layers) makes it a clipping layer. Top-level // text joins the layer below it (the first layer if none is). let is_layer = |im: &Importer, c: &usvg::Node| matches!(c, usvg::Node::Group(g) if !g.id().is_empty() && !made_up(g.id()) && !im.links.contains_key(g.id()) && !im.uses.contains_key(g.id()) && only_clips(g) && im.text_slot(g).is_none()); let is_text = |im: &Importer, c: &usvg::Node| matches!(c, usvg::Node::Group(g) if im.text_slot(g).is_some()); let layer_mode = |im: &Importer, top: &usvg::Group| { top.children().iter().any(|c| is_layer(im, c)) && top.children().iter().all(|c| is_layer(im, c) || is_text(im, c)) }; // The layers may sit in more wrappers (a transform on all the art, a ``'s group). if !layer_mode(&im, top) { let (mut inner, mut ts) = (top, base); while let Some(g) = im.wrapper(inner) { ts *= aff(g.transform()); inner = g; } if layer_mode(&im, inner) { (top, base) = (inner, ts); } } if layer_mode(&im, top) { im.doc.layers.clear(); let mut loose = vec![]; for c in top.children() { let usvg::Node::Group(g) = c else { continue }; if !is_layer(&im, c) { let n: Vec<_> = im.node(c, base).map(Arc::new).into_iter().collect(); match im.doc.layers.pop() { Some(mut l) => { im.add_unclipped(Arc::make_mut(&mut l), usize::MAX, n); im.doc.layers.push(l); } None => loose.extend(n), } continue; } let ts = base * aff(g.transform()); let name = im.name_of(g.id()).to_string(); let mut art = im.children(g, ts); // A clip path with no shape shows nothing of the layer: it stays, empty. let clip = match g.clip_path() { Some(cp) => { art = im.clipped(cp, ts, art, &format!("layer '{name}'")).unwrap_or_default(); !art.is_empty() } None => false, }; let Some(id) = im.alloc_id() else { return Err(im.error.unwrap_or(SvgError::TooLarge(im.max_nodes))); }; let preset = (im.doc.layers.len() % vectorcraft_doc::LAYER_COLORS.len()) as u8; let mut l = Node::layer(id, &name, LayerColor::Preset(preset)); l.set_clips(clip); if let Some(ch) = l.children_mut() { *ch = art; } im.add_unclipped(&mut l, 0, std::mem::take(&mut loose)); l.visible = !im.hidden.contains(g.id()); im.keep_data(g.id(), &mut l); im.keep_events(g.id(), &mut l); im.keep_foreign(g.id(), &mut l); im.doc.layers.push(Arc::new(l)); } if let Some(err) = im.error.clone() { return Err(err); } } else { // An export of a one-layer document names the layer on the root element, its art (groups // among it) loose inside: the one layer takes that name. let children = im.children(top, base); if let Some(err) = im.error.clone() { return Err(err); } if let Some(l) = im.doc.layers.first_mut() { let l = Arc::make_mut(l); if let Some(ch) = l.children_mut() { *ch = children; } if root_name.is_some() { l.name = root_name; } } } if let Some(err) = im.error { return Err(err); } if !im.sublayer_groups.is_empty() { for l in &mut im.doc.layers { promote_sublayers(Arc::make_mut(l), &im.sublayer_groups); } } im.doc.limit_depth_noting(&mut im.warnings); Ok((im.doc, im.warnings)) } struct Importer { doc: Document, warnings: Vec, /// Opacity-mask options our export wrote, by mask id (see [`mask_flags`]). mask_flags: HashMap, /// The URL of each ``'s group, by its id ([`prepass`]). links: HashMap, /// Texts read from the XML, by placeholder ([`text`]). slots: TextSlots, /// Pattern swatch made for each usvg pattern. patterns: HashMap, /// Midpoint stops by gradient id (see [`midpoint_stops`]). midpoints: HashMap>>, /// Object names by element id (see [`labels`]). labels: HashMap, /// Ids of the undisplayed objects shown for usvg ([`prepass`]). hidden: HashSet, /// The `` id each `` of a symbol shows, by the ``'s id ([`prepass`]). uses: HashMap, /// The symbols made so far, by `` id. symbols: HashMap, /// Blend modes of the shadows and glows our export wrote, by filter id ([`fx::blends`]). blends: HashMap, /// The files `` elements link to. files: files::Files, /// Ids of the shapes whose strokes don't scale ([`vector_effect`]). non_scaling: HashSet, /// The `data-*` attributes of elements, by id ([`user_data`]). data: HashMap>, /// The event attributes of elements, by id ([`user_events`]). events: HashMap>, /// The attributes of elements in other applications' namespaces, by id ([`foreign`]). foreign: HashMap>, /// Ids of the `` elements marked as layers ([`marked_layer`]). sublayers: HashSet, /// The groups made of them, made sublayers where they sit in a layer ([`promote_sublayers`]). sublayer_groups: HashSet, /// Points per screen pixel (the root's user unit before its `viewBox`): what a non-scaling /// stroke's width is measured in. screen: f64, /// How many objects were converted so far. converted_nodes: usize, /// Most objects the document may expand to. max_nodes: usize, /// An error encountered while converting objects (e.g. [`SvgError::TooLarge`]). error: Option, /// How many nested SVG images were converted so far. nested_svg_images: usize, }