diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index aa827d762..fea237c61 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -22,6 +22,22 @@ pub use depth::MAX_ELEMENT_DEPTH; pub(crate) use depth::xml_too_deep; pub(crate) use fx::BLEND; +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; @@ -204,162 +220,6 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec` as a sublayer rather than a group: our export writes it on the layers -/// it writes inside layers. (Inkscape's `inkscape:groupmode="layer"` counts too.) -pub(crate) const SUBLAYER: &str = "data-vc-layer"; - -/// The Inkscape namespace (its layers and their labels). -const INKSCAPE: &str = "http://www.inkscape.org/namespaces/inkscape"; - -/// The layer name on root element `root` (``, or its `data-name`), as exports of -/// one-layer documents write it. An Inkscape root's id (`svg8`) is one Inkscape made up, not a name. -fn root_layer_name(root: XNode) -> Option { - let id = root.attribute("id").filter(|_| root.attribute((INKSCAPE, "version")).is_none()); - root.attribute("data-name").or(id).map(str::trim).filter(|n| !n.is_empty()).map(str::to_string) -} - -/// Is element `g` a `` marked as a layer ([`SUBLAYER`])? Only one with an id counts: the id (or -/// the label beside it) is its name. -fn marked_layer(g: XNode) -> bool { - g.tag_name().name() == "g" - && g.attribute("id").is_some_and(|id| !id.is_empty()) - && (g.has_attribute(SUBLAYER) || g.attribute((INKSCAPE, "groupmode")) == Some("layer")) -} - -/// Turn the groups made of elements marked as layers (`marked`, see [`marked_layer`]) right inside -/// layer `l` into its sublayers, in its colour, and theirs in turn. A marked group anywhere else -/// (in a group, a mask, a symbol) stays a group: only layers hold layers. -fn promote_sublayers(l: &mut Node, marked: &HashSet) { - let NodeKind::Layer { color, children, .. } = &mut l.kind else { return }; - let color = *color; - for c in children.iter_mut().filter(|c| marked.contains(&c.id)) { - let c = Arc::make_mut(c); - let NodeKind::Group { children, clip } = &mut c.kind else { continue }; - let mut sub = Node::layer(c.id, "", color); - sub.set_clips(*clip); - if let Some(ch) = sub.children_mut() { - *ch = std::mem::take(children); - } - c.kind = sub.kind; - promote_sublayers(c, marked); - } -} - -/// SVG's initial `font-size` (`medium`) in user units. -pub(super) const DEFAULT_FONT_SIZE: f64 = 12.0; - -/// How lengths become points, from the root ``'s `width` / `height`. -/// -/// A pixel (and a unitless user unit) is a point, as we export, and absolute lengths keep their -/// physical size at 72 pt per inch: `font-size="12pt"` is 12 pt, `1in` is 72 pt. A root size in -/// absolute units (`width="210mm"`) is that physical size, and its user units are CSS pixels of it -/// (96 per inch, 0.75 pt each), so a drawing without a `viewBox` keeps its proportions. With one -/// side absolute and the other not (`width="96pt" height="96"`), both are: the unitless side is -/// CSS pixels too, so the drawing isn't stretched (#1010). -struct RootUnits { - /// Pixels per inch usvg (and the text pass) convert absolute lengths at. - dpi: f64, - /// Points per user unit of the root, along both axes. - k: f64, - /// Document units: those of the root `width` (pixels when it has none). - unit: Unit, -} - -impl RootUnits { - fn of(root: XNode) -> Self { - let unit = |name: &str| root.attribute(name).and_then(|v| svgtypes::Length::from_str(v.trim()).ok()).map(|l| l.unit); - use svgtypes::LengthUnit as U; - let physical = |u: Option| matches!(u, Some(U::In | U::Cm | U::Mm | U::Pt | U::Pc)); - let (w, h) = (unit("width"), unit("height")); - let (dpi, k) = if physical(w) || physical(h) { (CSS_PX_PER_IN, PT_PER_IN / CSS_PX_PER_IN) } else { (PT_PER_IN, 1.0) }; - let unit = match w { - Some(U::Mm) => Unit::Millimeters, - Some(U::Cm) => Unit::Centimeters, - Some(U::In) => Unit::Inches, - Some(U::Pt) => Unit::Points, - Some(U::Pc) => Unit::Picas, - _ => Unit::Pixels, - }; - Self { dpi, k, unit } - } -} - -/// `'id' (feA, feB)` for each filter: its id and the primitives it uses, for warnings. -fn filter_names(filters: &[Arc]) -> String { - use usvg::filter::Kind as K; - let names: Vec = filters - .iter() - .map(|f| { - let mut kinds: Vec<&str> = Vec::new(); - for p in f.primitives() { - let k = match p.kind() { - K::Blend(_) => "feBlend", - K::ColorMatrix(_) => "feColorMatrix", - K::ComponentTransfer(_) => "feComponentTransfer", - K::Composite(_) => "feComposite", - K::ConvolveMatrix(_) => "feConvolveMatrix", - K::DiffuseLighting(_) => "feDiffuseLighting", - K::DisplacementMap(_) => "feDisplacementMap", - K::DropShadow(_) => "feDropShadow", - K::Flood(_) => "feFlood", - K::GaussianBlur(_) => "feGaussianBlur", - K::Image(_) => "feImage", - K::Merge(_) => "feMerge", - K::Morphology(_) => "feMorphology", - K::Offset(_) => "feOffset", - K::SpecularLighting(_) => "feSpecularLighting", - K::Tile(_) => "feTile", - K::Turbulence(_) => "feTurbulence", - }; - if !kinds.contains(&k) { - kinds.push(k); - } - } - let id = if f.id().is_empty() || made_up(f.id()) { String::new() } else { format!("'{}' ", f.id()) }; - format!("{id}({})", kinds.join(", ")) - }) - .collect(); - names.join(", ") -} - -/// Inkscape's pages (`` in its ``, in user units) as artboards -/// through `user` (user units → document points), named by their labels; `None` without any. -fn inkscape_pages(xml: &roxmltree::Document, user: Affine) -> Option> { - let num = |n: roxmltree::Node, a: &str| n.attribute(a).and_then(|v| v.trim().parse::().ok()).filter(|v| v.is_finite()); - let pages: Vec = xml - .descendants() - .filter(|n| n.is_element() && n.tag_name().name() == "page" && n.tag_name().namespace() == Some(INKSCAPE)) - .filter_map(|p| { - let (w, h) = (num(p, "width")?, num(p, "height")?); - let (x, y) = (num(p, "x").unwrap_or(0.0), num(p, "y").unwrap_or(0.0)); - // A box too large to add up or to place (1e308 wide) is no page. - let r = user.transform_rect_bbox(Rect::new(x, y, x + w, y + h)); - (w > 0.0 && h > 0.0 && r.is_finite()).then_some((p, r)) - }) - .take(1000) - .enumerate() - .map(|(i, (p, r))| vectorcraft_doc::Artboard { - id: i as u32 + 1, - name: p.attribute((INKSCAPE, "label")).filter(|l| !l.trim().is_empty()).map_or_else(|| format!("Artboard {}", i + 1), str::to_string), - rect: r, - locked: false, - show_center_mark: false, - show_cross_hairs: false, - show_video_safe_areas: false, - video_ruler_pixel_aspect_ratio: 1.0, - color: None, - }) - .collect(); - (!pages.is_empty()).then_some(pages) -} -/// How many clip paths deep (``) an object's clip is read. -const MAX_CLIP_NEST: usize = 16; - -/// Points per inch. -pub(super) const PT_PER_IN: f64 = 72.0; -/// CSS pixels per inch. -const CSS_PX_PER_IN: f64 = 96.0; - struct Importer { doc: Document, warnings: Vec, @@ -401,1189 +261,3 @@ struct Importer { /// stroke's width is measured in. screen: f64, } - -/// A `` made a symbol: its name, its art with no object ids (what each `` has to show -/// to be an instance of it) and the instance transforms the art can stand for. -struct SymbolMade { - name: String, - art: Node, - reuse: Reuse, -} - -/// `n` with every object id (its descendants' and its mask art's too) zeroed, to compare art. -fn without_ids(n: &Node) -> Node { - let mut n = n.clone(); - n.id = vectorcraft_doc::NodeId(0); - if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) { - m.art = Arc::new(without_ids(&m.art)); - } - for c in n.children_mut().into_iter().flatten() { - *c = Arc::new(without_ids(c)); - } - n -} - -/// `base`, or `base 2`, `base 3`… : the first that isn't `taken`. -fn unique_name(base: &str, taken: impl Fn(&str) -> bool) -> String { - let mut name = base.to_string(); - let mut k = 2; - while taken(&name) { - name = format!("{base} {k}"); - k += 1; - } - name -} - -/// The stops our export added for midpoints (`data-vc-midpoint`), by gradient id: for each stop, -/// the midpoint it stands for. -fn midpoint_stops(xml: &roxmltree::Document) -> HashMap>> { - xml.descendants() - .filter(|n| matches!(n.tag_name().name(), "linearGradient" | "radialGradient")) - .filter_map(|g| { - let id = g.attribute("id")?; - let marks: Vec> = g - .children() - .filter(|c| c.tag_name().name() == "stop") - .map(|c| c.attribute("data-vc-midpoint").and_then(|v| v.parse().ok())) - .collect(); - marks.iter().any(Option::is_some).then(|| (id.to_string(), marks)) - }) - .collect() -} - -/// A gradient's stops, with the midpoint stops `marks` names folded back into the midpoints of -/// the stops before them. -fn gradient_stops(g: &usvg::BaseGradient, marks: Option<&[Option]>) -> Vec { - let marks = marks.filter(|m| m.len() == g.stops().len()); - let mut out: Vec = Vec::with_capacity(g.stops().len()); - for (i, s) in g.stops().iter().enumerate() { - if let (Some(mid), Some(prev)) = (marks.and_then(|m| m.get(i).copied().flatten()), out.last_mut()) { - prev.midpoint = mid.clamp(0.0, 1.0); - continue; - } - let c = s.color(); - out.push(GradientStop { opacity: s.opacity().get(), ..GradientStop::new(s.offset().get(), Color::rgb8(c.red, c.green, c.blue)) }); - } - out -} - -/// The options [`crate::export::MASK_FLAGS`] records on exported `` elements, by mask id: -/// (clip, invert). -fn mask_flags(xml: &roxmltree::Document) -> HashMap { - let attr = crate::export::MASK_FLAGS; - if !xml.input_text().contains(attr) { - return HashMap::new(); - } - let flags = |n: roxmltree::Node| { - let has = |f: &str| n.attribute(attr).is_some_and(|v| v.split_whitespace().any(|x| x == f)); - Some((n.attribute("id")?.to_string(), (!has("noclip"), has("invert")))) - }; - xml.descendants().filter(|n| n.tag_name().name() == "mask" && n.has_attribute(attr)).filter_map(flags).collect() -} - -/// Prefix of the ids [`prepass`] gives the elements it has to find again that have none. -const MADE_UP_ID: &str = "vectorcraft-id-"; - -/// Is `id` one [`prepass`] made up (not a name)? -fn made_up(id: &str) -> bool { - id.starts_with(MADE_UP_ID) -} - -/// The URL an `` element links to. -pub(super) fn href<'a>(a: XNode<'a, '_>) -> Option<&'a str> { - a.attribute("href").or_else(|| a.attribute(("http://www.w3.org/1999/xlink", "href"))).filter(|h| !h.is_empty()) -} - -/// What usvg would lose, found in the XML before it runs ([`prepass`]). -#[derive(Default)] -struct Found { - /// The URL of each ``'s group, by its id. - links: HashMap, - /// Ids of the undisplayed objects shown for usvg: they come back hidden. - hidden: HashSet, - /// The `` id each `` of a symbol shows, by the ``'s id. - uses: 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>, - /// The JavaScript files `