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#[derive(Debug)]
90pub struct Partial {
91 pub read: Pizarra,
93
94 pub errors: Vec<Error>,
96}
97
98#[derive(Debug)]
102pub enum DeserializeResult {
103 Ok(Pizarra),
105
106 Partial(Partial),
110
111 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
139fn 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#[derive(Debug)]
274enum DeState {
275 Initial,
277
278 V2Started,
280
281 V2BackgroundOpened,
283
284 V2BackgroundRead {
286 background: Color,
287 },
288
289 V2StorageStarted {
290 storage: Storage,
291 background: Color,
292 },
293
294 V1Started,
296
297 V1StorageOpened,
299
300 V1Correct {
301 storage: Storage,
302 background: Color,
303 },
304
305 Finished {
306 storage: Storage,
307 background: Color,
308 },
309
310 External {
314 background: Color,
315 storage: Storage,
316 errors: Vec<Error>,
317 },
318
319 NotSvg(String),
322
323 Empty,
324}
325
326use DeState::*;
327
328fn 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
400macro_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 V2Started
423 } else {
424 external!(Error::FutureVersion(version.into()), config)
428 }
429 } else {
430 V1Started
433 }
434 }
435
436 (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 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 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 (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 (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 (state@External { .. }, XmlEvent::StartElement { name, .. }) if name.local_name == "g" => state,
674 (state@External { .. }, XmlEvent::EndDocument) => state,
675 (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 (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 #[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}