mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 12:24:19 +00:00
SVG import: refuse an SVG whose use copies and styles expand past the limits
usvg copies the element a <use> shows, and every copy gets its own copy of the declarations its style attribute and matching style sheet rules give it. A chain of <use>s that doubles at each level made usvg build those copies before the import's object limit was checked: a 101 kB file whose rectangle has a 100 kB style took 4.7 GB at 14 levels, and one 100 kB style sheet rule on 16,000 groups took 1.5 GB without any <use>. A plain chain stopped at usvg's own 1,000,000-node limit with a parse error. The import now measures, before usvg reads the SVG, how many elements it expands to and how many bytes of style values those elements copy, each element measured once and each <use> counted as one addition. References follow usvg's rules: first element of an id, href before xlink:href, and the self and back references usvg leaves out. An SVG past 999,999 elements (500,000 on the web) is refused as too large, a loop of <use> references usvg would copy without end is refused too, and one past 64 MB of copied style values is refused with the new SvgError::StylesTooLarge. SVG images count toward the same 64 MB as the SVG showing them, an image past either limit refuses the whole import, and an image that doesn't decode does not count.
This commit is contained in:
@@ -43,6 +43,7 @@ pub(super) use prepass::Edits;
|
||||
use prepass::{Extras, labels, prepass};
|
||||
pub use prepass::{MAX_FOREIGN, MAX_FOREIGN_VALUE};
|
||||
|
||||
mod budget;
|
||||
mod css;
|
||||
mod css_transform;
|
||||
mod depth;
|
||||
@@ -154,7 +155,13 @@ pub(crate) fn import_finding(
|
||||
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);
|
||||
// Before usvg copies what `<use>`s show and the style values of each copy: the text usvg reads.
|
||||
let styles = match &src {
|
||||
Cow::Borrowed(_) => budget::check(&xml, budget::MAX_COPIED_STYLES)?,
|
||||
Cow::Owned(prepared) => budget::check(&parse(prepared)?, budget::MAX_COPIED_STYLES)?,
|
||||
};
|
||||
let files = found.files;
|
||||
files.copied_styles(styles);
|
||||
let tree = {
|
||||
let opt = usvg::Options {
|
||||
dpi: units.dpi as f32,
|
||||
@@ -164,6 +171,9 @@ pub(crate) fn import_finding(
|
||||
};
|
||||
usvg::Tree::from_str(&src, &opt).map_err(|e| SvgError::Parse(e.to_string()))?
|
||||
};
|
||||
if let Some(err) = files.refused() {
|
||||
return Err(err);
|
||||
}
|
||||
let size = tree.size();
|
||||
let k = units.k;
|
||||
let mut doc = Document::new(size.width() as f64 * k, size.height() as f64 * k);
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
//! What usvg makes of an SVG, measured in the XML before usvg runs: how many elements it makes,
|
||||
//! and how many bytes of style values it copies into them.
|
||||
//!
|
||||
//! usvg makes an element of each SVG element, and of each element a `<use>` shows a copy of, by
|
||||
//! copying what the `<use>` refers to. Each copy gets its own copy of every declaration the style
|
||||
//! sheets and the `style` attribute give the element, so a long style on an element that `<use>`s
|
||||
//! copy, again and again in a chain, or that a style sheet rule gives to many elements, takes more
|
||||
//! memory than the file holds many times over. usvg stops past 1,000,000 elements, but only after
|
||||
//! it has made them and copied their styles. An SVG past [`MAX_SVG_ELEMENTS`] elements or
|
||||
//! [`MAX_COPIED_STYLES`] bytes of copied styles is refused here instead.
|
||||
//!
|
||||
//! The copies are counted, never made: each element's count is worked out once and added up, so a
|
||||
//! `<use>` of an element costs one addition, however many elements its copy holds. References
|
||||
//! follow usvg 0.48.1 (`parser/svgtree/parse.rs`): an id names the first element that has it, an
|
||||
//! unprefixed `href` wins over `xlink:href`, and a `<use>` of itself, of the `<use>` its own copy
|
||||
//! starts from, or of an element holding a `<use>` of the element or of the `<use>`, is left out.
|
||||
//! Any other loop of references makes usvg copy without end, so an SVG with one is refused. usvg
|
||||
//! looks for those `<use>`s inside an element only in the SVG namespace: in an SVG without one, a
|
||||
//! `<use>` that leads back to the `<use>` its copy starts from is refused here too, though usvg
|
||||
//! would leave it out.
|
||||
//!
|
||||
//! The style sheets' rules are matched against each element once, as usvg matches them against
|
||||
//! each copy.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use usvg::roxmltree;
|
||||
|
||||
use super::{MAX_NODES, SVG_NS, XNode};
|
||||
use crate::SvgError;
|
||||
|
||||
/// Most elements usvg may make of an SVG: [`MAX_NODES`], and below the 1,000,000 elements past
|
||||
/// which usvg stops (its count includes a root of its own).
|
||||
pub(crate) const MAX_SVG_ELEMENTS: usize = if MAX_NODES < 999_999 { MAX_NODES } else { 999_999 };
|
||||
|
||||
/// Most bytes of style values usvg may copy into the elements of an SVG and the SVG images it
|
||||
/// shows, all together: VectorCraft's own limit.
|
||||
pub(crate) const MAX_COPIED_STYLES: u64 = 64 << 20;
|
||||
|
||||
/// What usvg keeps for each value it copies besides its bytes (the count and length of a shared
|
||||
/// string).
|
||||
const COPY: u64 = 16;
|
||||
|
||||
/// The longhands a `font` shorthand resets before setting the ones it gives, and the most bytes of
|
||||
/// each reset value (`normal`).
|
||||
const FONT_RESETS: u64 = 12;
|
||||
const RESET_VALUE: u64 = 6;
|
||||
|
||||
/// The longhands a `font` shorthand sets from its own text.
|
||||
const FONT_PARTS: u64 = 6;
|
||||
|
||||
const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
|
||||
|
||||
/// The elements usvg reads (`EId` in usvg's `parser/svgtree/names.rs`); it skips the others with
|
||||
/// everything inside them.
|
||||
const ELEMENTS: [&str; 53] = [
|
||||
"a",
|
||||
"circle",
|
||||
"clipPath",
|
||||
"defs",
|
||||
"ellipse",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feDropShadow",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"filter",
|
||||
"g",
|
||||
"image",
|
||||
"line",
|
||||
"linearGradient",
|
||||
"marker",
|
||||
"mask",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"radialGradient",
|
||||
"rect",
|
||||
"stop",
|
||||
"style",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"textPath",
|
||||
"tref",
|
||||
"tspan",
|
||||
"use",
|
||||
];
|
||||
|
||||
/// The name of element `n` when usvg reads it: in the SVG namespace or none, and one of
|
||||
/// [`ELEMENTS`].
|
||||
fn svg_name<'a>(n: XNode<'a, '_>) -> Option<&'a str> {
|
||||
let name = n.tag_name();
|
||||
(n.is_element() && matches!(name.namespace(), None | Some(SVG_NS)) && ELEMENTS.contains(&name.name())).then(|| name.name())
|
||||
}
|
||||
|
||||
/// An element as the style sheets' selectors see it, as usvg matches them.
|
||||
struct Element<'a, 'i>(XNode<'a, 'i>);
|
||||
|
||||
impl simplecss::Element for Element<'_, '_> {
|
||||
fn parent_element(&self) -> Option<Self> {
|
||||
self.0.parent_element().map(Self)
|
||||
}
|
||||
|
||||
fn prev_sibling_element(&self) -> Option<Self> {
|
||||
self.0.prev_sibling_element().map(Self)
|
||||
}
|
||||
|
||||
fn has_local_name(&self, name: &str) -> bool {
|
||||
self.0.tag_name().name() == name
|
||||
}
|
||||
|
||||
fn attribute_matches(&self, name: &str, operator: simplecss::AttributeOperator<'_>) -> bool {
|
||||
self.0.attribute(name).is_some_and(|value| operator.matches(value))
|
||||
}
|
||||
|
||||
fn pseudo_class_matches(&self, class: simplecss::PseudoClass<'_>) -> bool {
|
||||
matches!(class, simplecss::PseudoClass::FirstChild) && self.0.prev_sibling_element().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes usvg copies for one declaration: three markers for `marker`, the resets and longhands
|
||||
/// for `font`, its value for any other. Counted for every declaration, the ones usvg then drops (a
|
||||
/// property it doesn't read, a value an earlier one overrides) too.
|
||||
fn copied(declaration: &simplecss::Declaration) -> u64 {
|
||||
let len = declaration.value.len() as u64;
|
||||
match declaration.name {
|
||||
"marker" => 3 * (COPY + len),
|
||||
"font" => (FONT_RESETS + FONT_PARTS) * COPY + FONT_RESETS * RESET_VALUE + len,
|
||||
_ => COPY + len,
|
||||
}
|
||||
}
|
||||
|
||||
/// What usvg makes of one element: elements (text nodes included) and bytes of copied styles.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct Cost {
|
||||
elements: u64,
|
||||
bytes: u64,
|
||||
}
|
||||
|
||||
impl Cost {
|
||||
fn add(self, other: Cost) -> Cost {
|
||||
Cost { elements: self.elements.saturating_add(other.elements), bytes: self.bytes.saturating_add(other.bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a `<use>` shows: nothing, the element it refers to with all it holds, or only that
|
||||
/// element (a `<use>` whose own reference leads back to the `<use>` showing it).
|
||||
enum Shows<'a, 'i> {
|
||||
Nothing,
|
||||
All(XNode<'a, 'i>),
|
||||
Bare(XNode<'a, 'i>),
|
||||
}
|
||||
|
||||
struct Measure<'a, 'i> {
|
||||
/// The first element of each id.
|
||||
ids: HashMap<&'a str, XNode<'a, 'i>>,
|
||||
sheet: simplecss::StyleSheet<'a>,
|
||||
/// The elements that hold a `<use>` of themselves.
|
||||
holds_use_of_itself: HashSet<roxmltree::NodeId>,
|
||||
/// The `<use>`s of each element, by the element.
|
||||
used_by: HashMap<roxmltree::NodeId, Vec<XNode<'a, 'i>>>,
|
||||
/// The bytes of text in each element a `<tref>` shows.
|
||||
tref_text: HashMap<roxmltree::NodeId, u64>,
|
||||
}
|
||||
|
||||
impl<'a, 'i> Measure<'a, 'i> {
|
||||
fn new(xml: &'a roxmltree::Document<'i>) -> Self {
|
||||
let mut ids = HashMap::new();
|
||||
for n in xml.descendants() {
|
||||
if let Some(id) = n.attribute("id") {
|
||||
ids.entry(id).or_insert(n);
|
||||
}
|
||||
}
|
||||
// The style sheets usvg reads: `<style>` elements of no type or of `text/css`, the first
|
||||
// text of each.
|
||||
let mut sheet = simplecss::StyleSheet::new();
|
||||
for n in xml.descendants().filter(|n| n.has_tag_name("style")) {
|
||||
if n.attribute("type").is_none_or(|t| t == "text/css")
|
||||
&& let Some(text) = n.text()
|
||||
{
|
||||
sheet.parse_more(text);
|
||||
}
|
||||
}
|
||||
let mut m = Measure { ids, sheet, holds_use_of_itself: HashSet::new(), used_by: HashMap::new(), tref_text: HashMap::new() };
|
||||
for u in xml.descendants().filter(|n| n.has_tag_name((SVG_NS, "use"))) {
|
||||
let Some(target) = m.target(u) else { continue };
|
||||
if u.ancestors().skip(1).any(|a| a == target) {
|
||||
m.holds_use_of_itself.insert(target.id());
|
||||
}
|
||||
m.used_by.entry(target.id()).or_default().push(u);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The element `<use>` `u` refers to, as usvg resolves it.
|
||||
fn target(&self, u: XNode<'a, 'i>) -> Option<XNode<'a, 'i>> {
|
||||
let link = u
|
||||
.attributes()
|
||||
.find(|a| a.name() == "href" && a.namespace().is_none())
|
||||
.or_else(|| u.attributes().find(|a| a.name() == "href" && a.namespace() == Some(XLINK_NS)))?;
|
||||
let id = svgtypes::IRI::from_str(link.value()).ok()?.0;
|
||||
self.ids.get(id).copied()
|
||||
}
|
||||
|
||||
/// What `<use>` `u` shows: see the module docs.
|
||||
fn shows(&self, u: XNode<'a, 'i>) -> Shows<'a, 'i> {
|
||||
let Some(link) = self.target(u) else { return Shows::Nothing };
|
||||
if link == u || svg_name(link).is_none() {
|
||||
return Shows::Nothing;
|
||||
}
|
||||
let inside = |v: &XNode| v.ancestors().skip(1).any(|a| a == link);
|
||||
if self.holds_use_of_itself.contains(&link.id()) || self.used_by.get(&u.id()).is_some_and(|uses| uses.iter().any(inside)) {
|
||||
return Shows::Nothing;
|
||||
}
|
||||
if svg_name(link) == Some("use") && self.target(link) == Some(u) {
|
||||
return Shows::Bare(link);
|
||||
}
|
||||
Shows::All(link)
|
||||
}
|
||||
|
||||
/// The bytes of style values usvg copies into one copy of element `n`.
|
||||
fn styles(&self, n: XNode) -> u64 {
|
||||
let mut bytes = 0u64;
|
||||
for rule in self.sheet.rules.iter().filter(|r| r.selector.matches(&Element(n))) {
|
||||
bytes = rule.declarations.iter().map(copied).fold(bytes, u64::saturating_add);
|
||||
}
|
||||
if let Some(style) = n.attribute("style") {
|
||||
bytes = simplecss::DeclarationTokenizer::from(style).map(|d| copied(&d)).fold(bytes, u64::saturating_add);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Element `n` alone.
|
||||
fn own(&self, n: XNode) -> Cost {
|
||||
Cost { elements: 1, bytes: self.styles(n) }
|
||||
}
|
||||
|
||||
/// `<text>` `t` and what usvg makes of what it holds: at most an element of each element, and
|
||||
/// a copy of each run of text and of the text each `<tref>` shows.
|
||||
fn text(&mut self, t: XNode<'a, 'i>) -> Cost {
|
||||
let mut cost = self.own(t);
|
||||
for d in t.descendants().skip(1) {
|
||||
if d.is_text() {
|
||||
let len = d.text().map_or(0, str::len) as u64;
|
||||
cost = cost.add(Cost { elements: 1, bytes: COPY + len });
|
||||
} else if svg_name(d).is_some() {
|
||||
cost = cost.add(self.own(d));
|
||||
if svg_name(d) == Some("tref") {
|
||||
cost = cost.add(Cost { elements: 1, bytes: COPY + self.tref(d) });
|
||||
}
|
||||
}
|
||||
}
|
||||
cost
|
||||
}
|
||||
|
||||
/// The bytes of text `<tref>` `r` shows.
|
||||
fn tref(&mut self, r: XNode<'a, 'i>) -> u64 {
|
||||
let link = r.attribute((XLINK_NS, "href")).or_else(|| r.attribute("href"));
|
||||
let Some(target) = link.and_then(|l| svgtypes::IRI::from_str(l).ok()).and_then(|iri| self.ids.get(iri.0).copied()) else { return 0 };
|
||||
*self.tref_text.entry(target.id()).or_insert_with(|| {
|
||||
target.descendants().filter(|n| n.is_text()).filter_map(|n| n.text()).map(|t| t.len() as u64).fold(0, u64::saturating_add)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes of style values usvg would copy into the elements of `xml`, when that is no more
|
||||
/// than `bytes_left` and it makes no more than [`MAX_SVG_ELEMENTS`] elements of it.
|
||||
pub(super) fn check(xml: &roxmltree::Document, bytes_left: u64) -> Result<u64, SvgError> {
|
||||
match measure(xml, MAX_SVG_ELEMENTS as u64, bytes_left) {
|
||||
Ok(cost) => Ok(cost.bytes),
|
||||
Err(Past::Elements) => Err(SvgError::TooLarge(MAX_SVG_ELEMENTS)),
|
||||
Err(Past::Bytes) => Err(SvgError::StylesTooLarge((MAX_COPIED_STYLES >> 20) as usize)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The limit [`measure`] stopped at.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Past {
|
||||
Elements,
|
||||
Bytes,
|
||||
}
|
||||
|
||||
/// What usvg makes of `xml`, up to `elements` elements and `bytes` bytes of copied styles. Without
|
||||
/// recursion; each element is measured once.
|
||||
fn measure(xml: &roxmltree::Document, elements: u64, bytes: u64) -> Result<Cost, Past> {
|
||||
let mut m = Measure::new(xml);
|
||||
let mut costs: HashMap<roxmltree::NodeId, Cost> = HashMap::new();
|
||||
// The elements entered and not yet left: one entered again is a loop.
|
||||
let mut open: HashSet<roxmltree::NodeId> = HashSet::new();
|
||||
enum Step<'a, 'i> {
|
||||
Enter(XNode<'a, 'i>),
|
||||
/// An element, what its cost adds up (its children, or what a `<use>` shows), and the one
|
||||
/// element a `<use>` shows alone.
|
||||
Leave(XNode<'a, 'i>, Vec<XNode<'a, 'i>>, Option<XNode<'a, 'i>>),
|
||||
}
|
||||
let root = xml.root_element();
|
||||
let mut stack = vec![Step::Enter(root)];
|
||||
while let Some(step) = stack.pop() {
|
||||
match step {
|
||||
Step::Enter(n) => {
|
||||
if costs.contains_key(&n.id()) || svg_name(n).is_none_or(|name| name == "style") {
|
||||
continue;
|
||||
}
|
||||
if !open.insert(n.id()) {
|
||||
// usvg copies a loop until it nests past what it reads.
|
||||
return Err(Past::Elements);
|
||||
}
|
||||
let (parts, bare) = match svg_name(n) {
|
||||
Some("text") => (vec![], None),
|
||||
Some("use") => match m.shows(n) {
|
||||
Shows::All(link) => (vec![link], None),
|
||||
Shows::Bare(link) => (vec![], Some(link)),
|
||||
Shows::Nothing => (vec![], None),
|
||||
},
|
||||
_ => (n.children().filter(|c| svg_name(*c).is_some_and(|name| name != "style")).collect(), None),
|
||||
};
|
||||
stack.push(Step::Leave(n, parts.clone(), bare));
|
||||
stack.extend(parts.into_iter().map(Step::Enter));
|
||||
}
|
||||
Step::Leave(n, parts, bare) => {
|
||||
open.remove(&n.id());
|
||||
let mut cost = match svg_name(n) {
|
||||
Some("text") => m.text(n),
|
||||
_ => m.own(n),
|
||||
};
|
||||
if let Some(link) = bare {
|
||||
cost = cost.add(m.own(link));
|
||||
}
|
||||
for part in parts {
|
||||
cost = cost.add(costs.get(&part.id()).copied().unwrap_or_default());
|
||||
}
|
||||
if cost.elements > elements {
|
||||
return Err(Past::Elements);
|
||||
}
|
||||
if cost.bytes > bytes {
|
||||
return Err(Past::Bytes);
|
||||
}
|
||||
costs.insert(n.id(), cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(costs.get(&root.id()).copied().unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn svg(body: &str) -> String {
|
||||
format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10">{body}</svg>"#)
|
||||
}
|
||||
|
||||
/// What [`measure`] makes of `text` with no limits, as (elements, bytes).
|
||||
fn cost(text: &str) -> (u64, u64) {
|
||||
let xml = roxmltree::Document::parse(text).unwrap();
|
||||
let c = measure(&xml, u64::MAX, u64::MAX).unwrap_or_else(|p| panic!("{p:?}"));
|
||||
(c.elements, c.bytes)
|
||||
}
|
||||
|
||||
fn past(text: &str, elements: u64, bytes: u64) -> Option<Past> {
|
||||
measure(&roxmltree::Document::parse(text).unwrap(), elements, bytes).err()
|
||||
}
|
||||
|
||||
/// Groups `g0` to `g{levels}`: `g0` a rectangle styled `style`, each other two `<use>`s of the
|
||||
/// one before; one `<use>` of the last shown.
|
||||
fn chain(levels: usize, style: &str) -> String {
|
||||
let links: String = (1..=levels).map(|i| format!("<g id='g{i}'><use href='#g{0}'/><use xlink:href='#g{0}'/></g>", i - 1)).collect();
|
||||
svg(&format!("<defs><g id='g0'><rect width='1' height='1' style='{style}'/></g>{links}</defs><use href='#g{levels}'/>"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_are_counted_with_their_styles_up_to_each_limit() {
|
||||
// svg, defs, g0 and its rect (2), g1 with two uses of g0 (1 + 2 × 3), the shown use (1 + 7).
|
||||
// Five rects, each copying `red` (3 bytes and a copy's 16).
|
||||
let one = chain(1, "fill:red");
|
||||
assert_eq!(cost(&one), (19, 5 * 19));
|
||||
assert_eq!(past(&one, 19, 95), None);
|
||||
assert_eq!(past(&one, 18, 95), Some(Past::Elements));
|
||||
assert_eq!(past(&one, 19, 94), Some(Past::Bytes));
|
||||
// Each level doubles what the shown use holds, without making any of it.
|
||||
let (elements, _) = cost(&chain(60, ""));
|
||||
assert!(elements > 1 << 60, "{elements}");
|
||||
assert_eq!(past(&chain(60, ""), MAX_SVG_ELEMENTS as u64, u64::MAX), Some(Past::Elements));
|
||||
assert_eq!(past(&chain(40, "font-family:a"), u64::MAX, MAX_COPIED_STYLES), Some(Past::Bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_element_limit_stays_below_where_usvg_stops() {
|
||||
assert_eq!(MAX_SVG_ELEMENTS, if cfg!(target_arch = "wasm32") { 500_000 } else { 999_999 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_sheet_rules_count_where_they_match() {
|
||||
let groups = "<g class='a'/>".repeat(4);
|
||||
// `g` matches the four groups (16 + 3 each), `.b` none, `svg` the root (16 + 4).
|
||||
let (_, bytes) = cost(&svg(&format!("<style>g{{fill:red}} .b{{fill:blue}} svg{{fill:none}}</style>{groups}")));
|
||||
assert_eq!(bytes, 4 * 19 + 20);
|
||||
// A style sheet of another type is not read; the style element itself makes nothing.
|
||||
assert_eq!(cost(&svg(&format!("<style type='text/x'>g{{fill:red}}</style>{groups}"))), (5, 0));
|
||||
// `marker` sets three properties; `font` resets twelve and sets six from its text.
|
||||
let (_, marker) = cost(&svg("<path style='marker:url(#m)'/>"));
|
||||
let (_, font) = cost(&svg("<path style='font:12px a'/>"));
|
||||
assert_eq!((marker, font), (3 * (16 + 7), 18 * 16 + 12 * 6 + 6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_usvg_leaves_out_copy_nothing() {
|
||||
// A use of itself, of a group holding it, of what isn't there or isn't SVG.
|
||||
let (n, _) = cost(&svg("<use id='u' href='#u'/><g id='a'><g><use href='#a'/></g></g><use href='#none'/><x id='x'/><use href='#x'/>"));
|
||||
assert_eq!(n, 1 + 1 + 3 + 1 + 1);
|
||||
// Two uses of each other: each shows the other alone.
|
||||
assert_eq!(cost(&svg("<use id='u1' href='#u2'/><use id='u2' href='#u1'/>")).0, 1 + 2 + 2);
|
||||
// A group holding a use of the use that shows it: that use shows nothing, and the one the
|
||||
// group holds shows it alone.
|
||||
let back = svg("<g id='g'><use id='inner' href='#outer'/></g><use id='outer' href='#g'/>");
|
||||
assert_eq!(cost(&back).0, 1 + 3 + 1);
|
||||
// The unprefixed href wins, and an id names its first element.
|
||||
let first = svg("<g id='a'><rect/><rect/></g><g id='a'/><rect id='b'/><use xlink:href='#a' href='#b'/><use href='#a'/>");
|
||||
assert_eq!(cost(&first).0, 1 + 3 + 1 + 1 + 2 + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_loop_usvg_copies_without_end_is_refused_as_usvg_refuses_it() {
|
||||
let looped = svg("<g id='a'><use href='#b'/></g><g id='b'><use href='#a'/></g>");
|
||||
assert_eq!(past(&looped, u64::MAX, u64::MAX), Some(Past::Elements));
|
||||
assert!(usvg::Tree::from_str(&looped, &usvg::Options::default()).is_err());
|
||||
// Without the loop, usvg reads it.
|
||||
let open = svg("<g id='a'><use href='#b'/></g><g id='b'><rect width='1' height='1'/></g>");
|
||||
assert!(past(&open, u64::MAX, u64::MAX).is_none() && usvg::Tree::from_str(&open, &usvg::Options::default()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_counts_its_runs_and_what_trefs_show() {
|
||||
let (n, bytes) = cost(&svg("<g id='t'>abcd</g><text>xy<tspan>z</tspan><tref href='#t'/></text>"));
|
||||
// svg, g (its text is not counted outside a text), text, its runs "xy" and "z", tspan, tref and its text.
|
||||
assert_eq!(n, 1 + 1 + 1 + 2 + 1 + 1 + 1);
|
||||
assert_eq!(bytes, (16 + 2) + (16 + 1) + (16 + 4));
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use usvg::{ImageHrefResolver, ImageKind, roxmltree};
|
||||
use vectorcraft_doc::LinkInfo;
|
||||
use vectorcraft_doc::links::missing_placeholder;
|
||||
|
||||
use super::{Edits, depth, href};
|
||||
use super::{Edits, budget, depth, href};
|
||||
use crate::{ImportOptions, MAX_ELEMENT_DEPTH, MAX_SVGZ, SvgError};
|
||||
|
||||
/// Most nested SVG images an imported SVG may decode (#3564).
|
||||
@@ -74,6 +74,12 @@ pub(in crate::import) struct Files {
|
||||
/// The CMYK TIFF stored in place of a turned CMYK image, by the address of the PNG usvg got
|
||||
/// (kept here too, so the address stays its own).
|
||||
cmyk: Mutex<HashMap<usize, (Bytes, Bytes)>>,
|
||||
/// The bytes of style values usvg copies into the elements of the SVG and of the SVG images
|
||||
/// read so far ([`budget`]).
|
||||
styles: AtomicU64,
|
||||
/// Why an SVG image refused the whole SVG: what usvg would make of it is past [`budget`]'s
|
||||
/// limits. The first one.
|
||||
refused: Mutex<Option<SvgError>>,
|
||||
}
|
||||
|
||||
/// Encoded image bytes, shared.
|
||||
@@ -479,6 +485,17 @@ impl Files {
|
||||
notes
|
||||
}
|
||||
|
||||
/// The SVG holding the images copies `bytes` of style values ([`budget::check`]): its images
|
||||
/// copy at most what is left.
|
||||
pub(in crate::import) fn copied_styles(&self, bytes: u64) {
|
||||
self.styles.store(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// The error of the first SVG image past [`budget`]'s limits, which refuses the SVG holding it.
|
||||
pub(in crate::import) fn refused(&self) -> Option<SvgError> {
|
||||
self.refused.lock().ok().and_then(|r| r.clone())
|
||||
}
|
||||
|
||||
fn note_text(&self, svg: &str) {
|
||||
if svg.contains("<text") {
|
||||
self.text.store(true, Ordering::Relaxed);
|
||||
@@ -559,6 +576,10 @@ impl Files {
|
||||
|
||||
/// [`Files::svg`]'s image once it is counted.
|
||||
fn svg_tree(&self, text: &str, file: Option<Arc<SvgFile>>, opts: &usvg::Options) -> Option<ImageKind> {
|
||||
// The SVG holding it is refused already: no more images are read.
|
||||
if self.refused().is_some() {
|
||||
return None;
|
||||
}
|
||||
// Measured as the SVG holding it is (`import`): the text before the XML reader.
|
||||
let used = self.depth.load(Ordering::Relaxed);
|
||||
let left = MAX_ELEMENT_DEPTH.saturating_sub(used);
|
||||
@@ -569,6 +590,19 @@ impl Files {
|
||||
let Some(height) = depth::height(&xml, left) else { return leave_out(&self.too_deep, true) };
|
||||
// usvg's parser would overflow on it, as on the SVG holding it (`import`).
|
||||
super::convolve_matrices_ok(&xml).ok()?;
|
||||
// What usvg would make of it, measured as the SVG holding it is (`import`), its copied
|
||||
// styles counted with theirs while usvg reads it, and kept only when it decodes: past the
|
||||
// limits, the whole SVG is refused.
|
||||
let spent = self.styles.load(Ordering::Relaxed);
|
||||
match budget::check(&xml, budget::MAX_COPIED_STYLES.saturating_sub(spent)) {
|
||||
Ok(bytes) => self.styles.store(spent.saturating_add(bytes), Ordering::Relaxed),
|
||||
Err(e) => {
|
||||
if let Ok(mut refused) = self.refused.lock() {
|
||||
refused.get_or_insert(e);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.depth.store(used + height, Ordering::Relaxed);
|
||||
let within = self.within.lock().ok().map(|mut w| w.push(file)).is_some();
|
||||
// usvg's `Tree::from_data_nested`, except that hrefs to files go to the resolver.
|
||||
@@ -592,7 +626,11 @@ impl Files {
|
||||
w.pop();
|
||||
}
|
||||
self.depth.store(used, Ordering::Relaxed);
|
||||
let tree = tree.ok()?;
|
||||
let Ok(tree) = tree else {
|
||||
// Left out: neither it nor the images inside it count.
|
||||
self.styles.store(spent, Ordering::Relaxed);
|
||||
return None;
|
||||
};
|
||||
self.note_text(text);
|
||||
Some(ImageKind::SVG(tree))
|
||||
}
|
||||
|
||||
@@ -366,6 +366,10 @@ pub enum SvgError {
|
||||
/// nested-image expansion bombs, #3564).
|
||||
#[error("the SVG expands to more than {0} nested SVG images")]
|
||||
TooManyImages(usize),
|
||||
/// The style values the reader would copy into the elements the SVG expands to, `<use>`
|
||||
/// copies and SVG images included, add up to more than this many megabytes.
|
||||
#[error("the SVG's styles expand to more than {0} MB")]
|
||||
StylesTooLarge(usize),
|
||||
}
|
||||
|
||||
/// Export a document as an SVG string (linked images are not returned: see [`export_full`]).
|
||||
|
||||
@@ -851,6 +851,66 @@ fn nested_image_within_budget_imports_and_use_fan_out_is_refused() {
|
||||
assert!(matches!(err, SvgError::TooLarge(15)), "{err:?}");
|
||||
}
|
||||
|
||||
/// Groups `g0` to `g{levels}`: `g0` a rectangle styled `style`, each other two `<use>`s of the
|
||||
/// one before; one `<use>` of the last shown.
|
||||
fn use_chain(levels: usize, style: &str) -> String {
|
||||
let links: String = (1..=levels).map(|i| format!(r##"<g id="g{i}"><use href="#g{0}"/><use href="#g{0}"/></g>"##, i - 1)).collect();
|
||||
svg(&format!(r##"<defs><g id="g0"><rect width="1" height="1" style="{style}"/></g>{links}</defs><use href="#g{levels}"/>"##))
|
||||
}
|
||||
|
||||
/// A style value of 100,000 bytes.
|
||||
fn long_style() -> String {
|
||||
format!("font-family:{}", "a".repeat(100_000))
|
||||
}
|
||||
|
||||
/// A long style that a chain of `<use>`s copies, or that a style sheet rule gives to many
|
||||
/// elements, is refused before usvg copies it; short styles import, a rule that matches nothing
|
||||
/// costing nothing.
|
||||
#[test]
|
||||
fn styles_that_expand_past_the_limit_are_refused() {
|
||||
let err = import_with(&use_chain(10, &long_style()), &ImportOptions::default()).unwrap_err();
|
||||
assert!(matches!(err, SvgError::StylesTooLarge(64)), "{err:?}");
|
||||
assert_eq!(err.to_string(), "the SVG's styles expand to more than 64 MB");
|
||||
// 1,000 groups that one rule gives the long style, without any `<use>`.
|
||||
let sheet = svg(&format!("<style>g{{{}}}</style>{}", long_style(), "<g/>".repeat(1000)));
|
||||
let err = import_with(&sheet, &ImportOptions::default()).unwrap_err();
|
||||
assert!(matches!(err, SvgError::StylesTooLarge(64)), "{err:?}");
|
||||
|
||||
// The chain with a short style shows its 1,024 rectangles.
|
||||
let (d, _) = import_with(&use_chain(10, "fill:red"), &ImportOptions::default()).unwrap();
|
||||
assert_eq!(art(&d).iter().filter(|n| n.children().is_none()).count(), 1024);
|
||||
// 5,000 rectangles of a class with short rules, beside a long rule that matches none of them.
|
||||
let rules = format!(".a{{fill:#123456;stroke:#654321;stroke-width:2;opacity:.5}} .b{{{}}}", long_style());
|
||||
let classed = svg(&format!("<style>{rules}</style>{}", r#"<rect class="a" width="1" height="1"/>"#.repeat(5000)));
|
||||
let (d, _) = import_with(&classed, &ImportOptions::default()).unwrap();
|
||||
assert_eq!(art(&d).len(), 5000);
|
||||
}
|
||||
|
||||
/// A chain of `<use>`s that makes more elements than usvg reads is refused before usvg makes them.
|
||||
#[test]
|
||||
fn a_use_chain_past_the_elements_usvg_reads_is_refused() {
|
||||
let err = import_with(&use_chain(20, ""), &ImportOptions::default()).unwrap_err();
|
||||
assert!(matches!(err, SvgError::TooLarge(n) if n == vectorcraft_svg::MAX_NODES.min(999_999)), "{err:?}");
|
||||
}
|
||||
|
||||
/// An SVG image whose styles expand past the limit refuses the SVG showing it, and the SVG images
|
||||
/// share the limit with the SVG showing them.
|
||||
#[test]
|
||||
fn svg_images_whose_styles_expand_past_the_limit_refuse_the_svg() {
|
||||
let err = import_with(&svg(&image_of("image/svg+xml", &use_chain(10, &long_style()))), &ImportOptions::default()).unwrap_err();
|
||||
assert!(matches!(err, SvgError::StylesTooLarge(64)), "{err:?}");
|
||||
// Each image copies its long style 383 times (about 38 MB): one imports, two together don't.
|
||||
let image = image_of("image/svg+xml", &use_chain(7, &long_style()));
|
||||
let (d, _) = import_with(&svg(&image), &ImportOptions::default()).unwrap();
|
||||
assert!(!art(&d).is_empty());
|
||||
let err = import_with(&svg(&image.repeat(2)), &ImportOptions::default()).unwrap_err();
|
||||
assert!(matches!(err, SvgError::StylesTooLarge(64)), "{err:?}");
|
||||
// An image that doesn't decode (no size) is left out, and what it would copy doesn't count.
|
||||
let sizeless = use_chain(7, &long_style()).replacen(r#"width="200" height="120""#, r#"width="0" height="0""#, 1);
|
||||
let (d, _) = import_with(&svg(&format!("{}{image}", image_of("image/svg+xml", &sizeless).repeat(2))), &ImportOptions::default()).unwrap();
|
||||
assert!(!art(&d).is_empty());
|
||||
}
|
||||
|
||||
/// Nested SVG images exceeding `MAX_NESTED_SVG_IMAGES` are left out with a warning note (#3564).
|
||||
#[test]
|
||||
fn nested_svg_images_past_count_cap_are_left_out() {
|
||||
|
||||
@@ -6332,6 +6332,7 @@ digits Malayalam Malaiàlam
|
||||
@msg the spine has no segments l’espina no té segments
|
||||
@msg the stroke has no weight el traç no té gruix
|
||||
@msg the SVG's elements are nested more than {0} deep els elements de l’SVG estan imbricats a més de {0} nivells de profunditat
|
||||
@msg the SVG's styles expand to more than {0} MB els estils de l’SVG s’expandeixen a més de {0} MB
|
||||
@msg the target layer is locked or hidden la capa de destinació està blocada o amagada
|
||||
@msg the thumbnail image is malformed la imatge de la miniatura està mal formada
|
||||
@msg the top object must be a path, compound path or text object to use as a clipping mask l’objecte superior ha de ser un traçat, un traçat compost o un objecte de text per fer-lo servir com a màscara de retall
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -9008,6 +9008,7 @@ breakText Glyphs Glyfy
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) soubor RTF je příliš velký na umístění jako text (nejvýše {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG se rozvine na více než {0} vnořených obrázků SVG
|
||||
@msg the SVG expands to more than {0} objects SVG se rozvine na více než {0} objektů
|
||||
@msg the SVG's styles expand to more than {0} MB styly SVG se rozvinou na více než {0} MB
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes soubor SVG je větší než {MAX_IMPORT_BYTES} bajtů
|
||||
@msg the SVG has no drawable objects SVG nemá žádné vykreslitelné objekty
|
||||
@msg the SVGZ file unpacks to more than {_1} MB soubor SVGZ se rozbalí na více než {_1} MB
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8576,6 +8576,7 @@ breakText Glyphs Glyphen
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) die RTF-Datei ist zu groß, um als Text platziert zu werden (höchstens {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images das SVG wird zu mehr als {0} verschachtelten SVG-Bildern erweitert
|
||||
@msg the SVG expands to more than {0} objects das SVG wird zu mehr als {0} Objekten erweitert
|
||||
@msg the SVG's styles expand to more than {0} MB die Stile des SVG werden auf mehr als {0} MB erweitert
|
||||
@msg the SVG has no drawable objects das SVG hat keine zeichenbaren Objekte
|
||||
@msg the SVGZ file unpacks to more than {_1} MB die SVGZ-Datei wird auf mehr als {_1} MB entpackt
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place das Word-Dokument hat mehr als {MAX_PAGES} Seiten an Eigenschaften, zu viele zum Platzieren
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4802,6 +4802,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg result would exceed the canvas (coordinates out of range) το αποτέλεσμα θα υπερέβαινε τον καμβά (συντεταγμένες εκτός εύρους)
|
||||
@msg objects can't be nested more than 32 deep τα αντικείμενα δεν μπορούν να έχουν ένθεση μεγαλύτερη των 32 επιπέδων
|
||||
@msg the SVG's elements are nested more than {0} deep τα στοιχεία SVG έχουν ένθεση μεγαλύτερη των {0} επιπέδων
|
||||
@msg the SVG's styles expand to more than {0} MB τα στυλ του SVG επεκτείνονται σε περισσότερα από {0} MB
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} το scale πρέπει να είναι αριθμός από {_1} έως {_2}, όχι {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} το scale πρέπει να είναι αριθμός από {_1} έως {_2}, όχι {s}
|
||||
@msg select (or target) objects whose appearance isn't basic επιλέξτε (ή στοχεύστε) αντικείμενα των οποίων η εμφάνιση δεν είναι βασική
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4846,6 +4846,7 @@ axis Both Ambos
|
||||
@msg result would exceed the canvas (coordinates out of range) el resultado excedería el lienzo (coordenadas fuera de rango)
|
||||
@msg objects can't be nested more than 32 deep los objetos no pueden anidarse a más de 32 niveles
|
||||
@msg the SVG's elements are nested more than {0} deep los elementos del SVG están anidados a más de {0} niveles
|
||||
@msg the SVG's styles expand to more than {0} MB los estilos del SVG se expanden a más de {0} MB
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} scale debe ser un número de {_1} a {_2}, no {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} scale debe ser un número de {_1} a {_2}, no {s}
|
||||
@msg select (or target) objects whose appearance isn't basic selecciona (o apunta a) objetos cuya apariencia no sea básica
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5061,6 +5061,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg result would exceed the canvas (coordinates out of range) tulos ylittäisi kankaan (koordinaatit alueen ulkopuolella)
|
||||
@msg objects can't be nested more than 32 deep objekteja ei voi sisäkkäistää yli 32 tason syvyyteen
|
||||
@msg the SVG's elements are nested more than {0} deep elementit ovat SVG-tiedostossa sisäkkäin yli {0} tason syvyydessä
|
||||
@msg the SVG's styles expand to more than {0} MB SVG-tiedoston tyylit laajenevat yli {0} megatavuksi
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} scale-arvon on oltava luku väliltä {_1}–{_2}, ei {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} scale-arvon on oltava luku väliltä {_1}–{_2}, ei {s}
|
||||
@msg select (or target) objects whose appearance isn't basic valitse (tai kohdista) objektit, joiden ulkoasu ei ole perusulkoasu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -8872,6 +8872,7 @@ breakText Glyphs Glyphes
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) le fichier RTF est trop volumineux pour être importé comme texte (au plus {_1} Mo)
|
||||
@msg the SVG expands to more than {0} nested SVG images le SVG se développe en plus de {0} images SVG imbriquées
|
||||
@msg the SVG expands to more than {0} objects le SVG se développe en plus de {0} objets
|
||||
@msg the SVG's styles expand to more than {0} MB les styles du SVG se développent en plus de {0} Mo
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes le fichier SVG dépasse {MAX_IMPORT_BYTES} octets
|
||||
@msg the SVG has no drawable objects le SVG n’a aucun objet dessinable
|
||||
@msg the SVGZ file unpacks to more than {_1} MB le fichier SVGZ se décompresse en plus de {_1} Mo
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8280,6 +8280,7 @@ antiAlias Sharp शार्प
|
||||
@msg the subpath is closed or too short to close सबपैथ बंद है या बंद करने के लिए बहुत कम है
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG {0} नेस्टेड SVG इमेज से अधिक का विस्तार करता है
|
||||
@msg the SVG expands to more than {0} objects SVG {0} ऑब्जेक्ट्स से अधिक का विस्तार करता है
|
||||
@msg the SVG's styles expand to more than {0} MB SVG की शैलियाँ {0} MB से अधिक तक विस्तारित होती हैं
|
||||
@msg the SVG has no drawable objects SVG में कोई आकर्षक ऑब्जेक्ट नहीं है
|
||||
@msg the SVG's elements are nested more than {0} deep SVG के तत्वों को {0} से अधिक गहरा घोंसला दिया जाता है
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ फ़ाइल ने {_1} MB से अधिक के लिए अनपैक किया
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8877,6 +8877,7 @@ breakText Glyphs Glifák
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) az RTF-fájl túl nagy ahhoz, hogy szövegként elhelyezhető legyen (legfeljebb {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images az SVG kibontva több mint {0} beágyazott SVG-képet tartalmaz
|
||||
@msg the SVG expands to more than {0} objects az SVG kibontva több mint {0} objektumot tartalmaz
|
||||
@msg the SVG's styles expand to more than {0} MB az SVG stílusai kibontva több mint {0} MB-ot foglalnak
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes az SVG-fájl nagyobb, mint {MAX_IMPORT_BYTES} bájt
|
||||
@msg the SVG has no drawable objects az SVG-ben nincsenek kirajzolható objektumok
|
||||
@msg the SVGZ file unpacks to more than {_1} MB az SVGZ-fájl kicsomagolva nagyobb, mint {_1} MB
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8624,6 +8624,7 @@ breakText Glyphs Glifi
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) il file RTF è troppo grande per essere inserito come testo (al massimo {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images l'SVG si espande in più di {0} immagini SVG annidate
|
||||
@msg the SVG expands to more than {0} objects l'SVG si espande in più di {0} oggetti
|
||||
@msg the SVG's styles expand to more than {0} MB gli stili dell'SVG si espandono a oltre {0} MB
|
||||
@msg the SVG has no drawable objects l'SVG non ha oggetti disegnabili
|
||||
@msg the SVGZ file unpacks to more than {_1} MB il file SVGZ si decomprime in più di {_1} MB
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place il documento Word ha più di {MAX_PAGES} pagine di proprietà, troppe per essere inserito
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -7835,6 +7835,7 @@ axis Y Axis Y 軸
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) RTF ファイルが大きすぎて、文字として配置できません (最大 {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG を展開すると、入れ子の SVG 画像が {0} 個を超えます
|
||||
@msg the SVG expands to more than {0} objects SVG を展開すると、オブジェクトが {0} 個を超えます
|
||||
@msg the SVG's styles expand to more than {0} MB SVG のスタイルを展開すると {0} MB を超えます
|
||||
@msg the SVG has no drawable objects SVG に描画できるオブジェクトがありません
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ ファイルを展開すると {_1} MB を超えます
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 文書のプロパティが {MAX_PAGES} ページを超えており、多すぎて配置できません
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5678,6 +5678,7 @@ imageTrace Outline კონტური
|
||||
@msg object has no weighted stroke ობიექტს ცვლადი სისქის კონტური არ აქვს
|
||||
@msg objects can't be nested more than 32 deep ობიექტების ჩადგმის სიღრმე 32-ს ვერ გადააჭარბებს
|
||||
@msg the SVG's elements are nested more than {0} deep SVG-ის ელემენტების ჩადგმის სიღრმე {0}-ს აჭარბებს
|
||||
@msg the SVG's styles expand to more than {0} MB SVG-ის სტილები გაშლისას {0} MB-ს აჭარბებს
|
||||
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode უცნობი control `{name}`: fillStroke, coloring, drawMode ან screenMode
|
||||
@plural agrees with {n} default shortcut|agrees with {n} default shortcuts ემთხვევა ნაგულისხმევ მალსახმობებს: {n}|ემთხვევა ნაგულისხმევ მალსახმობებს: {n}
|
||||
@plural {n} part of the file wasn't read|{n} parts of the file weren't read ფაილის წაუკითხავი ნაწილები: {n}|ფაილის წაუკითხავი ნაწილები: {n}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8605,6 +8605,7 @@ breakText Glyphs 글리프
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) RTF 파일이 너무 커서 문자로 가져올 수 없습니다(최대 {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG를 확장하면 중첩된 SVG 이미지가 {0}개보다 많아집니다
|
||||
@msg the SVG expands to more than {0} objects SVG를 확장하면 개체가 {0}개보다 많아집니다
|
||||
@msg the SVG's styles expand to more than {0} MB SVG의 스타일을 확장하면 {0}MB를 넘습니다
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes SVG 파일이 {MAX_IMPORT_BYTES}바이트보다 큽니다
|
||||
@msg the SVG has no drawable objects SVG에 그릴 수 있는 개체가 없습니다
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ 파일의 압축을 풀면 {_1} MB를 넘습니다
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6957,6 +6957,7 @@ imageTrace Outline Contour
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) het RTF-bestand is te groot om als tekst te plaatsen (maximaal {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images de SVG wordt uitgevouwen tot meer dan {0} geneste SVG-afbeeldingen
|
||||
@msg the SVG expands to more than {0} objects de SVG wordt uitgevouwen tot meer dan {0} objecten
|
||||
@msg the SVG's styles expand to more than {0} MB de stijlen van de SVG worden uitgevouwen tot meer dan {0} MB
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes het SVG-bestand is groter dan {MAX_IMPORT_BYTES} bytes
|
||||
@msg the SVG has no drawable objects de SVG heeft geen tekenbare objecten
|
||||
@msg the SVGZ file unpacks to more than {_1} MB het SVGZ-bestand wordt uitgepakt tot meer dan {_1} MB
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8862,6 +8862,7 @@ breakText Glyphs Glify
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) plik RTF jest zbyt duży, aby umieścić go jako tekst (najwyżej {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG rozwija się do ponad {0} zagnieżdżonych obrazów SVG
|
||||
@msg the SVG expands to more than {0} objects SVG rozwija się do ponad {0} obiektów
|
||||
@msg the SVG's styles expand to more than {0} MB style SVG rozwijają się do ponad {0} MB
|
||||
@msg the SVG has no drawable objects SVG nie zawiera obiektów, które można narysować
|
||||
@msg the SVGZ file unpacks to more than {_1} MB plik SVGZ po rozpakowaniu ma ponad {_1} MB
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place dokument Word ma ponad {MAX_PAGES} stron właściwości, zbyt wiele, aby go umieścić
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -9074,6 +9074,7 @@ swatch Yellow Green Verde-amarelado
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) o arquivo RTF é grande demais para ser colocado como texto (no máximo {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images o SVG se expande em mais de {0} imagens SVG aninhadas
|
||||
@msg the SVG expands to more than {0} objects o SVG se expande em mais de {0} objetos
|
||||
@msg the SVG's styles expand to more than {0} MB os estilos do SVG se expandem para mais de {0} MB
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes o arquivo SVG tem mais de {MAX_IMPORT_BYTES} bytes
|
||||
@msg the SVG has no drawable objects o SVG não tem objetos desenháveis
|
||||
@msg the SVGZ file unpacks to more than {_1} MB o arquivo SVGZ é descompactado em mais de {_1} MB
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4883,6 +4883,7 @@ axis Both Оба
|
||||
@msg result would exceed the canvas (coordinates out of range) результат превысил бы холст (координаты вне диапазона)
|
||||
@msg objects can't be nested more than 32 deep объекты нельзя вкладывать глубже 32 уровней
|
||||
@msg the SVG's elements are nested more than {0} deep элементы SVG вложены глубже {0} уровней
|
||||
@msg the SVG's styles expand to more than {0} MB после развёртывания стили SVG занимают более {0} МБ
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} scale должен быть числом от {_1} до {_2}, а не {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} scale должен быть числом от {_1} до {_2}, а не {s}
|
||||
@msg select (or target) objects whose appearance isn't basic выделите (или укажите как объект) объекты, оформление которых не является основным
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8650,6 +8650,7 @@ digits Hindi Hindi
|
||||
@msg the subpath has no ends to extend (it is closed or a single point) delbanan har inga ändar att förlänga (den är stängd eller en enkel punkt)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG-filen expanderar till fler än {0} kapslade SVG-bilder
|
||||
@msg the SVG expands to more than {0} objects SVG-filen expanderar till fler än {0} objekt
|
||||
@msg the SVG's styles expand to more than {0} MB SVG-filens stilar expanderar till mer än {0} MB
|
||||
@msg the SVG has no drawable objects SVG-filen har inga ritbara objekt
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ-filen packas upp till mer än {_1} MB
|
||||
@msg the symbols are nested too deeply to expand their instances symbolerna är kapslade för djupt för att deras instanser ska kunna expanderas
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4616,6 +4616,7 @@ axis Both Her İkisi
|
||||
@msg result would exceed the canvas (coordinates out of range) sonuç tuvali aşacaktır (koordinatlar aralık dışındadır)
|
||||
@msg objects can't be nested more than 32 deep nesneler 32'den fazla derinliğe yerleştirilemez
|
||||
@msg the SVG's elements are nested more than {0} deep SVG'nin öğeleri {0} derinliğinden daha fazla yuvalanmıştır
|
||||
@msg the SVG's styles expand to more than {0} MB SVG'nin stilleri genişletildiğinde {0} MB'tan fazla yer kaplıyor
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} scale {_1} ile {_2} arasında bir sayı olmalıdır, {_3} değil
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} scale {_1} ile {_2} arasında bir sayı olmalıdır, {s} değil
|
||||
@msg select (or target) objects whose appearance isn't basic Görünümü temel olmayan nesneleri seçin (veya hedefleyin)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -8490,6 +8490,7 @@ breakText Glyphs Гліфи
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) файл RTF завеликий, щоб помістити його як текст (не більше {_1} МБ)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG розгортається у понад {0} вкладених зображень SVG
|
||||
@msg the SVG expands to more than {0} objects SVG розгортається у понад {0} об’єктів
|
||||
@msg the SVG's styles expand to more than {0} MB стилі SVG розгортаються у понад {0} МБ
|
||||
@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes файл SVG більший за {MAX_IMPORT_BYTES} байтів
|
||||
@msg the SVG has no drawable objects SVG не містить об’єктів, які можна намалювати
|
||||
@msg the SVGZ file unpacks to more than {_1} MB файл SVGZ розпаковується у понад {_1} МБ
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5257,6 +5257,7 @@ theme Appearance Giao diện
|
||||
@msg result would exceed the canvas (coordinates out of range) kết quả sẽ vượt quá vùng vẽ (tọa độ ngoài phạm vi)
|
||||
@msg objects can't be nested more than 32 deep không thể lồng các đối tượng sâu quá 32 cấp
|
||||
@msg the SVG's elements are nested more than {0} deep các phần tử SVG lồng nhau sâu quá {0} cấp
|
||||
@msg the SVG's styles expand to more than {0} MB các kiểu của SVG khi mở rộng chiếm hơn {0} MB
|
||||
@msg Save for Web: the image is empty Lưu cho Web: ảnh trống
|
||||
@msg Save writes {_1}, not {_2}: use document.export for other formats Lưu ghi {_1}, không phải {_2}: dùng document.export cho các định dạng khác
|
||||
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Lưu ghi {_1}, không phải {_2}: dùng file.saveAsTemplate để ghi định dạng này
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -8848,6 +8848,7 @@ breakText Glyphs 字形
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) RTF 文件过大,无法作为文字置入 (最多 {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG 展开后的嵌套 SVG 图像超过 {0} 个
|
||||
@msg the SVG expands to more than {0} objects SVG 展开后的对象超过 {0} 个
|
||||
@msg the SVG's styles expand to more than {0} MB SVG 的样式展开后超过 {0} MB
|
||||
@msg the SVG has no drawable objects SVG 中没有可绘制的对象
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ 文件解压后超过 {_1} MB
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 文档的属性页超过 {MAX_PAGES} 页,过多而无法置入
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8773,6 +8773,7 @@ breakText Glyphs 字符
|
||||
@msg the RTF file is too large to place as type (at most {_1} MB) RTF 檔案過大,無法置入為文字 (最多 {_1} MB)
|
||||
@msg the SVG expands to more than {0} nested SVG images SVG 展開後超過 {0} 個巢狀 SVG 影像
|
||||
@msg the SVG expands to more than {0} objects SVG 展開後超過 {0} 個物件
|
||||
@msg the SVG's styles expand to more than {0} MB SVG 的樣式展開後超過 {0} MB
|
||||
@msg the SVG has no drawable objects SVG 中沒有可繪製的物件
|
||||
@msg the SVGZ file unpacks to more than {_1} MB SVGZ 檔案解壓縮後超過 {_1} MB
|
||||
@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 文件的屬性超過 {MAX_PAGES} 頁,數量過多而無法置入
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1833,6 +1833,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- An image that Open and Place store again keeps the ICC profile its file embeds and its 16-bit samples. A turned PNG, JPEG, WebP or TIFF stored as PNG, and a TIFF the image decoder reads, keep their gray or RGB samples at 8 or 16 bits with their gray or RGB profile in the PNG's `iCCP` chunk ([PNG specification, iCCP](https://www.w3.org/TR/png-3/#11iCCP)). A CMYK TIFF stored with one plane per ink and a turned CMYK JPEG or TIFF keep their CMYK profile as TIFF tag 34675, a JPEG's put together from all its APP2 segments ([ICC image-format guide](https://github.com/InternationalColorConsortium/iccDEV/blob/master/iccdev-mcp/docs/icc-format-guide.md)). A profile whose color space differs from the stored samples' is left out. The profile is kept in the stored image; how CMYK inks are drawn and exported to PDF does not change (`crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- SVG import shows a raster `<image>` whose EXIF orientation or TIFF Orientation tag turns or flips it upright, linked or as a data URL, and inside an SVG shown as an image: the pixels are stored turned, the width and height swap, and the `<image>` box fits the turned image under `preserveAspectRatio`, as CSS `image-orientation: from-image` describes (<https://www.w3.org/TR/css-images-3/#the-image-orientation>). A turned CMYK JPEG or TIFF keeps its inks as a CMYK TIFF, as does a CMYK TIFF that is not turned. TIFF data URLs (`image/tiff`, or untyped TIFF bytes) are read, and TIFFs with an alpha or other extra channel, CCITT Group 3 TIFFs and CMYK TIFFs stored one plane per ink are read as Open and Place read them. The pixels are decoded within the decode allowance, and a TIFF claiming more strips or tiles than the decoder can count is refused before it is decoded (`crates/svg/tests/it/image_orientation.rs`).
|
||||
- SVG import leaves out an SVG shown as an image that it can't read (text that isn't XML, an SVG without a size) and goes on with the images after it, as SVG 2's error processing describes (<https://www.w3.org/TR/SVG2/conform.html>). Only images that decode count toward the 256 nested SVG images an SVG may show, so an image after many broken ones is imported. Every nested SVG image read, decoded or not, counts toward VectorCraft's own limits of 1,024 reads and 512 MB of text per import; past them, nested SVG images are left out with a warning (`crates/svg/tests/it/import_details.rs`).
|
||||
- SVG import measures what an SVG expands to before the SVG reader builds it: every copy a `<use>` shows (<https://www.w3.org/TR/SVG2/struct.html#UseElement>), chains of `<use>`s included, and the style values each copy gets from its `style` attribute and from the style sheet rules that match it. An SVG that expands to more than 999,999 elements (500,000 on the web) or to more than 64 MB of copied style values is refused with an error, and so is a loop of `<use>` references that would be copied without end; both limits are VectorCraft's own. An SVG shown as an image counts toward the same 64 MB as the SVG showing it, and one past either limit refuses the whole import (`crates/svg/src/import/budget.rs`, `crates/svg/tests/it/import_details.rs`).
|
||||
- SVG import reads the files that a linked SVG file's own `<image>` elements link to, relative paths from that file's folder (<https://www.rfc-editor.org/rfc/rfc3986#section-5.1.3>), as it reads the imported SVG's links: a raster stays linked, an SVG file imports as vector art, and one that can't be read gets the placeholder, its link and a warning. SVG 2 tells a viewer not to load these links (<https://www.w3.org/TR/SVG2/conform.html#secure-static-mode>); reading them is VectorCraft's own rule. The same href in SVG files of two folders names two files. A link that leads back to an SVG file showing it gets the placeholder and a warning, and reading the links of linked SVG files stops at the nesting depth cap and at 1,024 reads and 512 MB per import, the same limits decoded nested SVG images have, counted separately from them. The text of every linked SVG file read counts, and so does every file read for the links of a linked SVG file, raster or SVG; rasters the imported SVG links to don't. Once either limit is passed, no file linked from a linked SVG file is read and its image is left out with a warning, and an SVG file the imported SVG links to is still shown, without the files its own links name, with a warning. Another href to an SVG file already read, spelled differently or in another SVG file, does not read it again, unless a link back or the depth cap changes what it shows there (`crates/svg/tests/it/import_details.rs`, `crates/svg/src/import/files.rs`).
|
||||
- File › Place and a file dropped on the canvas refuse a PNG, JPEG, GIF or WebP file whose pixels can't be read, such as one cut short by a download or a sync still running, and the status bar shows "Couldn't place <name>: the image's pixels can't be read". Paste and Relink refuse such an image too, and Update Links, or Open when it reads the file again, counts it as missing. A JPEG cut short still places when its pixels can be decoded (`file.place`, `crates/engine/src/tests_place.rs`).
|
||||
- On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them.
|
||||
|
||||
Reference in New Issue
Block a user