Skip to main content

document_svg/svg/
reader.rs

1//! Zero-allocation SVG path tokenizer and vector geometry extractor.
2//!
3//! Provides a streaming interface for extracting 2D vector primitives
4//! (lines, rectangles, circles, polylines, paths, text) from SVG markup
5//! with minimal heap allocation.
6
7use quick_xml::Reader;
8use quick_xml::events::Event;
9
10pub use super::color::{ParsedStyle, parse_color, parse_color_hex, parse_style};
11pub use super::geometry::{
12    Point2D, Transform2D, parse_transform, sample_cubic_bezier, sample_elliptical_arc,
13    sample_quad_bezier,
14};
15use crate::error::Result;
16
17/// Extracted SVG 2D vector element.
18#[derive(Clone, Debug, PartialEq)]
19pub enum SvgElement {
20    Line {
21        p1: Point2D,
22        p2: Point2D,
23        stroke_color: Option<u32>,
24        stroke_width: f64,
25        layer: String,
26    },
27    Rect {
28        x: f64,
29        y: f64,
30        width: f64,
31        height: f64,
32        stroke_color: Option<u32>,
33        fill_color: Option<u32>,
34        layer: String,
35    },
36    Circle {
37        center: Point2D,
38        radius: f64,
39        stroke_color: Option<u32>,
40        fill_color: Option<u32>,
41        layer: String,
42    },
43    Polyline {
44        points: Vec<Point2D>,
45        is_closed: bool,
46        stroke_color: Option<u32>,
47        fill_color: Option<u32>,
48        layer: String,
49    },
50    Text {
51        pos: Point2D,
52        font_size: f64,
53        color: Option<u32>,
54        layer: String,
55        content: String,
56    },
57}
58
59/// Zero-allocation tokenizer for SVG path `d` attribute data.
60pub struct SvgPathTokenizer<'a> {
61    remaining: &'a str,
62}
63
64impl<'a> SvgPathTokenizer<'a> {
65    pub fn new(d: &'a str) -> Self {
66        Self { remaining: d }
67    }
68}
69
70/// Token parsed from an SVG path data string.
71#[derive(Clone, Copy, Debug, PartialEq)]
72pub enum PathToken<'a> {
73    Command(char),
74    Number(f64),
75    Raw(&'a str),
76}
77
78impl<'a> Iterator for SvgPathTokenizer<'a> {
79    type Item = PathToken<'a>;
80
81    fn next(&mut self) -> Option<Self::Item> {
82        let bytes = self.remaining.as_bytes();
83        let mut i = 0;
84
85        // Skip whitespace and commas
86        while i < bytes.len() && (bytes[i].is_ascii_whitespace() || bytes[i] == b',') {
87            i += 1;
88        }
89        if i >= bytes.len() {
90            self.remaining = "";
91            return None;
92        }
93
94        let b = bytes[i];
95        // If it's an ASCII alphabetic command character
96        if b.is_ascii_alphabetic() {
97            let cmd = b as char;
98            self.remaining = &self.remaining[i + 1..];
99            return Some(PathToken::Command(cmd));
100        }
101
102        // Parse number (may start with '+', '-', or '.')
103        let start = i;
104        if b == b'+' || b == b'-' {
105            i += 1;
106        }
107        let mut has_digits = false;
108        while i < bytes.len() && bytes[i].is_ascii_digit() {
109            has_digits = true;
110            i += 1;
111        }
112        if i < bytes.len() && bytes[i] == b'.' {
113            i += 1;
114            while i < bytes.len() && bytes[i].is_ascii_digit() {
115                has_digits = true;
116                i += 1;
117            }
118        }
119        // Exponent part (e.g. 1e-4, 2.5E+2)
120        if has_digits && i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
121            let exp_start = i;
122            i += 1;
123            if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
124                i += 1;
125            }
126            let exp_digits_start = i;
127            while i < bytes.len() && bytes[i].is_ascii_digit() {
128                i += 1;
129            }
130            if i == exp_digits_start {
131                // Invalid exponent, roll back
132                i = exp_start;
133            }
134        }
135
136        if has_digits {
137            let slice = &self.remaining[start..i];
138            self.remaining = &self.remaining[i..];
139            if let Ok(num) = slice.parse::<f64>() {
140                return Some(PathToken::Number(num));
141            }
142            return Some(PathToken::Raw(slice));
143        }
144
145        // Fallback: advance 1 byte
146        let slice = &self.remaining[start..start + 1];
147        self.remaining = &self.remaining[start + 1..];
148        Some(PathToken::Raw(slice))
149    }
150}
151
152/// Decomposes an SVG path `d` attribute into a list of 2D polylines (points and closed flag).
153///
154/// Supports all W3C SVG 1.1 path commands: M/m, L/l, H/h, V/v, C/c, S/s, Q/q, T/t, A/a, Z/z.
155pub fn decompose_svg_path(d: &str) -> Vec<(Vec<Point2D>, bool)> {
156    let mut polylines = Vec::new();
157    let mut current_verts = Vec::new();
158    let mut is_closed = false;
159
160    let mut cur = Point2D::new(0.0, 0.0);
161    let mut start = Point2D::new(0.0, 0.0);
162    let mut last_cubic_ctrl: Option<Point2D> = None;
163    let mut last_quad_ctrl: Option<Point2D> = None;
164
165    let mut tokenizer = SvgPathTokenizer::new(d);
166    let mut cur_cmd = 'M';
167
168    while let Some(tok) = tokenizer.next() {
169        match tok {
170            PathToken::Command(c) => {
171                cur_cmd = c;
172            }
173            PathToken::Number(first_num) => {
174                // We encountered a number without an explicit command token; repeat previous command
175                match cur_cmd {
176                    'M' | 'm' => {
177                        last_cubic_ctrl = None;
178                        last_quad_ctrl = None;
179                        if !current_verts.is_empty() {
180                            polylines.push((std::mem::take(&mut current_verts), is_closed));
181                            is_closed = false;
182                        }
183                        let py = if let Some(PathToken::Number(y)) = tokenizer.next() {
184                            y
185                        } else {
186                            0.0
187                        };
188                        if cur_cmd == 'm' {
189                            cur.x += first_num;
190                            cur.y += py;
191                        } else {
192                            cur.x = first_num;
193                            cur.y = py;
194                        }
195                        start = cur;
196                        current_verts.push(cur);
197                        cur_cmd = if cur_cmd == 'm' { 'l' } else { 'L' };
198                    }
199                    'L' | 'l' => {
200                        last_cubic_ctrl = None;
201                        last_quad_ctrl = None;
202                        let py = if let Some(PathToken::Number(y)) = tokenizer.next() {
203                            y
204                        } else {
205                            0.0
206                        };
207                        if cur_cmd == 'l' {
208                            cur.x += first_num;
209                            cur.y += py;
210                        } else {
211                            cur.x = first_num;
212                            cur.y = py;
213                        }
214                        current_verts.push(cur);
215                    }
216                    'H' | 'h' => {
217                        last_cubic_ctrl = None;
218                        last_quad_ctrl = None;
219                        if cur_cmd == 'h' {
220                            cur.x += first_num;
221                        } else {
222                            cur.x = first_num;
223                        }
224                        current_verts.push(cur);
225                    }
226                    'V' | 'v' => {
227                        last_cubic_ctrl = None;
228                        last_quad_ctrl = None;
229                        if cur_cmd == 'v' {
230                            cur.y += first_num;
231                        } else {
232                            cur.y = first_num;
233                        }
234                        current_verts.push(cur);
235                    }
236                    'C' | 'c' => {
237                        let y1 = if let Some(PathToken::Number(v)) = tokenizer.next() {
238                            v
239                        } else {
240                            0.0
241                        };
242                        let x2 = if let Some(PathToken::Number(v)) = tokenizer.next() {
243                            v
244                        } else {
245                            0.0
246                        };
247                        let y2 = if let Some(PathToken::Number(v)) = tokenizer.next() {
248                            v
249                        } else {
250                            0.0
251                        };
252                        let x = if let Some(PathToken::Number(v)) = tokenizer.next() {
253                            v
254                        } else {
255                            0.0
256                        };
257                        let y = if let Some(PathToken::Number(v)) = tokenizer.next() {
258                            v
259                        } else {
260                            0.0
261                        };
262
263                        let (p1, p2, p3) = if cur_cmd == 'c' {
264                            (
265                                Point2D::new(cur.x + first_num, cur.y + y1),
266                                Point2D::new(cur.x + x2, cur.y + y2),
267                                Point2D::new(cur.x + x, cur.y + y),
268                            )
269                        } else {
270                            (
271                                Point2D::new(first_num, y1),
272                                Point2D::new(x2, y2),
273                                Point2D::new(x, y),
274                            )
275                        };
276
277                        sample_cubic_bezier(cur, p1, p2, p3, 8, |pt| {
278                            current_verts.push(pt);
279                        });
280                        cur = p3;
281                        last_cubic_ctrl = Some(p2);
282                        last_quad_ctrl = None;
283                    }
284                    'S' | 's' => {
285                        let y2 = if let Some(PathToken::Number(v)) = tokenizer.next() {
286                            v
287                        } else {
288                            0.0
289                        };
290                        let x = if let Some(PathToken::Number(v)) = tokenizer.next() {
291                            v
292                        } else {
293                            0.0
294                        };
295                        let y = if let Some(PathToken::Number(v)) = tokenizer.next() {
296                            v
297                        } else {
298                            0.0
299                        };
300
301                        let (p2, p3) = if cur_cmd == 's' {
302                            (
303                                Point2D::new(cur.x + first_num, cur.y + y2),
304                                Point2D::new(cur.x + x, cur.y + y),
305                            )
306                        } else {
307                            (Point2D::new(first_num, y2), Point2D::new(x, y))
308                        };
309
310                        let p1 = if let Some(last_ctrl) = last_cubic_ctrl {
311                            Point2D::new(2.0 * cur.x - last_ctrl.x, 2.0 * cur.y - last_ctrl.y)
312                        } else {
313                            cur
314                        };
315
316                        sample_cubic_bezier(cur, p1, p2, p3, 8, |pt| {
317                            current_verts.push(pt);
318                        });
319                        cur = p3;
320                        last_cubic_ctrl = Some(p2);
321                        last_quad_ctrl = None;
322                    }
323                    'Q' | 'q' => {
324                        let y1 = if let Some(PathToken::Number(v)) = tokenizer.next() {
325                            v
326                        } else {
327                            0.0
328                        };
329                        let x = if let Some(PathToken::Number(v)) = tokenizer.next() {
330                            v
331                        } else {
332                            0.0
333                        };
334                        let y = if let Some(PathToken::Number(v)) = tokenizer.next() {
335                            v
336                        } else {
337                            0.0
338                        };
339
340                        let (p1, p2) = if cur_cmd == 'q' {
341                            (
342                                Point2D::new(cur.x + first_num, cur.y + y1),
343                                Point2D::new(cur.x + x, cur.y + y),
344                            )
345                        } else {
346                            (Point2D::new(first_num, y1), Point2D::new(x, y))
347                        };
348
349                        sample_quad_bezier(cur, p1, p2, 8, |pt| {
350                            current_verts.push(pt);
351                        });
352                        cur = p2;
353                        last_quad_ctrl = Some(p1);
354                        last_cubic_ctrl = None;
355                    }
356                    'T' | 't' => {
357                        let y = if let Some(PathToken::Number(v)) = tokenizer.next() {
358                            v
359                        } else {
360                            0.0
361                        };
362                        let p2 = if cur_cmd == 't' {
363                            Point2D::new(cur.x + first_num, cur.y + y)
364                        } else {
365                            Point2D::new(first_num, y)
366                        };
367
368                        let p1 = if let Some(last_ctrl) = last_quad_ctrl {
369                            Point2D::new(2.0 * cur.x - last_ctrl.x, 2.0 * cur.y - last_ctrl.y)
370                        } else {
371                            cur
372                        };
373
374                        sample_quad_bezier(cur, p1, p2, 8, |pt| {
375                            current_verts.push(pt);
376                        });
377                        cur = p2;
378                        last_quad_ctrl = Some(p1);
379                        last_cubic_ctrl = None;
380                    }
381                    'A' | 'a' => {
382                        let ry = if let Some(PathToken::Number(v)) = tokenizer.next() {
383                            v
384                        } else {
385                            0.0
386                        };
387                        let x_rot = if let Some(PathToken::Number(v)) = tokenizer.next() {
388                            v
389                        } else {
390                            0.0
391                        };
392                        let large_arc = if let Some(PathToken::Number(v)) = tokenizer.next() {
393                            v
394                        } else {
395                            0.0
396                        };
397                        let sweep = if let Some(PathToken::Number(v)) = tokenizer.next() {
398                            v
399                        } else {
400                            0.0
401                        };
402                        let x = if let Some(PathToken::Number(v)) = tokenizer.next() {
403                            v
404                        } else {
405                            0.0
406                        };
407                        let y = if let Some(PathToken::Number(v)) = tokenizer.next() {
408                            v
409                        } else {
410                            0.0
411                        };
412
413                        let dest = if cur_cmd == 'a' {
414                            Point2D::new(cur.x + x, cur.y + y)
415                        } else {
416                            Point2D::new(x, y)
417                        };
418
419                        sample_elliptical_arc(
420                            cur,
421                            dest,
422                            first_num,
423                            ry,
424                            x_rot,
425                            large_arc != 0.0,
426                            sweep != 0.0,
427                            16,
428                            |pt| current_verts.push(pt),
429                        );
430                        cur = dest;
431                        last_cubic_ctrl = None;
432                        last_quad_ctrl = None;
433                    }
434                    _ => {}
435                }
436            }
437            PathToken::Raw(_) => {}
438        }
439
440        if cur_cmd == 'Z' || cur_cmd == 'z' {
441            if !current_verts.is_empty() {
442                current_verts.push(start);
443                is_closed = true;
444                polylines.push((std::mem::take(&mut current_verts), is_closed));
445                is_closed = false;
446            }
447            cur = start;
448            last_cubic_ctrl = None;
449            last_quad_ctrl = None;
450        }
451    }
452
453    if !current_verts.is_empty() {
454        polylines.push((current_verts, is_closed));
455    }
456
457    polylines
458}
459
460/// High-level parsed SVG vector document data.
461#[derive(Clone, Debug, Default)]
462pub struct SvgVectorDocument {
463    pub width: f64,
464    pub height: f64,
465    pub elements: Vec<SvgElement>,
466}
467
468/// Type alias for [`SvgVectorDocument`].
469pub type SvgDocument = SvgVectorDocument;
470
471/// Parses standard SVG XML content into a structured collection of 2D vector elements.
472///
473/// Applies `<g>` layer nesting and active 2D affine transformations (`transform="translate(...) rotate(...) matrix(...)"`).
474pub fn parse_svg_elements(svg_content: &str) -> Result<SvgVectorDocument> {
475    let mut reader = Reader::from_str(svg_content);
476    reader.config_mut().trim_text(true);
477
478    let mut doc = SvgVectorDocument {
479        width: 800.0,
480        height: 600.0,
481        elements: Vec::new(),
482    };
483
484    let mut current_layer = "0".to_string();
485    let mut layer_stack = Vec::new();
486    let mut current_transform = Transform2D::identity();
487    let mut transform_stack = Vec::new();
488    let mut buf = Vec::new();
489
490    let mut current_text_state: Option<(Point2D, f64, Option<u32>, String, String, Transform2D)> =
491        None;
492
493    while let Ok(event) = reader.read_event_into(&mut buf) {
494        match event {
495            Event::Start(e) | Event::Empty(e) => {
496                let name = e.name().as_ref().to_vec();
497                let tag = String::from_utf8_lossy(&name).to_ascii_lowercase();
498
499                if tag == "svg" {
500                    for attr in e.attributes().flatten() {
501                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
502                        let v = String::from_utf8_lossy(&attr.value);
503                        if k == "viewbox" {
504                            let parts: Vec<f64> =
505                                v.split([' ', ',']).filter_map(|s| s.parse().ok()).collect();
506                            if parts.len() >= 4 && parts[2] > 0.0 && parts[3] > 0.0 {
507                                doc.width = parts[2];
508                                doc.height = parts[3];
509                            }
510                        } else if k == "width"
511                            && let Ok(w) = v
512                                .trim_end_matches("pt")
513                                .trim_end_matches("px")
514                                .parse::<f64>()
515                            && w > 0.0
516                        {
517                            doc.width = w;
518                        } else if k == "height"
519                            && let Ok(h) = v
520                                .trim_end_matches("pt")
521                                .trim_end_matches("px")
522                                .parse::<f64>()
523                            && h > 0.0
524                        {
525                            doc.height = h;
526                        }
527                    }
528                } else if tag == "g" {
529                    let mut layer_name = current_layer.clone();
530                    let mut g_transform = current_transform;
531                    for attr in e.attributes().flatten() {
532                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
533                        let v = String::from_utf8_lossy(&attr.value);
534                        if k == "id" {
535                            let sanitized = v.trim_start_matches("layer-");
536                            if !sanitized.is_empty() {
537                                layer_name = sanitized.to_string();
538                            }
539                        } else if k == "transform" {
540                            let tf = parse_transform(&v);
541                            g_transform = current_transform.multiply(&tf);
542                        }
543                    }
544                    layer_stack.push(current_layer.clone());
545                    current_layer = layer_name;
546                    transform_stack.push(current_transform);
547                    current_transform = g_transform;
548                } else if tag == "line" {
549                    let mut p1 = Point2D::default();
550                    let mut p2 = Point2D::default();
551                    let mut stroke = None;
552                    let mut width = 1.0;
553                    let mut elem_tf = Transform2D::identity();
554                    for attr in e.attributes().flatten() {
555                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
556                        let v = String::from_utf8_lossy(&attr.value);
557                        match k.as_str() {
558                            "x1" => p1.x = v.parse().unwrap_or(0.0),
559                            "y1" => p1.y = v.parse().unwrap_or(0.0),
560                            "x2" => p2.x = v.parse().unwrap_or(0.0),
561                            "y2" => p2.y = v.parse().unwrap_or(0.0),
562                            "stroke" => stroke = parse_color(&v),
563                            "stroke-width" => {
564                                width = v
565                                    .trim_end_matches("px")
566                                    .trim_end_matches("pt")
567                                    .parse()
568                                    .unwrap_or(1.0)
569                            }
570                            "style" => {
571                                let st = parse_style(&v);
572                                if let Some(s) = st.stroke {
573                                    stroke = s;
574                                }
575                                if let Some(w) = st.stroke_width {
576                                    width = w;
577                                }
578                            }
579                            "transform" => elem_tf = parse_transform(&v),
580                            _ => {}
581                        }
582                    }
583                    let tf = current_transform.multiply(&elem_tf);
584                    doc.elements.push(SvgElement::Line {
585                        p1: tf.apply(p1),
586                        p2: tf.apply(p2),
587                        stroke_color: stroke,
588                        stroke_width: width,
589                        layer: current_layer.clone(),
590                    });
591                } else if tag == "rect" {
592                    let mut x = 0.0;
593                    let mut y = 0.0;
594                    let mut width = 0.0;
595                    let mut height = 0.0;
596                    let mut stroke = None;
597                    let mut fill = None;
598                    let mut elem_tf = Transform2D::identity();
599                    for attr in e.attributes().flatten() {
600                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
601                        let v = String::from_utf8_lossy(&attr.value);
602                        match k.as_str() {
603                            "x" => x = v.parse().unwrap_or(0.0),
604                            "y" => y = v.parse().unwrap_or(0.0),
605                            "width" => width = v.parse().unwrap_or(0.0),
606                            "height" => height = v.parse().unwrap_or(0.0),
607                            "stroke" => stroke = parse_color(&v),
608                            "fill" => fill = parse_color(&v),
609                            "style" => {
610                                let st = parse_style(&v);
611                                if let Some(s) = st.stroke {
612                                    stroke = s;
613                                }
614                                if let Some(f) = st.fill {
615                                    fill = f;
616                                }
617                            }
618                            "transform" => elem_tf = parse_transform(&v),
619                            _ => {}
620                        }
621                    }
622                    if width > 0.0 && height > 0.0 {
623                        let tf = current_transform.multiply(&elem_tf);
624                        if tf.b.abs() < 1e-9 && tf.c.abs() < 1e-9 && tf.a > 0.0 && tf.d > 0.0 {
625                            let origin = tf.apply(Point2D::new(x, y));
626                            doc.elements.push(SvgElement::Rect {
627                                x: origin.x,
628                                y: origin.y,
629                                width: width * tf.a,
630                                height: height * tf.d,
631                                stroke_color: stroke,
632                                fill_color: fill,
633                                layer: current_layer.clone(),
634                            });
635                        } else {
636                            // Rotated or skewed rectangle: convert to polyline
637                            let p0 = tf.apply(Point2D::new(x, y));
638                            let p1 = tf.apply(Point2D::new(x + width, y));
639                            let p2 = tf.apply(Point2D::new(x + width, y + height));
640                            let p3 = tf.apply(Point2D::new(x, y + height));
641                            doc.elements.push(SvgElement::Polyline {
642                                points: vec![p0, p1, p2, p3, p0],
643                                is_closed: true,
644                                stroke_color: stroke,
645                                fill_color: fill,
646                                layer: current_layer.clone(),
647                            });
648                        }
649                    }
650                } else if tag == "circle" {
651                    let mut center = Point2D::default();
652                    let mut radius = 0.0;
653                    let mut stroke = None;
654                    let mut fill = None;
655                    let mut elem_tf = Transform2D::identity();
656                    for attr in e.attributes().flatten() {
657                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
658                        let v = String::from_utf8_lossy(&attr.value);
659                        match k.as_str() {
660                            "cx" => center.x = v.parse().unwrap_or(0.0),
661                            "cy" => center.y = v.parse().unwrap_or(0.0),
662                            "r" => radius = v.parse().unwrap_or(0.0),
663                            "stroke" => stroke = parse_color(&v),
664                            "fill" => fill = parse_color(&v),
665                            "style" => {
666                                let st = parse_style(&v);
667                                if let Some(s) = st.stroke {
668                                    stroke = s;
669                                }
670                                if let Some(f) = st.fill {
671                                    fill = f;
672                                }
673                            }
674                            "transform" => elem_tf = parse_transform(&v),
675                            _ => {}
676                        }
677                    }
678                    if radius > 0.0 {
679                        let tf = current_transform.multiply(&elem_tf);
680                        let center_trans = tf.apply(center);
681                        if tf.b.abs() < 1e-9
682                            && tf.c.abs() < 1e-9
683                            && (tf.a.abs() - tf.d.abs()).abs() < 1e-6
684                        {
685                            doc.elements.push(SvgElement::Circle {
686                                center: center_trans,
687                                radius: radius * tf.a.abs(),
688                                stroke_color: stroke,
689                                fill_color: fill,
690                                layer: current_layer.clone(),
691                            });
692                        } else {
693                            // Non-uniformly scaled or sheared circle: sample to polyline
694                            let mut pts = Vec::with_capacity(33);
695                            for i in 0..=32 {
696                                let angle = (i as f64 / 32.0) * std::f64::consts::PI * 2.0;
697                                let px = center.x + radius * angle.cos();
698                                let py = center.y + radius * angle.sin();
699                                pts.push(tf.apply(Point2D::new(px, py)));
700                            }
701                            doc.elements.push(SvgElement::Polyline {
702                                points: pts,
703                                is_closed: true,
704                                stroke_color: stroke,
705                                fill_color: fill,
706                                layer: current_layer.clone(),
707                            });
708                        }
709                    }
710                } else if tag == "polyline" || tag == "polygon" {
711                    let is_poly = tag == "polygon";
712                    let mut points = Vec::new();
713                    let mut stroke = None;
714                    let mut fill = None;
715                    let mut elem_tf = Transform2D::identity();
716                    for attr in e.attributes().flatten() {
717                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
718                        let v = String::from_utf8_lossy(&attr.value);
719                        if k == "points" {
720                            let coords: Vec<f64> =
721                                v.split([' ', ',']).filter_map(|s| s.parse().ok()).collect();
722                            points = coords
723                                .chunks_exact(2)
724                                .map(|c| Point2D::new(c[0], c[1]))
725                                .collect();
726                        } else if k == "stroke" {
727                            stroke = parse_color(&v);
728                        } else if k == "fill" {
729                            fill = parse_color(&v);
730                        } else if k == "style" {
731                            let st = parse_style(&v);
732                            if let Some(s) = st.stroke {
733                                stroke = s;
734                            }
735                            if let Some(f) = st.fill {
736                                fill = f;
737                            }
738                        } else if k == "transform" {
739                            elem_tf = parse_transform(&v);
740                        }
741                    }
742                    if !points.is_empty() {
743                        let tf = current_transform.multiply(&elem_tf);
744                        let transformed_pts: Vec<Point2D> =
745                            points.into_iter().map(|p| tf.apply(p)).collect();
746                        doc.elements.push(SvgElement::Polyline {
747                            points: transformed_pts,
748                            is_closed: is_poly,
749                            stroke_color: stroke,
750                            fill_color: fill,
751                            layer: current_layer.clone(),
752                        });
753                    }
754                } else if tag == "path" {
755                    let mut d_str = String::new();
756                    let mut stroke = None;
757                    let mut fill = None;
758                    let mut elem_tf = Transform2D::identity();
759                    for attr in e.attributes().flatten() {
760                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
761                        let v = String::from_utf8_lossy(&attr.value);
762                        if k == "d" {
763                            d_str = v.to_string();
764                        } else if k == "stroke" {
765                            stroke = parse_color(&v);
766                        } else if k == "fill" {
767                            fill = parse_color(&v);
768                        } else if k == "style" {
769                            let st = parse_style(&v);
770                            if let Some(s) = st.stroke {
771                                stroke = s;
772                            }
773                            if let Some(f) = st.fill {
774                                fill = f;
775                            }
776                        } else if k == "transform" {
777                            elem_tf = parse_transform(&v);
778                        }
779                    }
780                    if !d_str.is_empty() {
781                        let tf = current_transform.multiply(&elem_tf);
782                        let polylines = decompose_svg_path(&d_str);
783                        for (pts, is_closed) in polylines {
784                            if !pts.is_empty() {
785                                let transformed_pts =
786                                    pts.into_iter().map(|p| tf.apply(p)).collect();
787                                doc.elements.push(SvgElement::Polyline {
788                                    points: transformed_pts,
789                                    is_closed,
790                                    stroke_color: stroke,
791                                    fill_color: fill,
792                                    layer: current_layer.clone(),
793                                });
794                            }
795                        }
796                    }
797                } else if tag == "text" {
798                    let mut x = 0.0;
799                    let mut y = 0.0;
800                    let mut size = 12.0;
801                    let mut fill = None;
802                    let mut elem_tf = Transform2D::identity();
803                    for attr in e.attributes().flatten() {
804                        let k = String::from_utf8_lossy(attr.key.as_ref()).to_ascii_lowercase();
805                        let v = String::from_utf8_lossy(&attr.value);
806                        if k == "x" {
807                            x = v.parse().unwrap_or(0.0);
808                        } else if k == "y" {
809                            y = v.parse().unwrap_or(0.0);
810                        } else if k == "font-size" {
811                            size = v
812                                .trim_end_matches("px")
813                                .trim_end_matches("pt")
814                                .parse()
815                                .unwrap_or(12.0);
816                        } else if k == "fill" {
817                            fill = parse_color(&v);
818                        } else if k == "style" {
819                            let st = parse_style(&v);
820                            if let Some(f) = st.fill {
821                                fill = f;
822                            }
823                            if let Some(sz) = st.font_size {
824                                size = sz;
825                            }
826                        } else if k == "transform" {
827                            elem_tf = parse_transform(&v);
828                        }
829                    }
830                    let tf = current_transform.multiply(&elem_tf);
831                    current_text_state = Some((
832                        Point2D::new(x, y),
833                        size,
834                        fill,
835                        current_layer.clone(),
836                        String::new(),
837                        tf,
838                    ));
839                }
840            }
841            Event::Text(e) => {
842                if let Some((_, _, _, _, ref mut text, _)) = current_text_state {
843                    let text_content = String::from_utf8_lossy(&e);
844                    text.push_str(&text_content);
845                }
846            }
847            Event::End(e) => {
848                let name = e.name().as_ref().to_vec();
849                let tag = String::from_utf8_lossy(&name).to_ascii_lowercase();
850                if tag == "g" {
851                    if let Some(prev) = layer_stack.pop() {
852                        current_layer = prev;
853                    }
854                    if let Some(prev_tf) = transform_stack.pop() {
855                        current_transform = prev_tf;
856                    }
857                } else if tag == "text"
858                    && let Some((pos, font_size, color, layer, text, tf)) =
859                        current_text_state.take()
860                {
861                    let trimmed = text.trim();
862                    if !trimmed.is_empty() {
863                        doc.elements.push(SvgElement::Text {
864                            pos: tf.apply(pos),
865                            font_size,
866                            color,
867                            layer,
868                            content: trimmed.to_string(),
869                        });
870                    }
871                }
872            }
873            Event::Eof => break,
874            _ => {}
875        }
876        buf.clear();
877    }
878
879    Ok(doc)
880}
881
882/// Extracts the embedded diagram or DSL source from the root `<svg>` element `content` attribute,
883/// if present. Decodes XML entities safely.
884pub fn extract_embedded_source(svg_bytes: &[u8]) -> Option<String> {
885    let mut reader = Reader::from_reader(svg_bytes);
886    reader.config_mut().trim_text(false);
887    let mut buffer = Vec::new();
888
889    loop {
890        match reader.read_event_into(&mut buffer).ok()? {
891            Event::Start(start) | Event::Empty(start) => {
892                let name = start.name();
893                if name.as_ref() == b"svg" {
894                    for attr in start.attributes().flatten() {
895                        if attr.key.as_ref() == b"content" {
896                            let val_str = std::str::from_utf8(&attr.value).ok()?;
897                            let unescaped = quick_xml::escape::unescape(val_str).ok()?;
898                            if !unescaped.trim().is_empty() {
899                                return Some(unescaped.into_owned());
900                            }
901                        }
902                    }
903                    return None;
904                }
905            }
906            Event::Eof => return None,
907            _ => {}
908        }
909        buffer.clear();
910    }
911}
912
913/// Extracts the original source-format tag from the root SVG element.
914pub fn extract_source_format(svg_bytes: &[u8]) -> Option<String> {
915    let mut reader = Reader::from_reader(svg_bytes);
916    reader.config_mut().trim_text(false);
917    let mut buffer = Vec::new();
918    loop {
919        match reader.read_event_into(&mut buffer).ok()? {
920            Event::Start(start) | Event::Empty(start) if start.name().as_ref() == b"svg" => {
921                for attr in start.attributes().flatten() {
922                    if attr.key.as_ref() == b"data-source-format" {
923                        let value = std::str::from_utf8(&attr.value).ok()?;
924                        return quick_xml::escape::unescape(value)
925                            .ok()
926                            .map(|value| value.into_owned());
927                    }
928                }
929                return None;
930            }
931            Event::Eof => return None,
932            _ => {}
933        }
934        buffer.clear();
935    }
936}