mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-15 03:36:51 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c10da25ce4 |
No files matched your search
@@ -214,6 +214,7 @@ pub fn import(bytes: &[u8], o: &ImportOptions) -> Result<Imported, String> {
|
||||
let (mut document, mut warnings) = build::Build::new(&d, o).run(layout, natural, ratio)?;
|
||||
document.units = if o.fit { unit } else { o.unit.unwrap_or(unit) };
|
||||
warnings.extend(damaged);
|
||||
document.limit_depth_noting(&mut warnings);
|
||||
Ok(Imported { document, warnings })
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ pub struct Clip {
|
||||
}
|
||||
|
||||
/// Deepest nesting of clips, and of groups, an importer records ([`nest`] goes one level deeper
|
||||
/// for each): deeper ones are left out.
|
||||
pub const MAX_NEST: usize = 128;
|
||||
/// for each): deeper ones are left out. As deep as a document's objects may nest
|
||||
/// ([`crate::MAX_DEPTH`]) under a layer, around the art; clips in groups that deep together are
|
||||
/// merged by [`Document::limit_depth`].
|
||||
pub const MAX_NEST: usize = crate::MAX_DEPTH - 2;
|
||||
|
||||
/// The warning for clips left out for nesting deeper than [`MAX_NEST`].
|
||||
pub fn deep_clips() -> String {
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
//! How deep objects may nest.
|
||||
//!
|
||||
//! The tree is walked by recursion everywhere (drawing, bounds, hit tests, saving, exporting), so
|
||||
//! a tree of unbounded depth overflows the stack: an abort no guard catches. No document deeper
|
||||
//! than [`MAX_DEPTH`] is ever kept, nor one whose symbols and patterns draw deeper than
|
||||
//! [`MAX_DRAWN_DEPTH`] (a symbol holding an instance of itself, or a pattern painting its own
|
||||
//! tile, draws without end): importers and the native reader merge deeper objects into the group
|
||||
//! around them and leave such instances and paints out
|
||||
//! ([`Document::limit_depth`], with [`merged_note`] and [`symbols_note`] as their warnings), and
|
||||
//! the engine refuses an edit that would nest deeper ([`Document::too_deep`]).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::Paint;
|
||||
|
||||
use crate::{Document, Node, NodeKind};
|
||||
|
||||
/// The deepest an object may be: a top-level layer is 1 deep, what it holds 2, and so on (the art
|
||||
/// of an opacity mask is one deeper than the object it masks). A symbol's or pattern's art counts
|
||||
/// from 1 again.
|
||||
///
|
||||
/// A native file writes three levels of JSON for each level of objects, and its reader stops at
|
||||
/// 128 levels: 32 leaves the deepest object 29 of them for its own data, so every document that
|
||||
/// can be made can be saved and opened again.
|
||||
pub const MAX_DEPTH: usize = 32;
|
||||
|
||||
/// The deepest an object may be drawn: as [`MAX_DEPTH`], the art of a symbol counted under its
|
||||
/// instance and a pattern's tile under the object it paints (and what those use, under them).
|
||||
/// Symbols a few objects deep nest in one another dozens deep within it.
|
||||
pub const MAX_DRAWN_DEPTH: usize = 4 * MAX_DEPTH;
|
||||
|
||||
/// The warning of a file whose objects were nested deeper than [`MAX_DEPTH`].
|
||||
pub fn merged_note() -> String {
|
||||
format!("objects nested more than {MAX_DEPTH} deep were merged into the group around them")
|
||||
}
|
||||
|
||||
/// The warning of a file with symbols or patterns that use themselves or are drawn deeper than
|
||||
/// [`MAX_DRAWN_DEPTH`].
|
||||
pub fn symbols_note() -> String {
|
||||
format!(
|
||||
"symbol instances and pattern paints inside their own symbol or pattern, or drawn more than {MAX_DRAWN_DEPTH} objects deep, were left out"
|
||||
)
|
||||
}
|
||||
|
||||
/// Run `f` where recursion has room: on a thread of its own with a 16 MB stack (the main thread
|
||||
/// has 1 MB on Windows), or here where there are no threads (the web) or none can be started. For
|
||||
/// readers whose recursion is capped but heavy; a panic in `f` goes on to the caller.
|
||||
pub fn with_deep_stack<T: Send>(f: impl Fn() -> T + Sync) -> T {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let threaded = std::thread::scope(|s| {
|
||||
let thread = std::thread::Builder::new().stack_size(16 << 20).spawn_scoped(s, &f).ok()?;
|
||||
Some(thread.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic)))
|
||||
});
|
||||
if let Some(done) = threaded {
|
||||
return done;
|
||||
}
|
||||
}
|
||||
f()
|
||||
}
|
||||
|
||||
/// Is anything in `now` (each `depth` deep) deeper than [`MAX_DEPTH`], or `over` (given its
|
||||
/// depth)? Without recursion: the trees may be of any depth. With `before` (the same list before
|
||||
/// an edit, within the caps), only what the edit changed is looked at: a subtree still shared with
|
||||
/// it, in the same place, is as it was.
|
||||
fn deeper(now: &[Arc<Node>], before: Option<&[Arc<Node>]>, depth: usize, over: &dyn Fn(&Node, usize) -> bool) -> bool {
|
||||
type Pending<'a> = Vec<(&'a Node, Option<&'a Node>, usize)>;
|
||||
fn push<'a>(stack: &mut Pending<'a>, now: &'a [Arc<Node>], before: Option<&'a [Arc<Node>]>, depth: usize) {
|
||||
for (i, n) in now.iter().enumerate() {
|
||||
let was = before.and_then(|b| b.get(i));
|
||||
if !was.is_some_and(|w| Arc::ptr_eq(w, n)) {
|
||||
stack.push((n, was.map(|w| &**w), depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut stack = vec![];
|
||||
push(&mut stack, now, before, depth);
|
||||
while let Some((n, was, d)) = stack.pop() {
|
||||
if d > MAX_DEPTH || over(n, d) {
|
||||
return true;
|
||||
}
|
||||
if let Some(ch) = n.children() {
|
||||
push(&mut stack, ch, was.and_then(Node::children).map(Vec::as_slice), d + 1);
|
||||
}
|
||||
if let Some(m) = &n.extras().mask {
|
||||
let was = was.and_then(|w| w.extras().mask.as_ref()).map(|m| std::slice::from_ref(&m.art));
|
||||
push(&mut stack, std::slice::from_ref(&m.art), was, d + 1);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// [`deeper`] for the nesting alone.
|
||||
fn nested_deeper(now: &[Arc<Node>], depth: usize) -> bool {
|
||||
deeper(now, None, depth, &|_, _| false)
|
||||
}
|
||||
|
||||
/// Merge what is deeper than [`MAX_DEPTH`] in `nodes` (each `depth` deep) into the group around
|
||||
/// it: a container at the cap is replaced by what it holds (a clipping group's clipping path and
|
||||
/// an opacity mask too deep are left out); what it held stays hidden or locked as it was. Returns
|
||||
/// whether anything changed. Recurses no deeper than the cap.
|
||||
fn limit(nodes: &mut Vec<Arc<Node>>, depth: usize) -> bool {
|
||||
let mut changed = false;
|
||||
let mut i = 0;
|
||||
while let Some(n) = nodes.get_mut(i) {
|
||||
if !nested_deeper(std::slice::from_ref(n), depth) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
let n = Arc::make_mut(n);
|
||||
if n.extras().mask.as_ref().is_some_and(|m| nested_deeper(std::slice::from_ref(&m.art), depth + 1)) {
|
||||
n.extras_mut().mask = None;
|
||||
}
|
||||
if depth < MAX_DEPTH {
|
||||
if let Some(ch) = n.children_mut() {
|
||||
limit(ch, depth + 1);
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let clip = matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. });
|
||||
let (visible, locked) = (n.visible, n.locked);
|
||||
let mut held = n.children_mut().map(std::mem::take).unwrap_or_default();
|
||||
if clip && !held.is_empty() {
|
||||
held.remove(0);
|
||||
}
|
||||
for c in &mut held {
|
||||
if (!visible && c.visible) || (locked && !c.locked) {
|
||||
let c = Arc::make_mut(c);
|
||||
c.visible &= visible;
|
||||
c.locked |= locked;
|
||||
}
|
||||
}
|
||||
// What it held takes its place, and is looked at in its turn.
|
||||
nodes.splice(i..=i, held);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// A symbol (`false`) or a pattern (`true`) of the document, by name: what art draws by name, an
|
||||
/// instance its symbol's art and a pattern paint its tile's.
|
||||
type Def<'a> = (bool, &'a str);
|
||||
|
||||
/// How deep each symbol's and pattern's art is drawn, the art of the symbols and patterns it uses
|
||||
/// included (drawn under the object that uses them): `None` for one that uses itself, in its art
|
||||
/// or through what that uses, or is drawn deeper than [`MAX_DRAWN_DEPTH`].
|
||||
type Heights<'a> = HashMap<Def<'a>, Option<usize>>;
|
||||
|
||||
/// What `n` itself draws by name: the symbol it is an instance of, the patterns that paint it.
|
||||
fn uses(n: &Node) -> impl Iterator<Item = Def<'_>> {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => Some((false, symbol.as_str())),
|
||||
_ => None,
|
||||
};
|
||||
let patterns = crate::swatches::node_paints(n).filter_map(|p| match p {
|
||||
Paint::Pattern { pattern, .. } => Some((true, pattern.as_str())),
|
||||
_ => None,
|
||||
});
|
||||
symbol.into_iter().chain(patterns)
|
||||
}
|
||||
|
||||
/// [`Heights`] of the document's symbols and patterns. Without recursion: they may use one
|
||||
/// another in a chain of any length. Their own art is within [`MAX_DEPTH`].
|
||||
fn heights(doc: &Document) -> Heights<'_> {
|
||||
// Each one's own depth, and what its art uses with the deepest use of each.
|
||||
type Own<'a> = (usize, Vec<(Def<'a>, usize)>);
|
||||
let arts = (doc.symbols.iter().map(|s| ((false, s.name.as_str()), std::slice::from_ref(&s.art))))
|
||||
.chain(doc.patterns.iter().map(|p| ((true, p.name.as_str()), &p.art[..])));
|
||||
let mut own: HashMap<Def, Own> = HashMap::new();
|
||||
for (def, art) in arts {
|
||||
let (mut deepest, mut used) = (0, HashMap::<Def, usize>::new());
|
||||
let mut stack: Vec<(&Node, usize)> = art.iter().map(|n| (&**n, 1)).collect();
|
||||
while let Some((n, d)) = stack.pop() {
|
||||
deepest = deepest.max(d);
|
||||
for u in uses(n) {
|
||||
let at = used.entry(u).or_default();
|
||||
*at = (*at).max(d);
|
||||
}
|
||||
stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| (&**c, d + 1)));
|
||||
}
|
||||
// (The first of two of one name is the one drawn.)
|
||||
own.entry(def).or_insert((deepest, used.into_iter().collect()));
|
||||
}
|
||||
// Depth first over what uses what. One met again while it is being worked out (`None` in its
|
||||
// entry) uses itself.
|
||||
let mut found: HashMap<Def, Option<Option<usize>>> = HashMap::new();
|
||||
let starts: Vec<Def> = own.keys().copied().collect();
|
||||
for start in starts {
|
||||
if found.contains_key(&start) {
|
||||
continue;
|
||||
}
|
||||
// (which, the next of its uses to look at, the deepest found so far: `None` once it can't be drawn)
|
||||
let mut path: Vec<(Def, usize, Option<usize>)> = vec![(start, 0, own.get(&start).map(|o| o.0))];
|
||||
found.insert(start, None);
|
||||
while let Some(&(def, next, deepest)) = path.last() {
|
||||
let used = own.get(&def).map_or(&[][..], |o| &o.1);
|
||||
let Some(&(other, at)) = used.get(next) else {
|
||||
// Everything it uses is worked out.
|
||||
let deepest = deepest.filter(|d| *d <= MAX_DRAWN_DEPTH);
|
||||
found.insert(def, Some(deepest));
|
||||
path.pop();
|
||||
if let Some((user, next, held)) = path.last_mut() {
|
||||
let at = own.get(user).and_then(|o| o.1.get(next.wrapping_sub(1))).map_or(1, |u| u.1);
|
||||
*held = held.zip(deepest).map(|(h, d)| h.max(at + d));
|
||||
}
|
||||
continue;
|
||||
};
|
||||
let known = found.get(&other).copied();
|
||||
if known.is_none() && own.contains_key(&other) {
|
||||
found.insert(other, None);
|
||||
}
|
||||
let Some((_, next, deepest)) = path.last_mut() else { break };
|
||||
*next += 1;
|
||||
match known {
|
||||
// Not in the document: it draws nothing.
|
||||
None if !own.contains_key(&other) => {}
|
||||
None => path.push((other, 0, own.get(&other).map(|o| o.0))),
|
||||
Some(Some(Some(d))) => *deepest = deepest.map(|h| h.max(at + d)),
|
||||
// It uses itself (it is on the way down here), or can't be drawn.
|
||||
Some(None | Some(None)) => *deepest = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
found.into_iter().map(|(def, h)| (def, h.flatten())).collect()
|
||||
}
|
||||
|
||||
/// Can't `used`, by an object `depth` deep, be drawn there?
|
||||
fn undrawable(heights: &Heights, used: Def, depth: usize) -> bool {
|
||||
heights.get(&used).is_some_and(|h| h.is_none_or(|h| depth + h > MAX_DRAWN_DEPTH))
|
||||
}
|
||||
|
||||
/// Take out of `nodes` (each `depth` deep) the uses `bad` picks (given the depth of the object):
|
||||
/// an instance goes, a pattern paint becomes no paint. Returns whether anything changed. For trees
|
||||
/// within [`MAX_DEPTH`].
|
||||
fn strip(nodes: &mut Vec<Arc<Node>>, depth: usize, bad: &dyn Fn(Def, usize) -> bool) -> bool {
|
||||
let any = |n: &Node, d: usize| uses(n).any(|u| bad(u, d));
|
||||
let before = nodes.len();
|
||||
nodes.retain(|n| !matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if bad((false, symbol.as_str()), depth)));
|
||||
let mut changed = nodes.len() != before;
|
||||
for n in nodes.iter_mut() {
|
||||
if !deeper(std::slice::from_ref(n), None, depth, &any) {
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
let n = Arc::make_mut(n);
|
||||
if uses(n).any(|u| bad(u, depth)) {
|
||||
for p in crate::swatches::node_paints_mut(n) {
|
||||
if matches!(p, Paint::Pattern { pattern, .. } if bad((true, pattern.as_str()), depth)) {
|
||||
*p = Paint::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// An opacity mask whose art is such an instance goes with it.
|
||||
let mut mask = n.extras().mask.as_ref().map(|m| vec![m.art.clone()]);
|
||||
if let Some(art) = &mut mask
|
||||
&& strip(art, depth + 1, bad)
|
||||
{
|
||||
match art.pop() {
|
||||
Some(art) => {
|
||||
if let Some(m) = n.extras_mut().mask.as_mut() {
|
||||
m.art = art;
|
||||
}
|
||||
}
|
||||
None => n.extras_mut().mask = None,
|
||||
}
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
strip(ch, depth + 1, bad);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Is anything nested deeper than [`MAX_DEPTH`], or drawn deeper than [`MAX_DRAWN_DEPTH`]
|
||||
/// (a symbol or pattern that uses itself is)? With `before` (this document before an edit,
|
||||
/// itself within the caps), only what the edit changed is looked at.
|
||||
pub fn too_deep(&self, before: Option<&Document>) -> bool {
|
||||
fn symbol(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
|
||||
d.symbols.get(i).map(|s| std::slice::from_ref(&s.art))
|
||||
}
|
||||
fn pattern(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
|
||||
d.patterns.get(i).map(|p| &p.art[..])
|
||||
}
|
||||
let nested = |_: &Node, _: usize| false;
|
||||
if (0..self.symbols.len()).any(|i| symbol(self, i).is_some_and(|art| deeper(art, before.and_then(|b| symbol(b, i)), 1, &nested)))
|
||||
|| (0..self.patterns.len()).any(|i| pattern(self, i).is_some_and(|art| deeper(art, before.and_then(|b| pattern(b, i)), 1, &nested)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if self.symbols.is_empty() && self.patterns.is_empty() {
|
||||
return deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &nested);
|
||||
}
|
||||
let heights = heights(self);
|
||||
if heights.values().any(Option::is_none) {
|
||||
return true;
|
||||
}
|
||||
// Symbols and patterns draw as deep as before when none changed; else every use of them
|
||||
// is looked at again.
|
||||
let same = |x: &[Arc<Node>], y: &[Arc<Node>]| x.len() == y.len() && x.iter().zip(y).all(|(x, y)| Arc::ptr_eq(x, y));
|
||||
let before = before.filter(|b| {
|
||||
b.symbols.len() == self.symbols.len()
|
||||
&& b.patterns.len() == self.patterns.len()
|
||||
&& b.symbols.iter().zip(&self.symbols).all(|(x, y)| x.name == y.name && Arc::ptr_eq(&x.art, &y.art))
|
||||
&& b.patterns.iter().zip(&self.patterns).all(|(x, y)| x.name == y.name && same(&x.art, &y.art))
|
||||
});
|
||||
let over = |n: &Node, d: usize| uses(n).any(|u| undrawable(&heights, u, d));
|
||||
deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &over)
|
||||
}
|
||||
|
||||
/// [`Self::limit_depth`] → whether objects were merged, and whether uses of symbols and
|
||||
/// patterns were left out.
|
||||
fn limit_depths(&mut self) -> (bool, bool) {
|
||||
let mut merged = limit(&mut self.layers, 1);
|
||||
for s in &mut self.symbols {
|
||||
// The art itself is 1 deep, so it stays the one object it is.
|
||||
let mut art = vec![s.art.clone()];
|
||||
if limit(&mut art, 1)
|
||||
&& let Some(one) = art.pop()
|
||||
{
|
||||
s.art = one;
|
||||
merged = true;
|
||||
}
|
||||
}
|
||||
for p in &mut self.patterns {
|
||||
merged |= limit(&mut p.art, 1);
|
||||
}
|
||||
if self.symbols.is_empty() && self.patterns.is_empty() {
|
||||
return (merged, false);
|
||||
}
|
||||
// The symbols and patterns that can't be drawn lose what their art uses: each is then as
|
||||
// deep as its own art, and what uses it can be drawn.
|
||||
let (symbols, patterns): (Vec<usize>, Vec<usize>) = {
|
||||
let heights = heights(self);
|
||||
let bad = |def: Def| heights.get(&def).is_some_and(Option::is_none);
|
||||
(
|
||||
(0..self.symbols.len()).filter(|i| self.symbols.get(*i).is_some_and(|s| bad((false, s.name.as_str())))).collect(),
|
||||
(0..self.patterns.len()).filter(|i| self.patterns.get(*i).is_some_and(|p| bad((true, p.name.as_str())))).collect(),
|
||||
)
|
||||
};
|
||||
let mut left_out = !(symbols.is_empty() && patterns.is_empty());
|
||||
let all = |_: Def, _: usize| true;
|
||||
for i in symbols {
|
||||
let Some(s) = self.symbols.get_mut(i) else { continue };
|
||||
let mut art = vec![s.art.clone()];
|
||||
strip(&mut art, 1, &all);
|
||||
// (An instance alone was the whole art: an empty group stands in.)
|
||||
s.art = art.pop().unwrap_or_else(|| Arc::new(Node::group(s.art.id, vec![])));
|
||||
}
|
||||
for i in patterns {
|
||||
if let Some(p) = self.patterns.get_mut(i) {
|
||||
strip(&mut p.art, 1, &all);
|
||||
}
|
||||
}
|
||||
// Then the uses drawn too deep where they are.
|
||||
let mut layers = self.layers.clone();
|
||||
left_out |= {
|
||||
let heights = heights(self);
|
||||
strip(&mut layers, 1, &|used, depth| undrawable(&heights, used, depth))
|
||||
};
|
||||
self.layers = layers;
|
||||
(merged, left_out)
|
||||
}
|
||||
|
||||
/// Bring the document within the caps: merge the objects nested deeper than [`MAX_DEPTH`]
|
||||
/// into the group around them, and leave out the uses of symbols and patterns that can't be
|
||||
/// drawn (inside their own symbol or pattern, or deeper than [`MAX_DRAWN_DEPTH`]). What an
|
||||
/// importer does to what it read. Returns whether anything changed.
|
||||
pub fn limit_depth(&mut self) -> bool {
|
||||
let (merged, left_out) = self.limit_depths();
|
||||
merged || left_out
|
||||
}
|
||||
|
||||
/// [`Self::limit_depth`], adding [`merged_note`] and [`symbols_note`] to an importer's
|
||||
/// `warnings` for what it did.
|
||||
pub fn limit_depth_noting(&mut self, warnings: &mut Vec<String>) {
|
||||
let (merged, left_out) = self.limit_depths();
|
||||
warnings.extend(merged.then(merged_note));
|
||||
warnings.extend(left_out.then(symbols_note));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{NodeId, OpacityMask};
|
||||
|
||||
/// One path in `levels` groups, each holding a second path beside the next group.
|
||||
fn nested(levels: usize) -> Arc<Node> {
|
||||
let mut id = 0;
|
||||
let mut next = || {
|
||||
id += 1;
|
||||
NodeId(id)
|
||||
};
|
||||
let path = |id| Arc::new(Node::path(id, Default::default(), Default::default()));
|
||||
let mut inner = path(next());
|
||||
for _ in 0..levels {
|
||||
inner = Arc::new(Node::group(next(), vec![inner, path(next())]));
|
||||
}
|
||||
inner
|
||||
}
|
||||
|
||||
fn doc(art: Arc<Node>) -> Document {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
|
||||
ch.push(art);
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_at_the_cap_is_left_alone() {
|
||||
// The layer, the groups and the path in them: MAX_DEPTH deep.
|
||||
let mut d = doc(nested(MAX_DEPTH - 2));
|
||||
let before = d.clone();
|
||||
assert!(!d.too_deep(None));
|
||||
assert!(!d.limit_depth());
|
||||
assert_eq!(d, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeper_objects_are_merged_into_the_group_around_them() {
|
||||
for levels in [MAX_DEPTH - 1, MAX_DEPTH + 5, 100_000] {
|
||||
let mut d = doc(nested(levels));
|
||||
assert!(d.too_deep(None), "{levels}");
|
||||
assert!(d.limit_depth(), "{levels}");
|
||||
assert!(!d.too_deep(None), "{levels}");
|
||||
// Every path is kept, under the layer and the groups down to the cap.
|
||||
assert_eq!(d.node_count(), 1 + (MAX_DEPTH - 2) + (levels + 1), "{levels}");
|
||||
assert!(!d.limit_depth(), "nothing more to merge");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clipping_path_and_a_mask_too_deep_are_left_out_and_hidden_art_stays_hidden() {
|
||||
let mut inner = Node::new(NodeId(9000), NodeKind::Group { children: vec![nested(0), nested(0)], clip: true });
|
||||
inner.visible = false;
|
||||
let mut art = Arc::new(inner);
|
||||
for i in 0..MAX_DEPTH {
|
||||
art = Arc::new(Node::group(NodeId(9001 + i as u64), vec![art]));
|
||||
}
|
||||
let mut masked = Node::group(NodeId(8000), vec![]);
|
||||
masked.extras_mut().mask = Some(Box::new(OpacityMask::new((*nested(MAX_DEPTH)).clone(), true)));
|
||||
let mut d = doc(art);
|
||||
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
|
||||
ch.push(Arc::new(masked));
|
||||
}
|
||||
assert!(d.limit_depth());
|
||||
assert!(!d.too_deep(None));
|
||||
let mut leaves = vec![];
|
||||
d.walk(|n| {
|
||||
if n.children().is_none() {
|
||||
leaves.push(n.visible);
|
||||
}
|
||||
});
|
||||
assert_eq!(leaves, [false], "the clipping path is gone, what it clipped is still hidden");
|
||||
assert!(d.node(NodeId(8000)).is_some_and(|n| n.extras().mask.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edit_is_checked_by_what_it_changed() {
|
||||
let before = doc(nested(MAX_DEPTH - 2));
|
||||
let mut wrapped = before.clone();
|
||||
if let Some(ch) = wrapped.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
|
||||
let all = std::mem::take(ch);
|
||||
ch.push(Arc::new(Node::group(NodeId(7000), all)));
|
||||
}
|
||||
assert!(wrapped.too_deep(Some(&before)), "the same art, one deeper");
|
||||
let mut beside = before.clone();
|
||||
if let Some(ch) = beside.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
|
||||
ch.push(nested(3));
|
||||
}
|
||||
assert!(!beside.too_deep(Some(&before)));
|
||||
assert!(!before.too_deep(Some(&before)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_and_patterns_are_limited_too() {
|
||||
let mut d = Document::new(10.0, 10.0);
|
||||
d.symbols.push(crate::Symbol { name: "s".into(), art: nested(MAX_DEPTH + 3) });
|
||||
assert!(d.too_deep(None));
|
||||
assert!(d.limit_depth());
|
||||
assert!(!d.too_deep(None));
|
||||
assert!(d.symbols.first().is_some_and(|s| s.art.children().is_some()), "still one object");
|
||||
}
|
||||
|
||||
fn instance(id: u64, of: &str) -> Arc<Node> {
|
||||
Arc::new(Node::new(NodeId(id), NodeKind::SymbolInstance { symbol: of.into(), xf: Default::default() }))
|
||||
}
|
||||
|
||||
/// `count` symbols, each `levels` groups around an instance of the next; the last one's of `last`.
|
||||
fn symbols(count: usize, levels: usize, last: &str) -> Document {
|
||||
let mut d = doc(instance(1, "s0"));
|
||||
for i in 0..count {
|
||||
let mut art = instance(2, &if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
|
||||
for _ in 0..levels {
|
||||
art = Arc::new(Node::group(NodeId(3), vec![art]));
|
||||
}
|
||||
d.symbols.push(crate::Symbol { name: format!("s{i}"), art });
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
fn instances(d: &Document) -> usize {
|
||||
let mut n = 0;
|
||||
let mut count = |x: &Node| n += usize::from(matches!(x.kind, NodeKind::SymbolInstance { .. }));
|
||||
d.walk(&mut count);
|
||||
d.symbols.iter().for_each(|s| s.art.walk(&mut count));
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_in_symbols_are_drawn_only_so_deep() {
|
||||
// Three objects deep each (two groups and the instance), under the instance on the layer.
|
||||
let fits = MAX_DRAWN_DEPTH / 3 - 1;
|
||||
let mut d = symbols(fits, 2, "none");
|
||||
assert!(!d.too_deep(None) && !d.limit_depth());
|
||||
let mut d = symbols(fits + 2, 2, "none");
|
||||
assert!(d.too_deep(None));
|
||||
let before = instances(&d);
|
||||
assert!(d.limit_depth() && !d.too_deep(None));
|
||||
assert!(instances(&d) < before && instances(&d) > 0, "only the instances drawn too deep go");
|
||||
// A chain of any length is measured, not walked by recursion.
|
||||
let mut d = symbols(100_000, 0, "none");
|
||||
assert!(d.too_deep(None) && d.limit_depth() && !d.too_deep(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_holding_itself_loses_the_instance() {
|
||||
for (count, last) in [(1, "s0"), (3, "s0"), (3, "s1")] {
|
||||
let mut d = symbols(count, 1, last);
|
||||
assert!(d.too_deep(None), "{count} {last}");
|
||||
let mut warnings = vec![];
|
||||
d.limit_depth_noting(&mut warnings);
|
||||
assert_eq!(warnings, [symbols_note()]);
|
||||
assert!(!d.too_deep(None), "{count} {last}");
|
||||
assert!(d.symbols.iter().all(|s| s.art.children().is_some()), "the art is still a group");
|
||||
}
|
||||
// An edit that makes one hold itself is seen, whatever else stayed as it was.
|
||||
let before = symbols(2, 1, "none");
|
||||
let mut after = before.clone();
|
||||
after.symbols[1].art = Arc::new(Node::group(NodeId(9), vec![instance(8, "s0")]));
|
||||
assert!(!before.too_deep(None) && after.too_deep(Some(&before)));
|
||||
}
|
||||
|
||||
/// A path painted with the pattern named `with`.
|
||||
fn painted(id: u64, with: &str) -> Arc<Node> {
|
||||
let fill = Paint::Pattern { pattern: with.into(), xf: Default::default() };
|
||||
Arc::new(Node::path(NodeId(id), Default::default(), crate::Appearance::basic(fill, Paint::None, 0.0)))
|
||||
}
|
||||
|
||||
fn pattern_fills(d: &Document) -> usize {
|
||||
let mut n = 0;
|
||||
d.visit_paints(&mut |p| n += usize::from(matches!(p, Paint::Pattern { .. })));
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pattern_painting_its_own_tile_loses_the_paint() {
|
||||
// One that paints its own tile; two that paint each other's; one in a symbol it holds.
|
||||
let itself = |d: &mut Document| d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "p")]));
|
||||
let each_other = |d: &mut Document| {
|
||||
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
|
||||
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "p")]));
|
||||
};
|
||||
let through_a_symbol = |d: &mut Document| {
|
||||
d.patterns.push(crate::PatternDef::new("p", vec![instance(2, "s")]));
|
||||
d.symbols.push(crate::Symbol { name: "s".into(), art: painted(3, "p") });
|
||||
};
|
||||
for make in [&itself as &dyn Fn(&mut Document), &each_other, &through_a_symbol] {
|
||||
let mut d = doc(painted(1, "p"));
|
||||
make(&mut d);
|
||||
assert!(d.too_deep(None));
|
||||
let mut warnings = vec![];
|
||||
d.limit_depth_noting(&mut warnings);
|
||||
assert_eq!(warnings, [symbols_note()]);
|
||||
assert!(!d.too_deep(None));
|
||||
assert_eq!(pattern_fills(&d), 1, "the object on the layer keeps its pattern");
|
||||
}
|
||||
// A pattern whose tile is painted with another is fine.
|
||||
let mut d = doc(painted(1, "p"));
|
||||
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
|
||||
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "none")]));
|
||||
assert!(!d.too_deep(None) && !d.limit_depth());
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod blend;
|
||||
pub mod clipnest;
|
||||
pub mod cmyk;
|
||||
pub mod corners;
|
||||
pub mod depth;
|
||||
pub mod graph;
|
||||
pub mod hit;
|
||||
mod id_index;
|
||||
@@ -79,6 +80,7 @@ pub use appearance::{
|
||||
};
|
||||
pub use assets::ExportAsset;
|
||||
pub use corners::LiveCorners;
|
||||
pub use depth::MAX_DEPTH;
|
||||
pub use graph::{
|
||||
GraphDesign, GraphKind, GraphSpec, LabelStyles, MAX_GRAPH_CATEGORIES, MAX_GRAPH_SERIES, PiePosition, PieSort, SeriesPaint, TickLength,
|
||||
ValueAxisSide,
|
||||
|
||||
@@ -224,7 +224,7 @@ fn item_paint_mut(it: &mut AppearanceItem) -> &mut Paint {
|
||||
}
|
||||
|
||||
/// The paints a node holds itself (not its children): appearance items, then text runs.
|
||||
fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
pub(crate) fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
let runs = match &n.kind {
|
||||
NodeKind::Text(t) => t.runs.as_slice(),
|
||||
_ => &[],
|
||||
@@ -233,7 +233,7 @@ fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
}
|
||||
|
||||
/// Mutable [`node_paints`], in the same order.
|
||||
fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
pub(crate) fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
let runs = match &mut n.kind {
|
||||
NodeKind::Text(t) => t.runs.as_mut_slice(),
|
||||
_ => &mut [],
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) fn import(name: &str, bytes: &[u8]) -> Result<Imported> {
|
||||
Ok(d) => {
|
||||
let mut b = Builder { doc: Document::new(0.0, 0.0), warnings: Vec::new(), k: 72.0 / d.dpi, layer: 0 };
|
||||
b.document(&d)?;
|
||||
b.doc.limit_depth_noting(&mut b.warnings);
|
||||
let mut warnings = d.warnings.iter().map(|w| format!("Affinity: not imported or approximated: {w}.")).collect::<Vec<_>>();
|
||||
warnings.extend(b.warnings.iter().map(|w| format!("Affinity: {w}.")));
|
||||
Ok(Imported { doc: b.doc, warnings, preview_only: false })
|
||||
|
||||
@@ -139,7 +139,8 @@ fn restore_or_import(
|
||||
/// (with a warning for each that can't be used).
|
||||
pub(crate) fn native_file(bytes: &[u8]) -> Result<(Document, vectorcraft_format::FileInfo, Vec<String>)> {
|
||||
let f = vectorcraft_format::load_file(bytes).map_err(err)?;
|
||||
let warnings = super::native::install_profiles(f.profiles);
|
||||
let mut warnings = super::native::install_profiles(f.profiles);
|
||||
warnings.extend(f.depth_notes);
|
||||
Ok((f.doc, f.info, warnings))
|
||||
}
|
||||
|
||||
|
||||
@@ -583,6 +583,10 @@ pub(crate) fn is_guide_path(d: &Document, id: NodeId) -> bool {
|
||||
d.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Path { guide: true, .. }))
|
||||
}
|
||||
|
||||
/// The error of an edit that would nest objects deeper than [`vectorcraft_doc::MAX_DEPTH`].
|
||||
pub const TOO_DEEP: &str = "objects can't be nested more than 32 deep";
|
||||
const _: () = assert!(vectorcraft_doc::MAX_DEPTH == 32, "TOO_DEEP (and its catalog rows) names the cap");
|
||||
|
||||
/// Cheap sanity check after an edit: artboards and the objects just touched (the selection) must
|
||||
/// have finite, in-range geometry, so saved files always reload and renderers never see NaN/∞.
|
||||
fn doc_sane(d: &Document, sel: &Selection) -> bool {
|
||||
@@ -1245,6 +1249,9 @@ impl Session {
|
||||
}
|
||||
/// Add a document and make it active.
|
||||
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
|
||||
// Every edit is checked against the document before it, so the first one is within the cap
|
||||
// (importers and the native reader have seen to theirs already, with a warning).
|
||||
doc.limit_depth();
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
// Guides from files saved before guides had layers go on a layer.
|
||||
doc.adopt_guides();
|
||||
@@ -1262,6 +1269,7 @@ impl Session {
|
||||
if index >= self.docs.len() {
|
||||
return false;
|
||||
}
|
||||
doc.limit_depth();
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
doc.adopt_guides();
|
||||
if self.active == Some(index) {
|
||||
@@ -1476,7 +1484,10 @@ impl Session {
|
||||
if !st.doc.variables.bindings.is_empty() {
|
||||
Arc::make_mut(&mut st.doc).prune_variable_bindings();
|
||||
}
|
||||
if doc_sane(&st.doc, &st.selection) {
|
||||
if st.doc.too_deep(Some(&before)) {
|
||||
// Deeper trees overflow the stack of what walks them.
|
||||
Err(EngineError::Other(TOO_DEEP.into()))
|
||||
} else if doc_sane(&st.doc, &st.selection) {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()))
|
||||
|
||||
@@ -2440,3 +2440,345 @@ proptest! {
|
||||
checked("mutated script", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "sample.jsx"}))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- deep nesting ----------
|
||||
//
|
||||
// The tree is walked by recursion, so a file nested deeply enough overflowed the stack: an abort,
|
||||
// which nothing catches. Every reader caps what it reads, and no document is deeper than
|
||||
// `MAX_DEPTH`. These run on a thread with the stack of a Windows main thread (1 MB), where an
|
||||
// overflow ends the test run.
|
||||
|
||||
use vectorcraft_doc::MAX_DEPTH;
|
||||
|
||||
/// `f` on a thread with a 1 MB stack.
|
||||
fn on_a_small_stack(f: impl FnOnce() + Send + 'static) {
|
||||
std::thread::Builder::new().stack_size(1 << 20).spawn(f).unwrap().join().unwrap();
|
||||
}
|
||||
|
||||
/// Open `bytes` as the file `name`: an error, or a document within the cap that renders and
|
||||
/// exports. Returns the document and the import's warnings.
|
||||
fn open_deep(name: &str, bytes: &[u8]) -> Option<(Document, Vec<String>)> {
|
||||
let loaded = vectorcraft_engine::cmd::fileio::load(name, bytes).ok()?;
|
||||
assert!(!loaded.doc.too_deep(None), "{name}: deeper than the cap");
|
||||
let doc = loaded.doc.clone();
|
||||
survive(name, move || Some(doc)).unwrap();
|
||||
Some((loaded.doc, loaded.warnings))
|
||||
}
|
||||
|
||||
fn merged(warnings: &[String]) -> bool {
|
||||
warnings.iter().any(|w| w.contains("nested more than"))
|
||||
}
|
||||
|
||||
fn paths_in(doc: &Document) -> usize {
|
||||
let mut paths = 0;
|
||||
doc.walk(|n| paths += usize::from(n.path_data().is_some()));
|
||||
paths
|
||||
}
|
||||
|
||||
const SVG_NS: &str = "xmlns=\"http://www.w3.org/2000/svg\"";
|
||||
|
||||
/// `n` groups in one another, each named (so it is kept) and with a rectangle beside the next.
|
||||
fn nested_svg(n: usize) -> String {
|
||||
let open: String = (0..n).map(|i| format!("<g id=\"g{i}\"><rect width=\"5\" height=\"5\"/>")).collect();
|
||||
format!("<svg {SVG_NS} width=\"100\" height=\"100\">{open}<rect width=\"10\" height=\"10\"/>{}</svg>", "</g>".repeat(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_svg_is_refused_or_merged() {
|
||||
on_a_small_stack(|| {
|
||||
// Within what the reader takes: opened, the groups past the cap merged into the last.
|
||||
let (doc, warnings) = open_deep("deep.svg", nested_svg(40).as_bytes()).expect("40 groups open");
|
||||
assert!(merged(&warnings), "{warnings:?}");
|
||||
assert_eq!(paths_in(&doc), 41, "no art is lost");
|
||||
let (_, warnings) = open_deep("shallow.svg", nested_svg(20).as_bytes()).expect("20 groups open");
|
||||
assert!(!merged(&warnings), "{warnings:?}");
|
||||
// As deep as the reader goes (on a stack of its own): opened and merged the same.
|
||||
let most = vectorcraft_svg::MAX_ELEMENT_DEPTH;
|
||||
let (doc, warnings) = open_deep("deepest.svg", nested_svg(most - 2).as_bytes()).expect("opens at the reader's cap");
|
||||
assert!(merged(&warnings), "{warnings:?}");
|
||||
assert_eq!(paths_in(&doc), most - 1, "no art is lost");
|
||||
// Past it: an error that says why, whatever the depth.
|
||||
for n in [most, 2 * most, 200_000] {
|
||||
let e = vectorcraft_engine::cmd::fileio::load("deep.svg", nested_svg(n).as_bytes()).err().expect("refused").to_string();
|
||||
assert!(e.contains(&format!("nested more than {most} deep")), "{n}: {e}");
|
||||
}
|
||||
// Unnamed groups of one object aren't kept, but usvg walks them all the same.
|
||||
let plain = format!(
|
||||
"<svg {SVG_NS} width=\"9\" height=\"9\">{}<rect width=\"1\" height=\"1\"/>{}</svg>",
|
||||
"<g>".repeat(2 * most),
|
||||
"</g>".repeat(2 * most)
|
||||
);
|
||||
assert!(open_deep("plain.svg", plain.as_bytes()).is_none());
|
||||
// A chain of `<use>`: shallow XML, a deep tree.
|
||||
let links: String = (0..3000).map(|i| format!("<g id=\"u{i}\"><use href=\"#u{}\"/></g>", i + 1)).collect();
|
||||
let chain = format!(
|
||||
"<svg {SVG_NS} width=\"9\" height=\"9\"><defs>{links}<rect id=\"u3000\" width=\"1\" height=\"1\"/></defs><use href=\"#u0\"/></svg>"
|
||||
);
|
||||
assert!(open_deep("chain.svg", chain.as_bytes()).is_none());
|
||||
// Placed and pasted, as opened.
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||||
for n in [40, 2 * most] {
|
||||
let p = json!({"name": "deep.svg", "dataBase64": vectorcraft_format::base64_encode(nested_svg(n).as_bytes())});
|
||||
let _ = no_panic(s.execute("file.place", &p));
|
||||
let _ = no_panic(s.execute("clipboard.importSvg", &json!({"svg": nested_svg(n)})));
|
||||
let _ = no_panic(s.execute("edit.paste", &json!({})));
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A page whose content is `content`, with `resources`.
|
||||
fn deep_pdf(content: &str, resources: &str) -> Vec<u8> {
|
||||
handmade_pdf(content, resources, "[0 0 100 100]")
|
||||
}
|
||||
|
||||
/// An EPS whose program is `body`.
|
||||
fn deep_eps(body: &str) -> Vec<u8> {
|
||||
format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n").into_bytes()
|
||||
}
|
||||
|
||||
/// A test for each file: it opens as an error or as a document within the cap, on a small stack.
|
||||
macro_rules! deep_files {
|
||||
($($test:ident: $name:literal, $bytes:expr;)*) => {$(
|
||||
#[test]
|
||||
fn $test() {
|
||||
on_a_small_stack(|| {
|
||||
open_deep($name, &$bytes);
|
||||
});
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
deep_files! {
|
||||
// PDF: clips inside clips (saved states never restored); arrays in arrays as an operand and as
|
||||
// an object; dictionaries in dictionaries as a resource and inline; marked content never ended.
|
||||
pdf_clips_in_clips: "clips.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "q 0 0 50 50 re W n ".repeat(20_000)), "<< >>");
|
||||
pdf_arrays_in_an_operand: "arrays.pdf", deep_pdf(&format!("BT {}{} TJ ET 0 0 5 5 re f", "[".repeat(100_000), "]".repeat(100_000)), "<< >>");
|
||||
pdf_arrays_in_an_object: "boxes.pdf", handmade_pdf("0 0 5 5 re f", "<< >>", &format!("{}0 0 100 100{}", "[".repeat(100_000), "]".repeat(100_000)));
|
||||
pdf_dictionaries_in_resources: "dicts.pdf", deep_pdf("0 0 5 5 re f", &format!("{}{}", "<< /A ".repeat(50_000), ">> ".repeat(50_000)));
|
||||
pdf_dictionaries_in_content: "inline.pdf", deep_pdf(&format!("/P {}{} DP 0 0 5 5 re f", "<< /A ".repeat(50_000), ">> ".repeat(50_000)), "<< >>");
|
||||
pdf_marked_content_never_ended: "marked.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "/Span << /MCID 0 >> BDC ".repeat(50_000)), "<< >>");
|
||||
// PostScript: clips inside clips; procedures in procedures; arrays in arrays; a procedure that
|
||||
// calls itself; dictionaries never ended.
|
||||
eps_clips_in_clips: "clips.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "gsave 0 0 50 50 rectclip ".repeat(20_000)));
|
||||
eps_procedures_in_procedures: "procs.eps", deep_eps(&format!("{}{} 0 0 5 5 rectfill", "{".repeat(100_000), "}".repeat(100_000)));
|
||||
eps_arrays_in_arrays: "arrays.eps", deep_eps(&format!("{}{} pop 0 0 5 5 rectfill", "[".repeat(100_000), "]".repeat(100_000)));
|
||||
eps_a_procedure_calling_itself: "runaway.eps", deep_eps("/f { 0 0 5 5 rectfill f } def f");
|
||||
eps_dictionaries_never_ended: "dicts.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "1 dict begin ".repeat(100_000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_dxf_blocks_are_read_within_the_cap() {
|
||||
on_a_small_stack(|| {
|
||||
// Block `B0` inserts `B1`, which inserts `B2`, and so on; the last one holds a line.
|
||||
let pair = |c: i32, v: &str| (c, v.to_string());
|
||||
let mut pairs = vec![pair(0, "SECTION"), pair(2, "BLOCKS")];
|
||||
for i in 0..2_000 {
|
||||
pairs.extend([pair(0, "BLOCK"), pair(2, &format!("B{i}")), pair(10, "0"), pair(20, "0")]);
|
||||
if i + 1 < 2_000 {
|
||||
pairs.extend([pair(0, "INSERT"), pair(2, &format!("B{}", i + 1)), pair(10, "1"), pair(20, "1")]);
|
||||
} else {
|
||||
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "5"), pair(21, "5")]);
|
||||
}
|
||||
pairs.push(pair(0, "ENDBLK"));
|
||||
}
|
||||
pairs.extend([pair(0, "ENDSEC"), pair(0, "SECTION"), pair(2, "ENTITIES"), pair(0, "INSERT"), pair(2, "B0"), pair(10, "0"), pair(20, "0")]);
|
||||
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "9"), pair(21, "9"), pair(0, "ENDSEC"), pair(0, "EOF")]);
|
||||
open_deep("blocks.dxf", dxf_text(&pairs).as_bytes()).expect("opens");
|
||||
});
|
||||
}
|
||||
|
||||
/// A layer holding `levels` groups in one another around a rectangle, each with a second
|
||||
/// rectangle beside the next: `levels + 2` deep.
|
||||
fn nested_document(levels: usize) -> Document {
|
||||
use vectorcraft_doc::Node;
|
||||
use vectorcraft_testkit::fixtures::DocBuilder;
|
||||
let mut b = DocBuilder::new(100.0, 100.0);
|
||||
let rect =
|
||||
|b: &mut DocBuilder, x: f64| b.rect_node(vectorcraft_geom::Rect::new(x, 0.0, x + 10.0, 10.0), vectorcraft_color::Color::rgb8(200, 0, 0));
|
||||
let mut inner = rect(&mut b, 0.0);
|
||||
for _ in 0..levels {
|
||||
let beside = rect(&mut b, 20.0);
|
||||
inner = Node::group(b.alloc(), vec![std::sync::Arc::new(inner), std::sync::Arc::new(beside)]);
|
||||
inner.opacity = 0.9;
|
||||
}
|
||||
b.add(inner);
|
||||
b.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_at_the_cap_saves_opens_and_exports() {
|
||||
on_a_small_stack(|| {
|
||||
let doc = nested_document(MAX_DEPTH - 2);
|
||||
assert!(!doc.too_deep(None));
|
||||
let back = vectorcraft_format::load_file(&vectorcraft_format::save_file(&doc)).expect("what can be made opens again");
|
||||
assert!(back.depth_notes.is_empty());
|
||||
assert_eq!(back.doc.layers, doc.layers);
|
||||
survive("at the cap", || Some(doc.clone())).unwrap();
|
||||
// Every format that keeps the nesting reads its own file back within the cap.
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(doc, None);
|
||||
for format in ["svg", "pdf", "eps", "ai"] {
|
||||
let r = s.execute("document.export", &json!({"format": format})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||||
open_deep(&format!("cap.{format}"), &bytes).unwrap_or_else(|| panic!("{format} opens"));
|
||||
}
|
||||
for cmd in ["select.all", "document.inspect", "document.info", "edit.copy", "edit.paste", "object.ungroup", "edit.undo"] {
|
||||
no_panic(s.execute(cmd, &json!({}))).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_native_file_nested_too_deep_is_merged_or_refused() {
|
||||
on_a_small_stack(|| {
|
||||
// Saved before the cap by a version that could still open it: opened, the deepest merged.
|
||||
let old = vectorcraft_format::save_file(&nested_document(MAX_DEPTH + 2));
|
||||
let (doc, warnings) = open_deep("old.vectorcraft", &old).expect("opens");
|
||||
assert!(merged(&warnings), "{warnings:?}");
|
||||
assert_eq!(paths_in(&doc), MAX_DEPTH + 3, "no art is lost");
|
||||
// Deeper than any version could open, and a hostile file: an error that says why. Written
|
||||
// by hand: a tree this deep can't be built and saved.
|
||||
let text = String::from_utf8(vectorcraft_format::save(&nested_document(0), false)).unwrap();
|
||||
let (open, close) = ("{\"id\":900000,\"kind\":{\"type\":\"group\",\"children\":[", "]}}");
|
||||
let at = text.find("\"children\":[").expect("the layer's children") + "\"children\":[".len();
|
||||
for levels in [100, 50_000] {
|
||||
let deep = format!("{}{}{},{}", &text[..at], open.repeat(levels), close.repeat(levels), &text[at..]);
|
||||
let e = vectorcraft_engine::cmd::fileio::load("deep.vectorcraft", deep.as_bytes()).err().expect("refused").to_string();
|
||||
assert!(e.contains("nested too deep"), "{levels}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_cannot_nest_deeper_than_the_cap() {
|
||||
on_a_small_stack(|| {
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
let mut grouped = 0;
|
||||
for _ in 0..10_000 {
|
||||
match no_panic(s.execute("object.group", &json!({}))) {
|
||||
Ok(_) => grouped += 1,
|
||||
Err(e) => assert!(e.to_string().contains(vectorcraft_engine::TOO_DEEP), "after {grouped} groups: {e}"),
|
||||
}
|
||||
}
|
||||
// The layer, the groups and the rectangle.
|
||||
assert_eq!(grouped, MAX_DEPTH - 2);
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None));
|
||||
// Other ways down: a clipping mask, a copy pasted, a symbol made of it.
|
||||
for (cmd, p) in [
|
||||
("object.clippingMask.make", json!({})),
|
||||
("edit.copy", json!({})),
|
||||
("edit.pasteInFront", json!({})),
|
||||
("symbol.new", json!({"name": "deep"})),
|
||||
] {
|
||||
let _ = no_panic(s.execute(cmd, &p));
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd}");
|
||||
}
|
||||
survive("grouped to the cap", || Some((*s.doc().unwrap().doc).clone())).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/// A document whose one object is an instance of the first of `count` symbols: each symbol's art
|
||||
/// is `levels` groups around an instance of the next symbol, the last one of `last`.
|
||||
fn symbols_document(count: usize, levels: usize, last: &str) -> Document {
|
||||
use vectorcraft_doc::{Node, NodeKind, Symbol};
|
||||
let instance =
|
||||
|doc: &mut Document, of: String| Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: of, xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let mut doc = nested_document(1);
|
||||
for i in 0..count {
|
||||
let mut art = instance(&mut doc, if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
|
||||
for _ in 0..levels {
|
||||
art = Node::group(doc.alloc_id(), vec![std::sync::Arc::new(art)]);
|
||||
}
|
||||
doc.symbols.push(Symbol { name: format!("s{i}"), art: std::sync::Arc::new(art) });
|
||||
}
|
||||
let first = instance(&mut doc, "s0".into());
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, first).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
/// `doc` saved (as an older version, or a hostile file, could have) and opened again: within the
|
||||
/// caps (`kept`: as it was), drawn, exported and edited.
|
||||
fn symbols_survive(what: &str, doc: Document, kept: bool) {
|
||||
let (back, warnings) = open_deep("symbols.vectorcraft", &vectorcraft_format::save_file(&doc)).expect("opens");
|
||||
assert_eq!(!warnings.iter().any(|w| w.contains("symbol instances")), kept, "{what}: {warnings:?}");
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(back, None);
|
||||
for cmd in [
|
||||
"select.all",
|
||||
"document.inspect",
|
||||
"document.info",
|
||||
"object.expand",
|
||||
"symbol.breakLink",
|
||||
"object.flattenTransparency",
|
||||
"edit.copy",
|
||||
"edit.paste",
|
||||
] {
|
||||
let _ = no_panic(s.execute(cmd, &json!({})));
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None), "{what}: {cmd}");
|
||||
}
|
||||
for format in ["svg", "pdf", "eps", "png"] {
|
||||
let _ = no_panic(s.execute("document.export", &json!({"format": format})));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_symbol_holding_itself_opens_without_the_instance() {
|
||||
on_a_small_stack(|| {
|
||||
symbols_survive("a symbol holding itself", symbols_document(1, 1, "s0"), false);
|
||||
symbols_survive("symbols holding one another", symbols_document(3, 2, "s0"), false);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_in_symbols_are_drawn_only_so_deep() {
|
||||
on_a_small_stack(|| {
|
||||
// As deep as symbols draw: each one's art as deep as art goes.
|
||||
symbols_survive("symbols at the cap", symbols_document(3, MAX_DEPTH - 2, "none"), true);
|
||||
symbols_survive("symbols in symbols", symbols_document(40, MAX_DEPTH - 1, "none"), false);
|
||||
// A symbol can't be made to hold itself, or one that holds it, by editing either.
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(symbols_document(2, 1, "none"), None);
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
for (cmd, p) in [
|
||||
("symbol.new", json!({"name": "s1"})),
|
||||
("symbol.update", json!({"name": "s1"})),
|
||||
("symbol.update", json!({"name": "s0"})),
|
||||
("symbol.replace", json!({"name": "s1"})),
|
||||
] {
|
||||
let _ = no_panic(s.execute(cmd, &p));
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd} {p}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pattern_painting_its_own_tile_opens_without_the_paint() {
|
||||
on_a_small_stack(|| {
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Appearance, PatternDef};
|
||||
let painted = |doc: &mut Document, with: &str| {
|
||||
let fill = Paint::Pattern { pattern: with.into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 20.0, 20.0));
|
||||
vectorcraft_doc::Node::path(doc.alloc_id(), square, Appearance::basic(fill, Paint::None, 0.0))
|
||||
};
|
||||
// One pattern painting its own tile, and two painting each other's.
|
||||
for names in [["p", "p"], ["p", "q"]] {
|
||||
let mut doc = nested_document(1);
|
||||
let (first, second) = (painted(&mut doc, names[1]), painted(&mut doc, names[0]));
|
||||
doc.patterns.push(PatternDef::new(names[0], vec![std::sync::Arc::new(first)]));
|
||||
if names[0] != names[1] {
|
||||
doc.patterns.push(PatternDef::new(names[1], vec![std::sync::Arc::new(second)]));
|
||||
}
|
||||
let on_layer = painted(&mut doc, "p");
|
||||
let layer = doc.layers[0].id;
|
||||
doc.insert(Some(layer), usize::MAX, on_layer).unwrap();
|
||||
symbols_survive("a pattern painting its own tile", doc, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
//! Every operation counts against a budget and procedures nest only so deep, so a looping or
|
||||
//! recursing program ends with [`PsError::Limit`] instead of hanging.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use vectorcraft_geom::Affine;
|
||||
@@ -86,7 +85,7 @@ struct SaveMark {
|
||||
}
|
||||
|
||||
fn new_dict() -> DictRef {
|
||||
Rc::new(RefCell::new(Dict::new()))
|
||||
DictRef::new(Dict::new())
|
||||
}
|
||||
|
||||
/// An encoding vector: 256 `/.notdef` names (type is read as Latin-1 whatever the encoding).
|
||||
|
||||
@@ -268,5 +268,7 @@ fn finish_with(out: Out, layers: Vec<Arc<Node>>) -> Imported {
|
||||
}
|
||||
}
|
||||
doc.layers = layers;
|
||||
Imported { document: doc, warnings: out.warnings, preview: false }
|
||||
let mut warnings = out.warnings;
|
||||
doc.limit_depth_noting(&mut warnings);
|
||||
Imported { document: doc, warnings, preview: false }
|
||||
}
|
||||
@@ -993,6 +993,7 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr
|
||||
}
|
||||
None => unmark(&mut doc.layers, true, &mut vec![]),
|
||||
}
|
||||
doc.limit_depth_noting(&mut notes);
|
||||
Ok((doc, notes))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,73 @@ use super::graphics::GState;
|
||||
pub(crate) type Dict = HashMap<Key, Obj>;
|
||||
|
||||
/// A shared, mutable dictionary.
|
||||
pub(crate) type DictRef = Rc<RefCell<Dict>>;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DictRef(Rc<RefCell<Dict>>);
|
||||
|
||||
impl DictRef {
|
||||
pub fn new(d: Dict) -> Self {
|
||||
Self(Rc::new(RefCell::new(d)))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DictRef {
|
||||
type Target = Rc<RefCell<Dict>>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DictRef {
|
||||
fn drop(&mut self) {
|
||||
if Rc::strong_count(&self.0) == 1
|
||||
&& let Ok(mut d) = self.0.try_borrow_mut()
|
||||
{
|
||||
drop_held(d.drain().map(|(_, v)| v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// What the composite objects being dropped held, waiting for their turn (while any is).
|
||||
static HELD: RefCell<Option<Vec<Obj>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Drop what a composite object held, now that nothing else refers to it, without recursion: a
|
||||
/// program nests arrays and dictionaries as deep as it likes (`[ exch ]` in a loop), and dropping
|
||||
/// them one inside the other overflows the stack. What `held` holds in its turn waits in [`HELD`]
|
||||
/// for the outermost drop, which works through it.
|
||||
fn drop_held(held: impl Iterator<Item = Obj>) {
|
||||
let holds = |o: &Obj| matches!(o, Obj::Array { .. } | Obj::Dict(_) | Obj::File { .. } | Obj::GState(_));
|
||||
// (Without the list, when the thread is ending: dropped as they come.)
|
||||
let outermost = HELD.try_with(|h| {
|
||||
let Ok(mut h) = h.try_borrow_mut() else { return false };
|
||||
let outermost = h.is_none();
|
||||
h.get_or_insert_with(Vec::new).extend(held.filter(holds));
|
||||
outermost
|
||||
});
|
||||
if !matches!(outermost, Ok(true)) {
|
||||
return;
|
||||
}
|
||||
// Each drop here puts what it held on the list instead of dropping it.
|
||||
while let Ok(Some(next)) = HELD.try_with(|h| h.try_borrow_mut().ok().and_then(|mut h| h.as_mut().and_then(Vec::pop))) {
|
||||
drop(next);
|
||||
}
|
||||
let _ = HELD.try_with(|h| h.try_borrow_mut().map(|mut h| *h = None)); // Nothing to do when the thread is ending.
|
||||
}
|
||||
|
||||
/// What [`Shared`] values may be: bytes, or objects (which may hold more of themselves).
|
||||
pub(crate) trait Held: Sized {
|
||||
/// Drop the values of storage nothing refers to any more.
|
||||
fn release(_values: Vec<Self>) {}
|
||||
}
|
||||
|
||||
impl Held for u8 {}
|
||||
|
||||
impl Held for Obj {
|
||||
fn release(values: Vec<Self>) {
|
||||
drop_held(values.into_iter());
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary key.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -72,7 +138,7 @@ impl Obj {
|
||||
}
|
||||
|
||||
pub fn dict(d: Dict) -> Self {
|
||||
Self::Dict(Rc::new(RefCell::new(d)))
|
||||
Self::Dict(DictRef::new(d))
|
||||
}
|
||||
|
||||
/// The number this is (integers and reals).
|
||||
@@ -135,13 +201,23 @@ impl Obj {
|
||||
/// The values of a string or an array: a run of storage that other strings or arrays may share
|
||||
/// (`getinterval` makes a new object over part of the same values, as PostScript does).
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Shared<T> {
|
||||
pub(crate) struct Shared<T: Held> {
|
||||
buf: Rc<RefCell<Vec<T>>>,
|
||||
at: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T: Clone> Shared<T> {
|
||||
impl<T: Held> Drop for Shared<T> {
|
||||
fn drop(&mut self) {
|
||||
if Rc::strong_count(&self.buf) == 1
|
||||
&& let Ok(mut values) = self.buf.try_borrow_mut()
|
||||
{
|
||||
T::release(std::mem::take(&mut *values));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Held> Shared<T> {
|
||||
pub fn new(v: Vec<T>) -> Self {
|
||||
Self { len: v.len(), buf: Rc::new(RefCell::new(v)), at: 0 }
|
||||
}
|
||||
@@ -198,7 +274,7 @@ impl<T: Clone> Shared<T> {
|
||||
}
|
||||
|
||||
/// Hashes what [`Shared::same`] compares: the storage and the run of it.
|
||||
impl<T> Hash for Shared<T> {
|
||||
impl<T: Held> Hash for Shared<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Rc::as_ptr(&self.buf).hash(state);
|
||||
(self.at, self.len).hash(state);
|
||||
|
||||
@@ -389,7 +389,7 @@ impl Interp<'_> {
|
||||
d.insert(Key::name("FontMatrix"), matrix_obj(Affine::scale(0.001)));
|
||||
d.insert(Key::name("FontBBox"), Obj::array(vec![Obj::Int(0), Obj::Int(-250), Obj::Int(1000), Obj::Int(1000)]));
|
||||
d.insert(Key::name("Encoding"), Obj::array(vec![Obj::name(".notdef"); 256]));
|
||||
let font = Rc::new(std::cell::RefCell::new(d));
|
||||
let font = DictRef::new(d);
|
||||
self.fonts.borrow_mut().insert(key, Obj::Dict(font.clone()));
|
||||
Ok(font)
|
||||
}
|
||||
@@ -489,7 +489,7 @@ fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
let mut d = font.borrow().clone();
|
||||
let fm = d.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
|
||||
d.insert(Key::name("FontMatrix"), matrix_obj(m * fm));
|
||||
Rc::new(std::cell::RefCell::new(d))
|
||||
DictRef::new(d)
|
||||
}
|
||||
|
||||
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
|
||||
|
||||
@@ -482,6 +482,9 @@ pub fn load(bytes: &[u8]) -> Result<Document, FormatError> {
|
||||
#[derive(Debug)]
|
||||
pub struct NativeFile {
|
||||
pub doc: Document,
|
||||
/// What was done to bring the document within the nesting caps
|
||||
/// ([`Document::limit_depth_noting`]: a file saved before them, or a damaged one).
|
||||
pub depth_notes: Vec<String>,
|
||||
pub info: FileInfo,
|
||||
/// ICC files by profile name (saved with Embed ICC Profiles; undecodable ones are left out).
|
||||
pub profiles: BTreeMap<String, Vec<u8>>,
|
||||
@@ -511,7 +514,11 @@ pub fn load_info(bytes: &[u8]) -> Result<(Document, FileInfo), FormatError> {
|
||||
/// [`load`], with everything else the file carries.
|
||||
pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
|
||||
let file = unpack::Unpacked::new(bytes)?;
|
||||
let f: File = serde_json::from_slice(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
|
||||
// The JSON reader stops at 128 levels (a document's objects use three each).
|
||||
let f: File = serde_json::from_slice(file.json()).map_err(|e| {
|
||||
let why = if e.to_string().contains("recursion limit") { format!("its objects are nested too deep ({e})") } else { e.to_string() };
|
||||
FormatError::NotVectorcraft(why)
|
||||
})?;
|
||||
if f.format != EXTENSION && f.format != LEGACY_EXTENSION {
|
||||
return Err(FormatError::NotVectorcraft(format!("format is `{}`", f.format)));
|
||||
}
|
||||
@@ -545,8 +552,10 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
|
||||
// Untrusted: capped, named and pointing at objects the file has.
|
||||
doc.tidy_saved_selections();
|
||||
doc.tidy_graph_designs();
|
||||
let mut depth_notes = vec![];
|
||||
doc.limit_depth_noting(&mut depth_notes);
|
||||
let profiles = f.profiles.into_iter().filter_map(|(name, icc)| Some((name, bytes_of(&icc)?))).collect();
|
||||
Ok(NativeFile { doc, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
|
||||
Ok(NativeFile { doc, depth_notes, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
|
||||
}
|
||||
|
||||
/// The members of a native file besides its document (which is skipped, not decoded).
|
||||
|
||||
@@ -102,7 +102,9 @@ pub struct Imported {
|
||||
/// Read an EMF or WMF file (with or without a placeable header) into a document whose one
|
||||
/// artboard is the picture's frame.
|
||||
pub fn import(bytes: &[u8]) -> Result<Imported, String> {
|
||||
import::import(bytes)
|
||||
let mut i = import::import(bytes)?;
|
||||
i.document.limit_depth_noting(&mut i.warnings);
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -149,6 +149,7 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
|
||||
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
|
||||
let original = crate::pages::open(bytes, opts.password.as_deref())?;
|
||||
let picked = crate::pages::picked(opts, original.pages().len())?;
|
||||
picked.iter().filter_map(|&n| original.pages().get(n)).try_for_each(crate::pages::content_too_deep)?;
|
||||
let mut ocgs = Ocgs::read(&original);
|
||||
let route = opts.layers && !ocgs.list.is_empty();
|
||||
// Content that is off imports as hidden layers: an update of the file turns every group on
|
||||
@@ -166,6 +167,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
let all_on = turned_on.is_some();
|
||||
let pdf = turned_on.unwrap_or(original);
|
||||
let pages = pdf.pages();
|
||||
picked.iter().filter_map(|&n| pages.get(n)).try_for_each(crate::pages::content_too_deep)?;
|
||||
|
||||
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
|
||||
let sink2 = sink.clone();
|
||||
@@ -417,6 +419,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.limit_depth_noting(&mut warnings);
|
||||
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text })
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,76 @@ pub fn is_postscript(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
|
||||
}
|
||||
|
||||
/// The deepest a file's arrays and dictionaries may nest. The PDF reader (hayro) reads them by
|
||||
/// recursion, without a cap of its own: a file nested deeply enough overflows the stack, which is
|
||||
/// an abort, not an error. Far beyond what a PDF writer makes.
|
||||
const MAX_OBJECT_NESTING: usize = 256;
|
||||
|
||||
fn too_deep() -> PdfError {
|
||||
PdfError::Parse(format!("its arrays and dictionaries are nested more than {MAX_OBJECT_NESTING} deep"))
|
||||
}
|
||||
|
||||
/// Do the arrays and dictionaries in the text of `pdf` nest deeper than [`MAX_OBJECT_NESTING`]?
|
||||
/// Read as tokens (strings and comments hold none), an object at a time. The data of a stream
|
||||
/// with a filter is skipped: it isn't PDF text until decoded, which this doesn't do, so nesting
|
||||
/// inside compressed streams is not seen here.
|
||||
fn nests_too_deep(pdf: &[u8]) -> bool {
|
||||
let after =
|
||||
|from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
|
||||
let word = |at: usize, w: &[u8]| pdf.get(at..).is_some_and(|rest| rest.starts_with(w));
|
||||
let (mut depth, mut filtered) = (0usize, false);
|
||||
let mut i = 0;
|
||||
while let Some(&c) = pdf.get(i) {
|
||||
i += 1;
|
||||
match c {
|
||||
b'%' => i += pdf.get(i..).and_then(|rest| rest.iter().position(|c| matches!(c, b'\n' | b'\r'))).unwrap_or(pdf.len()),
|
||||
b'(' => {
|
||||
// A string: parentheses nest in it, a backslash takes the next byte.
|
||||
let mut open = 1usize;
|
||||
while open > 0 {
|
||||
match pdf.get(i) {
|
||||
None => return false,
|
||||
Some(b'\\') => i += 1,
|
||||
Some(b'(') => open += 1,
|
||||
Some(b')') => open -= 1,
|
||||
Some(_) => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
b'[' => depth += 1,
|
||||
b']' => depth = depth.saturating_sub(1),
|
||||
b'<' if pdf.get(i) == Some(&b'<') => (depth, i) = (depth + 1, i + 1),
|
||||
b'>' if pdf.get(i) == Some(&b'>') => (depth, i) = (depth.saturating_sub(1), i + 1),
|
||||
// A string in hexadecimal.
|
||||
b'<' => i = after(i, b">").unwrap_or(pdf.len()),
|
||||
b'/' if word(i - 1, b"/Filter") => filtered = true,
|
||||
b'e' if word(i - 1, b"endobj") => (depth, filtered) = (0, false),
|
||||
b'e' if word(i - 1, b"endstream") => i += 8,
|
||||
b's' if word(i - 1, b"stream") => {
|
||||
depth = 0;
|
||||
if std::mem::take(&mut filtered) {
|
||||
i = after(i, b"endstream").unwrap_or(pdf.len());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if depth > MAX_OBJECT_NESTING {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
|
||||
/// password opens a file too.
|
||||
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
|
||||
if is_postscript(bytes) {
|
||||
return Err(PdfError::PostScript);
|
||||
}
|
||||
if nests_too_deep(bytes) {
|
||||
return Err(too_deep());
|
||||
}
|
||||
let password = password.unwrap_or_default();
|
||||
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
|
||||
read(password)
|
||||
@@ -57,6 +121,12 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
|
||||
})
|
||||
}
|
||||
|
||||
/// The error of a page whose content, decoded, nests arrays and dictionaries too deep (see
|
||||
/// [`MAX_OBJECT_NESTING`]): its operands are read the same way as the file's objects.
|
||||
pub(crate) fn content_too_deep(page: &Page<'_>) -> Result<(), PdfError> {
|
||||
if page.page_stream().is_some_and(nests_too_deep) { Err(too_deep()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// The pages and their boxes. An encrypted PDF needs its `password`
|
||||
/// ([`PdfError::NeedsPassword`] / [`PdfError::WrongPassword`]).
|
||||
pub fn info(bytes: &[u8], password: Option<&str>) -> Result<PdfInfo, PdfError> {
|
||||
|
||||
+34
-14
@@ -18,10 +18,13 @@ 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(crate) use fx::BLEND;
|
||||
|
||||
mod css;
|
||||
mod css_transform;
|
||||
mod depth;
|
||||
mod files;
|
||||
mod fx;
|
||||
mod spread;
|
||||
@@ -29,11 +32,30 @@ mod symbol_units;
|
||||
mod text;
|
||||
mod vector_effect;
|
||||
|
||||
/// The XML of `svg`.
|
||||
fn parse(svg: &str) -> Result<roxmltree::Document<'_>, SvgError> {
|
||||
roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
|
||||
.map_err(|e| SvgError::Parse(e.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
|
||||
let (svg, found) = prepass(svg, opts);
|
||||
// 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);
|
||||
if depth::xml_too_deep(svg) {
|
||||
return Err(too_deep());
|
||||
}
|
||||
let xml = parse(svg)?;
|
||||
if depth::too_deep(&xml) {
|
||||
return Err(too_deep());
|
||||
}
|
||||
let (svg, found) = prepass(svg, &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 xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
|
||||
.map_err(|e| SvgError::Parse(e.to_string()))?;
|
||||
let units = RootUnits::of(xml.root_element());
|
||||
let root_name = root_layer_name(xml.root_element());
|
||||
let mut warnings = found.warnings;
|
||||
@@ -165,6 +187,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
|
||||
promote_sublayers(Arc::make_mut(l), &im.sublayer_groups);
|
||||
}
|
||||
}
|
||||
im.doc.limit_depth_noting(&mut im.warnings);
|
||||
Ok((im.doc, im.warnings))
|
||||
}
|
||||
|
||||
@@ -554,7 +577,7 @@ impl Edits {
|
||||
/// * linked files are read ([`files`]);
|
||||
/// * percentage lengths in a `<symbol>` with a `viewBox` become numbers ([`symbol_units`]);
|
||||
/// * what style sheet rules with a `:not()` set goes in `style` attributes ([`not_rules`]).
|
||||
fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
|
||||
fn prepass<'s>(svg: &'s str, xml: &roxmltree::Document, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
|
||||
let mut found = Found::default();
|
||||
// (`:` for prefixed elements such as `<svg:image>`.)
|
||||
let css_transform = has_css_transform(svg);
|
||||
@@ -565,9 +588,6 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
|
||||
{
|
||||
return (svg.into(), found);
|
||||
}
|
||||
let Ok(xml) = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }) else {
|
||||
return (svg.into(), found);
|
||||
};
|
||||
let mut edits = Edits::default();
|
||||
let elements = || xml.descendants().filter(|n| n.is_element());
|
||||
for a in elements().filter(|n| n.tag_name().name() == "a") {
|
||||
@@ -592,24 +612,24 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
|
||||
}
|
||||
let (display, non_scaling) = (svg.contains("display"), svg.contains(vector_effect::NON_SCALING));
|
||||
if display || non_scaling || not {
|
||||
let css = css::Styles::new(&xml);
|
||||
let css = css::Styles::new(xml);
|
||||
// First: an undisplayed object shown for usvg is shown after these.
|
||||
if not && css.has_not_rules() {
|
||||
not_rules(svg, &xml, &css, &mut edits);
|
||||
not_rules(svg, xml, &css, &mut edits);
|
||||
}
|
||||
if display {
|
||||
hidden_objects(svg, &xml, &css, &mut edits, &mut found.hidden);
|
||||
hidden_objects(svg, xml, &css, &mut edits, &mut found.hidden);
|
||||
}
|
||||
if non_scaling {
|
||||
vector_effect::find(svg, &xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
|
||||
vector_effect::find(svg, xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
|
||||
}
|
||||
}
|
||||
found.files = files::Files::read(svg, &xml, opts, &mut edits, &mut found.warnings);
|
||||
found.files = files::Files::read(svg, xml, opts, &mut edits, &mut found.warnings);
|
||||
if !symbols.is_empty() && svg.contains('%') {
|
||||
edits.list.extend(symbol_units::rewrites(&xml));
|
||||
edits.list.extend(symbol_units::rewrites(xml));
|
||||
}
|
||||
if css_transform {
|
||||
edits.list.extend(css_transform::rewrites(svg, &xml));
|
||||
edits.list.extend(css_transform::rewrites(svg, xml));
|
||||
}
|
||||
(edits.apply(svg), found)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>;
|
||||
|
||||
/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. The
|
||||
/// `font` shorthand becomes its longhands, as usvg reads it for the rest of the drawing (#1557).
|
||||
fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
|
||||
pub(super) fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
|
||||
s.split(';')
|
||||
.filter_map(|d| {
|
||||
let (k, v) = d.split_once(':')?;
|
||||
@@ -371,8 +371,13 @@ impl Styles {
|
||||
Self { rules, index, cache: RefCell::default() }
|
||||
}
|
||||
|
||||
/// Do the document's style sheets have any rules?
|
||||
pub(super) fn has_rules(&self) -> bool {
|
||||
!self.rules.is_empty()
|
||||
}
|
||||
|
||||
/// Properties set on `n` by rules and its `style` attribute, after the cascade.
|
||||
fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
|
||||
pub(super) fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
|
||||
if let Some(m) = self.cache.borrow().get(&n.id()) {
|
||||
return m.clone();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
//! How deep an SVG's elements nest, found in the XML before usvg runs: usvg converts the tree by
|
||||
//! recursion, a few stack frames an element, so a file nested deeply enough overflows the stack
|
||||
//! (an abort, not an error). Such a file is refused: its nesting can't be merged before usvg has
|
||||
//! read it, and usvg is what can't read it.
|
||||
//!
|
||||
//! What counts is the tree usvg walks, not the XML alone: a `<use>` holds a copy of what it
|
||||
//! refers to, and a clip path, mask, pattern, marker or filter is converted where it is used
|
||||
//! (`clip-path: url(#a)` on an element of a clip path makes a chain as deep as there are clip
|
||||
//! paths). So each element's depth is the deepest of what it holds and what it refers to.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use usvg::roxmltree;
|
||||
|
||||
use super::css::{Styles, XNode, parse_decls};
|
||||
|
||||
/// The deepest an SVG's elements may nest, references followed. Far beyond what drawing apps
|
||||
/// write, and deeper than a document's objects may nest ([`vectorcraft_doc::MAX_DEPTH`]: wrappers
|
||||
/// that hold one object aren't kept, and what is still deeper is merged into the group around
|
||||
/// it). usvg uses about 15 kB of stack for each level: the importer runs on a thread with 16 MB
|
||||
/// ([`vectorcraft_doc::depth::with_deep_stack`]), except on the web, where there is the one
|
||||
/// 1 MB stack.
|
||||
pub const MAX_ELEMENT_DEPTH: usize = if cfg!(target_arch = "wasm32") { 48 } else { 256 };
|
||||
|
||||
/// Properties whose value an element's descendants use too: the paint server they name is
|
||||
/// converted at each descendant, under everything between.
|
||||
const INHERITED: [&str; 6] = ["fill", "stroke", "marker", "marker-start", "marker-mid", "marker-end"];
|
||||
|
||||
/// The ids of the `url(#id)` references in a property value.
|
||||
fn urls(value: &str) -> impl Iterator<Item = &str> {
|
||||
value.split("url(").skip(1).filter_map(|rest| {
|
||||
let id = rest.trim_start().trim_start_matches(['"', '\'']).strip_prefix('#')?;
|
||||
id.split([')', '"', '\'']).next().map(str::trim).filter(|id| !id.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
/// The elements `n` refers to, each with whether `n`'s descendants use it too.
|
||||
fn references<'a, 'i>(n: XNode<'a, 'i>, ids: &HashMap<&str, XNode<'a, 'i>>, css: &Styles, out: &mut Vec<(XNode<'a, 'i>, bool)>) {
|
||||
let add = |out: &mut Vec<(XNode<'a, 'i>, bool)>, name: &str, value: &str| {
|
||||
let inherited = INHERITED.contains(&name);
|
||||
out.extend(urls(value).filter_map(|id| ids.get(id)).map(|t| (*t, inherited)));
|
||||
};
|
||||
for a in n.attributes() {
|
||||
match a.name() {
|
||||
"href" => out.extend(a.value().trim().strip_prefix('#').and_then(|id| ids.get(id)).map(|t| (*t, false))),
|
||||
// Read with the style sheets' rules below.
|
||||
"style" if css.has_rules() => {}
|
||||
"style" => parse_decls(a.value()).iter().for_each(|(k, v, _)| add(out, k, v)),
|
||||
name => add(out, name, a.value()),
|
||||
}
|
||||
}
|
||||
if css.has_rules() {
|
||||
css.declared(n).iter().for_each(|(k, v)| add(out, k, v));
|
||||
}
|
||||
}
|
||||
|
||||
/// Do the elements of the XML text `svg` nest deeper than [`MAX_ELEMENT_DEPTH`]? Read from the
|
||||
/// text, before any XML reader: roxmltree (ours and usvg's) reads elements by recursion too. Tags
|
||||
/// are counted as the text has them; markup in the values of a document type's entities counts
|
||||
/// ten times (how deep entities may refer to one another), on top of the deepest element.
|
||||
pub(crate) fn xml_too_deep(svg: &str) -> bool {
|
||||
let b = svg.as_bytes();
|
||||
let find =
|
||||
|from: usize, what: &[u8]| b.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
|
||||
// The end of the tag starting at `from` (past its `>`), and whether it closes itself.
|
||||
let tag_end = |from: usize| {
|
||||
let mut quote = None;
|
||||
for (i, c) in b.iter().enumerate().skip(from) {
|
||||
match (quote, c) {
|
||||
(None, b'"' | b'\'') => quote = Some(*c),
|
||||
(Some(q), c) if q == *c => quote = None,
|
||||
(None, b'>') => return (i + 1, b.get(i.wrapping_sub(1)) == Some(&b'/')),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(b.len(), false)
|
||||
};
|
||||
// (now, deepest) of the elements, and of the markup inside the document type.
|
||||
let (mut elements, mut entities) = ((0usize, 0usize), (0usize, 0usize));
|
||||
let mut doctype_end = 0;
|
||||
let mut i = 0;
|
||||
while let Some(at) = b.get(i..).and_then(|rest| rest.iter().position(|c| *c == b'<')).map(|at| i + at) {
|
||||
let rest = b.get(at..).unwrap_or_default();
|
||||
let depth = if at < doctype_end { &mut entities } else { &mut elements };
|
||||
i = if rest.starts_with(b"<!--") {
|
||||
find(at, b"-->").unwrap_or(b.len())
|
||||
} else if rest.starts_with(b"<![CDATA[") {
|
||||
find(at, b"]]>").unwrap_or(b.len())
|
||||
} else if rest.starts_with(b"<?") {
|
||||
find(at, b"?>").unwrap_or(b.len())
|
||||
} else if rest.starts_with(b"<!DOCTYPE") {
|
||||
// Up to the `>` that ends it: after its internal subset (`[` … `]`) when it has one.
|
||||
let plain = tag_end(at).0;
|
||||
let subset = b.get(at..plain).is_some_and(|head| head.contains(&b'['));
|
||||
doctype_end = if subset { find(at, b"]>").unwrap_or(b.len()) } else { plain };
|
||||
at + 2
|
||||
} else if rest.starts_with(b"<!") {
|
||||
// A declaration of the document type: the markup in its value is counted as it comes.
|
||||
at + 2
|
||||
} else if rest.starts_with(b"</") {
|
||||
depth.0 = depth.0.saturating_sub(1);
|
||||
tag_end(at).0
|
||||
} else if at < doctype_end {
|
||||
// In an entity's value, itself in quotes: up to the tag's `>`.
|
||||
let end = find(at, b">").unwrap_or(b.len());
|
||||
depth.1 = depth.1.max(depth.0 + 1);
|
||||
depth.0 += usize::from(b.get(end.wrapping_sub(2)) != Some(&b'/'));
|
||||
end
|
||||
} else {
|
||||
let (end, empty) = tag_end(at);
|
||||
depth.1 = depth.1.max(depth.0 + 1);
|
||||
depth.0 += usize::from(!empty);
|
||||
end
|
||||
};
|
||||
if elements.1 + 10 * entities.1 > MAX_ELEMENT_DEPTH {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Does anything in `xml` nest deeper than [`MAX_ELEMENT_DEPTH`], references followed? Without
|
||||
/// recursion.
|
||||
pub(super) fn too_deep(xml: &roxmltree::Document) -> bool {
|
||||
let elements = || xml.descendants().filter(|n| n.is_element());
|
||||
let ids: HashMap<&str, XNode> = elements().filter_map(|n| Some((n.attribute("id")?, n))).collect();
|
||||
let css = Styles::new(xml);
|
||||
// How deep each element's own tree goes, itself included; 0 while it is being worked out: a
|
||||
// reference back to it then is a loop, which usvg leaves out.
|
||||
let mut heights: HashMap<roxmltree::NodeId, usize> = HashMap::new();
|
||||
enum Step<'a, 'i> {
|
||||
Enter(XNode<'a, 'i>),
|
||||
Leave(XNode<'a, 'i>, Vec<(XNode<'a, 'i>, bool)>),
|
||||
}
|
||||
let mut stack = vec![Step::Enter(xml.root_element())];
|
||||
while let Some(step) = stack.pop() {
|
||||
match step {
|
||||
Step::Enter(n) => {
|
||||
match heights.entry(n.id()) {
|
||||
Entry::Occupied(_) => continue,
|
||||
Entry::Vacant(v) => v.insert(0),
|
||||
};
|
||||
let mut refs = vec![];
|
||||
references(n, &ids, &css, &mut refs);
|
||||
let targets: Vec<XNode> = refs.iter().map(|(t, _)| *t).collect();
|
||||
stack.push(Step::Leave(n, refs));
|
||||
stack.extend(n.children().filter(|c| c.is_element()).chain(targets).map(Step::Enter));
|
||||
}
|
||||
Step::Leave(n, refs) => {
|
||||
let of = |n: XNode| heights.get(&n.id()).copied().unwrap_or(0);
|
||||
let held = n.children().filter(|c| c.is_element()).map(of).max().unwrap_or(0);
|
||||
let used = |inherited: bool| refs.iter().filter(|(_, i)| *i == inherited).map(|(t, _)| of(*t)).max().unwrap_or(0);
|
||||
let height = 1 + held.max(used(false)) + used(true);
|
||||
if height > MAX_ELEMENT_DEPTH {
|
||||
return true;
|
||||
}
|
||||
heights.insert(n.id(), height);
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn deep(svg: &str) -> bool {
|
||||
too_deep(&roxmltree::Document::parse(svg).unwrap())
|
||||
}
|
||||
|
||||
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>"#)
|
||||
}
|
||||
|
||||
/// `n` groups in one another around a rectangle.
|
||||
fn groups(n: usize) -> String {
|
||||
format!("{}<rect width='1' height='1'/>{}", "<g>".repeat(n), "</g>".repeat(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elements_nest_up_to_the_cap() {
|
||||
// The root, the groups and the rectangle.
|
||||
let (at, past) = (svg(&groups(MAX_ELEMENT_DEPTH - 2)), svg(&groups(MAX_ELEMENT_DEPTH - 1)));
|
||||
assert!(!deep(&at) && !xml_too_deep(&at));
|
||||
assert!(deep(&past) && xml_too_deep(&past));
|
||||
assert!(xml_too_deep(&svg(&groups(200_000))), "any depth is measured");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_text_is_measured_as_xml_reads_it() {
|
||||
let many = r#"<g a='>' b="<g>"><rect/></g>"#.repeat(500);
|
||||
assert!(!xml_too_deep(&svg(&many)), "side by side, with `>` and tags in attribute values");
|
||||
let hidden = format!("<!-- {0} --><![CDATA[{0}]]><?pi {0}?>", "<g>".repeat(500));
|
||||
assert!(!xml_too_deep(&svg(&hidden)), "comments, character data and instructions hold no elements");
|
||||
assert!(xml_too_deep(&format!("<svg>{}", "<g>".repeat(MAX_ELEMENT_DEPTH))), "never closed");
|
||||
assert!(!xml_too_deep(&format!("{}<svg/>", "</g>".repeat(2 * MAX_ELEMENT_DEPTH))), "ends without beginnings");
|
||||
// Entities: namespaces as drawing apps declare them, and markup that would be read ten deep.
|
||||
let names = r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ <!ENTITY ns_svg "http://www.w3.org/2000/svg"> ]>"#;
|
||||
assert!(!xml_too_deep(&format!("{names}{}", svg(&groups(30)))));
|
||||
let tenth = MAX_ELEMENT_DEPTH / 10 + 1;
|
||||
let bomb = format!("<!DOCTYPE svg [ <!ENTITY a \"{}&a;{}\"> ]>", "<g>".repeat(tenth), "</g>".repeat(tenth));
|
||||
assert!(xml_too_deep(&format!("{bomb}<svg>&a;</svg>")));
|
||||
assert!(!xml_too_deep(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chain_of_uses_is_as_deep_as_what_it_copies() {
|
||||
let chain = |n: usize| {
|
||||
let defs: String = (0..n).map(|i| format!("<g id='g{i}'><use xlink:href='#g{}'/></g>", i + 1)).collect();
|
||||
svg(&format!("<defs>{defs}<rect id='g{n}' width='1' height='1'/></defs><use href='#g0'/>"))
|
||||
};
|
||||
assert!(!deep(&chain(10)));
|
||||
assert!(deep(&chain(MAX_ELEMENT_DEPTH / 2)), "two elements a link");
|
||||
assert!(deep(&chain(5000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chains_of_clips_masks_and_patterns_count_by_attribute_style_and_style_sheet() {
|
||||
let by_attribute = |n: usize| -> String {
|
||||
(0..n).map(|i| format!("<clipPath id='c{i}' clip-path='url(#c{})'><rect width='1' height='1'/></clipPath>", i + 1)).collect()
|
||||
};
|
||||
let by_style = |n: usize| -> String {
|
||||
(0..n).map(|i| format!("<mask id='c{i}' style='mask: url( \"#c{}\" )'><rect width='1' height='1'/></mask>", i + 1)).collect()
|
||||
};
|
||||
let by_sheet = |n: usize| -> String {
|
||||
let rules: String = (0..n).map(|i| format!(".p{i} {{ fill: url(#c{}) }}", i + 1)).collect();
|
||||
let patterns: String = (0..n)
|
||||
.map(|i| format!("<pattern id='c{i}' width='1' height='1'><g class='p{i}'><rect width='1' height='1'/></g></pattern>"))
|
||||
.collect();
|
||||
format!("<style>{rules}</style>{patterns}")
|
||||
};
|
||||
for chain in [&by_attribute as &dyn Fn(usize) -> String, &by_style, &by_sheet] {
|
||||
let file =
|
||||
|n| svg(&format!("<defs>{}</defs><rect width='5' height='5' fill='url(#c0)' clip-path='url(#c0)' mask='url(#c0)'/>", chain(n)));
|
||||
assert!(!deep(&file(5)));
|
||||
assert!(deep(&file(MAX_ELEMENT_DEPTH)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_paint_servers_used_side_by_side_are_not_a_chain() {
|
||||
// As drawing apps write gradients: a class each, used by objects that aren't inside one another.
|
||||
let rules: String = (0..300).map(|i| format!(".c{i}{{fill:url(#g{i})}}")).collect();
|
||||
let defs: String = (0..300).map(|i| format!("<linearGradient id='g{i}'><stop class='c{i}' offset='0'/></linearGradient>")).collect();
|
||||
let art: String = (0..300).map(|i| format!("<g class='c{i}'><rect class='c{i}' width='1' height='1'/></g>")).collect();
|
||||
assert!(!deep(&svg(&format!("<style>{rules}</style><defs>{defs}</defs>{art}"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_loop_of_references_is_not_deep() {
|
||||
assert!(!deep(&svg("<g id='a'><use href='#b'/></g><g id='b'><use href='#a'/></g><clipPath id='c' clip-path='url(#c)'/>")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_inherited_paint_is_converted_under_everything_between() {
|
||||
// Neither the pattern nor the art is too deep; the pattern under the art's last object is.
|
||||
let half = MAX_ELEMENT_DEPTH / 2;
|
||||
let pattern = format!("<pattern id='p' width='1' height='1'>{}</pattern>", groups(half));
|
||||
assert!(!deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half / 2)))));
|
||||
assert!(deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half)))));
|
||||
}
|
||||
}
|
||||
@@ -203,12 +203,16 @@ impl Files {
|
||||
ImageHrefResolver {
|
||||
resolve_data: Box::new(move |mime, bytes, opts| {
|
||||
if mime == "image/svg+xml" {
|
||||
if nested_too_deep(&bytes) {
|
||||
return None;
|
||||
}
|
||||
self.note_text(&bytes);
|
||||
}
|
||||
data(mime, bytes, opts)
|
||||
}),
|
||||
resolve_string: Box::new(move |h, opts| match self.sources.get(h).or_else(|| self.sources.get(h.trim()))? {
|
||||
Source::Raster(kind, bytes) => Some(kind(bytes.clone())),
|
||||
Source::Svg(bytes) if nested_too_deep(bytes) => None,
|
||||
Source::Svg(bytes) => {
|
||||
self.note_text(bytes);
|
||||
usvg::Tree::from_data_nested(bytes, opts).ok().map(ImageKind::SVG)
|
||||
@@ -218,6 +222,12 @@ impl Files {
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the SVG image `bytes` nested too deep for usvg to read (as the SVG holding it would be)?
|
||||
/// It is then left out, as an image usvg can't read.
|
||||
fn nested_too_deep(bytes: &[u8]) -> bool {
|
||||
crate::text_of(bytes).is_ok_and(|text| super::xml_too_deep(&text))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+11
-3
@@ -312,6 +312,9 @@ pub struct LinkedImage {
|
||||
pub enum SvgError {
|
||||
#[error("SVG parse error: {0}")]
|
||||
Parse(String),
|
||||
/// Its elements nest (references followed) deeper than the reader goes.
|
||||
#[error("the SVG's elements are nested more than {0} deep")]
|
||||
TooDeep(usize),
|
||||
}
|
||||
|
||||
/// Export a document as an SVG string (linked images are not returned: see [`export_full`]).
|
||||
@@ -347,7 +350,8 @@ pub struct Editing {
|
||||
|
||||
/// The native document an SVG carries, if any, and whether the SVG was edited since.
|
||||
pub fn editing(svg: &str) -> Option<Editing> {
|
||||
if !svg.contains(EDITING_NS) {
|
||||
// (An SVG nested too deep for the XML reader carries none: it isn't one of ours.)
|
||||
if !svg.contains(EDITING_NS) || import::xml_too_deep(svg) {
|
||||
return None;
|
||||
}
|
||||
let opts = usvg::roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
|
||||
@@ -500,14 +504,18 @@ pub fn import_with_report(svg: &str) -> Result<(Document, Vec<String>), SvgError
|
||||
import_with(svg, &ImportOptions::default())
|
||||
}
|
||||
|
||||
pub use import::MAX_ELEMENT_DEPTH;
|
||||
|
||||
/// Import an SVG document reading the files it links to as `opts` says, also returning warnings.
|
||||
pub fn import_with(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
|
||||
import::import(svg, opts)
|
||||
// usvg converts the tree by recursion, in large stack frames: the nesting it is given is
|
||||
// capped (`import::depth`), and it gets room for that cap whatever thread asks.
|
||||
vectorcraft_doc::depth::with_deep_stack(|| import::import(svg, opts))
|
||||
}
|
||||
|
||||
/// Reads a linked file: its bytes and the link to it (absolute path, size, time and hash), or
|
||||
/// `None` when there is no readable file at the (absolute) path.
|
||||
pub type ReadFile<'a> = &'a dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)>;
|
||||
pub type ReadFile<'a> = &'a (dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)> + Sync);
|
||||
|
||||
/// How [`import_with`] reads the files an SVG's `<image>` elements link to.
|
||||
///
|
||||
|
||||
@@ -4099,6 +4099,8 @@ imageTrace Outline Obrys
|
||||
@msg recovery copy `{file}`: {e} kopie pro obnovení `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi rozlišení musí být mezi 1 a 2400 ppi
|
||||
@msg result would exceed the canvas (coordinates out of range) výsledek by přesáhl plátno (souřadnice mimo rozsah)
|
||||
@msg objects can't be nested more than 32 deep objekty nelze vnořit hlouběji než do 32 úrovní
|
||||
@msg the SVG's elements are nested more than {0} deep prvky SVG jsou vnořeny hlouběji než do {0} úrovní
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} měřítko musí být číslo od {_1} do {_2}, ne {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} měřítko musí být číslo od {_1} do {_2}, ne {s}
|
||||
@msg select (or target) objects whose appearance isn't basic vyberte (nebo zacilte) objekty, jejichž vzhled není základní
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3705,6 +3705,8 @@ theme Appearance Erscheinungsbild
|
||||
@msg recovery copy `{file}`: {e} Wiederherstellungskopie `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi Auflösungen müssen zwischen 1 und 2400 ppi liegen
|
||||
@msg result would exceed the canvas (coordinates out of range) das Ergebnis würde die Arbeitsfläche überschreiten (Koordinaten außerhalb des Bereichs)
|
||||
@msg objects can't be nested more than 32 deep Objekte können nicht tiefer als 32 Ebenen verschachtelt werden
|
||||
@msg the SVG's elements are nested more than {0} deep die Elemente der SVG-Datei sind tiefer als {0} Ebenen verschachtelt
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} scale muss eine Zahl von {_1} bis {_2} sein, nicht {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} scale muss eine Zahl von {_1} bis {_2} sein, nicht {s}
|
||||
@msg select (or target) objects whose appearance isn't basic wählen Sie Objekte aus (oder als Ziel), deren Aussehen nicht einfach ist
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3804,6 +3804,8 @@ axis Both Ambos
|
||||
@msg recovery copy `{file}`: {e} copia de recuperación `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi las resoluciones deben estar entre 1 y 2400 ppi
|
||||
@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 scale must be a number from {_1} to {_2}, not {_3} la escala debe ser un número de {_1} a {_2}, no {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} la escala 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.
|
@@ -3801,6 +3801,8 @@ axis Both Les deux
|
||||
@msg recovery copy `{file}`: {e} copie de récupération `{file}` : {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi les résolutions doivent être comprises entre 1 et 2400 ppp
|
||||
@msg result would exceed the canvas (coordinates out of range) le résultat dépasserait la zone de travail (coordonnées hors limites)
|
||||
@msg objects can't be nested more than 32 deep les objets ne peuvent pas être imbriqués sur plus de 32 niveaux
|
||||
@msg the SVG's elements are nested more than {0} deep les éléments du SVG sont imbriqués sur plus de {0} niveaux
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {s}
|
||||
@msg select (or target) objects whose appearance isn't basic sélectionnez (ou ciblez) des objets dont l’aspect n’est pas basique
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4441,6 +4441,8 @@ imageTrace Outline Körvonal
|
||||
@msg resolutions must be between 1 and 2400 ppi a felbontás 1 és 2400 ppi között lehet
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Helyreállított dokumentumok: {_1}; mentsd őket, ha meg akarod tartani
|
||||
@msg result would exceed the canvas (coordinates out of range) az eredmény túllépné a vásznat (a koordináták tartományon kívül esnek)
|
||||
@msg objects can't be nested more than 32 deep az objektumok legfeljebb 32 szint mélyen ágyazhatók egymásba
|
||||
@msg the SVG's elements are nested more than {0} deep az SVG elemei több mint {0} szint mélyen vannak egymásba ágyazva
|
||||
@msg Save for Web: the image is empty Mentés webre: a kép üres
|
||||
@msg Save writes {_1}, not {_2}: use document.export for other formats a Mentés ezt írja: {_1}, ezt nem: {_2}; más formátumhoz használd a document.export parancsot
|
||||
@msg saved panel layout is invalid; reset the workspace first a mentett panelelrendezés érvénytelen; előbb állítsd vissza a munkaterületet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3804,6 +3804,8 @@ axis Both Entrambi
|
||||
@msg recovery copy `{file}`: {e} copia di recupero `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi le risoluzioni devono essere comprese tra 1 e 2400 ppi
|
||||
@msg result would exceed the canvas (coordinates out of range) il risultato supererebbe l'area di disegno (coordinate fuori intervallo)
|
||||
@msg objects can't be nested more than 32 deep gli oggetti non possono essere nidificati oltre 32 livelli
|
||||
@msg the SVG's elements are nested more than {0} deep gli elementi dell'SVG sono nidificati oltre {0} livelli
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} la scala deve essere un numero da {_1} a {_2}, non {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} la scala deve essere un numero da {_1} a {_2}, non {s}
|
||||
@msg select (or target) objects whose appearance isn't basic seleziona (o imposta come destinazione) oggetti con un aspetto non di base
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3730,6 +3730,8 @@ theme Appearance 外観
|
||||
@msg no interaction in progress 進行中の操作がありません
|
||||
@msg no such object {0} オブジェクト {0} が見つかりません
|
||||
@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 unknown command `{0}` `{0}` は不明なコマンドです
|
||||
@msg `{what}` closes nothing `{what}` に対応する開始部分がありません
|
||||
@msg `{what}` closes something it didn't open `{what}` が対応しない開始部分を閉じています
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3708,6 +3708,8 @@ theme Appearance 외관
|
||||
@msg no interaction in progress 진행 중인 상호작용이 없습니다
|
||||
@msg no such object {0} 개체 {0}이(가) 없습니다
|
||||
@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 unknown command `{0}` 알 수 없는 명령 `{0}`
|
||||
@msg `{what}` closes nothing `{what}`이(가) 닫는 항목이 없습니다
|
||||
@msg `{what}` closes something it didn't open `{what}`이(가) 열지 않은 항목을 닫습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3965,6 +3965,8 @@ imageTrace Outline Kontur
|
||||
@msg recovery copy `{file}`: {e} kopia odzyskiwania `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi rozdzielczości muszą mieścić się w zakresie od 1 do 2400 ppi
|
||||
@msg result would exceed the canvas (coordinates out of range) wynik wykroczyłby poza płótno (współrzędne poza zakresem)
|
||||
@msg objects can't be nested more than 32 deep obiektów nie można zagnieżdżać głębiej niż na 32 poziomy
|
||||
@msg the SVG's elements are nested more than {0} deep elementy SVG są zagnieżdżone głębiej niż na {0} poziomów
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} skala musi być liczbą od {_1} do {_2}, nie {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} skala musi być liczbą od {_1} do {_2}, nie {s}
|
||||
@msg select (or target) objects whose appearance isn't basic zaznacz (lub wskaż) obiekty, których wygląd nie jest podstawowy
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3449,6 +3449,8 @@ axis Both Ambos
|
||||
@msg recovery copy `{file}`: {e} cópia de recuperação `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi as resoluções devem estar entre 1 e 2400 ppi
|
||||
@msg result would exceed the canvas (coordinates out of range) o resultado excederia a tela (coordenadas fora do intervalo)
|
||||
@msg objects can't be nested more than 32 deep os objetos não podem ser aninhados em mais de 32 níveis
|
||||
@msg the SVG's elements are nested more than {0} deep os elementos do SVG estão aninhados em mais de {0} níveis
|
||||
@msg scale must be a number from {_1} to {_2}, not {_3} a escala deve ser um número de {_1} a {_2}, não {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} a escala deve ser um número de {_1} a {_2}, não {s}
|
||||
@msg select (or target) objects whose appearance isn't basic selecione (ou direcione) objetos cuja aparência não seja básica
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3800,6 +3800,8 @@ axis Both Оба
|
||||
@msg recovery copy `{file}`: {e} копия восстановления `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi разрешения должны быть от 1 до 2400 ppi
|
||||
@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 scale must be a number from {_1} to {_2}, not {_3} масштаб должен быть числом от {_1} до {_2}, а не {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} масштаб должен быть числом от {_1} до {_2}, а не {s}
|
||||
@msg select (or target) objects whose appearance isn't basic выделите (или укажите как объект) объекты, оформление которых не является основным
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3788,6 +3788,8 @@ axis Both Обидві
|
||||
@msg recovery copy `{file}`: {e} копія відновлення `{file}`: {e}
|
||||
@msg resolutions must be between 1 and 2400 ppi дозволи мають бути від 1 до 2400 ppi
|
||||
@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 scale must be a number from {_1} to {_2}, not {_3} масштаб має бути числом від {_1} до {_2}, а не {_3}
|
||||
@msg scale must be a number from {_1} to {_2}, not {s} масштаб має бути числом від {_1} до {_2}, а не {s}
|
||||
@msg select (or target) objects whose appearance isn't basic виділіть (або вкажіть як об’єкт) об’єкти, оформлення яких не є основним
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 713 and column 40.
|
@@ -4071,6 +4071,8 @@ theme Appearance Giao diện
|
||||
@msg resolutions must be between 1 and 2400 ppi độ phân giải phải nằm trong khoảng 1 đến 2400 ppi
|
||||
@msg Restored {_1} recovered document(s): save them to keep them Đã phục hồi {_1} tài liệu: hãy lưu để giữ lại
|
||||
@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 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 saved panel layout is invalid; reset the workspace first bố cục bảng đã lưu không hợp lệ; hãy đặt lại không gian làm việc trước
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3642 and column 10.
|
Reference in new issue
Block a user