1use crate::parser::types::{
2 Command, FontBoldness, FontSlant, FontStretch, JustifyX, JustifyY, Phase, PinType, Side,
3 WireType,
4};
5use base64::{Engine, engine::general_purpose};
6use image::ImageFormat;
7use std::collections::HashMap;
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11use svg::Document;
12use svg::node::element::{
13 Circle, Definitions, Group, Image, Polygon, Polyline, Rectangle, TSpan, Text,
14};
15use svg::node::{Text as TextNode, Value};
16use thiserror::Error;
17
18#[derive(Debug, Clone)]
19pub enum ThemeValue {
20 String(String),
21 Float(f32),
22 Int(u32),
23 FontSlant(FontSlant),
24 FontBoldness(FontBoldness),
25 FontStretch(FontStretch),
26}
27
28impl From<String> for ThemeValue {
29 fn from(value: String) -> Self {
30 ThemeValue::String(value)
31 }
32}
33
34impl From<&str> for ThemeValue {
35 fn from(value: &str) -> Self {
36 ThemeValue::String(value.to_string())
37 }
38}
39
40impl From<f32> for ThemeValue {
41 fn from(value: f32) -> Self {
42 ThemeValue::Float(value)
43 }
44}
45
46impl From<u32> for ThemeValue {
47 fn from(value: u32) -> Self {
48 ThemeValue::Int(value)
49 }
50}
51
52impl From<FontSlant> for ThemeValue {
53 fn from(value: FontSlant) -> Self {
54 ThemeValue::FontSlant(value)
55 }
56}
57
58impl From<FontBoldness> for ThemeValue {
59 fn from(value: FontBoldness) -> Self {
60 ThemeValue::FontBoldness(value)
61 }
62}
63
64impl From<FontStretch> for ThemeValue {
65 fn from(value: FontStretch) -> Self {
66 ThemeValue::FontStretch(value)
67 }
68}
69
70pub trait FromThemeValue {
72 fn from_theme_value(value: &ThemeValue) -> Option<Self>
73 where
74 Self: Sized;
75}
76
77impl FromThemeValue for String {
78 fn from_theme_value(value: &ThemeValue) -> Option<Self> {
79 Some(value.as_string())
80 }
81}
82
83impl FromThemeValue for f32 {
84 fn from_theme_value(value: &ThemeValue) -> Option<Self> {
85 value.as_float()
86 }
87}
88
89impl FromThemeValue for u32 {
90 fn from_theme_value(value: &ThemeValue) -> Option<Self> {
91 value.as_int()
92 }
93}
94
95impl ThemeValue {
96 pub fn as_string(&self) -> String {
97 match self {
98 ThemeValue::String(s) => s.clone(),
99 ThemeValue::Float(f) => f.to_string(),
100 ThemeValue::Int(i) => i.to_string(),
101 ThemeValue::FontSlant(fs) => fs.to_string(),
102 ThemeValue::FontBoldness(fb) => fb.to_string(),
103 ThemeValue::FontStretch(fs) => fs.to_string(),
104 }
105 }
106
107 pub fn as_float(&self) -> Option<f32> {
108 match self {
109 ThemeValue::Float(f) => Some(*f),
110 ThemeValue::Int(i) => Some(*i as f32),
111 ThemeValue::String(s) => s.parse().ok(),
112 _ => None,
113 }
114 }
115
116 pub fn as_int(&self) -> Option<u32> {
117 match self {
118 ThemeValue::Int(i) => Some(*i),
119 ThemeValue::Float(f) => Some(*f as u32),
120 ThemeValue::String(s) => s.parse().ok(),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Debug, Error)]
127pub enum RenderError {
128 #[error("SVG rendering error: {0}")]
129 SvgError(String),
130
131 #[error("IO error: {0}")]
132 IoError(#[from] std::io::Error),
133
134 #[error("Image error: {0}")]
135 ImageError(#[from] image::ImageError),
136
137 #[error("Invalid phase: expected {expected:?}, got {got:?}")]
138 InvalidPhase { expected: Phase, got: Phase },
139
140 #[error("Missing required command data: {0}")]
141 MissingData(String),
142}
143
144pub struct SvgRenderer {
145 document: Document,
146 page_dimensions: (f32, f32), page_resolution: (u32, u32), dpi: u32,
149 page_type: String,
150 themes: HashMap<String, HashMap<String, ThemeValue>>,
151 anchor_x: f32,
152 anchor_y: f32,
153 offset_x: f32,
154 offset_y: f32,
155 line_settings: HashMap<String, Value>,
156 message_settings: HashMap<String, Value>,
157 current_text: Option<Text>,
158 pin_func_types: Vec<String>,
159 definitions: Definitions,
160}
161
162impl SvgRenderer {
163 pub fn new() -> Self {
164 let page_type = "A4-L".to_string();
165 let dpi = 300;
166
167 let page_dimensions = (297.0, 210.0); let page_resolution = (
172 ((page_dimensions.0 * dpi as f32) / 25.4) as u32,
173 ((page_dimensions.1 * dpi as f32) / 25.4) as u32,
174 );
175
176 let document = Document::new()
178 .set("viewBox", (0, 0, page_resolution.0, page_resolution.1))
179 .set("width", format!("{}mm", page_dimensions.0))
180 .set("height", format!("{}mm", page_dimensions.1));
181
182 SvgRenderer {
183 document,
184 page_dimensions,
185 page_resolution,
186 dpi,
187 page_type,
188 themes: HashMap::new(),
189 anchor_x: 0.0,
190 anchor_y: 0.0,
191 offset_x: 0.0,
192 offset_y: 0.0,
193 line_settings: HashMap::new(),
194 message_settings: HashMap::new(),
195 current_text: None,
196 pin_func_types: Vec::new(),
197 definitions: Definitions::new(),
198 }
199 }
200
201 pub fn process_commands(&mut self, commands: &[Command]) -> Result<(), RenderError> {
202 let mut phase = Phase::Setup;
203
204 for command in commands {
205 match (command, phase) {
206 (Command::Draw, Phase::Setup) => {
207 self.check_boxes()?;
209 phase = Phase::Draw;
210 }
211 (cmd, current_phase) => {
212 let cmd_phase = self.get_command_phase(cmd);
213 if cmd_phase != current_phase {
214 return Err(RenderError::InvalidPhase {
215 expected: cmd_phase,
216 got: current_phase,
217 });
218 }
219
220 self.execute_command(cmd)?;
221 }
222 }
223 }
224
225 if self.current_text.is_some() {
227 self.end_message()?;
228 }
229
230 self.document = self.document.clone().add(self.definitions.clone());
232
233 Ok(())
234 }
235
236 fn get_command_phase(&self, command: &Command) -> Phase {
237 match command {
238 Command::Draw => Phase::Setup, Command::Labels { .. } => Phase::Setup,
242 Command::BorderColor { .. } => Phase::Setup,
243 Command::BorderWidth { .. } => Phase::Setup,
244 Command::BorderOpacity { .. } => Phase::Setup,
245 Command::FillColor { .. } => Phase::Setup,
246 Command::Opacity { .. } => Phase::Setup,
247 Command::Font { .. } => Phase::Setup,
248 Command::FontSize { .. } => Phase::Setup,
249 Command::FontColor { .. } => Phase::Setup,
250 Command::FontSlant { .. } => Phase::Setup,
251 Command::FontBold { .. } => Phase::Setup,
252 Command::FontStretch { .. } => Phase::Setup,
253 Command::FontOutline { .. } => Phase::Setup,
254 Command::FontOutlineThickness { .. } => Phase::Setup,
255 Command::Type { .. } => Phase::Setup,
256 Command::Wire { .. } => Phase::Setup,
257 Command::Group { .. } => Phase::Setup,
258 Command::BoxTheme { .. } => Phase::Setup,
259 Command::TextFont { .. } => Phase::Setup,
260 Command::Page { .. } => Phase::Setup,
261 Command::Dpi { .. } => Phase::Setup,
262
263 Command::GoogleFont { .. } => Phase::Draw,
265 Command::Image { .. } => Phase::Draw,
266 Command::Icon { .. } => Phase::Draw,
267 Command::Anchor { .. } => Phase::Draw,
268 Command::PinSet { .. } => Phase::Draw,
269 Command::Pin { .. } => Phase::Draw,
270 Command::PinText { .. } => Phase::Draw,
271 Command::Box { .. } => Phase::Draw,
272 Command::Message { .. } => Phase::Draw,
273 Command::Text { .. } => Phase::Draw,
274 Command::EndMessage => Phase::Draw,
275 }
276 }
277
278 fn execute_command(&mut self, command: &Command) -> Result<(), RenderError> {
279 match command {
280 Command::Labels {
282 default,
283 pin_type,
284 group,
285 labels,
286 } => self.set_labels(default, pin_type, group, labels),
287 Command::FillColor {
288 default,
289 pin_type,
290 group,
291 colors,
292 } => {
293 let string_colors: Vec<&str> = colors.iter().map(|s| s.as_str()).collect();
294 self.set_theme(
295 "FILL COLOR",
296 default.as_str(),
297 pin_type.as_deref(),
298 group.as_deref(),
299 &string_colors,
300 )
301 }
302 Command::Opacity {
303 default,
304 pin_type,
305 group,
306 opacities,
307 } => self.set_theme("OPACITY", *default, *pin_type, *group, opacities),
308 Command::BorderColor {
309 default,
310 pin_type,
311 group,
312 colors,
313 } => {
314 let string_colors: Vec<&str> = colors.iter().map(|s| s.as_str()).collect();
315 self.set_theme(
316 "BORDER COLOR",
317 default.as_str(),
318 pin_type.as_deref(),
319 group.as_deref(),
320 &string_colors,
321 )
322 }
323 Command::BorderWidth { width } => self.set_border_width(*width),
324 Command::BorderOpacity { opacity } => self.set_border_opacity(*opacity),
325 Command::Font {
326 default,
327 pin_type,
328 group,
329 fonts,
330 } => {
331 let string_fonts: Vec<&str> = fonts.iter().map(|s| s.as_str()).collect();
332 self.set_theme(
333 "FONT",
334 default.as_str(),
335 pin_type.as_deref(),
336 group.as_deref(),
337 &string_fonts,
338 )
339 }
340 Command::FontSize {
341 default,
342 pin_type,
343 group,
344 sizes,
345 } => self.set_theme("FONT SIZE", *default, *pin_type, *group, sizes),
346 Command::FontColor {
347 default,
348 pin_type,
349 group,
350 colors,
351 } => {
352 let string_colors: Vec<&str> = colors.iter().map(|s| s.as_str()).collect();
353 self.set_theme(
354 "FONT COLOR",
355 default.as_str(),
356 pin_type.as_deref(),
357 group.as_deref(),
358 &string_colors,
359 )
360 }
361 Command::FontSlant {
362 default,
363 pin_type,
364 group,
365 slants,
366 } => self.set_font_slant(*default, *pin_type, *group, slants),
367 Command::FontBold {
368 default,
369 pin_type,
370 group,
371 boldness,
372 } => self.set_font_bold(*default, *pin_type, *group, boldness),
373 Command::FontStretch {
374 default,
375 pin_type,
376 group,
377 stretches,
378 } => self.set_font_stretch(*default, *pin_type, *group, stretches),
379 Command::FontOutline {
380 default,
381 pin_type,
382 group,
383 colors,
384 } => {
385 let string_colors: Vec<&str> = colors.iter().map(|s| s.as_str()).collect();
386 self.set_theme(
387 "FONT OUTLINE",
388 default.as_str(),
389 pin_type.as_deref(),
390 group.as_deref(),
391 &string_colors,
392 )
393 }
394 Command::FontOutlineThickness {
395 default,
396 pin_type,
397 group,
398 thickness,
399 } => self.set_theme(
400 "FONT OUTLINE THICKNESS",
401 *default,
402 *pin_type,
403 *group,
404 thickness,
405 ),
406
407 Command::Page { page_name } => self.set_page_size(page_name),
408 Command::Dpi { dpi } => self.set_dpi(*dpi),
409 Command::Type {
410 pin_type,
411 color,
412 opacity,
413 } => self.set_pin_type(*pin_type, color, *opacity),
414 Command::Wire {
415 wire_type,
416 color,
417 opacity,
418 thickness,
419 } => self.set_wire_type(*wire_type, color, *opacity, *thickness),
420 Command::Group {
421 name,
422 color,
423 opacity,
424 } => self.set_group(name, color, *opacity),
425 Command::BoxTheme {
426 name,
427 border_color,
428 border_opacity,
429 fill_color,
430 fill_opacity,
431 line_width,
432 box_width,
433 box_height,
434 box_cr_x,
435 box_cr_y,
436 box_skew,
437 box_skew_offset,
438 } => self.define_box(
439 name,
440 border_color,
441 *border_opacity,
442 fill_color,
443 *fill_opacity,
444 *line_width,
445 *box_width,
446 *box_height,
447 *box_cr_x,
448 *box_cr_y,
449 *box_skew,
450 *box_skew_offset,
451 ),
452 Command::TextFont {
453 theme_name,
454 font,
455 size,
456 outline_color,
457 color,
458 slant,
459 bold,
460 stretch,
461 } => self.define_text_font(
462 theme_name,
463 font,
464 *size,
465 outline_color,
466 color,
467 *slant,
468 *bold,
469 *stretch,
470 ),
471
472 Command::Draw => Ok(()), Command::GoogleFont { _link } => {
475 Ok(())
477 }
478 Command::Image {
479 name,
480 x,
481 y,
482 w,
483 h,
484 cx,
485 cy,
486 cw,
487 ch,
488 rot,
489 } => self.write_image(name, *x, *y, *w, *h, *cx, *cy, *cw, *ch, *rot),
490 Command::Icon {
491 name,
492 x,
493 y,
494 w,
495 h,
496 rot,
497 } => self.write_icon(name, *x, *y, *w, *h, *rot),
498 Command::Anchor { x, y } => self.move_anchor(*x, *y),
499 Command::PinSet {
500 side,
501 packed,
502 justify_x,
503 justify_y,
504 line_step,
505 pin_width,
506 group_width,
507 leader_offset,
508 column_gap,
509 leader_h_step,
510 } => self.start_pin_set(
511 *side,
512 *packed,
513 *justify_x,
514 *justify_y,
515 *line_step,
516 *pin_width,
517 *group_width,
518 *leader_offset,
519 *column_gap,
520 *leader_h_step,
521 ),
522 Command::Pin {
523 wire,
524 pin_type,
525 group,
526 attributes,
527 } => self.write_pin(*wire, *pin_type, group, attributes),
528 Command::PinText {
529 wire,
530 pin_type,
531 pin_group,
532 msg_theme,
533 label,
534 message,
535 } => self.write_pin_text(*wire, *pin_type, pin_group, msg_theme, label, message),
536 Command::Box {
537 theme,
538 x,
539 y,
540 box_width,
541 box_height,
542 x_justify,
543 y_justify,
544 message,
545 } => self.draw_box(
546 theme,
547 *x,
548 *y,
549 *box_width,
550 *box_height,
551 *x_justify,
552 *y_justify,
553 message,
554 ),
555 Command::Message {
556 x,
557 y,
558 line_step,
559 font,
560 font_size,
561 x_justify,
562 y_justify,
563 } => self
564 .start_text_message(*x, *y, *line_step, font, *font_size, *x_justify, *y_justify),
565 Command::Text {
566 edge_color,
567 color,
568 message,
569 new_line,
570 } => self.write_text(edge_color, color, message, *new_line),
571 Command::EndMessage => self.end_message(),
572 }
573 }
574
575 fn set_labels(
576 &mut self,
577 default: &str,
578 pin_type: &Option<String>,
579 group: &Option<String>,
580 labels: &[String],
581 ) -> Result<(), RenderError> {
582 let fixed_theme_entries = vec![
584 "DEFAULT".to_string(),
585 "TYPE".to_string(),
586 "GROUP".to_string(),
587 ];
588
589 if self.pin_func_types.is_empty() {
591 if default == "DEFAULT"
593 && (pin_type.is_none() || pin_type.as_ref().unwrap() == "TYPE")
594 && (group.is_none() || group.as_ref().unwrap() == "GROUP")
595 {
596 self.pin_func_types = labels.to_vec();
598
599 for entry in &fixed_theme_entries {
601 self.themes.insert(entry.clone(), HashMap::new());
602 }
603
604 for label in labels {
605 self.themes.insert(label.clone(), HashMap::new());
606 }
607
608 Ok(())
609 } else {
610 Err(RenderError::SvgError(format!(
611 "Error: First labels must be {:?}!",
612 fixed_theme_entries
613 )))
614 }
615 } else {
616 Err(RenderError::SvgError(
617 "Error: Can only set the pin function labels ONCE!".to_string(),
618 ))
619 }
620 }
621
622 fn set_theme<T>(
624 &mut self,
625 entry: &str,
626 default: T,
627 pin_type: Option<T>,
628 group: Option<T>,
629 values: &[T],
630 ) -> Result<(), RenderError>
631 where
632 T: Clone + Into<ThemeValue>,
633 {
634 self.set_theme_value("DEFAULT", entry, default.into());
636
637 if let Some(pt) = pin_type {
639 self.set_theme_value("TYPE", entry, pt.into());
640 }
641
642 if let Some(g) = group {
644 self.set_theme_value("GROUP", entry, g.into());
645 }
646
647 for (i, value) in values.iter().enumerate() {
649 if i < self.pin_func_types.len() {
650 let pin_func = &self.pin_func_types[i].clone();
651 self.set_theme_value(pin_func, entry, value.clone().into());
652 }
653 }
654
655 Ok(())
656 }
657
658 fn set_theme_value(&mut self, theme: &str, entry: &str, value: ThemeValue) {
659 if let Some(theme_map) = self.themes.get_mut(theme) {
660 theme_map.insert(entry.to_string(), value);
661 } else {
662 let mut theme_map = HashMap::new();
663 theme_map.insert(entry.to_string(), value);
664 self.themes.insert(theme.to_string(), theme_map);
665 }
666 }
667
668 fn set_border_width(&mut self, width: u32) -> Result<(), RenderError> {
669 self.set_theme_value("DEFAULT", "BORDER WIDTH", width.into());
670 Ok(())
671 }
672
673 fn set_border_opacity(&mut self, opacity: f32) -> Result<(), RenderError> {
674 self.set_theme_value("DEFAULT", "BORDER OPACITY", opacity.into());
675 Ok(())
676 }
677
678 fn set_font_slant(
679 &mut self,
680 default: FontSlant,
681 pin_type: Option<FontSlant>,
682 group: Option<FontSlant>,
683 slants: &[FontSlant],
684 ) -> Result<(), RenderError> {
685 self.set_theme("FONT SLANT", default, pin_type, group, slants)
686 }
687
688 fn set_font_bold(
689 &mut self,
690 default: FontBoldness,
691 pin_type: Option<FontBoldness>,
692 group: Option<FontBoldness>,
693 boldness: &[FontBoldness],
694 ) -> Result<(), RenderError> {
695 self.set_theme("FONT BOLD", default, pin_type, group, boldness)
696 }
697
698 fn set_font_stretch(
699 &mut self,
700 default: FontStretch,
701 pin_type: Option<FontStretch>,
702 group: Option<FontStretch>,
703 stretches: &[FontStretch],
704 ) -> Result<(), RenderError> {
705 self.set_theme("FONT STRETCH", default, pin_type, group, stretches)
706 }
707
708 fn set_pin_type(
709 &mut self,
710 pin_type: PinType,
711 color: &str,
712 opacity: f32,
713 ) -> Result<(), RenderError> {
714 let theme_entry = format!("PINTYPE_{}", pin_type);
715
716 let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
718
719 theme_map.insert(
721 "FILL COLOR".to_string(),
722 ThemeValue::String(color.to_string()),
723 );
724 theme_map.insert("OPACITY".to_string(), ThemeValue::Float(opacity));
725
726 Ok(())
727 }
728
729 fn set_wire_type(
730 &mut self,
731 wire_type: WireType,
732 color: &str,
733 opacity: f32,
734 thickness: f32,
735 ) -> Result<(), RenderError> {
736 let theme_entry = format!("PINWIRE_{}", wire_type);
737
738 let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
740
741 theme_map.insert(
743 "FILL COLOR".to_string(),
744 ThemeValue::String(color.to_string()),
745 );
746 theme_map.insert("OPACITY".to_string(), ThemeValue::Float(opacity));
747 theme_map.insert("THICKNESS".to_string(), ThemeValue::Float(thickness));
748
749 Ok(())
750 }
751
752 fn set_group(&mut self, name: &str, color: &str, opacity: f32) -> Result<(), RenderError> {
753 let theme_entry = format!("GROUP_{}", name);
754
755 let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
757
758 theme_map.insert(
760 "FILL COLOR".to_string(),
761 ThemeValue::String(color.to_string()),
762 );
763 theme_map.insert("OPACITY".to_string(), ThemeValue::Float(opacity));
764
765 Ok(())
766 }
767
768 fn define_box(
769 &mut self,
770 name: &str,
771 border_color: &str,
772 border_opacity: f32,
773 fill_color: &str,
774 fill_opacity: f32,
775 line_width: f32,
776 box_width: f32,
777 box_height: f32,
778 box_cr_x: f32,
779 box_cr_y: f32,
780 box_skew: f32,
781 box_skew_offset: f32,
782 ) -> Result<(), RenderError> {
783 let theme_entry = format!("BOX_{}", name);
784
785 let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
787
788 theme_map.insert(
790 "BORDER COLOR".to_string(),
791 ThemeValue::String(border_color.to_string()),
792 );
793 theme_map.insert(
794 "BORDER OPACITY".to_string(),
795 ThemeValue::Float(border_opacity),
796 );
797 theme_map.insert(
798 "FILL COLOR".to_string(),
799 ThemeValue::String(fill_color.to_string()),
800 );
801 theme_map.insert("OPACITY".to_string(), ThemeValue::Float(fill_opacity));
802 theme_map.insert("BORDER WIDTH".to_string(), ThemeValue::Float(line_width));
803 theme_map.insert("WIDTH".to_string(), ThemeValue::Float(box_width));
804 theme_map.insert("HEIGHT".to_string(), ThemeValue::Float(box_height));
805 theme_map.insert("CORNER RX".to_string(), ThemeValue::Float(box_cr_x));
806 theme_map.insert("CORNER RY".to_string(), ThemeValue::Float(box_cr_y));
807 theme_map.insert("SKEW".to_string(), ThemeValue::Float(box_skew));
808 theme_map.insert(
809 "SKEW OFFSET".to_string(),
810 ThemeValue::Float(box_skew_offset),
811 );
812
813 Ok(())
814 }
815
816 fn define_text_font(
817 &mut self,
818 theme_name: &str,
819 font: &str,
820 size: f32,
821 outline_color: &str,
822 color: &str,
823 slant: FontSlant,
824 bold: FontBoldness,
825 stretch: FontStretch,
826 ) -> Result<(), RenderError> {
827 let theme_entry = format!("FONT_{}", theme_name);
828
829 let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
831
832 theme_map.insert("FONT".to_string(), ThemeValue::String(font.to_string()));
834 theme_map.insert("FONT SIZE".to_string(), ThemeValue::Float(size));
835 theme_map.insert(
836 "OUTLINE COLOR".to_string(),
837 ThemeValue::String(outline_color.to_string()),
838 );
839 theme_map.insert(
840 "FONT COLOR".to_string(),
841 ThemeValue::String(color.to_string()),
842 );
843 theme_map.insert("FONT SLANT".to_string(), ThemeValue::FontSlant(slant));
844 theme_map.insert("FONT BOLD".to_string(), ThemeValue::FontBoldness(bold));
845 theme_map.insert("FONT STRETCH".to_string(), ThemeValue::FontStretch(stretch));
846
847 Ok(())
848 }
849
850 fn set_page_size(&mut self, page_name: &str) -> Result<(), RenderError> {
851 let page_dimensions = match page_name.trim() {
852 "A4-P" => (210.0, 297.0), "A4-L" => (297.0, 210.0), "A3-P" => (297.0, 420.0), "A3-L" => (420.0, 297.0), _ => {
857 return Err(RenderError::SvgError(format!(
858 "Unknown page type: {}",
859 page_name
860 )));
861 }
862 };
863
864 self.page_type = page_name.to_string();
865 self.page_dimensions = page_dimensions;
866
867 self.page_resolution = (
869 ((self.page_dimensions.0 * self.dpi as f32) / 25.4) as u32,
870 ((self.page_dimensions.1 * self.dpi as f32) / 25.4) as u32,
871 );
872
873 self.document = self
875 .document
876 .clone()
877 .set(
878 "viewBox",
879 (0, 0, self.page_resolution.0, self.page_resolution.1),
880 )
881 .set("width", format!("{}mm", self.page_dimensions.0))
882 .set("height", format!("{}mm", self.page_dimensions.1));
883
884 Ok(())
885 }
886
887 fn set_dpi(&mut self, dpi: u32) -> Result<(), RenderError> {
888 if dpi < 50 || dpi > 1200 {
889 return Err(RenderError::SvgError(
890 "DPI must be between 50 and 1200".to_string(),
891 ));
892 }
893
894 self.dpi = dpi;
895
896 self.page_resolution = (
898 ((self.page_dimensions.0 * dpi as f32) / 25.4) as u32,
899 ((self.page_dimensions.1 * dpi as f32) / 25.4) as u32,
900 );
901
902 self.document = self
904 .document
905 .clone()
906 .set(
907 "viewBox",
908 (0, 0, self.page_resolution.0, self.page_resolution.1),
909 )
910 .set("width", format!("{}mm", self.page_dimensions.0))
911 .set("height", format!("{}mm", self.page_dimensions.1));
912
913 Ok(())
914 }
915
916 fn check_boxes(&self) -> Result<(), RenderError> {
917 for (theme_name, theme_map) in &self.themes {
918 if let Some(boxes) = theme_map.get("BOXES") {
919 let box_theme = format!("BOX_{}", boxes.as_string());
920 if !self.themes.contains_key(&box_theme) {
921 return Err(RenderError::SvgError(format!(
922 "Box {} used for {} theme, but not defined!",
923 boxes.as_string(),
924 theme_name
925 )));
926 }
927 }
928 }
929 Ok(())
930 }
931
932 fn write_image(
933 &mut self,
934 name: &str,
935 x: Option<f32>,
936 y: Option<f32>,
937 w: Option<f32>,
938 h: Option<f32>,
939 cx: Option<f32>,
940 cy: Option<f32>,
941 cw: Option<f32>,
942 ch: Option<f32>,
943 rot: Option<f32>,
944 ) -> Result<(), RenderError> {
945 let path = Path::new(name);
946 if !path.exists() {
947 return Err(RenderError::SvgError(format!(
948 "Image file not found: {}",
949 name
950 )));
951 }
952
953 let mut img = image::open(path)?;
955
956 let img = if cx.is_some() && cy.is_some() && cw.is_some() && ch.is_some() {
958 let cx = cx.unwrap() as u32;
959 let cy = cy.unwrap() as u32;
960 let cw = cw.unwrap() as u32;
961 let ch = ch.unwrap() as u32;
962
963 if cx + cw > img.width() || cy + ch > img.height() {
965 return Err(RenderError::SvgError("Invalid crop parameters".to_string()));
966 }
967
968 img.crop(cx, cy, cw, ch)
969 } else if cx.is_some() || cy.is_some() || cw.is_some() || ch.is_some() {
970 return Err(RenderError::SvgError(
971 "Crop parameters cx, cy, cw, ch must all be specified, or none".to_string(),
972 ));
973 } else {
974 img
975 };
976
977 let img = if w.is_some() || h.is_some() {
979 let w = get_size(w, img.width() as f32, None) as u32;
980 let h = get_size(h, img.height() as f32, None) as u32;
981
982 img.resize(w, h, image::imageops::FilterType::Lanczos3)
983 } else {
984 img
985 };
986
987 let img_width = img.width();
989 let img_height = img.height();
990
991 let x = get_size(x, self.page_resolution.0 as f32, Some(0.0));
993 let y = get_size(y, self.page_resolution.1 as f32, Some(0.0));
994
995 let x = x - (img_width as f32 / 2.0);
997 let y = y - (img_height as f32 / 2.0);
998
999 let mut buffer: Vec<u8> = Vec::new();
1001 let mut cursor = std::io::Cursor::new(&mut buffer);
1003 img.write_to(&mut cursor, ImageFormat::Png)?;
1004 let encoded = general_purpose::STANDARD.encode(&buffer);
1005 let data_url = format!("data:image/png;base64,{}", encoded);
1006
1007 let mut image = Image::new()
1009 .set("href", data_url)
1010 .set("x", x)
1011 .set("y", y)
1012 .set("width", img_width)
1013 .set("height", img_height);
1014
1015 if let Some(rot) = rot {
1017 let center_x = x + (img_width as f32 / 2.0);
1019 let center_y = y + (img_height as f32 / 2.0);
1020
1021 image = image.set(
1023 "transform",
1024 format!("rotate({} {} {})", rot, center_x, center_y),
1025 );
1026 }
1027
1028 self.document = self.document.clone().add(image);
1030
1031 Ok(())
1032 }
1033
1034 fn write_icon(
1035 &mut self,
1036 name: &str,
1037 x: Option<f32>,
1038 y: Option<f32>,
1039 w: Option<f32>,
1040 h: Option<f32>,
1041 rot: Option<f32>,
1042 ) -> Result<(), RenderError> {
1043 let path = Path::new(name);
1044 if !path.exists() {
1045 return Err(RenderError::SvgError(format!(
1046 "Icon file not found: {}",
1047 name
1048 )));
1049 }
1050
1051 if path.extension().map_or(false, |ext| ext != "svg") {
1053 return Err(RenderError::SvgError(
1054 "Icon must be an SVG file".to_string(),
1055 ));
1056 }
1057
1058 let mut file = File::open(path)?;
1060 let mut svg_content = String::new();
1061 file.read_to_string(&mut svg_content)?;
1062
1063 let (svg_width, svg_height) = Self::extract_svg_dimensions(&svg_content)?;
1065
1066 let encoded = general_purpose::STANDARD.encode(svg_content.as_bytes());
1068 let data_url = format!("data:image/svg+xml;base64,{}", encoded);
1069
1070 let x = get_size(x, self.page_resolution.0 as f32, Some(0.0));
1072 let y = get_size(y, self.page_resolution.1 as f32, Some(0.0));
1073 let w = get_size(w, svg_width, Some(100.0)); let h = get_size(h, svg_height, Some(100.0)); let x = x - (svg_width / 2.0);
1078 let y = y - (svg_height / 2.0);
1079
1080 let mut image = Image::new()
1082 .set("href", data_url)
1083 .set("x", x)
1084 .set("y", y)
1085 .set("width", w)
1086 .set("height", h);
1087
1088 if let Some(rot) = rot {
1090 let center_x = x + (w / 2.0);
1092 let center_y = y + (h / 2.0);
1093
1094 image = image.set(
1096 "transform",
1097 format!("rotate({} {} {})", rot, center_x, center_y),
1098 );
1099 }
1100
1101 self.document = self.document.clone().add(image);
1103
1104 Ok(())
1105 }
1106
1107 fn move_anchor(&mut self, x: f32, y: f32) -> Result<(), RenderError> {
1108 self.anchor_x = x;
1109 self.anchor_y = y;
1110 self.offset_x = 0.0;
1111 self.offset_y = 0.0;
1112
1113 Ok(())
1114 }
1115
1116 fn start_pin_set(
1117 &mut self,
1118 side: Side,
1119 packed: bool,
1120 justify_x: JustifyX,
1121 justify_y: JustifyY,
1122 line_step: f32,
1123 pin_width: f32,
1124 group_width: f32,
1125 leader_offset: f32,
1126 column_gap: f32,
1127 leader_h_step: f32,
1128 ) -> Result<(), RenderError> {
1129 self.line_settings.clear();
1131
1132 let side_str = match side {
1134 Side::Left => "LEFT",
1135 Side::Right => "RIGHT",
1136 Side::Top => "TOP",
1137 Side::Bottom => "BOTTOM",
1138 };
1139
1140 let justify_x_str = match justify_x {
1141 JustifyX::Left => "LEFT",
1142 JustifyX::Right => "RIGHT",
1143 JustifyX::Center => "CENTER",
1144 };
1145
1146 let justify_y_str = match justify_y {
1147 JustifyY::Top => "TOP",
1148 JustifyY::Bottom => "BOTTOM",
1149 JustifyY::Center => "CENTER",
1150 };
1151
1152 self.line_settings.insert("SIDE".into(), side_str.into());
1154 self.line_settings.insert(
1155 "PACK".into(),
1156 (if packed { "PACKED" } else { "UNPACKED" }).into(),
1157 );
1158 self.line_settings
1159 .insert("JUSTIFY X".into(), justify_x_str.into());
1160 self.line_settings
1161 .insert("JUSTIFY Y".into(), justify_y_str.into());
1162 self.line_settings
1163 .insert("PINWIDTH".into(), pin_width.into());
1164 self.line_settings
1165 .insert("GROUPWIDTH".into(), group_width.into());
1166 self.line_settings
1167 .insert("LINESTEP".into(), line_step.into());
1168 self.line_settings
1169 .insert("LEADER".into(), leader_offset.into());
1170 self.line_settings.insert("GAP".into(), column_gap.into());
1171 self.line_settings
1172 .insert("HSTEP".into(), leader_h_step.into());
1173 Ok(())
1174 }
1175
1176 fn write_pin(
1177 &mut self,
1178 wire: Option<WireType>,
1179 pin_type: Option<PinType>,
1180 group: &Option<String>,
1181 attributes: &[String],
1182 ) -> Result<(), RenderError> {
1183 if self.line_settings.is_empty() {
1184 return Err(RenderError::SvgError(
1185 "Line not setup with prior PINSET!".to_string(),
1186 ));
1187 }
1188
1189 let mut box_offset_x = self.print_pin(pin_type, wire, group)?;
1191
1192 let line_height = self
1194 .line_settings
1195 .get("LINESTEP")
1196 .unwrap()
1197 .parse::<f32>()
1198 .unwrap_or(10.0);
1199
1200 for (index, attr) in attributes.iter().enumerate() {
1202 if index < self.pin_func_types.len() {
1203 let pin_func = self.pin_func_types[index].clone();
1204
1205 if !attr.is_empty() {
1206 let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1208
1209 let justify_x = self
1211 .line_settings
1212 .get("JUSTIFY X")
1213 .unwrap_or(&Value::from("CENTER"))
1214 .to_string();
1215 let justify_y = self
1216 .line_settings
1217 .get("JUSTIFY Y")
1218 .unwrap_or(&Value::from("CENTER"))
1219 .to_string();
1220
1221 self.text_box(
1223 x,
1224 y,
1225 None,
1226 None,
1227 "BOX_SKEWED",
1228 &pin_func,
1229 attr,
1230 &justify_x,
1231 &justify_y,
1232 )?;
1233
1234 let side = self
1236 .line_settings
1237 .get("SIDE")
1238 .cloned()
1239 .unwrap_or(Value::from("LEFT"));
1240 box_offset_x = self.inc_offset_x(box_offset_x, &side, "BOX_SKEWED");
1241 } else if self
1242 .line_settings
1243 .get("PACK")
1244 .unwrap_or(&Value::from("UNPACKED"))
1245 .eq_ignore_ascii_case("UNPACKED")
1246 {
1247 let side = self
1249 .line_settings
1250 .get("SIDE")
1251 .cloned()
1252 .unwrap_or(Value::from("LEFT"));
1253 box_offset_x = self.inc_offset_x(box_offset_x, &side, &pin_func);
1254 }
1255 }
1256 }
1257
1258 self.offset_y += line_height;
1260
1261 Ok(())
1262 }
1263
1264 fn write_pin_text(
1265 &mut self,
1266 wire: Option<WireType>,
1267 pin_type: Option<PinType>,
1268 pin_group: &Option<String>,
1269 msg_theme: &str,
1270 label: &Option<String>,
1271 message: &str,
1272 ) -> Result<(), RenderError> {
1273 if self.line_settings.is_empty() {
1274 return Err(RenderError::SvgError(
1275 "Line not setup with prior PINSET!".to_string(),
1276 ));
1277 }
1278
1279 let mut box_offset_x = self.print_pin(pin_type, wire, pin_group)?;
1281
1282 let line_height = self
1284 .line_settings
1285 .get("LINESTEP")
1286 .unwrap()
1287 .parse::<f32>()
1288 .unwrap_or(10.0);
1289
1290 if let Some(label_text) = label {
1292 if !label_text.is_empty() {
1293 let pin_func = self.pin_func_types[0].clone(); let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1298
1299 let justify_x = self
1301 .line_settings
1302 .get("JUSTIFY X")
1303 .unwrap_or(&Value::from("CENTER"))
1304 .to_string();
1305 let justify_y = self
1306 .line_settings
1307 .get("JUSTIFY Y")
1308 .unwrap_or(&Value::from("CENTER"))
1309 .to_string();
1310
1311 self.text_box(
1313 x,
1314 y,
1315 None,
1316 None,
1317 "BOX_SKEWED",
1318 &pin_func,
1319 label_text,
1320 &justify_x,
1321 &justify_y,
1322 )?;
1323
1324 let side = self
1326 .line_settings
1327 .get("SIDE")
1328 .cloned()
1329 .unwrap_or(Value::from("LEFT"));
1330 if side.contains("RIGHT") {
1331 box_offset_x = self.inc_offset_x(box_offset_x, &side, "BOX_SKEWED");
1332 }
1333 }
1334 }
1335
1336 if !message.is_empty() {
1338 let font_theme = msg_theme;
1340 let font = self.get_theme(&font_theme, "FONT", "sans-serif".to_string());
1341 let font_size = self.get_theme(&font_theme, "FONT SIZE", 10.0f32);
1342 let font_color = self.get_theme(&font_theme, "FONT COLOR", "black".to_string());
1343 let font_slant = self.get_theme(&font_theme, "FONT SLANT", "normal".to_string());
1344 let font_bold = self.get_theme(&font_theme, "FONT BOLD", "normal".to_string());
1345 let font_stretch = self.get_theme(&font_theme, "FONT STRETCH", "normal".to_string());
1346
1347 let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1349 let side = self
1351 .line_settings
1352 .get("SIDE")
1353 .cloned()
1354 .unwrap_or(Value::from("LEFT"));
1355 let gap = self
1356 .line_settings
1357 .get("GAP")
1358 .unwrap()
1359 .parse::<f32>()
1360 .unwrap_or(10.0);
1361 let x = if side.contains("LEFT") {
1362 x - gap
1363 } else {
1364 x + gap
1365 };
1366
1367 let text_anchor = if side.contains("LEFT") {
1369 "end"
1370 } else {
1371 "start"
1372 };
1373
1374 let text_elem = Text::new("") .set("x", x)
1377 .set("y", y + (line_height / 2.0))
1378 .set("font-size", font_size)
1379 .set("font-family", font)
1380 .set("fill", font_color)
1381 .set("font-style", font_slant)
1382 .set("font-weight", font_bold)
1383 .set("font-stretch", font_stretch)
1384 .set("text-anchor", text_anchor)
1385 .add(TextNode::new(message));
1386
1387 self.document = self.document.clone().add(text_elem);
1389 }
1390
1391 self.offset_y += line_height;
1393
1394 Ok(())
1395 }
1396
1397 fn draw_box(
1398 &mut self,
1399 theme: &str,
1400 x: f32,
1401 y: f32,
1402 box_width: Option<f32>,
1403 box_height: Option<f32>,
1404 x_justify: Option<JustifyX>,
1405 y_justify: Option<JustifyY>,
1406 text: &Option<String>,
1407 ) -> Result<(), RenderError> {
1408 let box_theme = if theme.starts_with("BOX_") {
1410 theme.to_string()
1411 } else {
1412 format!("BOX_{}", theme)
1413 };
1414
1415 let x_justify_str = match x_justify {
1417 Some(JustifyX::Left) => "LEFT",
1418 Some(JustifyX::Right) => "RIGHT",
1419 Some(JustifyX::Center) => "CENTER",
1420 None => "CENTER", };
1422
1423 let y_justify_str = match y_justify {
1424 Some(JustifyY::Top) => "TOP",
1425 Some(JustifyY::Bottom) => "BOTTOM",
1426 Some(JustifyY::Center) => "CENTER",
1427 None => "CENTER", };
1429
1430 let text_content = text.as_deref().unwrap_or("");
1432 self.text_box(
1433 x,
1434 y,
1435 box_width,
1436 box_height,
1437 &box_theme,
1438 theme,
1439 text_content,
1440 x_justify_str,
1441 y_justify_str,
1442 )?;
1443
1444 Ok(())
1445 }
1446
1447 fn start_text_message(
1448 &mut self,
1449 x: Option<f32>,
1450 y: Option<f32>,
1451 line_step: Option<f32>,
1452 font: &Option<String>,
1453 font_size: Option<f32>,
1454 x_justify: Option<JustifyX>,
1455 y_justify: Option<JustifyY>,
1456 ) -> Result<(), RenderError> {
1457 self.end_message()?;
1459
1460 self.message_settings.insert("NEWLINE".into(), false.into());
1462
1463 if let Some(x_val) = x {
1465 self.message_settings.insert("X".into(), x_val.into());
1466 self.message_settings.insert("OFFSETX".into(), 0.0.into());
1467 }
1468
1469 if let Some(y_val) = y {
1470 self.message_settings.insert("Y".into(), y_val.into());
1471 self.message_settings.insert("OFFSETY".into(), 0.0.into());
1472 }
1473
1474 if let Some(step) = line_step {
1476 self.message_settings.insert("LINESTEP".into(), step.into());
1477 } else if !self.message_settings.contains_key("LINESTEP") {
1478 self.message_settings.insert("LINESTEP".into(), 15.0.into()); }
1480
1481 if let Some(f) = font {
1483 self.message_settings
1484 .insert("FONT".into(), f.clone().into());
1485 } else if !self.message_settings.contains_key("FONT") {
1486 self.message_settings
1487 .insert("FONT".into(), "sans-serif".into()); }
1489
1490 if let Some(size) = font_size {
1492 self.message_settings.insert("FONTSIZE".into(), size.into());
1493 } else if !self.message_settings.contains_key("FONTSIZE") {
1494 self.message_settings.insert("FONTSIZE".into(), 12.0.into()); }
1496
1497 let x_justify_str = match x_justify {
1499 Some(JustifyX::Left) => "LEFT",
1500 Some(JustifyX::Right) => "RIGHT",
1501 Some(JustifyX::Center) => "CENTER",
1502 None => "CENTER", };
1504
1505 let y_justify_str = match y_justify {
1506 Some(JustifyY::Top) => "TOP",
1507 Some(JustifyY::Bottom) => "BOTTOM",
1508 Some(JustifyY::Center) => "CENTER",
1509 None => "CENTER", };
1511
1512 self.message_settings
1513 .insert("XJUSTIFY".into(), x_justify_str.into());
1514 self.message_settings
1515 .insert("YJUSTIFY".into(), y_justify_str.into());
1516
1517 let text_anchor = match x_justify {
1519 Some(JustifyX::Left) => "start",
1520 Some(JustifyX::Right) => "end",
1521 Some(JustifyX::Center) | None => "middle",
1522 };
1523
1524 let font_size = self
1526 .message_settings
1527 .get("FONTSIZE")
1528 .unwrap()
1529 .parse::<f32>()
1530 .unwrap_or(12.0);
1531 let y_shift = match y_justify {
1532 Some(JustifyY::Top) => font_size / 2.0,
1533 Some(JustifyY::Bottom) => -(font_size / 2.0),
1534 Some(JustifyY::Center) | None => 0.0,
1535 };
1536
1537 self.message_settings
1538 .insert("YSHIFT".into(), y_shift.into());
1539
1540 let font_name = self
1542 .message_settings
1543 .get("FONT")
1544 .cloned()
1545 .unwrap_or(Value::from("sans-serif"));
1546 let font_theme = self.get_font_theme(&font_name);
1547
1548 let x = self
1550 .message_settings
1551 .get("X")
1552 .unwrap()
1553 .parse::<f32>()
1554 .unwrap_or(0.0)
1555 + self
1556 .message_settings
1557 .get("OFFSETX")
1558 .unwrap()
1559 .parse::<f32>()
1560 .unwrap_or(0.0);
1561
1562 let y = self
1563 .message_settings
1564 .get("Y")
1565 .unwrap()
1566 .parse::<f32>()
1567 .unwrap_or(0.0)
1568 + self
1569 .message_settings
1570 .get("OFFSETY")
1571 .unwrap()
1572 .parse::<f32>()
1573 .unwrap_or(0.0)
1574 + self
1575 .message_settings
1576 .get("YSHIFT")
1577 .unwrap()
1578 .parse::<f32>()
1579 .unwrap_or(0.0);
1580
1581 let font_size = self
1582 .message_settings
1583 .get("FONTSIZE")
1584 .unwrap()
1585 .parse::<f32>()
1586 .unwrap_or(12.0);
1587 let font_family = self.get_theme(&font_theme, "FONT", "sans-serif".to_string());
1588 let stroke = self.get_theme(&font_theme, "OUTLINE COLOR", "none".to_string());
1589 let fill = self.get_theme(&font_theme, "FONT COLOR", "black".to_string());
1590 let font_style = self.get_theme(&font_theme, "FONT SLANT", "normal".to_string());
1591 let font_weight = self.get_theme(&font_theme, "FONT BOLD", "normal".to_string());
1592 let font_stretch = self.get_theme(&font_theme, "FONT STRETCH", "normal".to_string());
1593
1594 let text_elem = Text::new("") .set("x", x)
1596 .set("y", y)
1597 .set("font-size", font_size)
1598 .set("font-family", font_family)
1599 .set("stroke", stroke)
1600 .set("fill", fill)
1601 .set("font-style", font_style)
1602 .set("font-weight", font_weight)
1603 .set("font-stretch", font_stretch)
1604 .set("text-anchor", text_anchor);
1605
1606 self.current_text = Some(text_elem);
1607
1608 Ok(())
1609 }
1610
1611 fn write_text(
1612 &mut self,
1613 edge_color: &str,
1614 color: &str,
1615 message: &str,
1616 new_line: bool,
1617 ) -> Result<(), RenderError> {
1618 if self.current_text.is_none() {
1619 return Err(RenderError::SvgError(
1620 "No multiline text message started!".to_string(),
1621 ));
1622 }
1623
1624 let font_theme = self.get_font_theme(
1625 &self
1626 .message_settings
1627 .get("FONT")
1628 .unwrap_or(&Value::from("sans-serif"))
1629 .to_string(),
1630 );
1631
1632 let color = if color.is_empty() {
1634 self.get_theme(&font_theme, "FONT COLOR", "black".to_string())
1635 } else {
1636 color.to_owned()
1637 };
1638
1639 let edge_color = if edge_color.is_empty() {
1641 "none"
1642 } else {
1643 edge_color
1644 };
1645
1646 let mut tspan = TSpan::new("");
1647
1648 if self
1650 .message_settings
1651 .get("NEWLINE")
1652 .unwrap()
1653 .parse()
1654 .unwrap_or(false)
1655 {
1656 self.message_settings.insert("NEWLINE".into(), false.into());
1658
1659 let offset_y = self
1661 .message_settings
1662 .get("OFFSETY")
1663 .unwrap()
1664 .parse::<f32>()
1665 .unwrap_or(0.0);
1666 let line_step = self
1667 .message_settings
1668 .get("LINESTEP")
1669 .unwrap()
1670 .parse::<f32>()
1671 .unwrap_or(15.0);
1672 self.message_settings
1673 .insert("OFFSETY".into(), (offset_y + line_step).into());
1674
1675 let x = self
1677 .message_settings
1678 .get("X")
1679 .unwrap()
1680 .parse::<f32>()
1681 .unwrap_or(0.0)
1682 + self
1683 .message_settings
1684 .get("OFFSETX")
1685 .unwrap()
1686 .parse::<f32>()
1687 .unwrap_or(0.0);
1688
1689 let y = self
1690 .message_settings
1691 .get("Y")
1692 .unwrap()
1693 .parse::<f32>()
1694 .unwrap_or(0.0)
1695 + self
1696 .message_settings
1697 .get("OFFSETY")
1698 .unwrap()
1699 .parse::<f32>()
1700 .unwrap_or(0.0)
1701 + self
1702 .message_settings
1703 .get("YSHIFT")
1704 .unwrap()
1705 .parse::<f32>()
1706 .unwrap_or(0.0);
1707
1708 tspan = tspan.set("x", x).set("y", y);
1709 }
1710
1711 tspan = tspan
1713 .set("stroke", edge_color)
1714 .set("fill", color)
1715 .add(TextNode::new(message));
1716
1717 if let Some(ref mut text) = self.current_text {
1719 *text = text.clone().add(tspan);
1720 }
1721
1722 if new_line {
1724 self.message_settings.insert("NEWLINE".into(), true.into());
1725 }
1726
1727 Ok(())
1728 }
1729
1730 fn end_message(&mut self) -> Result<(), RenderError> {
1731 if let Some(text) = self.current_text.take() {
1732 self.document = self.document.clone().add(text);
1733 }
1734 Ok(())
1735 }
1736
1737 fn get_theme<T>(&self, theme_name: &str, entry: &str, default: T) -> T
1739 where
1740 T: FromThemeValue + From<T>,
1741 {
1742 if let Some(theme_map) = self.themes.get(theme_name) {
1743 if let Some(value) = theme_map.get(entry) {
1744 if let Some(result) = T::from_theme_value(value) {
1745 return result;
1746 }
1747 }
1748 }
1749
1750 if theme_name != "DEFAULT" {
1752 if let Some(default_map) = self.themes.get("DEFAULT") {
1753 if let Some(value) = default_map.get(entry) {
1754 if let Some(result) = T::from_theme_value(value) {
1755 return result;
1756 }
1757 }
1758 }
1759 }
1760
1761 default
1762 }
1763
1764 fn get_font_theme(&self, font_name: &str) -> String {
1765 if self.themes.contains_key(font_name) {
1766 font_name.to_string()
1767 } else {
1768 format!("FONT_{}", font_name)
1769 }
1770 }
1771
1772 fn text_box(
1773 &mut self,
1774 x: f32,
1775 y: f32,
1776 box_width: Option<f32>,
1777 box_height: Option<f32>,
1778 box_theme: &str,
1779 pin_func: &str,
1780 text_content: &str,
1781 x_justify_str: &str,
1782 y_justify_str: &str,
1783 ) -> Result<f32, RenderError> {
1784 let border_color = self.get_theme(pin_func, "BORDER COLOR", "red".to_string());
1786 let border_width = self.get_theme(pin_func, "BORDER WIDTH", 1.0f32);
1787 let border_opacity = self.get_theme(pin_func, "BORDER OPACITY", 1.0f32);
1788 let fill_color = self.get_theme(pin_func, "FILL COLOR", "blue".to_string());
1789 let opacity = self.get_theme(pin_func, "OPACITY", 50.0f32);
1790 let font = self.get_theme(pin_func, "FONT", "sans-serif".to_string());
1791 let fontsize = self.get_theme(pin_func, "FONT SIZE", 10.0f32);
1792 let fontcolor = self.get_theme(pin_func, "FONT COLOR", "yellow".to_string());
1793 let fontslant = self.get_theme(pin_func, "FONT SLANT", "normal".to_string());
1794 let fontbold = self.get_theme(pin_func, "FONT BOLD", "normal".to_string());
1795 let fontstretch = self.get_theme(pin_func, "FONT STRETCH", "normal".to_string());
1796 let fontoutline = self.get_theme(pin_func, "FONT OUTLINE", fontcolor.clone());
1797 let fontoutthick = self.get_theme(pin_func, "FONT OUTLINE THICKNESS", 0.0f32);
1798
1799 let w = box_width.unwrap_or_else(|| self.get_theme(box_theme, "WIDTH", 0.0f32));
1800 let h = box_height.unwrap_or_else(|| self.get_theme(box_theme, "HEIGHT", 0.0f32));
1801 let corner_rx = self.get_theme(box_theme, "CORNER RX", 0.0f32);
1802 let corner_ry = self.get_theme(box_theme, "CORNER RY", 0.0f32);
1803 let skew = self.get_theme(box_theme, "SKEW", 0.0f32);
1804
1805 let (xanchor, xalign) = match x_justify_str {
1807 "LEFT" => ("start", -(w / 2.0)),
1808 "RIGHT" => ("end", w / 2.0),
1809 _ => ("middle", 0.0), };
1811
1812 let yalign = match y_justify_str {
1813 "TOP" => -(h / 2.0) + fontsize,
1814 "BOTTOM" => (h / 2.0) - (fontsize / 2.0),
1815 _ => 0.0 + (fontsize / 3.0), };
1817
1818 let mut boxgroup = Group::new();
1820
1821 let mut rect = Rectangle::new()
1823 .set("x", (0.0 - w) / 2.0)
1824 .set("y", (0.0 - h) / 2.0)
1825 .set("width", w)
1826 .set("height", h)
1827 .set("rx", corner_rx)
1828 .set("ry", corner_ry)
1829 .set("stroke", border_color)
1830 .set("fill-opacity", opacity) .set("fill", fill_color)
1832 .set("stroke-width", border_width)
1833 .set("stroke-opacity", border_opacity);
1834
1835 if skew != 0.0 {
1837 rect = rect.set("transform", format!("skewX({})", skew));
1838 }
1839
1840 boxgroup = boxgroup.add(rect);
1841
1842 if !text_content.is_empty() {
1844 let fontoutopacity = if fontoutthick > 0.0 { 1.0 } else { 0.0 };
1845
1846 let lines: Vec<&str> = text_content.split("\\n").collect();
1848
1849 let (yalign1, yalign2) = if lines.len() == 1 {
1850 (yalign, -1.0) } else {
1852 (yalign - (h / 5.0), yalign + (h / 5.0)) };
1854
1855 let text1 = Text::new("")
1857 .set("x", xalign)
1858 .set("y", yalign1)
1859 .set("font-size", fontsize)
1860 .set("font-family", font.clone())
1861 .set("fill", fontcolor.clone())
1862 .set("font-style", fontslant.clone())
1863 .set("font-weight", fontbold.clone())
1864 .set("font-stretch", fontstretch.clone())
1865 .set("stroke", fontoutline.clone())
1866 .set("stroke-opacity", fontoutopacity)
1867 .set("stroke-width", fontoutthick)
1868 .set("text-anchor", xanchor)
1869 .add(TextNode::new(lines[0]));
1870
1871 boxgroup = boxgroup.add(text1);
1872
1873 if yalign2 >= 0.0 && lines.len() > 1 {
1875 let text2 = Text::new("")
1876 .set("x", xalign)
1877 .set("y", yalign2)
1878 .set("font-size", fontsize)
1879 .set("font-family", font)
1880 .set("fill", fontcolor)
1881 .set("font-style", fontslant)
1882 .set("font-weight", fontbold)
1883 .set("font-stretch", fontstretch)
1884 .set("stroke", fontoutline)
1885 .set("stroke-opacity", fontoutopacity)
1886 .set("stroke-width", fontoutthick)
1887 .set("text-anchor", xanchor)
1888 .add(TextNode::new(lines[1]));
1889
1890 boxgroup = boxgroup.add(text2);
1891 }
1892 }
1893
1894 boxgroup = boxgroup.set(
1896 "transform",
1897 format!("translate({},{})", x + (w / 2.0), y + (h / 2.0)),
1898 );
1899
1900 self.document = self.document.clone().add(boxgroup);
1902
1903 Ok(w) }
1905
1906 fn get_box_theme(&self, theme: &str, entry: &str, default: &str) -> String {
1907 let box_theme = if !theme.starts_with("BOX_") {
1908 let box_name = self.get_theme(theme, "BOXES", "STD".to_string());
1910 if box_name == "STD" || box_name.is_empty() {
1911 theme.to_string() } else {
1913 format!("BOX_{}", box_name)
1914 }
1915 } else {
1916 theme.to_string()
1917 };
1918
1919 if !self.themes.contains_key(&box_theme) {
1920 return default.to_string();
1922 }
1923
1924 self.get_theme(&box_theme, entry, default.to_string())
1925 }
1926
1927 fn get_pin_box_xy(&self, box_offset_x: f32, theme: &str, line_height: f32) -> (f32, f32) {
1928 let mut x = self.anchor_x + self.offset_x + box_offset_x;
1929
1930 let side = self
1933 .line_settings
1934 .get("SIDE")
1935 .unwrap_or(&Value::from("LEFT"))
1936 .to_string();
1937 if side.contains("LEFT") {
1938 let box_width = self
1939 .get_box_theme(theme, "WIDTH", "0")
1940 .parse::<f32>()
1941 .unwrap_or(0.0);
1942 x = x - box_width;
1943 }
1944
1945 let mut y = self.anchor_y + self.offset_y;
1946 let box_height = self
1947 .get_box_theme(theme, "HEIGHT", "0")
1948 .parse::<f32>()
1949 .unwrap_or(0.0);
1950
1951 let justify_y = self
1952 .line_settings
1953 .get("JUSTIFY Y")
1954 .unwrap_or(&Value::from("CENTER"))
1955 .to_string();
1956
1957 if justify_y == "CENTER" {
1958 y = y + ((line_height - box_height) / 2.0);
1959 } else if justify_y == "BOTTOM" {
1960 y = y + (line_height - box_height);
1961 }
1962 (x, y)
1965 }
1966
1967 fn inc_offset_x(&self, box_offset_x: f32, side: &str, pin_func: &str) -> f32 {
1968 let gap = self
1969 .line_settings
1970 .get("GAP")
1971 .unwrap()
1972 .parse::<f32>()
1973 .unwrap_or(0.0);
1974
1975 let box_width = self
1976 .get_box_theme(pin_func, "WIDTH", "0")
1977 .parse::<f32>()
1978 .unwrap_or(0.0);
1979
1980 let x_span = gap + box_width;
1981
1982 if side.contains("LEFT") {
1983 box_offset_x - x_span
1984 } else if side.contains("RIGHT") {
1985 box_offset_x + x_span
1986 } else {
1987 box_offset_x }
1989 }
1990
1991 fn print_pin(
1992 &mut self,
1993 pin_type: Option<PinType>,
1994 wire: Option<WireType>,
1995 group: &Option<String>,
1996 ) -> Result<f32, RenderError> {
1997 let pin_width = self
1998 .line_settings
1999 .get("PINWIDTH")
2000 .unwrap()
2001 .parse::<f32>()
2002 .unwrap_or(10.0);
2003
2004 let group_width = self
2005 .line_settings
2006 .get("GROUPWIDTH")
2007 .unwrap()
2008 .parse::<f32>()
2009 .unwrap_or(20.0);
2010
2011 let leader_offset = self
2012 .line_settings
2013 .get("LEADER")
2014 .unwrap()
2015 .parse::<f32>()
2016 .unwrap_or(20.0);
2017
2018 let line_step = self
2019 .line_settings
2020 .get("LINESTEP")
2021 .unwrap()
2022 .parse::<f32>()
2023 .unwrap_or(10.0);
2024
2025 let side = self
2026 .line_settings
2027 .get("SIDE")
2028 .unwrap_or(&Value::from("LEFT"))
2029 .to_string();
2030
2031 let pin_box_offset = self.offset_x + (group_width / 2.0);
2032 let pin_center_x = if side.contains("RIGHT") {
2033 self.anchor_x + pin_box_offset
2034 } else {
2035 self.anchor_x - pin_box_offset
2036 };
2037
2038 let pin_center_y = self.anchor_y + self.offset_y + (line_step / 2.0);
2039
2040 if let Some(group_name) = group {
2042 let group_theme = format!("GROUP_{}", group_name);
2043 if self.themes.contains_key(&group_theme) {
2044 let fill_color = self.get_theme(&group_theme, "FILL COLOR", "black".to_string());
2045 let fill_opacity = self.get_theme(&group_theme, "OPACITY", 1.0f32);
2046
2047 let circle = Circle::new()
2048 .set("cx", pin_center_x)
2049 .set("cy", pin_center_y)
2050 .set("r", group_width / 2.0)
2051 .set("stroke", "black")
2052 .set("stroke-width", "2")
2053 .set("stroke-opacity", "1")
2054 .set("fill", fill_color)
2055 .set("fill-opacity", fill_opacity);
2056
2057 self.document = self.document.clone().add(circle);
2058 } else {
2059 return Err(RenderError::SvgError(format!(
2060 "Error: PinGroup {} is not defined",
2061 group_name
2062 )));
2063 }
2064 }
2065
2066 if let Some(pin_type_val) = pin_type {
2068 match pin_type_val {
2069 PinType::IO => {
2070 let circle = Circle::new()
2071 .set("cx", pin_center_x)
2072 .set("cy", pin_center_y)
2073 .set("r", pin_width / 2.0)
2074 .set("stroke", "black")
2075 .set("fill", "black")
2076 .set("opacity", "1");
2077
2078 self.document = self.document.clone().add(circle);
2079 }
2080 PinType::Input | PinType::Output => {
2081 let triangle_edge_length = (pin_width / 2.0) * 3.0_f32.sqrt();
2082 let triangle_center_shift = pin_width / 4.0;
2083
2084 let points = if (side.contains("LEFT") && pin_type_val == PinType::Output)
2085 || (side.contains("RIGHT") && pin_type_val == PinType::Input)
2086 {
2087 format!(
2088 "{},{} {},{} {},{}",
2089 triangle_center_shift,
2090 triangle_edge_length / 2.0,
2091 triangle_center_shift,
2092 -triangle_edge_length / 2.0,
2093 -pin_width / 2.0,
2094 0.0
2095 )
2096 } else {
2097 format!(
2098 "{},{} {},{} {},{}",
2099 -triangle_center_shift,
2100 triangle_edge_length / 2.0,
2101 -triangle_center_shift,
2102 -triangle_edge_length / 2.0,
2103 pin_width / 2.0,
2104 0.0
2105 )
2106 };
2107
2108 let polygon = Polygon::new()
2109 .set("points", points)
2110 .set("stroke", "black")
2111 .set("fill", "black")
2112 .set("opacity", "1")
2113 .set(
2114 "transform",
2115 format!("translate({},{})", pin_center_x, pin_center_y),
2116 );
2117
2118 self.document = self.document.clone().add(polygon);
2119 }
2120 }
2121 }
2122
2123 let return_pin_width = group_width + leader_offset;
2125
2126 if leader_offset > 0.0 {
2127 if let Some(wire_type) = wire {
2128 let wire_theme = format!("PINWIRE_{}", wire_type);
2129 let color = self.get_theme(&wire_theme, "FILL COLOR", "black".to_string());
2130 let opacity = self.get_theme(&wire_theme, "OPACITY", 1.0f32);
2131 let thickness = self.get_theme(&wire_theme, "THICKNESS", 1.0f32);
2132
2133 let points = match wire_type {
2134 WireType::Pwm => {
2135 let step = leader_offset / 4.0;
2137 format!(
2138 "0,0 {step},0 {step},{} {},{} {},{} {},{} {},{} {},0",
2139 -group_width / 2.0,
2140 step * 2.0,
2141 -group_width / 2.0,
2142 step * 2.0,
2143 group_width / 2.0,
2144 step * 3.0,
2145 group_width / 2.0,
2146 step * 3.0,
2147 0.0,
2148 step * 4.0
2149 )
2150 }
2151 WireType::Analog | WireType::HsAnalog => {
2152 let max_angle = if wire_type == WireType::Analog {
2154 360.0
2155 } else {
2156 720.0
2157 };
2158 let step = leader_offset / 4.0;
2159 let sine_width = step * 2.0;
2160
2161 let mut points_vec = vec![format!("0,0"), format!("{},0", step)];
2162
2163 for i in 0..((sine_width * 10.0) as i32) {
2164 let i_f = i as f32 / 10.0;
2165 let x = i_f + step;
2166 let y = ((max_angle / sine_width) * i_f).to_radians().sin()
2167 * (-group_width / 2.0);
2168 points_vec.push(format!("{},{}", x, y));
2169 }
2170 points_vec.push(format!("{},0", step * 4.0));
2171
2172 points_vec.join(" ")
2173 }
2174 _ => {
2175 format!("0,0 {},0", leader_offset)
2177 }
2178 };
2179
2180 let leader_x = if side.contains("LEFT") {
2181 pin_center_x - (group_width / 2.0) - leader_offset
2182 } else {
2183 pin_center_x + (group_width / 2.0)
2184 };
2185
2186 let polyline = Polyline::new()
2187 .set("points", points)
2188 .set("fill", "none")
2189 .set("stroke", color)
2190 .set("opacity", opacity)
2191 .set("stroke-width", thickness)
2192 .set(
2193 "transform",
2194 format!("translate({},{})", leader_x, pin_center_y),
2195 );
2196
2197 self.document = self.document.clone().add(polyline);
2198 }
2199 }
2200
2201 if side.contains("LEFT") {
2202 Ok(-return_pin_width)
2203 } else {
2204 Ok(return_pin_width)
2205 }
2206 }
2207
2208 pub fn save_to_file(&self, path: &str) -> Result<(), RenderError> {
2210 use std::fs::File;
2211 use std::io::Write;
2212
2213 let mut file = File::create(path)?;
2214 write!(file, "{}", self.document)?;
2215 Ok(())
2216 }
2217
2218 pub fn print_themes(&self) {
2220 println!("=== THEMES CONTENT ===");
2221 if self.themes.is_empty() {
2222 println!("No themes defined.");
2223 return;
2224 }
2225
2226 for (theme_name, theme_map) in &self.themes {
2227 println!("\nTheme: '{}'", theme_name);
2228 if theme_map.is_empty() {
2229 println!(" (empty)");
2230 } else {
2231 for (entry, value) in theme_map {
2232 println!(" {} = {}", entry, value.as_string());
2233 }
2234 }
2235 }
2236 println!("=== END THEMES ===\n");
2237 }
2238
2239 pub fn print_theme(&self, theme_name: &str) {
2241 println!("=== THEME: '{}' ===", theme_name);
2242 if let Some(theme_map) = self.themes.get(theme_name) {
2243 if theme_map.is_empty() {
2244 println!(" (empty)");
2245 } else {
2246 for (entry, value) in theme_map {
2247 println!(" {} = {}", entry, value.as_string());
2248 }
2249 }
2250 } else {
2251 println!(" Theme not found!");
2252 }
2253 println!("=== END THEME ===\n");
2254 }
2255
2256 fn extract_svg_dimensions(svg_content: &str) -> Result<(f32, f32), RenderError> {
2258 let svg_tag_start = svg_content
2260 .find("<svg")
2261 .ok_or_else(|| RenderError::SvgError("No <svg> tag found".to_string()))?;
2262
2263 let svg_tag_end = svg_content[svg_tag_start..]
2265 .find('>')
2266 .ok_or_else(|| RenderError::SvgError("Invalid <svg> tag".to_string()))?;
2267
2268 let svg_tag = &svg_content[svg_tag_start..svg_tag_start + svg_tag_end];
2269
2270 let width = Self::extract_dimension_attribute(svg_tag, "width")?;
2272 let height = Self::extract_dimension_attribute(svg_tag, "height")?;
2273
2274 if width.is_none() || height.is_none() {
2276 if let Some((vb_width, vb_height)) = Self::extract_viewbox_dimensions(svg_tag)? {
2277 return Ok((width.unwrap_or(vb_width), height.unwrap_or(vb_height)));
2278 }
2279 }
2280
2281 Ok((width.unwrap_or(100.0), height.unwrap_or(100.0)))
2282 }
2283
2284 fn extract_dimension_attribute(
2286 svg_tag: &str,
2287 attr_name: &str,
2288 ) -> Result<Option<f32>, RenderError> {
2289 let attr_pattern = format!("{}=\"", attr_name);
2290 if let Some(start) = svg_tag.find(&attr_pattern) {
2291 let value_start = start + attr_pattern.len();
2292 if let Some(end) = svg_tag[value_start..].find('"') {
2293 let value_str = &svg_tag[value_start..value_start + end];
2294 let numeric_str =
2296 value_str.trim_end_matches(|c: char| c.is_alphabetic() || c == '%');
2297 if let Ok(value) = numeric_str.parse::<f32>() {
2298 return Ok(Some(value));
2299 }
2300 }
2301 }
2302 Ok(None)
2303 }
2304
2305 fn extract_viewbox_dimensions(svg_tag: &str) -> Result<Option<(f32, f32)>, RenderError> {
2307 if let Some(start) = svg_tag.find("viewBox=\"") {
2308 let value_start = start + 9; if let Some(end) = svg_tag[value_start..].find('"') {
2310 let viewbox_str = &svg_tag[value_start..value_start + end];
2311 let parts: Vec<&str> = viewbox_str.split_whitespace().collect();
2312 if parts.len() == 4 {
2313 if let (Ok(width), Ok(height)) =
2314 (parts[2].parse::<f32>(), parts[3].parse::<f32>())
2315 {
2316 return Ok(Some((width, height)));
2317 }
2318 }
2319 }
2320 }
2321 Ok(None)
2322 }
2323
2324 }
2326
2327fn get_size(size: Option<f32>, max_size: f32, default: Option<f32>) -> f32 {
2328 match size {
2329 None => match default {
2330 None => max_size,
2331 Some(default_val) => default_val,
2332 },
2333 Some(size_val) => {
2334 if size_val >= 1.0 {
2335 size_val
2336 } else {
2337 (size_val / 0.9999) * max_size
2338 }
2339 }
2340 }
2341}
2342
2343pub fn generate_svg(commands: &[Command], output_path: &str) -> Result<(), RenderError> {
2345 let mut renderer = SvgRenderer::new();
2346 renderer.process_commands(commands)?;
2347
2348 renderer.save_to_file(output_path)?;
2352 Ok(())
2353}
2354
2355pub fn generate_svg_with_debug(
2357 commands: &[Command],
2358 output_path: &str,
2359 debug_themes: bool,
2360) -> Result<(), RenderError> {
2361 let mut renderer = SvgRenderer::new();
2362 renderer.process_commands(commands)?;
2363
2364 if debug_themes {
2365 renderer.print_themes();
2366 }
2367
2368 renderer.save_to_file(output_path)?;
2369 Ok(())
2370}