Skip to main content

img_svg/
doc.rs

1// doc -- the parsed document: elements with their kind, attributes, cascaded properties,
2// pre-built path data, the id map, the intrinsic size and the embedded raster images
3//
4// The cascade runs once at parse time: presentation attributes, then the matching rules of
5// every <style> sheet (by specificity, then order), then the style attribute; later
6// declarations win.  Inheritance and "inherit" are resolved while rendering (render.rs),
7// because `use` makes an element inherit from the referencing element.
8
9use alloc::collections::BTreeMap;
10use alloc::string::String;
11use alloc::vec::Vec;
12use core::sync::atomic::{AtomicU64, Ordering};
13
14use gfx::pathdata::PData;
15use gfx::xform::Matrix;
16use gfx::Surface;
17use rt::sync::SpinLock;
18
19use crate::css;
20use crate::values::{self, Len, Par};
21use crate::xml::{self, Item, NONE};
22
23/// the element kinds the renderer knows
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub(crate) enum Kind {
26    Svg,
27    G,
28    Defs,
29    Use,
30    Symbol,
31    Switch,
32    A,
33    Path,
34    Rect,
35    Circle,
36    Ellipse,
37    Line,
38    Polyline,
39    Polygon,
40    Text,
41    Tspan,
42    Image,
43    LinearGradient,
44    RadialGradient,
45    Stop,
46    ClipPath,
47    Style,
48    Title,
49    Other,
50}
51
52fn kind_of(name: &str) -> Kind {
53    match name {
54        "svg" => Kind::Svg,
55        "g" => Kind::G,
56        "defs" => Kind::Defs,
57        "use" => Kind::Use,
58        "symbol" => Kind::Symbol,
59        "switch" => Kind::Switch,
60        "a" => Kind::A,
61        "path" => Kind::Path,
62        "rect" => Kind::Rect,
63        "circle" => Kind::Circle,
64        "ellipse" => Kind::Ellipse,
65        "line" => Kind::Line,
66        "polyline" => Kind::Polyline,
67        "polygon" => Kind::Polygon,
68        "text" => Kind::Text,
69        "tspan" | "textPath" => Kind::Tspan,
70        "image" => Kind::Image,
71        "linearGradient" => Kind::LinearGradient,
72        "radialGradient" => Kind::RadialGradient,
73        "stop" => Kind::Stop,
74        "clipPath" => Kind::ClipPath,
75        "style" => Kind::Style,
76        "title" => Kind::Title,
77        _ => Kind::Other,
78    }
79}
80
81// the properties (index = id); presentation attributes of the same name set them too
82pub(crate) const P_FILL: u8 = 0;
83pub(crate) const P_FILL_OPACITY: u8 = 1;
84pub(crate) const P_FILL_RULE: u8 = 2;
85pub(crate) const P_STROKE: u8 = 3;
86pub(crate) const P_STROKE_WIDTH: u8 = 4;
87pub(crate) const P_STROKE_OPACITY: u8 = 5;
88pub(crate) const P_LINECAP: u8 = 6;
89pub(crate) const P_LINEJOIN: u8 = 7;
90pub(crate) const P_MITER: u8 = 8;
91pub(crate) const P_DASHARRAY: u8 = 9;
92pub(crate) const P_DASHOFFSET: u8 = 10;
93pub(crate) const P_OPACITY: u8 = 11;
94pub(crate) const P_COLOR: u8 = 12;
95pub(crate) const P_DISPLAY: u8 = 13;
96pub(crate) const P_VISIBILITY: u8 = 14;
97pub(crate) const P_CLIP_PATH: u8 = 15;
98pub(crate) const P_CLIP_RULE: u8 = 16;
99pub(crate) const P_STOP_COLOR: u8 = 17;
100pub(crate) const P_STOP_OPACITY: u8 = 18;
101pub(crate) const P_FONT_SIZE: u8 = 19;
102pub(crate) const P_FONT_FAMILY: u8 = 20;
103pub(crate) const P_FONT_WEIGHT: u8 = 21;
104pub(crate) const P_FONT_STYLE: u8 = 22;
105pub(crate) const P_TEXT_ANCHOR: u8 = 23;
106pub(crate) const P_OVERFLOW: u8 = 24;
107const PROP_NAMES: [&str; 25] = [
108    "fill",
109    "fill-opacity",
110    "fill-rule",
111    "stroke",
112    "stroke-width",
113    "stroke-opacity",
114    "stroke-linecap",
115    "stroke-linejoin",
116    "stroke-miterlimit",
117    "stroke-dasharray",
118    "stroke-dashoffset",
119    "opacity",
120    "color",
121    "display",
122    "visibility",
123    "clip-path",
124    "clip-rule",
125    "stop-color",
126    "stop-opacity",
127    "font-size",
128    "font-family",
129    "font-weight",
130    "font-style",
131    "text-anchor",
132    "overflow",
133];
134
135fn prop_id(name: &str) -> Option<u8> {
136    PROP_NAMES.iter().position(|&n| n == name).map(|k| k as u8)
137}
138
139pub(crate) struct Node {
140    pub kind: Kind,
141    pub attrs: Vec<(String, String)>,
142    /// cascaded declarations, later ones win
143    pub props: Vec<(u8, String)>,
144    pub kids: Vec<Item>,
145    pub parent: u32,
146    /// the transform attribute (gradientTransform for gradients, transform for clipPath)
147    pub tf: Option<Matrix>,
148    /// path data of path / polyline / polygon (built once)
149    pub pd: Option<PData>,
150    /// an embedded raster image (index into Svg::images)
151    pub img: Option<u32>,
152}
153
154impl Node {
155    pub fn attr(&self, name: &str) -> Option<&str> {
156        self.attrs.iter().find(|(n, _)| n == name).map(|(_, v)| v.as_str())
157    }
158
159    /// href or xlink:href
160    pub fn href(&self) -> Option<&str> {
161        self.attr("href").or_else(|| self.attr("xlink:href"))
162    }
163
164    /// a length attribute
165    pub fn len(&self, name: &str) -> Option<Len> {
166        self.attr(name).and_then(values::length)
167    }
168
169    /// the last declaration of property p
170    pub fn prop(&self, p: u8) -> Option<&str> {
171        self.props.iter().rev().find(|(k, _)| *k == p).map(|(_, v)| v.as_str())
172    }
173
174    /// the element's own text (title, style)
175    pub fn text(&self) -> String {
176        let mut s = String::new();
177        for k in &self.kids {
178            if let Item::Text(t) = k {
179                s.push_str(t);
180            }
181        }
182        s
183    }
184}
185
186/// an embedded raster image: its file bytes, decoded at the first use (None: not yet,
187/// Some(None): undecodable)
188pub(crate) struct Img {
189    data: Vec<u8>,
190    sf: SpinLock<Option<Option<Surface>>>,
191}
192
193/// a parsed SVG document (immutable; render it as often as needed)
194pub struct Svg {
195    pub(crate) nodes: Vec<Node>,
196    pub(crate) ids: BTreeMap<String, u32>,
197    pub(crate) images: Vec<Img>,
198    pub(crate) width: f64,
199    pub(crate) height: f64,
200    pub(crate) view_box: Option<[f64; 4]>,
201    pub(crate) par: Par,
202    title: String,
203    truncated: bool,
204    img_px: AtomicU64, // pixels of the embedded images decoded so far (the document budget)
205}
206
207const DEFAULT_W: f64 = 300.0;
208const DEFAULT_H: f64 = 150.0;
209const SIZE_MAX: f64 = 1.0e6;
210const CASCADE_STEPS: u32 = 50_000_000;
211// the bytes the cascade may copy out of style sheet rules, a value and its entry each (a rule
212// that matches every element copies its declarations for each): beyond, ERR_LIMIT
213const CASCADE_BYTES: usize = 128 << 20;
214const DECL_ENTRY: usize = core::mem::size_of::<(u8, String)>();
215
216// an absolute length of the root (em at 16 px; % and invalid values: None)
217fn abs_len(v: Option<&str>) -> Option<f64> {
218    let l = values::length(v?)?;
219    let px = match l.u {
220        values::Unit::Px => l.v,
221        values::Unit::Em => l.v * 16.0,
222        values::Unit::Ex => l.v * 8.0,
223        values::Unit::Pct => return None,
224    };
225    if px > 0.0 { Some(px.min(SIZE_MAX)) } else { None }
226}
227
228impl Svg {
229    /// parse an SVG document (UTF-8 XML; gzip data (.svgz) is inflated first):
230    /// Err(ERR_HDR) when it is no SVG, ERR_LIMIT beyond the size limits
231    pub fn parse(data: &[u8]) -> Result<Svg, i32> {
232        let inflated;
233        let data = if data.len() >= 3 && data[0] == 0x1F && data[1] == 0x8B && data[2] == 8 {
234            inflated = crate::gunzip(data)?;
235            &inflated[..]
236        } else {
237            data
238        };
239        let dom = xml::parse(data)?;
240        Svg::from_dom(dom)
241    }
242
243    fn from_dom(mut dom: xml::Dom) -> Result<Svg, i32> {
244        if dom.els.first().map(|e| e.name.as_str()) != Some("svg") {
245            return Err(img_core::ERR_HDR);
246        }
247        // the style sheets
248        let mut rules: Vec<css::Rule> = Vec::new();
249        let mut order = 0;
250        for e in &dom.els {
251            if e.name == "style" && e.attr("type").is_none_or(|t| t.trim().is_empty() || t.trim().eq_ignore_ascii_case("text/css")) {
252                let mut s = String::new();
253                for k in &e.kids {
254                    if let Item::Text(t) = k {
255                        s.push_str(t);
256                    }
257                }
258                css::sheet(&s, &mut order, &mut rules);
259            }
260        }
261        rules.sort_by(|a, b| (a.sel.spec, a.order).cmp(&(b.sel.spec, b.order)));
262        // the cascade
263        let mut steps = CASCADE_STEPS;
264        let mut copied = 0usize;
265        let mut props_all: Vec<Vec<(u8, String)>> = Vec::with_capacity(dom.els.len());
266        for k in 0..dom.els.len() {
267            let e = &dom.els[k];
268            let mut props: Vec<(u8, String)> = Vec::new();
269            for (n, v) in &e.attrs {
270                if let Some(p) = prop_id(n) {
271                    props.push((p, v.clone()));
272                }
273            }
274            for r in &rules {
275                if steps == 0 {
276                    break;
277                }
278                if r.sel.matches(&dom, k as u32, &mut steps) {
279                    // each declaration is a step too (a block may hold thousands)
280                    steps = steps.saturating_sub(u32::try_from(r.decls.len()).unwrap_or(u32::MAX));
281                    for (n, v) in r.decls.iter() {
282                        if let Some(p) = prop_id(n) {
283                            copied = copied.saturating_add(v.len() + DECL_ENTRY);
284                            if copied > CASCADE_BYTES {
285                                return Err(img_core::ERR_LIMIT);
286                            }
287                            props.push((p, v.clone()));
288                        }
289                    }
290                }
291            }
292            if let Some(st) = e.attr("style") {
293                for (n, v) in css::decls(st) {
294                    if let Some(p) = prop_id(&n) {
295                        props.push((p, v));
296                    }
297                }
298            }
299            // keep the last declaration of each property
300            let mut out: Vec<(u8, String)> = Vec::with_capacity(props.len());
301            for (p, v) in props.into_iter().rev() {
302                if !out.iter().any(|(q, _)| *q == p) {
303                    out.push((p, v));
304                }
305            }
306            out.reverse();
307            props_all.push(out);
308        }
309        // the nodes
310        let mut nodes = Vec::with_capacity(dom.els.len());
311        let mut ids = BTreeMap::new();
312        let mut images = Vec::new();
313        for (k, (e, props)) in dom.els.drain(..).zip(props_all).enumerate() {
314            let kind = kind_of(&e.name);
315            let tf_attr = match kind {
316                Kind::LinearGradient | Kind::RadialGradient => "gradientTransform",
317                _ => "transform",
318            };
319            let tf = e.attr(tf_attr).and_then(values::transform);
320            let pd = match kind {
321                Kind::Path => e.attr("d").map(crate::pathd::parse), // up to the first error
322                Kind::Polyline | Kind::Polygon => e.attr("points").map(|p| points(p, kind == Kind::Polygon)),
323                _ => None,
324            };
325            let img = if kind == Kind::Image {
326                let href = e.attr("href").or_else(|| e.attr("xlink:href"));
327                match href.and_then(crate::data_uri_bytes) {
328                    Some(data) => {
329                        images.push(Img { data, sf: SpinLock::new(None) });
330                        Some(images.len() as u32 - 1)
331                    }
332                    None => None,
333                }
334            } else {
335                None
336            };
337            if let Some(id) = e.attr("id") {
338                ids.entry(String::from(id)).or_insert(k as u32);
339            }
340            nodes.push(Node { kind, attrs: e.attrs, props, kids: e.kids, parent: e.parent, tf, pd, img });
341        }
342        // the root's size and viewBox
343        let root = &nodes[0];
344        let view_box = root.attr("viewBox").and_then(values::view_box);
345        let par = root.attr("preserveAspectRatio").map(values::par).unwrap_or_default();
346        let (mut w, mut h) = (abs_len(root.attr("width")), abs_len(root.attr("height")));
347        if let Some(vb) = view_box {
348            match (w, h) {
349                (None, None) => {
350                    w = Some(vb[2]);
351                    h = Some(vb[3]);
352                }
353                (Some(x), None) => h = Some(x * vb[3] / vb[2]),
354                (None, Some(y)) => w = Some(y * vb[2] / vb[3]),
355                _ => {}
356            }
357        }
358        let width = w.unwrap_or(DEFAULT_W).clamp(1.0e-3, SIZE_MAX);
359        let height = h.unwrap_or(DEFAULT_H).clamp(1.0e-3, SIZE_MAX);
360        // the title: the root's first <title>
361        let mut title = String::new();
362        for kid in &root.kids {
363            if let Item::El(c) = kid {
364                if let Some(n) = nodes.get(*c as usize) {
365                    if n.kind == Kind::Title {
366                        title = collapse(&n.text());
367                        break;
368                    }
369                }
370            }
371        }
372        Ok(Svg { nodes, ids, images, width, height, view_box, par, title, truncated: dom.truncated, img_px: AtomicU64::new(0) })
373    }
374
375    /// the intrinsic size in px (CSS px = DIP): width / height of the root, else its viewBox,
376    /// else 300 x 150
377    pub fn size(&self) -> (f64, f64) {
378        (self.width, self.height)
379    }
380
381    /// the root's viewBox (x, y, w, h), if it has one
382    pub fn view_box(&self) -> Option<[f64; 4]> {
383        self.view_box
384    }
385
386    /// the text of the root's <title> (blanks collapsed; empty without one)
387    pub fn title(&self) -> &str {
388        &self.title
389    }
390
391    /// the file ended early or a limit dropped elements (what was read is drawn)
392    pub fn truncated(&self) -> bool {
393        self.truncated
394    }
395
396    /// the number of elements
397    pub fn element_count(&self) -> usize {
398        self.nodes.len()
399    }
400
401    /// f(the decoded embedded image i) (decoded at the first call)
402    pub(crate) fn with_image<R>(&self, i: u32, f: impl FnOnce(&Surface) -> R) -> Option<R> {
403        let img = self.images.get(i as usize)?;
404        let mut g = img.sf.lock();
405        if g.is_none() {
406            let used = self.img_px.load(Ordering::Relaxed);
407            let sf = crate::decode_image(&img.data, crate::MAX_IMAGE_PX.saturating_sub(used));
408            if let Some(s) = &sf {
409                self.img_px.fetch_add(s.w as u64 * s.h as u64, Ordering::Relaxed);
410            }
411            *g = Some(sf);
412        }
413        match &*g {
414            Some(Some(sf)) => Some(f(sf)),
415            _ => None,
416        }
417    }
418
419    pub(crate) fn node(&self, k: u32) -> Option<&Node> {
420        self.nodes.get(k as usize)
421    }
422
423    /// the element with id (a "#id" fragment or a bare id)
424    pub(crate) fn by_id(&self, id: &str) -> Option<u32> {
425        let id = id.trim();
426        let id = id.strip_prefix('#').unwrap_or(id);
427        self.ids.get(id).copied()
428    }
429
430    /// the element an href points to ("#id" only: other documents are not loaded)
431    pub(crate) fn href_target(&self, n: &Node) -> Option<u32> {
432        let h = n.href()?.trim();
433        if !h.starts_with('#') {
434            return None;
435        }
436        self.by_id(h)
437    }
438
439    #[allow(dead_code)]
440    pub(crate) fn parent_of(&self, k: u32) -> u32 {
441        self.node(k).map_or(NONE, |n| n.parent)
442    }
443}
444
445/// blanks collapsed to one space, trimmed
446pub(crate) fn collapse(s: &str) -> String {
447    let mut out = String::with_capacity(s.len());
448    for w in s.split_ascii_whitespace() {
449        if !out.is_empty() {
450            out.push(' ');
451        }
452        out.push_str(w);
453    }
454    out
455}
456
457// points="x,y x,y ..." as path data (an odd number drops the last value)
458fn points(v: &str, close: bool) -> PData {
459    let s = v.as_bytes();
460    let mut pd = PData::new();
461    let mut i = 0;
462    let mut first = true;
463    loop {
464        values::sep(s, &mut i);
465        let Some(x) = values::number(s, &mut i) else { break };
466        values::sep(s, &mut i);
467        let Some(y) = values::number(s, &mut i) else { break };
468        if first {
469            pd.move_to(x, y);
470            first = false;
471        } else {
472            pd.line_to(x, y);
473        }
474    }
475    if close && !first {
476        pd.close();
477    }
478    pd.err = 0;
479    pd
480}