Skip to main content

pizarra/
deserialize.rs

1use std::collections::HashMap;
2
3use xml::{
4    attribute::OwnedAttribute, reader::{EventReader, XmlEvent, Error as XmlError}
5};
6use thiserror::Error;
7
8use crate::app::Pizarra;
9use crate::storage::Storage;
10use crate::shape::{ShapeStored, stored::{path::Path, ellipse::Ellipse}};
11use crate::color::Color;
12use crate::point::{Vec2D, Unit, WorldUnit};
13use crate::geom::Angle;
14use crate::config::Config;
15use crate::style::{Style, Stroke};
16
17mod path;
18mod impls;
19
20use path::{parse_path, PathBuilder as SvgPathBuilder};
21
22#[derive(Debug, Error)]
23pub enum Error {
24    #[error(transparent)]
25    Xml(#[from] XmlError),
26
27    #[error("A path was found without the '{0}' attribute")]
28    PathMissingAttribute(String),
29
30    #[error("An ellipse was found without the '{0}' attribtue")]
31    EllipseMissingAttribute(String),
32
33    #[error("The given color '{0}' was not understood")]
34    CouldntUnderstandColor(String),
35
36    #[error("The background color '{0}' was not understood")]
37    InvalidBackgroundColor(String),
38
39    #[error("Background color not specified in file")]
40    NoBackgroundColor,
41
42    #[error("Found an unsupported value for attribute {attr}: {value}")]
43    UnsupportedAttributeValue { attr: &'static str, value: String },
44
45    #[error(transparent)]
46    PathParseError(#[from] path::ParseError),
47
48    #[error("A point part of a path doesn't have a known shape: {0}")]
49    PointDoesntMatchRegex(String),
50
51    #[error("the given angle was not understood: {0}")]
52    AngleDoesntMatchRegex(String),
53
54    #[error("An SVG tag that pizarra doesn't use was found: {0}")]
55    UnsupportedTag(String),
56
57    #[error("File is not svg, instead its first element is <{0}>")]
58    NotSvg(String),
59
60    #[error("Future version of pizarra found (file format v{0}). Consider updating")]
61    FutureVersion(String),
62
63    #[error("The file's first group does not have an ID")]
64    FirstGroupWihoutId,
65
66    #[error("The file's first group is not the background, but '{0}'")]
67    FirstGroupIsNotBackground(String),
68
69    #[error("The file's first group's id is not storage but '{0}'")]
70    FirstGroupIsNotStorage(String),
71
72    #[error("The file's second group's expected id was 'shapes' but instead it is '{0}'")]
73    SecondGroupNotShapes(String),
74
75    #[error("The file's second group doesn't have an ID")]
76    SecondGroupWithoutId,
77
78    #[error("Shapes where found in a group that is only supposed to contain the background")]
79    ShapesInBackgroundGroup,
80
81    #[error("Shapes where found outside groups")]
82    ShapesOutsideGroups,
83
84    #[error("Shapes where found before the background")]
85    ShapesBeforeBackground,
86}
87
88/// A partial result of deserialization
89#[derive(Debug)]
90pub struct Partial {
91    /// The portion that was properly read
92    pub read: Pizarra,
93
94    /// All the errors that were found
95    pub errors: Vec<Error>,
96}
97
98/// Result of the deserialization process.
99///
100/// With this the user can be warned when a file was not created with pizarra.
101#[derive(Debug)]
102pub enum DeserializeResult {
103    /// The file was parsed without problems as the stabilized file format
104    Ok(Pizarra),
105
106    /// The file could be understood, but problems where found. Likely not made
107    /// with pizarra but with another program, or made with pizarra and edited
108    /// with non-supported features.
109    Partial(Partial),
110
111    /// Something is very wrong with the file, like the encoding or the
112    /// formatting, thus the file cannot yield any usable content.
113    Err(Error),
114}
115
116impl DeserializeResult {
117    pub fn unwrap(self) -> Pizarra {
118        match self {
119            DeserializeResult::Ok(p) => p,
120            _ => panic!("Deserialization was not successful: {self:?}"),
121        }
122    }
123
124    pub fn unwrap_partial(self) -> Partial {
125        match self {
126            DeserializeResult::Partial(p) => p,
127            _ => panic!("Result of deserialization is not partial: {self:?}"),
128        }
129    }
130
131    pub fn unwrap_err(self) -> Error {
132        match self {
133            DeserializeResult::Err(e) => e,
134            _ => panic!("Deserialization didn't error: {self:?}"),
135        }
136    }
137}
138
139/// Converst a string taken from an element's style="" attribtue to a hashmap of
140/// the applied styles.
141fn css_attrs_as_hashmap(attrs: &str) -> HashMap<&str, &str> {
142    attrs.split(';').filter_map(|s| {
143        let pieces: Vec<_> = s.split(':').collect();
144
145        if pieces.len() != 2 {
146            return None
147        }
148
149        Some((pieces[0].trim(), pieces[1].trim()))
150    }).collect()
151}
152
153fn xml_attrs_as_hashmap(attrs: Vec<OwnedAttribute>) -> HashMap<String, String> {
154    attrs.into_iter().map(|a| {
155        (a.name.local_name, a.value)
156    }).collect()
157}
158
159fn parse_color(color_str: &str) -> Result<Option<Color>, Error> {
160    if color_str.to_lowercase() == "none" {
161        Ok(None)
162    } else {
163        Ok(Some(color_str.parse()?))
164    }
165}
166
167fn parse_length(attr: &'static str, stroke: &str) -> Result<WorldUnit, Error> {
168    stroke
169        .trim_end_matches("px")
170        .parse()
171        .map_err(|_| Error::UnsupportedAttributeValue {
172            attr,
173            value: stroke.to_owned()
174        })
175}
176
177fn parse_stroke_opacity(opacity: &str) -> Result<f64, Error> {
178    opacity
179        .parse()
180        .map_err(|_| Error::UnsupportedAttributeValue {
181            attr: "stroke-opacity",
182            value: opacity.to_owned(),
183        })
184}
185
186fn parse_fill_opacity(opacity: &str) -> Result<f64, Error> {
187    opacity
188        .parse()
189        .map_err(|_| Error::UnsupportedAttributeValue {
190            attr: "fill-opacity",
191            value: opacity.to_owned(),
192        })
193}
194
195impl Path {
196    pub fn from_xml_attributes(style: &str, path: &str, config: Config) -> Result<Path, Error> {
197        let attrs = css_attrs_as_hashmap(style);
198
199        let color: Option<Color> = attrs.get("stroke").map(|s| parse_color(s)).transpose()?.flatten();
200        let fill: Option<Color> = attrs.get("fill").map(|s| parse_color(s)).transpose()?.flatten();
201        let thickness = attrs.get("stroke-width").map(|s| parse_length("stroke-width", s)).transpose()?.unwrap_or_else(|| config.thickness.val().into());
202        let alpha = attrs.get("stroke-opacity").map(|s| parse_stroke_opacity(s)).transpose()?.unwrap_or(1.0);
203        let fill_alpha = attrs.get("fill-opacity").map(|s| parse_fill_opacity(s)).transpose()?.unwrap_or(1.0);
204
205        let mut builder = SvgPathBuilder::new();
206
207        parse_path(path, &mut builder)?;
208
209        Ok(Path::from_parts(
210            builder.into_path(),
211            Style {
212                stroke: color.map(|c| Stroke {
213                    color: c.with_float_alpha(alpha),
214                    size: thickness,
215                }),
216                fill: fill.map(|c| c.with_float_alpha(fill_alpha)),
217            },
218        ))
219    }
220}
221
222trait Deserialize {
223    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>, Error>;
224}
225
226impl Deserialize for Path {
227    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>, Error> {
228        let styleattr = attributes.get("style").ok_or_else(|| Error::PathMissingAttribute("style".into()))?;
229        let dattr = attributes.get("d").ok_or_else(|| Error::PathMissingAttribute("d".into()))?;
230
231        Ok(Box::new(Path::from_xml_attributes(styleattr, dattr, config)?))
232    }
233}
234
235impl Deserialize for Ellipse {
236    fn deserialize(attributes: HashMap<String, String>, config: Config) -> Result<Box<dyn ShapeStored>, Error> {
237        let cx = parse_length("cx", attributes.get("cx").ok_or_else(|| Error::EllipseMissingAttribute("cx".into()))?)?;
238        let cy = parse_length("cy", attributes.get("cy").ok_or_else(|| Error::EllipseMissingAttribute("cy".into()))?)?;
239        let rx = parse_length("rx", attributes.get("rx").ok_or_else(|| Error::EllipseMissingAttribute("rx".into()))?)?;
240        let ry = parse_length("ry", attributes.get("ry").ok_or_else(|| Error::EllipseMissingAttribute("ry".into()))?)?;
241        let angle= attributes.get("transform").map(|s| s.as_str()).unwrap_or("rotate(0)");
242        let styleattr = attributes.get("style").ok_or_else(|| Error::EllipseMissingAttribute("style".into()))?;
243
244        let attrs = css_attrs_as_hashmap(styleattr);
245
246        let color: Option<Color> = attrs.get("stroke").map(|s| parse_color(s)).transpose()?.flatten();
247        let fill: Option<Color> = attrs.get("fill").map(|s| parse_color(s)).transpose()?.flatten();
248        let thickness = attrs.get("stroke-width").map(|s| parse_length("stroke-width", s)).transpose()?.unwrap_or_else(|| config.thickness.val().into());
249        let alpha = attrs.get("stroke-opacity").map(|s| parse_stroke_opacity(s)).transpose()?.unwrap_or(1.0);
250        let fill_alpha = attrs.get("fill-opacity").map(|s| parse_fill_opacity(s)).transpose()?.unwrap_or(1.0);
251
252        let angle: Angle = angle.parse()?;
253
254        let center = Vec2D::new(cx, cy);
255
256        Ok(Box::new(Ellipse::from_parts(
257            center,
258            rx,
259            ry,
260            angle,
261            Style {
262                stroke: color.map(|c| Stroke {
263                    color: c.with_float_alpha(alpha),
264                    size: thickness,
265                }),
266                fill: fill.map(|c| c.with_float_alpha(fill_alpha)),
267            },
268        )))
269    }
270}
271
272/// Keeps the state of deserialization
273#[derive(Debug)]
274enum DeState {
275    /// Start, we know nothing about the format yet
276    Initial,
277
278    /// So far it looks like a correct v2 file
279    V2Started,
280
281    /// <g id="background"> just seen
282    V2BackgroundOpened,
283
284    /// The rectangle with the fill color just appeared
285    V2BackgroundRead {
286        background: Color,
287    },
288
289    V2StorageStarted {
290        storage: Storage,
291        background: Color,
292    },
293
294    /// So far it looks like a correct v1 file
295    V1Started,
296
297    /// Just found <g id="storage">
298    V1StorageOpened,
299
300    V1Correct {
301        storage: Storage,
302        background: Color,
303    },
304
305    Finished {
306        storage: Storage,
307        background: Color,
308    },
309
310    /// This file its likely not made with pizarra or it was edited in
311    /// non-supported ways. Pizarra will do its best to understand it but the
312    /// user should be warned.
313    External {
314        background: Color,
315        storage: Storage,
316        errors: Vec<Error>,
317    },
318
319    /// Not an SVG file. This is a final state and contains the first tag that
320    /// was found
321    NotSvg(String),
322
323    Empty,
324}
325
326use DeState::*;
327
328/// Gets a single attribute's value from a list of them. Used for cases where
329/// only one attribute is needed to spare the hashmap allocation.
330fn get_single_attr<'a>(attrs: &'a [OwnedAttribute], key: &str, namespace: Option<&str>) -> Option<&'a str> {
331    for attr in attrs {
332        if attr.name.prefix.as_deref() == namespace && attr.name.local_name == key {
333            return Some(&attr.value)
334        }
335    }
336
337    None
338}
339
340macro_rules! parse_shape_v1 {
341    ($shape_type:ty, $attributes:ident, $config:ident, $storage:ident, $background:ident) => {
342        {
343            let attributes = xml_attrs_as_hashmap($attributes);
344            let path = <$shape_type>::deserialize(attributes, $config);
345
346            match path {
347                Ok(path) => {
348                    $storage.add(path);
349                    V1Correct { $storage, $background }
350                }
351                Err(e) => External {
352                    $storage, $background,
353                    errors: vec![e],
354                },
355            }
356        }
357    };
358}
359
360macro_rules! parse_shape_v2 {
361    ($shape_type:ty, $attributes:ident, $config:ident, $storage:ident, $background:ident) => {
362        {
363            let attributes = xml_attrs_as_hashmap($attributes);
364            let path = <$shape_type>::deserialize(attributes, $config);
365
366            match path {
367                Ok(path) => {
368                    $storage.add(path);
369                    V2StorageStarted { $storage, $background }
370                }
371                Err(e) => External {
372                    $storage, $background,
373                    errors: vec![e],
374                },
375            }
376        }
377    };
378}
379
380macro_rules! parse_shape_external {
381    ($shape_type:ty, $attributes:ident, $config:ident, $storage:ident, $background:ident, $errors:ident) => {
382        {
383            let attributes = xml_attrs_as_hashmap($attributes);
384            let path = <$shape_type>::deserialize(attributes, $config);
385
386            match path {
387                Ok(path) => {
388                    $storage.add(path);
389                    External { $storage, $background, $errors }
390                }
391                Err(e) => External {
392                    $storage, $background,
393                    errors: vec![e],
394                },
395            }
396        }
397    };
398}
399
400/// Used to quickly return a transition to the External state given a found
401/// error
402macro_rules! external {
403    ($error: expr, $config: ident) => {
404        External {
405            background: $config.background_color,
406            storage: Storage::new(),
407            errors: vec![$error],
408        }
409    }
410}
411
412fn step(state: DeState, event: XmlEvent, config: Config) -> DeState {
413    match (state, event) {
414        (state@Initial, XmlEvent::StartDocument { .. }) => state,
415        (Initial, XmlEvent::EndDocument) => Empty,
416        (state, XmlEvent::StartDocument { .. }) => unreachable!("Start document in state: {state:?}"),
417
418        (Initial, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "svg" => {
419            if let Some(version) = get_single_attr(&attributes, "format", Some("pizarra")) {
420                if version.trim() == "2" {
421                    // So far we are looking at a correct V2 file
422                    V2Started
423                } else {
424                    // version field exists but it is not "2". We might be
425                    // dealing with a file created using a future version of
426                    // pizarra
427                    external!(Error::FutureVersion(version.into()), config)
428                }
429            } else {
430                // No version field at all (or at least not in the pizarra
431                // namespace) so this looks so far like a V1 format
432                V1Started
433            }
434        }
435
436        // First element is not <svg>, discard this file faster for not even
437        // trying to be svg
438        (Initial, XmlEvent::StartElement { name, .. }) => NotSvg(name.local_name),
439        (state@NotSvg(_), _) => state,
440
441        (V2Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "g" => {
442            if let Some(id) = get_single_attr(&attributes, "id", None) {
443                if id == "background" {
444                    V2BackgroundOpened
445                } else {
446                    external!(Error::FirstGroupIsNotBackground(id.to_string()), config)
447                }
448            } else {
449                external!(Error::FirstGroupWihoutId, config)
450            }
451        }
452        (V2Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
453            let mut storage = Storage::new();
454            let background = config.background_color;
455            let errors = vec![Error::ShapesOutsideGroups];
456
457            parse_shape_external!(Path, attributes, config, storage, background, errors)
458        }
459        (V2Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
460            let mut storage = Storage::new();
461            let background = config.background_color;
462            let errors = vec![Error::ShapesOutsideGroups];
463
464            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
465        }
466        (V2Started, XmlEvent::StartElement { name, .. }) => external!(Error::UnsupportedTag(name.local_name), config),
467        (V2Started, XmlEvent::EndDocument) => Empty,
468
469        (V2BackgroundOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "rect" => {
470            // The first and only shape in the background group is a rectangle
471            // with the fill set to the background color.
472            if let Some(style) = get_single_attr(&attributes, "style", None) {
473                let attrs = css_attrs_as_hashmap(style);
474                let color = attrs.get("fill");
475
476                match color {
477                    Some(color) => match parse_color(color) {
478                        Ok(Some(color)) => V2BackgroundRead {
479                            background: color,
480                        },
481                        Ok(None) => V2BackgroundRead {
482                            background: config.background_color,
483                        },
484                        Err(_) => external!(Error::InvalidBackgroundColor(color.to_string()), config),
485                    }
486                    None => external!(Error::NoBackgroundColor, config),
487                }
488            } else {
489                external!(Error::NoBackgroundColor, config)
490            }
491        }
492        (V2BackgroundOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
493            let mut storage = Storage::new();
494            let background = config.background_color;
495            let errors = vec![Error::ShapesInBackgroundGroup];
496
497            parse_shape_external!(Path, attributes, config, storage, background, errors)
498        }
499        (V2BackgroundOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
500            let mut storage = Storage::new();
501            let background = config.background_color;
502            let errors = vec![Error::ShapesInBackgroundGroup];
503
504            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
505        }
506        (V2BackgroundOpened, XmlEvent::StartElement { name, .. }) => External {
507            background: config.background_color,
508            storage: Storage::new(),
509            errors: vec![Error::ShapesInBackgroundGroup, Error::UnsupportedTag(name.local_name)],
510        },
511        (V2BackgroundOpened, XmlEvent::EndDocument) => Empty,
512
513        (V2BackgroundRead { background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "g" => {
514            if let Some(id) = get_single_attr(&attributes, "id", None) {
515                if id == "shapes" {
516                    V2StorageStarted {
517                        storage: Storage::new(),
518                        background,
519                    }
520                } else {
521                    external!(Error::SecondGroupNotShapes(id.to_string()), config)
522                }
523            } else {
524                external!(Error::SecondGroupWithoutId, config)
525            }
526        }
527        (V2BackgroundRead { background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
528            let errors = vec![Error::ShapesOutsideGroups];
529            let mut storage = Storage::new();
530
531            parse_shape_external!(Path, attributes, config, storage, background, errors)
532        }
533        (V2BackgroundRead { background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
534            let errors = vec![Error::ShapesOutsideGroups];
535            let mut storage = Storage::new();
536
537            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
538        }
539        (V2BackgroundRead { background }, XmlEvent::StartElement { name, .. }) => External {
540            background,
541            storage: Storage::new(),
542            errors: vec![Error::ShapesOutsideGroups, Error::UnsupportedTag(name.local_name)],
543        },
544        (V2BackgroundRead { background }, XmlEvent::EndDocument) => Finished {
545            background,
546            storage: Storage::new(),
547        },
548
549        (V2StorageStarted { mut storage, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
550            parse_shape_v2!(Path, attributes, config, storage, background)
551        }
552        (V2StorageStarted { mut storage, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
553            parse_shape_v2!(Ellipse, attributes, config, storage, background)
554        }
555        (V2StorageStarted { storage, background }, XmlEvent::StartElement { name, .. }) => {
556            External {
557                background,
558                storage,
559                errors: vec![Error::UnsupportedTag(name.local_name)],
560            }
561        }
562        (V2StorageStarted { storage, background }, XmlEvent::EndDocument) => Finished { storage, background },
563
564        (V1Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "g" => {
565            if let Some(id) = get_single_attr(&attributes, "id", None) {
566                if id == "storage" {
567                    V1StorageOpened
568                } else {
569                    external!(Error::FirstGroupIsNotStorage(id.to_string()), config)
570                }
571            } else {
572                external!(Error::FirstGroupWihoutId, config)
573            }
574        }
575        (V1Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
576            let background = config.background_color;
577            let mut storage = Storage::new();
578            let errors = vec![Error::ShapesOutsideGroups];
579
580            parse_shape_external!(Path, attributes, config, storage, background, errors)
581        }
582        (V1Started, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
583            let background = config.background_color;
584            let mut storage = Storage::new();
585            let errors = vec![Error::ShapesOutsideGroups];
586
587            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
588        }
589        (V1Started, XmlEvent::StartElement { name, .. }) => {
590            External {
591                background: config.background_color,
592                storage: Storage::new(),
593                errors: vec![Error::ShapesOutsideGroups, Error::UnsupportedTag(name.local_name)],
594            }
595        }
596        (V1Started, XmlEvent::EndDocument) => Empty,
597
598        (V1StorageOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "rect" => {
599            // The first rect in this state is the background, lets read it.
600            if let Some(style) = get_single_attr(&attributes, "style", None) {
601                let attrs = css_attrs_as_hashmap(style);
602                let color = attrs.get("fill");
603
604                match color {
605                    Some(color) => match parse_color(color) {
606                        Ok(Some(color)) => V1Correct {
607                            storage: Storage::new(),
608                            background: color,
609                        },
610                        Ok(None) => V1Correct {
611                            storage: Storage::new(),
612                            background: config.background_color,
613                        },
614                        Err(_) => external!(Error::InvalidBackgroundColor(color.to_string()), config),
615                    },
616                    None => external!(Error::NoBackgroundColor, config),
617                }
618            } else {
619                external!(Error::NoBackgroundColor, config)
620            }
621        }
622        (V1StorageOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
623            let mut storage = Storage::new();
624            let background = config.background_color;
625            let errors = vec![Error::ShapesBeforeBackground];
626
627            parse_shape_external!(Path, attributes, config, storage, background, errors)
628        }
629        (V1StorageOpened, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
630            let mut storage = Storage::new();
631            let background = config.background_color;
632            let errors = vec![Error::ShapesBeforeBackground];
633
634            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
635        }
636        (V1StorageOpened, XmlEvent::StartElement { name, .. }) => External {
637            background: config.background_color,
638            storage: Storage::new(),
639            errors: vec![Error::ShapesBeforeBackground, Error::UnsupportedTag(name.local_name)],
640        },
641        (V1StorageOpened, XmlEvent::EndDocument) => Empty,
642
643        (V1Correct { mut storage, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
644            parse_shape_v1!(Path, attributes, config, storage, background)
645        }
646        (V1Correct { mut storage, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
647            parse_shape_v1!(Ellipse, attributes, config, storage, background)
648        }
649        (V1Correct { storage, background }, XmlEvent::StartElement { name, .. }) => External {
650            background,
651            storage,
652            errors: vec![Error::UnsupportedTag(name.local_name)],
653        },
654        (V1Correct { storage, background }, XmlEvent::EndDocument) => Finished { storage, background },
655
656        // Explicitly ignored events
657        (state, XmlEvent::Whitespace(_)) => state,
658        (state, XmlEvent::EndElement { .. }) => state,
659        (state, XmlEvent::ProcessingInstruction { .. }) => state,
660        (state, XmlEvent::CData(_)) => state,
661        (state, XmlEvent::Comment(_)) => state,
662        (state, XmlEvent::Characters(_)) => state,
663
664        // Once an error has ocurred while reading a file it is tagged as
665        // external and further deserializing happens here.
666        (External { mut storage, errors, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "path" => {
667            parse_shape_external!(Path, attributes, config, storage, background, errors)
668        }
669        (External { mut storage, errors, background }, XmlEvent::StartElement { name, attributes, .. }) if name.local_name == "ellipse" => {
670            parse_shape_external!(Ellipse, attributes, config, storage, background, errors)
671        }
672        // Ignore groups if reading external file
673        (state@External { .. }, XmlEvent::StartElement { name, .. }) if name.local_name == "g" => state,
674        (state@External { .. }, XmlEvent::EndDocument) => state,
675        // Catch all unknown tags
676        (External { storage, mut errors, background }, XmlEvent::StartElement { name, .. }) => {
677            errors.push(Error::UnsupportedTag(name.local_name));
678
679            External { storage, errors, background }
680        }
681
682        // Exotic states
683        (Finished { .. }, XmlEvent::StartElement { .. }) => unreachable!("Got start element but file was finished"),
684        (Finished { .. }, XmlEvent::EndDocument) => unreachable!("Why did we get more events after EndDocument?"),
685        (Empty, XmlEvent::StartElement{ .. }) => unreachable!("Element started after empty state"),
686        (Empty, XmlEvent::EndDocument) => unreachable!("Empty is a final state and got an EndDocument"),
687    }
688}
689
690impl Pizarra {
691    pub fn from_svg(svg: &str, config: Config) -> DeserializeResult {
692        let parser = EventReader::from_str(svg);
693        let mut state = Initial;
694
695        for e in parser {
696            state = match e {
697                Ok(event) => step(state, event, config),
698                Err(e) => return DeserializeResult::Err(e.into()),
699            };
700        }
701
702        match state {
703            Finished { storage, background } => {
704                let mut pizarra = Pizarra::new(Vec2D::new_screen(0.0, 0.0), config);
705
706                pizarra.set_bgcolor(background);
707                pizarra.set_storage(storage);
708
709                DeserializeResult::Ok(pizarra)
710            }
711            External { background, storage, errors } => {
712                let mut pizarra = Pizarra::new(Vec2D::new_screen(0.0, 0.0), config);
713
714                pizarra.set_bgcolor(background);
715                pizarra.set_storage(storage);
716
717                DeserializeResult::Partial(Partial {
718                    read: pizarra,
719                    errors,
720                })
721            }
722            NotSvg(tag) => DeserializeResult::Err(Error::NotSvg(tag)),
723            x => {
724                dbg!(x);
725                todo!()
726            }
727        }
728    }
729}
730
731#[cfg(test)]
732mod tests {
733    use crate::path_command::PathCommand;
734    use crate::draw_commands::DrawCommand;
735
736    use super::*;
737
738    #[test]
739    fn test_from_svg() {
740        let svg_data = include_str!("../res/simple_file_load.svg");
741        let partial = Pizarra::from_svg(svg_data, Default::default()).unwrap_partial();
742
743        assert_eq!(partial.read.storage().shape_count(), 1);
744
745        assert_eq!(partial.errors.len(), 2);
746        assert!(matches!(dbg!(&partial.errors[0]), Error::FirstGroupIsNotStorage(x) if x == "surface2538"));
747        assert!(matches!(dbg!(&partial.errors[1]), Error::UnsupportedTag(x) if x == "rect"));
748    }
749
750    /// This represents the very first SVG format of pizarra.
751    #[test]
752    fn read_original_serialization_test() {
753        let svg_data = include_str!("../res/deserialize/original.svg");
754        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
755
756        assert_eq!(app.storage().shape_count(), 1);
757    }
758
759    #[test]
760    fn test_can_deserialize_circle() {
761        let svg_data = include_str!("../res/circle.svg");
762        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
763
764        assert_eq!(app.storage().shape_count(), 1);
765    }
766
767    #[test]
768    fn test_can_deserialize_ellipse() {
769        let svg_data = include_str!("../res/ellipse.svg");
770        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
771
772        assert_eq!(app.storage().shape_count(), 1);
773    }
774
775    #[test]
776    fn test_point_bug() {
777        let svg_data = include_str!("../res/bug_opening.svg");
778        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
779
780        assert_eq!(app.storage().shape_count(), 4);
781    }
782
783    #[test]
784    fn test_line_from_xml_attributes() {
785        let line = Path::from_xml_attributes("fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(53.90625%,88.28125%,20.3125%);stroke-opacity:1;stroke-miterlimit:10;", "M 147.570312 40.121094 L 146.9375 40.121094 L 145.296875 40.460938 L 142.519531 41.710938 L 139.613281 43.304688 L 138.097656 44.894531 L 137.339844 46.714844 L 138.097656 47.621094 L 139.992188 47.851562 L 142.898438 47.621094 L 146.179688 47.167969 L 150.097656 47.964844 L 151.738281 49.667969 L 152.496094 51.714844 L 152.875 53.191406 ", Default::default()).unwrap();
786
787        if let DrawCommand::Path {
788            commands, style, ..
789        } = line.draw_commands() {
790            assert_eq!(style.stroke.unwrap().color, Color::from_float_rgb(0.5390625, 0.8828125, 0.203125));
791            assert_eq!(commands, vec![
792                PathCommand::MoveTo(Vec2D::new_world(147.570312, 40.121094)),
793                PathCommand::LineTo(Vec2D::new_world(146.9375, 40.121094)),
794                PathCommand::LineTo(Vec2D::new_world(145.296875, 40.460938)),
795                PathCommand::LineTo(Vec2D::new_world(142.519531, 41.710938)),
796                PathCommand::LineTo(Vec2D::new_world(139.613281, 43.304688)),
797                PathCommand::LineTo(Vec2D::new_world(138.097656, 44.894531)),
798                PathCommand::LineTo(Vec2D::new_world(137.339844, 46.714844)),
799                PathCommand::LineTo(Vec2D::new_world(138.097656, 47.621094)),
800                PathCommand::LineTo(Vec2D::new_world(139.992188, 47.851562)),
801                PathCommand::LineTo(Vec2D::new_world(142.898438, 47.621094)),
802                PathCommand::LineTo(Vec2D::new_world(146.179688, 47.167969)),
803                PathCommand::LineTo(Vec2D::new_world(150.097656, 47.964844)),
804                PathCommand::LineTo(Vec2D::new_world(151.738281, 49.667969)),
805                PathCommand::LineTo(Vec2D::new_world(152.496094, 51.714844)),
806                PathCommand::LineTo(Vec2D::new_world(152.875, 53.191406)),
807            ]);
808            assert_eq!(style.stroke.unwrap().size, 3.0.into());
809        } else {
810            panic!();
811        }
812    }
813
814    #[test]
815    fn test_parse_alpha() {
816        let line = Path::from_xml_attributes("fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:#FF0000;stroke-opacity:0.8;stroke-miterlimit:10;", "M 10 10 L 20 20", Default::default()).unwrap();
817
818        assert_eq!(line.style().stroke.unwrap().color, Color::red().with_float_alpha(0.8));
819    }
820
821    #[test]
822    fn can_deserialize_rotated_ellipse() {
823        let attrs: HashMap<String, String> = vec![
824            ("cx".into(), "20.4".into()),
825            ("cy".into(), "-30.5".into()),
826            ("rx".into(), "5.6".into()),
827            ("ry".into(), "3.5".into()),
828            ("transform".into(), "rotate(34.5)".into()),
829            ("style".into(), "stroke:#cabada;stroke-width:3.5;stroke-opacity:0.8".into()),
830        ].into_iter().collect();
831
832        let deserialized = Ellipse::deserialize(attrs, Default::default()).unwrap();
833
834        match deserialized.draw_commands() {
835            DrawCommand::Ellipse { ellipse: e, style } => {
836                assert_eq!(e.center, Vec2D::new_world(20.4, -30.5));
837                assert_eq!(e.semimajor, 5.6.into());
838                assert_eq!(e.semiminor, 3.5.into());
839                assert_eq!(e.angle.degrees(), 34.5);
840                assert_eq!(style.stroke.unwrap().color, Color::from_int_rgb(0xca, 0xba, 0xda).with_float_alpha(0.8));
841                assert_eq!(style.stroke.unwrap().size, 3.5.into());
842            },
843            _ => panic!()
844        }
845    }
846
847    #[test]
848    fn can_deserialize_ellipse_serialization_test() {
849        let svg_data = include_str!("../res/serialize/ellipse.svg");
850
851        Pizarra::from_svg(svg_data, Default::default()).unwrap();
852    }
853
854    #[test]
855    fn fill_and_stroke_can_be_none_in_path() {
856        let svg_data = include_str!("../res/color_fill_none_path.svg");
857        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
858
859        let commands = app.storage().draw_commands(app.storage().get_bounds().unwrap());
860        let command = &commands[0];
861
862        assert_eq!(command.color(), None);
863        assert_eq!(command.fill(), None);
864    }
865
866    #[test]
867    fn fill_and_stroke_can_be_none_in_ellipse() {
868        let svg_data = include_str!("../res/color_fill_none_ellipse.svg");
869        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
870
871        let commands = app.storage().draw_commands(app.storage().get_bounds().unwrap());
872        let command = &commands[0];
873
874        assert_eq!(command.color(), None);
875        assert_eq!(command.fill(), None);
876    }
877
878    #[test]
879    fn v2_format_can_be_read() {
880        let svg_data = include_str!("../res/deserialize/v2_format.svg");
881        let app = Pizarra::from_svg(svg_data, Default::default()).unwrap();
882
883        assert_eq!(app.bgcolor(), Color::from_int_rgb(0xba, 0xde, 0xba));
884
885        assert_eq!(app.storage().shape_count(), 78);
886    }
887
888    #[test]
889    fn file_made_in_another_software() {
890        let made_external = include_str!("../res/deserialize/made_external.svg");
891        let partial = Pizarra::from_svg(made_external, Default::default()).unwrap_partial();
892
893        assert_eq!(partial.read.storage().shape_count(), 1);
894        assert_eq!(partial.errors.len(), 4);
895
896        assert!(matches!(dbg!(&partial.errors[0]), Error::ShapesOutsideGroups));
897        assert!(matches!(dbg!(&partial.errors[1]), Error::UnsupportedTag(x) if x == "namedview"));
898        assert!(matches!(dbg!(&partial.errors[2]), Error::UnsupportedTag(x) if x == "defs"));
899        assert!(matches!(dbg!(&partial.errors[3]), Error::UnsupportedTag(x) if x == "rect"));
900    }
901
902    #[test]
903    fn file_with_errors() {
904        let errored_file_1 = include_str!("../res/deserialize/errored_file_1.svg");
905        let err = Pizarra::from_svg(errored_file_1, Default::default()).unwrap_err();
906
907        assert!(matches!(dbg!(err), Error::Xml(_)));
908    }
909
910    #[test]
911    fn non_svg_file() {
912        let file = include_str!("../res/deserialize/not.svg");
913        let err = Pizarra::from_svg(file, Default::default()).unwrap_err();
914
915        assert!(matches!(err, Error::NotSvg(x) if x == "foo"));
916    }
917
918    #[test]
919    fn v1_format_can_be_read() {
920        let v1_format_file = include_str!("../res/deserialize/v1_format_bgcolor.svg");
921        let pizarra = Pizarra::from_svg(v1_format_file, Default::default()).unwrap();
922
923        assert_eq!(pizarra.bgcolor(), Color::from_int_rgb(0xca, 0xe3, 0xff));
924
925        assert_eq!(pizarra.storage().shape_count(), 78);
926    }
927
928    #[test]
929    fn v1_edited_is_recovered() {
930        let file = include_str!("../res/deserialize/v1_edited.svg");
931        let partial = Pizarra::from_svg(file, Default::default()).unwrap_partial();
932
933        assert_eq!(partial.read.storage().shape_count(), 77);
934        assert_eq!(partial.errors.len(), 6);
935
936        assert!(matches!(dbg!(&partial.errors[0]), Error::ShapesOutsideGroups));
937        assert!(matches!(dbg!(&partial.errors[1]), Error::UnsupportedTag(x) if x == "defs"));
938        assert!(matches!(dbg!(&partial.errors[2]), Error::UnsupportedTag(x) if x == "namedview"));
939        assert!(matches!(dbg!(&partial.errors[3]), Error::UnsupportedTag(x) if x == "rect"));
940        assert!(matches!(dbg!(&partial.errors[4]), Error::UnsupportedTag(x) if x == "circle"));
941        assert!(matches!(dbg!(&partial.errors[5]), Error::UnsupportedTag(x) if x == "circle"));
942    }
943
944    #[test]
945    fn v2_edited_is_recovered() {
946        let file = include_str!("../res/deserialize/v2_edited.svg");
947        let partial = Pizarra::from_svg(file, Default::default()).unwrap_partial();
948
949        assert_eq!(partial.read.storage().shape_count(), 79);
950        assert_eq!(partial.errors.len(), 3);
951
952        assert!(matches!(dbg!(&partial.errors[0]), Error::UnsupportedTag(x) if x == "defs"));
953        assert!(matches!(dbg!(&partial.errors[1]), Error::UnsupportedTag(x) if x == "namedview"));
954        assert!(matches!(dbg!(&partial.errors[2]), Error::UnsupportedTag(x) if x == "rect"));
955    }
956}