Skip to main content

pinout/renderer/
svg.rs

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
70/// Trait for extracting typed values from ThemeValue
71pub 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), // mm
147    page_resolution: (u32, u32), // pixels
148    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        // Default page dimensions for A4 landscape
168        let page_dimensions = (297.0, 210.0); // mm
169
170        // Calculate resolution in pixels based on DPI
171        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        // Create the SVG document with the calculated dimensions
177        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                    // Transition from Setup to Draw phase
208                    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        // Ensure any open text message is closed
226        if self.current_text.is_some() {
227            self.end_message()?;
228        }
229
230        // Add definitions to document
231        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, // Special case handled separately
239
240            // Setup phase commands
241            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            // Draw phase commands
264            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            // Setup phase commands
281            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            // Draw phase commands
473            Command::Draw => Ok(()), // Already handled in process_commands
474            Command::GoogleFont { _link } => {
475                // todo!("handle font implementation")
476                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        // Define fixed theme entries
583        let fixed_theme_entries = vec![
584            "DEFAULT".to_string(),
585            "TYPE".to_string(),
586            "GROUP".to_string(),
587        ];
588
589        // Check if pin function types have already been initialized
590        if self.pin_func_types.is_empty() {
591            // Verify that default matches the first fixed entry
592            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                // Set pin_func_types to just the labels
597                self.pin_func_types = labels.to_vec();
598
599                // Initialize empty theme dictionaries for fixed entries and labels
600                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    /// Set theme values of any supported type
623    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        // Set the theme entry for the default theme
635        self.set_theme_value("DEFAULT", entry, default.into());
636
637        // Set for pin type if provided
638        if let Some(pt) = pin_type {
639            self.set_theme_value("TYPE", entry, pt.into());
640        }
641
642        // Set for group if provided
643        if let Some(g) = group {
644            self.set_theme_value("GROUP", entry, g.into());
645        }
646
647        // Set for each pin function type
648        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        // Create or get the theme map
717        let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
718
719        // Set the color and opacity
720        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        // Create or get the theme map
739        let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
740
741        // Set the color, opacity, and thickness
742        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        // Create or get the theme map
756        let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
757
758        // Set the color and opacity
759        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        // Create or get the theme map
786        let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
787
788        // Set all box theme parameters
789        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        // Create or get the theme map
830        let theme_map = self.themes.entry(theme_entry).or_insert_with(HashMap::new);
831
832        // Set all text font parameters
833        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), // mm (portrait)
853            "A4-L" => (297.0, 210.0), // mm (landscape)
854            "A3-P" => (297.0, 420.0), // mm (portrait)
855            "A3-L" => (420.0, 297.0), // mm (landscape)
856            _ => {
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        // Recalculate resolution in pixels based on DPI
868        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        // Update the document dimensions
874        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        // Recalculate resolution in pixels based on new DPI
897        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        // Update the document dimensions
903        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        // Load the image
954        let mut img = image::open(path)?;
955
956        // Apply crop if all crop parameters are provided
957        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            // Check if crop coordinates are valid
964            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        // Resize if width or height is specified
978        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        // Get image dimensions
988        let img_width = img.width();
989        let img_height = img.height();
990
991        // Calculate position (center of image)
992        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        // Adjust position to top-left corner for SVG image element
996        let x = x - (img_width as f32 / 2.0);
997        let y = y - (img_height as f32 / 2.0);
998
999        // Convert image to PNG and encode as base64
1000        let mut buffer: Vec<u8> = Vec::new();
1001        // Use Cursor to wrap the Vec<u8> to implement Seek trait
1002        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        // Create the image element
1008        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        // Apply rotation if specified
1016        if let Some(rot) = rot {
1017            // Calculate center of image for rotation
1018            let center_x = x + (img_width as f32 / 2.0);
1019            let center_y = y + (img_height as f32 / 2.0);
1020
1021            // Apply rotation transform around the center
1022            image = image.set(
1023                "transform",
1024                format!("rotate({} {} {})", rot, center_x, center_y),
1025            );
1026        }
1027
1028        // Add the image to the document
1029        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        // Check if it's an SVG file
1052        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        // Read the SVG file
1059        let mut file = File::open(path)?;
1060        let mut svg_content = String::new();
1061        file.read_to_string(&mut svg_content)?;
1062
1063        // Extract SVG dimensions from the content
1064        let (svg_width, svg_height) = Self::extract_svg_dimensions(&svg_content)?;
1065
1066        // Encode the SVG content as base64
1067        let encoded = general_purpose::STANDARD.encode(svg_content.as_bytes());
1068        let data_url = format!("data:image/svg+xml;base64,{}", encoded);
1069
1070        // Calculate position and dimensions
1071        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)); // Use SVG width as default if not specified
1074        let h = get_size(h, svg_height, Some(100.0)); // Use SVG height as default if not specified
1075
1076        // Adjust position to top-left corner for SVG image element
1077        let x = x - (svg_width / 2.0);
1078        let y = y - (svg_height / 2.0);
1079
1080        // Create the image element
1081        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        // Apply rotation if specified
1089        if let Some(rot) = rot {
1090            // Calculate center of image for rotation
1091            let center_x = x + (w / 2.0);
1092            let center_y = y + (h / 2.0);
1093
1094            // Apply rotation transform around the center
1095            image = image.set(
1096                "transform",
1097                format!("rotate({} {} {})", rot, center_x, center_y),
1098            );
1099        }
1100
1101        // Add the image to the document
1102        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        // Clear existing line settings
1130        self.line_settings.clear();
1131
1132        // Convert enums to strings for storage
1133        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        // Store all pin set settings
1153        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        // Print the pin icon and leader line, and get the box offset
1190        let mut box_offset_x = self.print_pin(pin_type, wire, group)?;
1191
1192        // Get line height from settings
1193        let line_height = self
1194            .line_settings
1195            .get("LINESTEP")
1196            .unwrap()
1197            .parse::<f32>()
1198            .unwrap_or(10.0);
1199
1200        // Process each attribute (columns after the pin type, wire, and group)
1201        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                    // Calculate position for the text box
1207                    let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1208
1209                    // Get justification settings before borrowing self mutably
1210                    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                    // Draw the text box
1222                    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                    // Increment the box offset for the next box
1235                    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                    // If not packed, still increment the offset for empty boxes
1248                    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        // Increment vertical offset for the next pin
1259        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        // Print the pin icon and leader line, and get the box offset
1280        let mut box_offset_x = self.print_pin(pin_type, wire, pin_group)?;
1281
1282        // Get line height from settings
1283        let line_height = self
1284            .line_settings
1285            .get("LINESTEP")
1286            .unwrap()
1287            .parse::<f32>()
1288            .unwrap_or(10.0);
1289
1290        // If a label is provided, draw the first box with the label
1291        if let Some(label_text) = label {
1292            if !label_text.is_empty() {
1293                // Use the first pin function type for the label
1294                let pin_func = self.pin_func_types[0].clone(); // First pin function type
1295
1296                // Calculate position for the text box
1297                let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1298
1299                // Get justification settings before borrowing self mutably
1300                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                // Draw the text box with the label
1312                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                // Increment the box offset for the text
1325                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 text is provided, draw it after the label
1337        if !message.is_empty() {
1338            // Get font settings from the theme
1339            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            // Calculate position for the text
1348            let (x, y) = self.get_pin_box_xy(box_offset_x, "BOX_SKEWED", line_height);
1349            // Adjust X position for the gap
1350            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            // Determine text anchor based on side
1368            let text_anchor = if side.contains("LEFT") {
1369                "end"
1370            } else {
1371                "start"
1372            };
1373
1374            // Create text element
1375            let text_elem = Text::new("") // TODO this can corrup nodes
1376                .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            // Add text to document
1388            self.document = self.document.clone().add(text_elem);
1389        }
1390
1391        // Increment vertical offset for the next pin
1392        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        // Get the box theme name (add BOX_ prefix if not already there)
1409        let box_theme = if theme.starts_with("BOX_") {
1410            theme.to_string()
1411        } else {
1412            format!("BOX_{}", theme)
1413        };
1414
1415        // Convert justify options to strings
1416        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", // Default
1421        };
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", // Default
1428        };
1429
1430        // Draw the text box
1431        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        // End any previous message
1458        self.end_message()?;
1459
1460        // Set message settings
1461        self.message_settings.insert("NEWLINE".into(), false.into());
1462
1463        // Set x and y if provided
1464        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        // Set line step if provided
1475        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()); // Default
1479        }
1480
1481        // Set font if provided
1482        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()); // Default
1488        }
1489
1490        // Set font size if provided
1491        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()); // Default
1495        }
1496
1497        // Set justify settings
1498        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", // Default
1503        };
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", // Default
1510        };
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        // Set text anchor based on x justification
1518        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        // Set y shift based on y justification
1525        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        // Get font theme
1541        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        // Create new text element
1549        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("") //TODO this can corrupt output
1595            .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        // Get default color if not specified
1633        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        // Get default edge color if not specified
1640        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        // Check if we need to start a new line
1649        if self
1650            .message_settings
1651            .get("NEWLINE")
1652            .unwrap()
1653            .parse()
1654            .unwrap_or(false)
1655        {
1656            // Reset newline flag
1657            self.message_settings.insert("NEWLINE".into(), false.into());
1658
1659            // Update Y offset
1660            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            // Set position for new line
1676            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        // Set text properties
1712        tspan = tspan
1713            .set("stroke", edge_color)
1714            .set("fill", color)
1715            .add(TextNode::new(message));
1716
1717        // Add tspan to current text element
1718        if let Some(ref mut text) = self.current_text {
1719            *text = text.clone().add(tspan);
1720        }
1721
1722        // Set newline flag if needed
1723        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    /// Get theme value of any supported type
1738    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        // Fall back to DEFAULT theme if the specific theme doesn't have the entry
1751        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        // Get theme values
1785        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        // Calculate alignment
1806        let (xanchor, xalign) = match x_justify_str {
1807            "LEFT" => ("start", -(w / 2.0)),
1808            "RIGHT" => ("end", w / 2.0),
1809            _ => ("middle", 0.0), // CENTER
1810        };
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), // CENTER
1816        };
1817
1818        // Create group
1819        let mut boxgroup = Group::new();
1820
1821        // Create rectangle
1822        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) // Convert percentage to decimal
1831            .set("fill", fill_color)
1832            .set("stroke-width", border_width)
1833            .set("stroke-opacity", border_opacity);
1834
1835        // Apply skew if needed
1836        if skew != 0.0 {
1837            rect = rect.set("transform", format!("skewX({})", skew));
1838        }
1839
1840        boxgroup = boxgroup.add(rect);
1841
1842        // Add text if content exists
1843        if !text_content.is_empty() {
1844            let fontoutopacity = if fontoutthick > 0.0 { 1.0 } else { 0.0 };
1845
1846            // Split content by "\\n" for multi-line support
1847            let lines: Vec<&str> = text_content.split("\\n").collect();
1848
1849            let (yalign1, yalign2) = if lines.len() == 1 {
1850                (yalign, -1.0) // Single line
1851            } else {
1852                (yalign - (h / 5.0), yalign + (h / 5.0)) // Multi-line
1853            };
1854
1855            // Add first line
1856            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            // Add second line if it exists
1874            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        // Apply translation
1895        boxgroup = boxgroup.set(
1896            "transform",
1897            format!("translate({},{})", x + (w / 2.0), y + (h / 2.0)),
1898        );
1899
1900        // Add to document
1901        self.document = self.document.clone().add(boxgroup);
1902
1903        Ok(w) // Return width as in the original signature
1904    }
1905
1906    fn get_box_theme(&self, theme: &str, entry: &str, default: &str) -> String {
1907        let box_theme = if !theme.starts_with("BOX_") {
1908            // Get the box name from the theme's "BOXES" entry
1909            let box_name = self.get_theme(theme, "BOXES", "STD".to_string());
1910            if box_name == "STD" || box_name.is_empty() {
1911                theme.to_string() // Use theme name directly if no specific box
1912            } else {
1913                format!("BOX_{}", box_name)
1914            }
1915        } else {
1916            theme.to_string()
1917        };
1918
1919        if !self.themes.contains_key(&box_theme) {
1920            // eprintln!("ERROR: BOX Theme {} not known!", box_theme);
1921            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        // On the Left side we need to pre-decrement the X coordinate
1931        // otherwise we align to the wrong box edge.
1932        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        // For "TOP", no adjustment needed (pass)
1963
1964        (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 // No change for other sides
1988        }
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        // Draw group circle if group is specified
2041        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        // Draw pin type indicator
2067        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        // Draw leader line if leader_offset > 0
2124        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                        // Square wave
2136                        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                        // Sine wave
2153                        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                        // Power and Digital - just a line
2176                        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    /// Save the SVG document to a file
2209    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    /// Print the content of all themes for debugging
2219    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    /// Print the content of a specific theme for debugging
2240    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    /// Extract width and height from SVG content
2257    fn extract_svg_dimensions(svg_content: &str) -> Result<(f32, f32), RenderError> {
2258        // Look for the opening <svg> tag
2259        let svg_tag_start = svg_content
2260            .find("<svg")
2261            .ok_or_else(|| RenderError::SvgError("No <svg> tag found".to_string()))?;
2262
2263        // Find the end of the opening tag
2264        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        // Try to extract width and height attributes
2271        let width = Self::extract_dimension_attribute(svg_tag, "width")?;
2272        let height = Self::extract_dimension_attribute(svg_tag, "height")?;
2273
2274        // If width/height not found, try to extract from viewBox
2275        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    /// Extract a dimension attribute (width or height) from SVG tag
2285    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                // Remove units (px, pt, em, etc.) and parse
2295                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    /// Extract dimensions from viewBox attribute
2306    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; // len of "viewBox=\""
2309            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    // Helper methods
2325}
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
2343/// Generate SVG file from commands
2344pub fn generate_svg(commands: &[Command], output_path: &str) -> Result<(), RenderError> {
2345    let mut renderer = SvgRenderer::new();
2346    renderer.process_commands(commands)?;
2347
2348    // Print themes for debugging (you can comment this out in production)
2349    //renderer.print_themes();
2350
2351    renderer.save_to_file(output_path)?;
2352    Ok(())
2353}
2354
2355/// Generate SVG file from commands with optional theme debugging
2356pub 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}