Files
vectorcraft/crates/svg/src/import.rs
T

367 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 `<use>` 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;
use prepass::{Extras, labels, prepass};
pub use prepass::{MAX_FOREIGN, MAX_FOREIGN_VALUE};
mod css;
mod css_transform;
mod depth;
mod files;
mod fx;
mod linear_rgb;
mod spread;
mod symbol_units;
mod text;
mod vector_effect;
/// The XML of `svg`.
fn parse(svg: &str) -> Result<roxmltree::Document<'_>, 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()))
}
const SVG_NS: &str = "http://www.w3.org/2000/svg";
/// Refuse the one `feConvolveMatrix` usvg cannot handle: an `order` whose two sides overflow the
/// `order_x * order_y` u32 multiply its parser makes (`usvg-0.48.1/src/parser/filter.rs:673`, and
/// `tree/filter.rs:458` through `ConvolveMatrixData::new`). usvg parses `order` through f64 and
/// `as i32`, so a large finite value like `1066570220609` saturates to i32::MAX — positive, so it
/// passes the positivity guard — and the multiply overflows: a debug or overflow-checks build
/// panics while opening the file. A release build wraps instead, and with a one-entry
/// `kernelMatrix` the wrapped product passes usvg's length check, leaving a filter that claims a
/// 2147483647×2147483647 kernel (which VectorCraft then drops as an unsupported filter primitive,
/// so nothing reaches the document). Any order usvg can multiply — however large — keeps
/// importing exactly as before: a missing, unparseable or non-positive `order` falls back to 3,
/// and a `kernelMatrix` that fails to parse or match the order leaves the filter empty, which
/// usvg drops. The attribute read here is the one usvg reads: unprefixed (XML keeps unprefixed
/// attributes outside any namespace), on an element in the SVG namespace, with entities resolved
/// by the parse above.
fn convolve_matrices_ok(xml: &roxmltree::Document) -> Result<(), SvgError> {
for fe in xml.descendants() {
if !fe.has_tag_name((SVG_NS, "feConvolveMatrix")) {
continue;
}
if let Some(value) = fe.attribute("order") {
// The parse usvg does: svgtypes numbers, `as i32` (saturation included), the first
// side defaulting the second, and both positive or both back to the 3×3 default.
let side = |n: Option<Result<f64, svgtypes::Error>>, default: i32| n.and_then(|v| v.ok()).map(|v| v as i32).unwrap_or(default);
let mut numbers = svgtypes::NumberListParser::from(value);
let x = side(numbers.next(), 3);
let y = side(numbers.next(), x);
if x > 0 && y > 0 && (x as u32).checked_mul(y as u32).is_none() {
return Err(SvgError::Parse(format!(
"a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes"
)));
}
}
}
Ok(())
}
/// The SVG filters of SVG text `svg` ([`vectorcraft_effects::svg_filter::filters_of`]), read with
/// the limits on depth and entities that [`import`] reads documents with: up to `room`, leaving
/// out the names `taken` says.
pub(crate) fn svg_filters(svg: &str, room: usize, taken: impl Fn(&str) -> bool) -> Result<(Vec<(String, String)>, usize), SvgError> {
let too_deep = || SvgError::TooDeep(depth::MAX_ELEMENT_DEPTH);
if depth::prescan(svg).too_deep {
return Err(too_deep());
}
let xml = parse(svg)?;
if depth::height(&xml, depth::MAX_ELEMENT_DEPTH).is_none() {
return Err(too_deep());
}
Ok(vectorcraft_effects::svg_filter::filters_of(&xml, svg, room, taken))
}
/// 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<String>), 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)?,
};
// Before usvg sees the XML: its parser overflows a hostile feConvolveMatrix's order
// (`convolve_matrices_ok` below), and there is no panic guard to fall back on.
convolve_matrices_ok(&xml)?;
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 text_xml = found.text_source.as_deref().map(parse).transpose()?;
let (src, slots) = text::prepare(svg, &xml, text_xml.as_ref(), 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),
linear_rgb: linear_rgb::linear_rgb_ids(&xml, &tree),
labels: labels(&xml),
hidden: found.hidden,
uses: found.uses,
symbols: HashMap::new(),
symbol_names: HashSet::new(),
symbol_suffixes: HashMap::new(),
pattern_suffixes: HashMap::new(),
blends: fx::blends(&xml, &scan.markers),
filter_sources: fx::sources(&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(),
ellipses: found.ellipses,
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 `<g id>` 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 `<switch>`'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<String>,
/// Opacity-mask options our export wrote, by mask id (see [`mask_flags`]).
mask_flags: HashMap<String, (bool, bool)>,
/// The URL of each `<a href>`'s group, by its id ([`prepass`]).
links: HashMap<String, String>,
/// Texts read from the XML, by placeholder ([`text`]).
slots: TextSlots,
/// Pattern swatch made for each usvg pattern.
patterns: HashMap<usize, String>,
/// Midpoint stops by gradient id (see [`midpoint_stops`]).
midpoints: HashMap<String, Vec<Option<f32>>>,
/// Ids of the gradients mixed in linear light (see [`linear_rgb::linear_rgb_ids`]).
linear_rgb: HashSet<String>,
/// Object names by element id (see [`labels`]).
labels: HashMap<String, String>,
/// Ids of the undisplayed objects shown for usvg ([`prepass`]).
hidden: HashSet<String>,
/// The `<symbol>` id each `<use>` of a symbol shows, by the `<use>`'s id ([`prepass`]).
uses: HashMap<String, String>,
/// The symbols made so far, by `<symbol>` id.
symbols: HashMap<String, SymbolMade>,
/// The names of the symbols made so far.
symbol_names: HashSet<String>,
/// Where the suffixes of each symbol and pattern name base go on from (`unique_name`).
symbol_suffixes: HashMap<String, usize>,
pattern_suffixes: HashMap<String, usize>,
/// Blend modes of the shadows and glows our export wrote, by filter id ([`fx::blends`]).
blends: HashMap<String, String>,
/// Each `<filter>` element as written, by id ([`fx::sources`]).
filter_sources: HashMap<String, fx::Source>,
/// The files `<image>` elements link to.
files: files::Files,
/// Ids of the shapes whose strokes don't scale ([`vector_effect`]).
non_scaling: HashSet<String>,
/// The `data-*` attributes of elements, by id ([`user_data`]).
data: HashMap<String, Extras>,
/// The event attributes of elements, by id ([`user_events`]).
events: HashMap<String, Vec<(String, String)>>,
/// The attributes of elements in other applications' namespaces, by id ([`foreign`]).
foreign: HashMap<String, Vec<ForeignAttribute>>,
/// Ids of the `<g>` elements marked as layers ([`marked_layer`]).
sublayers: HashSet<String>,
/// The groups made of them, made sublayers where they sit in a layer ([`promote_sublayers`]).
sublayer_groups: HashSet<NodeId>,
/// Ids of the `<circle>` and `<ellipse>` elements ([`prepass`]).
ellipses: HashSet<String>,
/// 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<SvgError>,
/// How many nested SVG images were converted so far.
nested_svg_images: usize,
}