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
212// an absolute length of the root (em at 16 px; % and invalid values: None)
213fn abs_len(v: Option<&str>) -> Option<f64> {
214    let l = values::length(v?)?;
215    let px = match l.u {
216        values::Unit::Px => l.v,
217        values::Unit::Em => l.v * 16.0,
218        values::Unit::Ex => l.v * 8.0,
219        values::Unit::Pct => return None,
220    };
221    if px > 0.0 { Some(px.min(SIZE_MAX)) } else { None }
222}
223
224impl Svg {
225    /// parse an SVG document (UTF-8 XML; gzip data (.svgz) is inflated first):
226    /// Err(ERR_HDR) when it is no SVG, ERR_LIMIT beyond the size limits
227    pub fn parse(data: &[u8]) -> Result<Svg, i32> {
228        let inflated;
229        let data = if data.len() >= 3 && data[0] == 0x1F && data[1] == 0x8B && data[2] == 8 {
230            inflated = crate::gunzip(data)?;
231            &inflated[..]
232        } else {
233            data
234        };
235        let dom = xml::parse(data)?;
236        Svg::from_dom(dom)
237    }
238
239    fn from_dom(mut dom: xml::Dom) -> Result<Svg, i32> {
240        if dom.els.first().map(|e| e.name.as_str()) != Some("svg") {
241            return Err(img_core::ERR_HDR);
242        }
243        // the style sheets
244        let mut rules: Vec<css::Rule> = Vec::new();
245        let mut order = 0;
246        for e in &dom.els {
247            if e.name == "style" && e.attr("type").is_none_or(|t| t.trim().is_empty() || t.trim().eq_ignore_ascii_case("text/css")) {
248                let mut s = String::new();
249                for k in &e.kids {
250                    if let Item::Text(t) = k {
251                        s.push_str(t);
252                    }
253                }
254                css::sheet(&s, &mut order, &mut rules);
255            }
256        }
257        rules.sort_by(|a, b| (a.sel.spec, a.order).cmp(&(b.sel.spec, b.order)));
258        // the cascade
259        let mut steps = CASCADE_STEPS;
260        let mut props_all: Vec<Vec<(u8, String)>> = Vec::with_capacity(dom.els.len());
261        for k in 0..dom.els.len() {
262            let e = &dom.els[k];
263            let mut props: Vec<(u8, String)> = Vec::new();
264            for (n, v) in &e.attrs {
265                if let Some(p) = prop_id(n) {
266                    props.push((p, v.clone()));
267                }
268            }
269            for r in &rules {
270                if steps == 0 {
271                    break;
272                }
273                if r.sel.matches(&dom, k as u32, &mut steps) {
274                    for (n, v) in &r.decls {
275                        if let Some(p) = prop_id(n) {
276                            props.push((p, v.clone()));
277                        }
278                    }
279                }
280            }
281            if let Some(st) = e.attr("style") {
282                for (n, v) in css::decls(st) {
283                    if let Some(p) = prop_id(&n) {
284                        props.push((p, v));
285                    }
286                }
287            }
288            // keep the last declaration of each property
289            let mut out: Vec<(u8, String)> = Vec::with_capacity(props.len());
290            for (p, v) in props.into_iter().rev() {
291                if !out.iter().any(|(q, _)| *q == p) {
292                    out.push((p, v));
293                }
294            }
295            out.reverse();
296            props_all.push(out);
297        }
298        // the nodes
299        let mut nodes = Vec::with_capacity(dom.els.len());
300        let mut ids = BTreeMap::new();
301        let mut images = Vec::new();
302        for (k, (e, props)) in dom.els.drain(..).zip(props_all).enumerate() {
303            let kind = kind_of(&e.name);
304            let tf_attr = match kind {
305                Kind::LinearGradient | Kind::RadialGradient => "gradientTransform",
306                _ => "transform",
307            };
308            let tf = e.attr(tf_attr).and_then(values::transform);
309            let pd = match kind {
310                Kind::Path => e.attr("d").map(crate::pathd::parse), // up to the first error
311                Kind::Polyline | Kind::Polygon => e.attr("points").map(|p| points(p, kind == Kind::Polygon)),
312                _ => None,
313            };
314            let img = if kind == Kind::Image {
315                let href = e.attr("href").or_else(|| e.attr("xlink:href"));
316                match href.and_then(crate::data_uri_bytes) {
317                    Some(data) => {
318                        images.push(Img { data, sf: SpinLock::new(None) });
319                        Some(images.len() as u32 - 1)
320                    }
321                    None => None,
322                }
323            } else {
324                None
325            };
326            if let Some(id) = e.attr("id") {
327                ids.entry(String::from(id)).or_insert(k as u32);
328            }
329            nodes.push(Node { kind, attrs: e.attrs, props, kids: e.kids, parent: e.parent, tf, pd, img });
330        }
331        // the root's size and viewBox
332        let root = &nodes[0];
333        let view_box = root.attr("viewBox").and_then(values::view_box);
334        let par = root.attr("preserveAspectRatio").map(values::par).unwrap_or_default();
335        let (mut w, mut h) = (abs_len(root.attr("width")), abs_len(root.attr("height")));
336        if let Some(vb) = view_box {
337            match (w, h) {
338                (None, None) => {
339                    w = Some(vb[2]);
340                    h = Some(vb[3]);
341                }
342                (Some(x), None) => h = Some(x * vb[3] / vb[2]),
343                (None, Some(y)) => w = Some(y * vb[2] / vb[3]),
344                _ => {}
345            }
346        }
347        let width = w.unwrap_or(DEFAULT_W).clamp(1.0e-3, SIZE_MAX);
348        let height = h.unwrap_or(DEFAULT_H).clamp(1.0e-3, SIZE_MAX);
349        // the title: the root's first <title>
350        let mut title = String::new();
351        for kid in &root.kids {
352            if let Item::El(c) = kid {
353                if let Some(n) = nodes.get(*c as usize) {
354                    if n.kind == Kind::Title {
355                        title = collapse(&n.text());
356                        break;
357                    }
358                }
359            }
360        }
361        Ok(Svg { nodes, ids, images, width, height, view_box, par, title, truncated: dom.truncated, img_px: AtomicU64::new(0) })
362    }
363
364    /// the intrinsic size in px (CSS px = DIP): width / height of the root, else its viewBox,
365    /// else 300 x 150
366    pub fn size(&self) -> (f64, f64) {
367        (self.width, self.height)
368    }
369
370    /// the root's viewBox (x, y, w, h), if it has one
371    pub fn view_box(&self) -> Option<[f64; 4]> {
372        self.view_box
373    }
374
375    /// the text of the root's <title> (blanks collapsed; empty without one)
376    pub fn title(&self) -> &str {
377        &self.title
378    }
379
380    /// the file ended early or a limit dropped elements (what was read is drawn)
381    pub fn truncated(&self) -> bool {
382        self.truncated
383    }
384
385    /// the number of elements
386    pub fn element_count(&self) -> usize {
387        self.nodes.len()
388    }
389
390    /// f(the decoded embedded image i) (decoded at the first call)
391    pub(crate) fn with_image<R>(&self, i: u32, f: impl FnOnce(&Surface) -> R) -> Option<R> {
392        let img = self.images.get(i as usize)?;
393        let mut g = img.sf.lock();
394        if g.is_none() {
395            let used = self.img_px.load(Ordering::Relaxed);
396            let sf = crate::decode_image(&img.data, crate::MAX_IMAGE_PX.saturating_sub(used));
397            if let Some(s) = &sf {
398                self.img_px.fetch_add(s.w as u64 * s.h as u64, Ordering::Relaxed);
399            }
400            *g = Some(sf);
401        }
402        match &*g {
403            Some(Some(sf)) => Some(f(sf)),
404            _ => None,
405        }
406    }
407
408    pub(crate) fn node(&self, k: u32) -> Option<&Node> {
409        self.nodes.get(k as usize)
410    }
411
412    /// the element with id (a "#id" fragment or a bare id)
413    pub(crate) fn by_id(&self, id: &str) -> Option<u32> {
414        let id = id.trim();
415        let id = id.strip_prefix('#').unwrap_or(id);
416        self.ids.get(id).copied()
417    }
418
419    /// the element an href points to ("#id" only: other documents are not loaded)
420    pub(crate) fn href_target(&self, n: &Node) -> Option<u32> {
421        let h = n.href()?.trim();
422        if !h.starts_with('#') {
423            return None;
424        }
425        self.by_id(h)
426    }
427
428    #[allow(dead_code)]
429    pub(crate) fn parent_of(&self, k: u32) -> u32 {
430        self.node(k).map_or(NONE, |n| n.parent)
431    }
432}
433
434/// blanks collapsed to one space, trimmed
435pub(crate) fn collapse(s: &str) -> String {
436    let mut out = String::with_capacity(s.len());
437    for w in s.split_ascii_whitespace() {
438        if !out.is_empty() {
439            out.push(' ');
440        }
441        out.push_str(w);
442    }
443    out
444}
445
446// points="x,y x,y ..." as path data (an odd number drops the last value)
447fn points(v: &str, close: bool) -> PData {
448    let s = v.as_bytes();
449    let mut pd = PData::new();
450    let mut i = 0;
451    let mut first = true;
452    loop {
453        values::sep(s, &mut i);
454        let Some(x) = values::number(s, &mut i) else { break };
455        values::sep(s, &mut i);
456        let Some(y) = values::number(s, &mut i) else { break };
457        if first {
458            pd.move_to(x, y);
459            first = false;
460        } else {
461            pd.line_to(x, y);
462        }
463    }
464    if close && !first {
465        pd.close();
466    }
467    pd.err = 0;
468    pd
469}