Skip to main content

excelize_rs/
vml.rs

1//! VML-based features: comments, form controls, and header/footer images.
2//!
3//! Ported from Go `vml.go`, `vmlDrawing.go`, and `picture.go`.
4
5use std::borrow::Cow;
6use std::collections::HashMap;
7
8use quick_xml::Reader;
9use quick_xml::events::Event;
10
11use crate::constants::{
12    MAX_FIELD_LENGTH, MAX_FORM_CONTROL_VALUE, SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_COMMENTS,
13    SOURCE_RELATIONSHIP_DRAWING_VML, SOURCE_RELATIONSHIP_IMAGE, TOTAL_CELL_CHARS,
14};
15use crate::errors::{ErrParameterInvalid, new_add_comment_error, new_invalid_optional_value};
16use crate::file::File;
17use crate::lib_util::{
18    cell_name_to_coordinates, coordinates_to_cell_name, count_utf16_string, in_str_slice,
19    truncate_utf16_units,
20};
21use crate::xml::common::{
22    AttrValBool, AttrValFloat, AttrValInt, AttrValString, RichTextRun, XlsxColor, XlsxR, XlsxRPr,
23    XlsxT,
24};
25use crate::xml::drawing::GraphicOptions;
26use crate::xml::vml::{
27    VmlDrawing, VmlFormula, VmlFormulas, VmlIdmap, VmlImageData, VmlLock, VmlPath, VmlShape,
28    VmlShapeLayout, VmlShapeType, VmlStroke,
29};
30
31// ------------------------------------------------------------------
32// Public types
33// ------------------------------------------------------------------
34
35pub use crate::xml::comments::Comment;
36
37/// Supported form control types.
38#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
39#[repr(u8)]
40pub enum FormControlType {
41    /// Comment indicator (note).
42    #[default]
43    Note,
44    /// Button.
45    Button,
46    /// Option button / radio button.
47    OptionButton,
48    /// Spin button / spinner.
49    SpinButton,
50    /// Check box.
51    CheckBox,
52    /// Group box.
53    GroupBox,
54    /// Label.
55    Label,
56    /// Scroll bar.
57    ScrollBar,
58}
59
60/// Header/footer image horizontal position.
61#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
62pub enum HeaderFooterImagePositionType {
63    /// Left section.
64    #[default]
65    Left,
66    /// Center section.
67    Center,
68    /// Right section.
69    Right,
70}
71
72/// Form control options.
73#[derive(Debug, Default, Clone, PartialEq)]
74pub struct FormControl {
75    pub cell: String,
76    pub macro_name: String,
77    pub width: u32,
78    pub height: u32,
79    pub checked: bool,
80    pub current_val: u32,
81    pub min_val: u32,
82    pub max_val: u32,
83    pub inc_change: u32,
84    pub page_change: u32,
85    pub horizontally: bool,
86    pub cell_link: String,
87    pub text: String,
88    pub paragraph: Vec<RichTextRun>,
89    pub r#type: FormControlType,
90    pub format: GraphicOptions,
91}
92
93/// Header/footer image options.
94#[derive(Debug, Default, Clone, PartialEq)]
95pub struct HeaderFooterImageOptions {
96    pub position: HeaderFooterImagePositionType,
97    pub file: Vec<u8>,
98    pub is_footer: bool,
99    pub first_page: bool,
100    pub extension: String,
101    pub width: String,
102    pub height: String,
103}
104
105// ------------------------------------------------------------------
106// Public API
107// ------------------------------------------------------------------
108
109impl File {
110    /// Retrieve all comments in a worksheet.
111    pub fn get_comments(&self, sheet: &str) -> crate::errors::Result<Vec<Comment>> {
112        let mut comments = Vec::new();
113        let sheet_xml_path = self.get_sheet_xml_path(sheet).ok_or_else(|| {
114            Box::new(crate::errors::ErrSheetNotExist {
115                sheet_name: sheet.to_string(),
116            }) as Box<dyn std::error::Error + Send + Sync>
117        })?;
118        let mut comments_xml = self.get_sheet_comments(&sheet_xml_path);
119        if !comments_xml.starts_with('/') {
120            comments_xml = format!("xl{}", comments_xml.trim_start_matches(".."));
121        }
122        let comments_xml = comments_xml.trim_start_matches('/').to_string();
123        let cmts = self.comments_reader(&comments_xml)?;
124        if let Some(cmts) = cmts {
125            for cmt in &cmts.comment_list.comment {
126                let mut comment = Comment::default();
127                if (cmt.author_id as usize) < cmts.authors.author.len() {
128                    comment.author = cmts.authors.author[cmt.author_id as usize].clone();
129                }
130                comment.cell = cmt.r#ref.clone();
131                comment.author_id = cmt.author_id;
132                if let Some(t) = &cmt.text.t {
133                    comment.text.push_str(t);
134                }
135                for run in &cmt.text.r {
136                    if let Some(t) = &run.t {
137                        let mut rtr = RichTextRun {
138                            text: t.val.clone(),
139                            ..Default::default()
140                        };
141                        if let Some(rpr) = &run.r_pr {
142                            rtr.font = Some(rpr_to_font(rpr));
143                        }
144                        comment.paragraph.push(rtr);
145                    }
146                }
147                comments.push(comment);
148            }
149        }
150        Ok(comments)
151    }
152
153    /// Add a comment to a worksheet.
154    pub fn add_comment(&self, sheet: &str, opts: Comment) -> crate::errors::Result<()> {
155        self.add_vml_object(VmlOptions {
156            sheet: sheet.to_string(),
157            comment: Some(opts.clone()),
158            form_control: Some(FormControl {
159                cell: opts.cell.clone(),
160                text: opts.text.clone(),
161                paragraph: opts.paragraph.clone(),
162                width: opts.width,
163                height: opts.height,
164                r#type: FormControlType::Note,
165                ..Default::default()
166            }),
167            ..Default::default()
168        })
169    }
170
171    /// Delete a comment in a worksheet by cell reference.
172    pub fn delete_comment(&self, sheet: &str, cell: &str) -> crate::errors::Result<()> {
173        let ws = self.work_sheet_reader(sheet)?;
174        if ws.legacy_drawing.is_none() {
175            return Ok(());
176        }
177        let sheet_xml_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
178        let mut comments_xml = self.get_sheet_comments(&sheet_xml_path);
179        if !comments_xml.starts_with('/') {
180            comments_xml = format!("xl{}", comments_xml.trim_start_matches(".."));
181        }
182        let comments_xml = comments_xml.trim_start_matches('/').to_string();
183        if let Some(cmts) = self.comments_reader(&comments_xml)? {
184            let mut cmts = cmts;
185            cmts.comment_list.comment.retain(|c| c.r#ref != cell);
186            cmts.cells.retain(|c| c != cell);
187            self.comments.insert(comments_xml, cmts);
188        }
189        let rid = ws
190            .legacy_drawing
191            .as_ref()
192            .and_then(|d| d.rid.clone())
193            .unwrap_or_default();
194        let sheet_relationships_drawing_vml =
195            self.get_sheet_relationships_target_by_id(sheet, &rid);
196        self.delete_vml_shape(&sheet_relationships_drawing_vml, cell, true)
197    }
198
199    /// Add a form control to a worksheet.
200    pub fn add_form_control(&self, sheet: &str, opts: FormControl) -> crate::errors::Result<()> {
201        self.add_vml_object(VmlOptions {
202            sheet: sheet.to_string(),
203            form_control: Some(opts),
204            ..Default::default()
205        })
206    }
207
208    /// Delete a form control in a worksheet by cell reference.
209    pub fn delete_form_control(&self, sheet: &str, cell: &str) -> crate::errors::Result<()> {
210        let ws = self.work_sheet_reader(sheet)?;
211        if ws.legacy_drawing.is_none() {
212            return Ok(());
213        }
214        let rid = ws
215            .legacy_drawing
216            .as_ref()
217            .and_then(|d| d.rid.clone())
218            .unwrap_or_default();
219        let sheet_relationships_drawing_vml =
220            self.get_sheet_relationships_target_by_id(sheet, &rid);
221        self.delete_vml_shape(&sheet_relationships_drawing_vml, cell, false)
222    }
223
224    /// Retrieve all form controls in a worksheet.
225    pub fn get_form_controls(&self, sheet: &str) -> crate::errors::Result<Vec<FormControl>> {
226        let mut form_controls = Vec::new();
227        let ws = self.work_sheet_reader(sheet)?;
228        if ws.legacy_drawing.is_none() {
229            return Ok(form_controls);
230        }
231        let rid = ws
232            .legacy_drawing
233            .as_ref()
234            .and_then(|d| d.rid.clone())
235            .unwrap_or_default();
236        let target = self.get_sheet_relationships_target_by_id(sheet, &rid);
237        let drawing_vml = target.replace("..", "xl");
238        let vml = self.vml_drawing_reader(&drawing_vml)?.unwrap_or_default();
239        for sp in &vml.shape {
240            if sp.shape_type != "#_x0000_t201" {
241                continue;
242            }
243            let fc = extract_form_control(&sp.inner_xml)?;
244            if fc.r#type == FormControlType::Note || fc.cell.is_empty() {
245                continue;
246            }
247            form_controls.push(fc);
248        }
249        Ok(form_controls)
250    }
251
252    /// Add an image that can be referenced from the worksheet header/footer.
253    pub fn add_header_footer_image(
254        &self,
255        sheet: &str,
256        opts: &HeaderFooterImageOptions,
257    ) -> crate::errors::Result<()> {
258        let ws = self.work_sheet_reader(sheet)?;
259        let ext = opts.extension.to_lowercase();
260        let image_types = supported_image_types();
261        if !image_types.contains_key(&ext) {
262            return Err(Box::new(crate::errors::ErrImgExt));
263        }
264        let sheet_id = self.get_sheet_id(sheet);
265        let vml_id = self.count_vml_drawing() + 1;
266        let drawing_vml = format!("xl/drawings/vmlDrawing{vml_id}.vml");
267        let sheet_relationships_drawing_vml = format!("../drawings/vmlDrawing{vml_id}.vml");
268        let sheet_xml_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
269        let sheet_rels = format!(
270            "xl/worksheets/_rels/{}.rels",
271            sheet_xml_path.trim_start_matches("xl/worksheets/")
272        );
273
274        let (vml_id, drawing_vml, _sheet_relationships_drawing_vml) =
275            if let Some(ld) = ws.legacy_drawing_hf.as_ref() {
276                let target = self
277                    .get_sheet_relationships_target_by_id(sheet, ld.rid.as_deref().unwrap_or(""));
278                let id: i32 = target
279                    .trim_start_matches("../drawings/vmlDrawing")
280                    .trim_end_matches(".vml")
281                    .parse()
282                    .unwrap_or(vml_id);
283                (id, target.replace("..", "xl"), target)
284            } else {
285                let r_id = self.add_rels(
286                    &sheet_rels,
287                    SOURCE_RELATIONSHIP_DRAWING_VML,
288                    &sheet_relationships_drawing_vml,
289                    "",
290                );
291                self.add_sheet_name_space(sheet, SOURCE_RELATIONSHIP);
292                self.add_sheet_legacy_drawing_hf(sheet, r_id)?;
293                (vml_id, drawing_vml, sheet_relationships_drawing_vml)
294            };
295
296        let mut vml = self
297            .vml_drawing_reader(&drawing_vml)?
298            .unwrap_or_else(|| default_header_footer_vml_drawing(sheet_id));
299
300        let shape_id = format!(
301            "{}{}{}",
302            match opts.position {
303                HeaderFooterImagePositionType::Left => "L",
304                HeaderFooterImagePositionType::Center => "C",
305                HeaderFooterImagePositionType::Right => "R",
306            },
307            if opts.is_footer { "F" } else { "H" },
308            if opts.first_page { "FIRST" } else { "" }
309        );
310
311        vml.shape.retain(|s| s.id != shape_id);
312
313        let style = format!(
314            "position:absolute;margin-left:0;margin-top:0;width:{};height:{};z-index:1",
315            opts.width, opts.height
316        );
317        let drawing_vml_rels = format!("xl/drawings/_rels/vmlDrawing{vml_id}.vml.rels");
318        let media = self.add_media(&opts.file, &ext);
319        let media_str = format!("..{}", media.trim_start_matches("xl"));
320        let image_id = self.add_rels(&drawing_vml_rels, SOURCE_RELATIONSHIP_IMAGE, &media_str, "");
321
322        let sp = EncodeShape {
323            image_data: Some(VmlImageData {
324                rel_id: Some(format!("rId{image_id}")),
325                ..Default::default()
326            }),
327            lock: Some(VmlLock {
328                ext: "edit".to_string(),
329                rotation: Some("t".to_string()),
330                ..Default::default()
331            }),
332            ..Default::default()
333        };
334        let inner = build_shape_inner_xml(&sp, &FormCtrlPreset::default(), &VmlOptions::default());
335        vml.shape.push(VmlShape {
336            id: shape_id,
337            spid: Some("_x0000_s1025".to_string()),
338            shape_type: "#_x0000_t75".to_string(),
339            style,
340            inner_xml: inner,
341            ..Default::default()
342        });
343        self.vml_drawing.insert(drawing_vml, vml);
344
345        crate::sheet::set_content_type_part_image_extensions(self)?;
346        self.set_content_type_part_vml_extensions()
347    }
348}
349
350// ------------------------------------------------------------------
351// Internal options bundle
352// ------------------------------------------------------------------
353
354#[derive(Debug, Default, Clone)]
355struct VmlOptions {
356    sheet: String,
357    comment: Option<Comment>,
358    form_control: Option<FormControl>,
359}
360
361impl VmlOptions {
362    fn is_form_control(&self) -> bool {
363        self.form_control
364            .as_ref()
365            .map(|f| f.r#type != FormControlType::Note)
366            .unwrap_or(false)
367    }
368
369    fn form_control(&self) -> &FormControl {
370        self.form_control.as_ref().unwrap()
371    }
372
373    fn comment(&self) -> &Comment {
374        self.comment.as_ref().unwrap()
375    }
376}
377
378// ------------------------------------------------------------------
379// Internal helpers
380// ------------------------------------------------------------------
381
382impl File {
383    fn get_sheet_comments(&self, sheet_xml_path: &str) -> String {
384        let sheet_file = std::path::Path::new(sheet_xml_path)
385            .file_name()
386            .map(|s| s.to_string_lossy().to_string())
387            .unwrap_or_default();
388        let rels = self
389            .rels_reader(&format!("xl/worksheets/_rels/{sheet_file}.rels"))
390            .unwrap_or_default();
391        if let Some(rels) = rels {
392            for rel in &rels.relationships {
393                if rel.r#type == SOURCE_RELATIONSHIP_COMMENTS {
394                    return rel.target.clone();
395                }
396            }
397        }
398        String::new()
399    }
400
401    fn delete_vml_shape(
402        &self,
403        sheet_relationships_drawing_vml: &str,
404        cell: &str,
405        is_comment: bool,
406    ) -> crate::errors::Result<()> {
407        let (col, row) = cell_name_to_coordinates(cell)
408            .map_err(|e| Box::<dyn std::error::Error + Send + Sync>::from(e))?;
409        let vml_id: i32 = sheet_relationships_drawing_vml
410            .trim_start_matches("../drawings/vmlDrawing")
411            .trim_end_matches(".vml")
412            .parse()
413            .unwrap_or(0);
414        let drawing_vml = sheet_relationships_drawing_vml.replace("..", "xl");
415        let mut vml = self
416            .vml_drawing_reader(&drawing_vml)?
417            .unwrap_or_else(|| VmlDrawing {
418                xmlns_v: "urn:schemas-microsoft-com:vml".to_string(),
419                xmlns_o: "urn:schemas-microsoft-com:office:office".to_string(),
420                xmlns_x: "urn:schemas-microsoft-com:office:excel".to_string(),
421                xmlns_mv: Some("http://macVmlSchemaUri".to_string()),
422                shape_layout: Some(VmlShapeLayout {
423                    ext: "edit".to_string(),
424                    idmap: Some(VmlIdmap {
425                        ext: "edit".to_string(),
426                        data: vml_id,
427                    }),
428                }),
429                shape_type: Some(VmlShapeType {
430                    stroke: Some(VmlStroke {
431                        join_style: "miter".to_string(),
432                    }),
433                    v_path: Some(VmlPath {
434                        gradient_shape_ok: Some("t".to_string()),
435                        connect_type: "rect".to_string(),
436                        ..Default::default()
437                    }),
438                    ..Default::default()
439                }),
440                ..Default::default()
441            });
442
443        let cond = |object_type: &str| {
444            if is_comment {
445                object_type == "Note"
446            } else {
447                object_type != "Note"
448            }
449        };
450        let mut removed = false;
451        vml.shape.retain(|sp| {
452            if removed {
453                return true;
454            }
455            let object_type = extract_object_type(&sp.inner_xml);
456            let anchor = extract_anchor(&sp.inner_xml);
457            if cond(&object_type) && !anchor.is_empty() {
458                if let Ok((left_col, top_row)) = extract_anchor_cell(&anchor) {
459                    if left_col == col - 1 && top_row == row - 1 {
460                        removed = true;
461                        return false;
462                    }
463                }
464            }
465            true
466        });
467        self.vml_drawing.insert(drawing_vml, vml);
468        Ok(())
469    }
470
471    fn add_vml_object(&self, opts: VmlOptions) -> crate::errors::Result<()> {
472        let ws = self.work_sheet_reader(&opts.sheet)?;
473        let mut vml_id = self.count_comments() + 1;
474        if opts.is_form_control() {
475            if let Some(fc) = &opts.form_control {
476                if fc.r#type as u8 > FormControlType::ScrollBar as u8 {
477                    return Err(Box::new(ErrParameterInvalid));
478                }
479            }
480            vml_id = self.count_vml_drawing() + 1;
481        }
482        let sheet_id = self.get_sheet_id(&opts.sheet);
483        let drawing_vml = format!("xl/drawings/vmlDrawing{vml_id}.vml");
484        let sheet_relationships_drawing_vml = format!("../drawings/vmlDrawing{vml_id}.vml");
485        let sheet_xml_path = self.get_sheet_xml_path(&opts.sheet).unwrap_or_default();
486        let sheet_rels = format!(
487            "xl/worksheets/_rels/{}.rels",
488            sheet_xml_path.trim_start_matches("xl/worksheets/")
489        );
490
491        let (vml_id, drawing_vml, _sheet_relationships_drawing_vml) = if let Some(ld) =
492            ws.legacy_drawing.as_ref()
493        {
494            let target = self
495                .get_sheet_relationships_target_by_id(&opts.sheet, ld.rid.as_deref().unwrap_or(""));
496            let id: i32 = target
497                .trim_start_matches("../drawings/vmlDrawing")
498                .trim_end_matches(".vml")
499                .parse()
500                .unwrap_or(vml_id);
501            (id, target.replace("..", "xl"), target)
502        } else {
503            let r_id = self.add_rels(
504                &sheet_rels,
505                SOURCE_RELATIONSHIP_DRAWING_VML,
506                &sheet_relationships_drawing_vml,
507                "",
508            );
509            self.add_sheet_name_space(&opts.sheet, SOURCE_RELATIONSHIP);
510            self.add_sheet_legacy_drawing(&opts.sheet, r_id)?;
511            (vml_id, drawing_vml, sheet_relationships_drawing_vml)
512        };
513
514        let opts = prepare_form_ctrl_options(opts);
515        self.add_drawing_vml(sheet_id, &drawing_vml, &opts)?;
516        if !opts.is_form_control() {
517            let comments_xml = format!("xl/comments{vml_id}.xml");
518            self.add_comment_internal(&comments_xml, &opts)?;
519            if self.get_sheet_comments(&sheet_xml_path).is_empty() {
520                let sheet_relationships_comments = format!("../comments{vml_id}.xml");
521                self.add_rels(
522                    &sheet_rels,
523                    SOURCE_RELATIONSHIP_COMMENTS,
524                    &sheet_relationships_comments,
525                    "",
526                );
527            }
528        }
529        self.add_content_type_part(vml_id, "comments")
530    }
531
532    fn add_comment_internal(
533        &self,
534        comments_xml: &str,
535        opts: &VmlOptions,
536    ) -> crate::errors::Result<()> {
537        let mut author = opts.comment().author.clone();
538        if author.is_empty() {
539            author = "Author".to_string();
540        }
541        if count_utf16_string(&author) > MAX_FIELD_LENGTH {
542            author = truncate_utf16_units(&author, MAX_FIELD_LENGTH);
543        }
544        let mut cmts = self.comments_reader(comments_xml)?.unwrap_or_else(|| {
545            let mut c = crate::xml::comments::XlsxComments::default();
546            c.authors.author.push(author.clone());
547            c
548        });
549        if in_str_slice(&cmts.cells, &opts.comment().cell, true) != -1 {
550            return Err(new_add_comment_error(&opts.comment().cell).into());
551        }
552        let mut author_id = in_str_slice(&cmts.authors.author, &author, true);
553        if author_id == -1 {
554            cmts.authors.author.push(author.clone());
555            author_id = cmts.authors.author.len() as i32 - 1;
556        }
557        let default_font = self.get_default_font()?;
558        let mut chars = 0usize;
559        let mut cmt = crate::xml::comments::XlsxComment {
560            r#ref: opts.comment().cell.clone(),
561            author_id,
562            text: crate::xml::comments::XlsxText {
563                r: Vec::new(),
564                ..Default::default()
565            },
566        };
567        if !opts.comment().text.is_empty() {
568            let mut text = opts.comment().text.clone();
569            if count_utf16_string(&text) > TOTAL_CELL_CHARS {
570                text = truncate_utf16_units(&text, TOTAL_CELL_CHARS);
571            }
572            cmt.text.t = Some(text);
573            chars += count_utf16_string(&opts.comment().text);
574        }
575        for run in &opts.comment().paragraph {
576            if chars == TOTAL_CELL_CHARS {
577                break;
578            }
579            let mut text = run.text.clone();
580            if chars + count_utf16_string(&text) > TOTAL_CELL_CHARS {
581                text = truncate_utf16_units(&text, TOTAL_CELL_CHARS - chars);
582            }
583            chars += count_utf16_string(&text);
584            let mut r = XlsxR {
585                r_pr: Some(XlsxRPr {
586                    sz: Some(AttrValFloat { val: Some(9.0) }),
587                    color: Some(XlsxColor {
588                        indexed: Some(81),
589                        ..Default::default()
590                    }),
591                    r_font: Some(AttrValString {
592                        val: Some(default_font.clone()),
593                    }),
594                    family: Some(AttrValInt { val: Some(2) }),
595                    ..Default::default()
596                }),
597                t: Some(XlsxT {
598                    space: Some("preserve".to_string()),
599                    val: text,
600                }),
601            };
602            if let Some(font) = &run.font {
603                r.r_pr = Some(font_to_rpr(font));
604            }
605            cmt.text.r.push(r);
606        }
607        cmts.comment_list.comment.push(cmt);
608        cmts.cells.push(opts.comment().cell.clone());
609        self.comments.insert(comments_xml.to_string(), cmts);
610        Ok(())
611    }
612
613    fn add_drawing_vml(
614        &self,
615        sheet_id: i32,
616        drawing_vml: &str,
617        opts: &VmlOptions,
618    ) -> crate::errors::Result<()> {
619        let cell = &opts.form_control().cell;
620        let (col, row) = cell_name_to_coordinates(cell)
621            .map_err(|e| Box::<dyn std::error::Error + Send + Sync>::from(e))?;
622        let (left_offset, vml_id) = if opts.is_form_control() {
623            (0, 201)
624        } else {
625            (23, 202)
626        };
627        let style = if opts.is_form_control() {
628            "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight"
629                .to_string()
630        } else {
631            "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden"
632                .to_string()
633        };
634        let fc = opts.form_control();
635        let format = &fc.format;
636        let (col_start, row_start, col_end, row_end, _x1, _y1, x2, y2) = self
637            .position_object_pixels(
638                &opts.sheet,
639                col,
640                row,
641                fc.width as i32,
642                fc.height as i32,
643                format,
644            )?;
645        let anchor = format!(
646            "{}, {}, {}, 0, {}, {}, {}, {}",
647            col_start, left_offset, row_start, col_end, x2, row_end, y2
648        );
649
650        let mut vml = self
651            .vml_drawing_reader(drawing_vml)?
652            .unwrap_or_else(|| default_vml_drawing(sheet_id, vml_id));
653
654        let preset = form_ctrl_preset(fc.r#type);
655        let sp = self.add_form_ctrl_shape(&preset, col, row, &anchor, opts)?;
656        let inner_xml = build_shape_inner_xml(&sp, &preset, opts);
657
658        vml.shape.push(VmlShape {
659            id: "_x0000_s1025".to_string(),
660            shape_type: format!("#_x0000_t{vml_id}"),
661            style,
662            button: preset.stroke_button.clone(),
663            filled: preset.filled.clone(),
664            fill_color: preset.fill_color.clone(),
665            stroked: preset.stroked.clone(),
666            stroke_color: preset.stroke_color.clone(),
667            inner_xml,
668            ..Default::default()
669        });
670        self.vml_drawing.insert(drawing_vml.to_string(), vml);
671        Ok(())
672    }
673
674    fn add_form_ctrl_shape(
675        &self,
676        preset: &FormCtrlPreset,
677        col: i32,
678        row: i32,
679        anchor: &str,
680        opts: &VmlOptions,
681    ) -> crate::errors::Result<EncodeShape> {
682        let fc = opts.form_control();
683        let mut sp = EncodeShape {
684            fill: preset.fill.clone(),
685            shadow: preset.shadow.clone(),
686            path: Some(VmlPath {
687                connect_type: "none".to_string(),
688                ..Default::default()
689            }),
690            text_box: Some(VmlTextBox {
691                style: "mso-direction-alt:auto".to_string(),
692                div_style: "text-align:left".to_string(),
693                font: Vec::new(),
694            }),
695            image_data: None,
696            client_data: VmlClientData {
697                object_type: preset.object_type.clone(),
698                anchor: anchor.to_string(),
699                auto_fill: preset.auto_fill.clone(),
700                row: Some(row - 1),
701                column: Some(col - 1),
702                text_h_align: preset.text_h_align.clone(),
703                text_v_align: preset.text_v_align.clone(),
704                no_three_d: preset.no_three_d.clone(),
705                first_button: preset.first_button.clone(),
706                ..Default::default()
707            },
708            lock: None,
709        };
710        if let Some(po) = fc.format.print_object {
711            if !po {
712                sp.client_data.print_object = Some("False".to_string());
713            }
714        }
715        if !fc.format.positioning.is_empty() {
716            let supported = ["absolute", "oneCell", "twoCell"];
717            let idx = in_str_slice(&supported, &fc.format.positioning, true);
718            if idx == -1 {
719                return Err(new_invalid_optional_value(
720                    "Positioning",
721                    &fc.format.positioning,
722                    &["absolute", "oneCell", "twoCell"],
723                )
724                .into());
725            }
726            let idx = idx as usize;
727            sp.client_data.move_with_cells = [Some("".to_string()), None, None][idx].clone();
728            sp.client_data.size_with_cells =
729                [Some("".to_string()), Some("".to_string()), None][idx].clone();
730        }
731        if fc.r#type == FormControlType::Note {
732            sp.client_data.move_with_cells = Some("".to_string());
733            sp.client_data.size_with_cells = Some("".to_string());
734        }
735        if !opts.is_form_control() {
736            return Ok(sp);
737        }
738        if let Some(tb) = sp.text_box.as_mut() {
739            tb.font = form_ctrl_text(fc);
740        }
741        sp.client_data.fmla_macro.clone_from(&fc.macro_name);
742        if (fc.r#type == FormControlType::CheckBox || fc.r#type == FormControlType::OptionButton)
743            && fc.checked
744        {
745            sp.client_data.checked = 1;
746        }
747        if fc.r#type == FormControlType::CheckBox {
748            sp.client_data.fmla_link.clone_from(&fc.cell_link);
749        }
750        self.add_form_ctrl_values(&mut sp, fc)?;
751        Ok(sp)
752    }
753
754    fn add_form_ctrl_values(
755        &self,
756        sp: &mut EncodeShape,
757        fc: &FormControl,
758    ) -> crate::errors::Result<()> {
759        if fc.r#type != FormControlType::ScrollBar && fc.r#type != FormControlType::SpinButton {
760            return Ok(());
761        }
762        if fc.current_val > MAX_FORM_CONTROL_VALUE as u32
763            || fc.min_val > MAX_FORM_CONTROL_VALUE as u32
764            || fc.max_val > MAX_FORM_CONTROL_VALUE as u32
765            || fc.inc_change > MAX_FORM_CONTROL_VALUE as u32
766            || fc.page_change > MAX_FORM_CONTROL_VALUE as u32
767        {
768            return Err(Box::new(crate::errors::ErrFormControlValue));
769        }
770        if !fc.cell_link.is_empty() {
771            cell_name_to_coordinates(&fc.cell_link)
772                .map_err(|e| Box::<dyn std::error::Error + Send + Sync>::from(e))?;
773        }
774        sp.client_data.fmla_link.clone_from(&fc.cell_link);
775        sp.client_data.val = fc.current_val;
776        sp.client_data.min = fc.min_val;
777        sp.client_data.max = fc.max_val;
778        sp.client_data.inc = fc.inc_change;
779        sp.client_data.page = fc.page_change;
780        if fc.r#type == FormControlType::ScrollBar {
781            if fc.horizontally {
782                sp.client_data.horiz = Some("".to_string());
783            }
784            sp.client_data.dx = 15;
785        }
786        Ok(())
787    }
788}
789
790// ------------------------------------------------------------------
791// VML shape inner XML builders
792// ------------------------------------------------------------------
793
794#[derive(Debug, Default, Clone, PartialEq)]
795struct EncodeShape {
796    fill: Option<VmlFill>,
797    shadow: Option<VmlShadow>,
798    path: Option<VmlPath>,
799    text_box: Option<VmlTextBox>,
800    image_data: Option<VmlImageData>,
801    client_data: VmlClientData,
802    lock: Option<VmlLock>,
803}
804
805#[derive(Debug, Default, Clone, PartialEq)]
806struct VmlFill {
807    angle: i32,
808    color2: String,
809    r#type: String,
810    fill: Option<VmlOFill>,
811}
812
813#[derive(Debug, Default, Clone, PartialEq)]
814struct VmlOFill {
815    ext: String,
816    r#type: String,
817}
818
819#[derive(Debug, Default, Clone, PartialEq)]
820struct VmlShadow {
821    on: String,
822    color: String,
823    obscured: String,
824}
825
826#[derive(Debug, Default, Clone, PartialEq)]
827struct VmlTextBox {
828    style: String,
829    div_style: String,
830    font: Vec<VmlFont>,
831}
832
833#[derive(Debug, Default, Clone, PartialEq)]
834struct VmlFont {
835    face: Option<String>,
836    size: Option<u32>,
837    color: Option<String>,
838    content: String,
839}
840
841#[derive(Debug, Default, Clone, PartialEq)]
842struct VmlClientData {
843    object_type: String,
844    anchor: String,
845    move_with_cells: Option<String>,
846    size_with_cells: Option<String>,
847    locked: Option<String>,
848    print_object: Option<String>,
849    auto_fill: String,
850    fmla_macro: String,
851    text_h_align: String,
852    text_v_align: String,
853    row: Option<i32>,
854    column: Option<i32>,
855    checked: u32,
856    fmla_link: String,
857    no_three_d: Option<String>,
858    first_button: Option<String>,
859    val: u32,
860    min: u32,
861    max: u32,
862    inc: u32,
863    page: u32,
864    horiz: Option<String>,
865    dx: u32,
866}
867
868fn build_shape_inner_xml(sp: &EncodeShape, preset: &FormCtrlPreset, opts: &VmlOptions) -> String {
869    let mut parts = Vec::new();
870    if let Some(fill) = &sp.fill {
871        let mut s = format!(
872            "<v:fill angle=\"{}\" color2=\"{}\"",
873            fill.angle, fill.color2
874        );
875        if !fill.r#type.is_empty() {
876            s.push_str(&format!(" type=\"{}\"", fill.r#type));
877        }
878        if let Some(of) = &fill.fill {
879            s.push_str(&format!(
880                ">\n        <o:fill ext=\"{}\" type=\"{}\"/>\n      </v:fill>",
881                of.ext, of.r#type
882            ));
883        } else {
884            s.push_str("/>");
885        }
886        parts.push(s);
887    }
888    if let Some(shadow) = &sp.shadow {
889        parts.push(format!(
890            "<v:shadow on=\"{}\" color=\"{}\" obscured=\"{}\"/>",
891            shadow.on, shadow.color, shadow.obscured
892        ));
893    }
894    if let Some(path) = &sp.path {
895        parts.push(format!(
896            "<v:path o:connecttype=\"{}\"{}/>",
897            path.connect_type,
898            path.gradient_shape_ok
899                .as_ref()
900                .map(|v| format!(" gradientshapeok=\"{v}\""))
901                .unwrap_or_default()
902        ));
903    }
904    if let Some(tb) = &sp.text_box {
905        let mut font_xml = String::new();
906        for font in &tb.font {
907            let mut start = String::from("<font");
908            if let Some(face) = &font.face {
909                start.push_str(&format!(" face=\"{}\"", html_escape(face)));
910            }
911            if let Some(size) = font.size {
912                start.push_str(&format!(" size=\"{}\"", size));
913            }
914            if let Some(color) = &font.color {
915                start.push_str(&format!(" color=\"{}\"", html_escape(color)));
916            }
917            start.push('>');
918            font_xml.push_str(&start);
919            font_xml.push_str(&font.content);
920            font_xml.push_str("</font>");
921        }
922        parts.push(format!(
923            "<v:textbox style=\"{}\">\n        <div style=\"{}\">{}</div>\n      </v:textbox>",
924            tb.style, tb.div_style, font_xml
925        ));
926    }
927    if let Some(image_data) = &sp.image_data {
928        parts.push(image_data.to_xml_string());
929    }
930    parts.push(build_client_data_xml(&sp.client_data, preset, opts));
931    if let Some(lock) = &sp.lock {
932        parts.push(lock.to_xml_string());
933    }
934    parts.join("\n      ")
935}
936
937fn build_client_data_xml(
938    cd: &VmlClientData,
939    _preset: &FormCtrlPreset,
940    _opts: &VmlOptions,
941) -> String {
942    let mut s = format!("<x:ClientData ObjectType=\"{}\"", cd.object_type);
943    s.push('>');
944    if let Some(v) = &cd.move_with_cells {
945        s.push_str(&format!(
946            "\n        <x:MoveWithCells>{}</x:MoveWithCells>",
947            v
948        ));
949    }
950    if let Some(v) = &cd.size_with_cells {
951        s.push_str(&format!(
952            "\n        <x:SizeWithCells>{}</x:SizeWithCells>",
953            v
954        ));
955    }
956    s.push_str(&format!("\n        <x:Anchor>{}</x:Anchor>", cd.anchor));
957    if let Some(v) = &cd.locked {
958        s.push_str(&format!("\n        <x:Locked>{}</x:Locked>", v));
959    }
960    if let Some(v) = &cd.print_object {
961        s.push_str(&format!("\n        <x:PrintObject>{}</x:PrintObject>", v));
962    }
963    if !cd.auto_fill.is_empty() {
964        s.push_str(&format!(
965            "\n        <x:AutoFill>{}</x:AutoFill>",
966            cd.auto_fill
967        ));
968    }
969    if !cd.fmla_macro.is_empty() {
970        s.push_str(&format!(
971            "\n        <x:FmlaMacro>{}</x:FmlaMacro>",
972            cd.fmla_macro
973        ));
974    }
975    if !cd.text_h_align.is_empty() {
976        s.push_str(&format!(
977            "\n        <x:TextHAlign>{}</x:TextHAlign>",
978            cd.text_h_align
979        ));
980    }
981    if !cd.text_v_align.is_empty() {
982        s.push_str(&format!(
983            "\n        <x:TextVAlign>{}</x:TextVAlign>",
984            cd.text_v_align
985        ));
986    }
987    if let Some(v) = cd.row {
988        s.push_str(&format!("\n        <x:Row>{}</x:Row>", v));
989    }
990    if let Some(v) = cd.column {
991        s.push_str(&format!("\n        <x:Column>{}</x:Column>", v));
992    }
993    if cd.checked != 0 {
994        s.push_str(&format!("\n        <x:Checked>{}</x:Checked>", cd.checked));
995    }
996    if !cd.fmla_link.is_empty() {
997        s.push_str(&format!(
998            "\n        <x:FmlaLink>{}</x:FmlaLink>",
999            cd.fmla_link
1000        ));
1001    }
1002    if let Some(v) = &cd.no_three_d {
1003        s.push_str(&format!("\n        <x:NoThreeD>{}</x:NoThreeD>", v));
1004    }
1005    if let Some(v) = &cd.first_button {
1006        s.push_str(&format!("\n        <x:FirstButton>{}</x:FirstButton>", v));
1007    }
1008    if cd.val != 0 {
1009        s.push_str(&format!("\n        <x:Val>{}</x:Val>", cd.val));
1010    }
1011    if cd.min != 0 {
1012        s.push_str(&format!("\n        <x:Min>{}</x:Min>", cd.min));
1013    }
1014    if cd.max != 0 {
1015        s.push_str(&format!("\n        <x:Max>{}</x:Max>", cd.max));
1016    }
1017    if cd.inc != 0 {
1018        s.push_str(&format!("\n        <x:Inc>{}</x:Inc>", cd.inc));
1019    }
1020    if cd.page != 0 {
1021        s.push_str(&format!("\n        <x:Page>{}</x:Page>", cd.page));
1022    }
1023    if let Some(v) = &cd.horiz {
1024        s.push_str(&format!("\n        <x:Horiz>{}</x:Horiz>", v));
1025    }
1026    if cd.dx != 0 {
1027        s.push_str(&format!("\n        <x:Dx>{}</x:Dx>", cd.dx));
1028    }
1029    s.push_str("\n      </x:ClientData>");
1030    s
1031}
1032
1033fn default_vml_drawing(sheet_id: i32, vml_id: i32) -> VmlDrawing {
1034    VmlDrawing {
1035        xmlns_v: "urn:schemas-microsoft-com:vml".to_string(),
1036        xmlns_o: "urn:schemas-microsoft-com:office:office".to_string(),
1037        xmlns_x: "urn:schemas-microsoft-com:office:excel".to_string(),
1038        xmlns_mv: Some("http://macVmlSchemaUri".to_string()),
1039        shape_layout: Some(VmlShapeLayout {
1040            ext: "edit".to_string(),
1041            idmap: Some(VmlIdmap {
1042                ext: "edit".to_string(),
1043                data: sheet_id,
1044            }),
1045        }),
1046        shape_type: Some(VmlShapeType {
1047            id: format!("_x0000_t{vml_id}"),
1048            coord_size: "21600,21600".to_string(),
1049            spt: 202,
1050            path: "m0,0l0,21600,21600,21600,21600,0xe".to_string(),
1051            stroke: Some(VmlStroke {
1052                join_style: "miter".to_string(),
1053            }),
1054            v_path: Some(VmlPath {
1055                gradient_shape_ok: Some("t".to_string()),
1056                connect_type: "rect".to_string(),
1057                ..Default::default()
1058            }),
1059            ..Default::default()
1060        }),
1061        ..Default::default()
1062    }
1063}
1064
1065fn default_header_footer_vml_drawing(sheet_id: i32) -> VmlDrawing {
1066    VmlDrawing {
1067        xmlns_v: "urn:schemas-microsoft-com:vml".to_string(),
1068        xmlns_o: "urn:schemas-microsoft-com:office:office".to_string(),
1069        xmlns_x: "urn:schemas-microsoft-com:office:excel".to_string(),
1070        shape_layout: Some(VmlShapeLayout {
1071            ext: "edit".to_string(),
1072            idmap: Some(VmlIdmap {
1073                ext: "edit".to_string(),
1074                data: sheet_id,
1075            }),
1076        }),
1077        shape_type: Some(VmlShapeType {
1078            id: "_x0000_t75".to_string(),
1079            coord_size: "21600,21600".to_string(),
1080            spt: 75,
1081            prefer_relative: Some("t".to_string()),
1082            path: "m@4@5l@4@11@9@11@9@5xe".to_string(),
1083            filled: Some("f".to_string()),
1084            stroked: Some("f".to_string()),
1085            stroke: Some(VmlStroke {
1086                join_style: "miter".to_string(),
1087            }),
1088            formulas: Some(VmlFormulas {
1089                formula: vec![
1090                    "if lineDrawn pixelLineWidth 0",
1091                    "sum @0 1 0",
1092                    "sum 0 0 @1",
1093                    "prod @2 1 2",
1094                    "prod @3 21600 pixelWidth",
1095                    "prod @3 21600 pixelHeight",
1096                    "sum @0 0 1",
1097                    "prod @6 1 2",
1098                    "prod @7 21600 pixelWidth",
1099                    "sum @8 21600 0",
1100                    "prod @7 21600 pixelHeight",
1101                    "sum @10 21600 0",
1102                ]
1103                .into_iter()
1104                .map(|e| VmlFormula {
1105                    equation: e.to_string(),
1106                })
1107                .collect(),
1108            }),
1109            v_path: Some(VmlPath {
1110                extrusion_ok: Some("f".to_string()),
1111                gradient_shape_ok: Some("t".to_string()),
1112                connect_type: "rect".to_string(),
1113            }),
1114            lock: Some(VmlLock {
1115                ext: "edit".to_string(),
1116                aspect_ratio: Some("t".to_string()),
1117                ..Default::default()
1118            }),
1119            ..Default::default()
1120        }),
1121        ..Default::default()
1122    }
1123}
1124
1125// ------------------------------------------------------------------
1126// Form control presets
1127// ------------------------------------------------------------------
1128
1129#[derive(Debug, Default, Clone, PartialEq)]
1130struct FormCtrlPreset {
1131    object_type: String,
1132    auto_fill: String,
1133    filled: Option<String>,
1134    fill_color: Option<String>,
1135    stroked: Option<String>,
1136    stroke_color: Option<String>,
1137    stroke_button: Option<String>,
1138    fill: Option<VmlFill>,
1139    shadow: Option<VmlShadow>,
1140    text_h_align: String,
1141    text_v_align: String,
1142    no_three_d: Option<String>,
1143    first_button: Option<String>,
1144}
1145
1146fn form_ctrl_preset(t: FormControlType) -> FormCtrlPreset {
1147    let none = FormCtrlPreset {
1148        object_type: String::new(),
1149        auto_fill: String::new(),
1150        filled: None,
1151        fill_color: None,
1152        stroked: None,
1153        stroke_color: None,
1154        stroke_button: None,
1155        fill: None,
1156        shadow: None,
1157        text_h_align: String::new(),
1158        text_v_align: String::new(),
1159        no_three_d: None,
1160        first_button: None,
1161    };
1162    match t {
1163        FormControlType::Note => FormCtrlPreset {
1164            object_type: "Note".to_string(),
1165            auto_fill: "True".to_string(),
1166            fill_color: Some("#FBF6D6".to_string()),
1167            stroke_color: Some("#EDEAA1".to_string()),
1168            fill: Some(VmlFill {
1169                angle: -180,
1170                color2: "#FBFE82".to_string(),
1171                r#type: "gradient".to_string(),
1172                fill: Some(VmlOFill {
1173                    ext: "view".to_string(),
1174                    r#type: "gradientUnscaled".to_string(),
1175                }),
1176            }),
1177            shadow: Some(VmlShadow {
1178                on: "t".to_string(),
1179                color: "black".to_string(),
1180                obscured: "t".to_string(),
1181            }),
1182            ..none
1183        },
1184        FormControlType::Button => FormCtrlPreset {
1185            object_type: "Button".to_string(),
1186            auto_fill: "True".to_string(),
1187            fill_color: Some("buttonFace [67]".to_string()),
1188            stroke_color: Some("windowText [64]".to_string()),
1189            stroke_button: Some("t".to_string()),
1190            fill: Some(VmlFill {
1191                angle: -180,
1192                color2: "buttonFace [67]".to_string(),
1193                r#type: "gradient".to_string(),
1194                fill: Some(VmlOFill {
1195                    ext: "view".to_string(),
1196                    r#type: "gradientUnscaled".to_string(),
1197                }),
1198            }),
1199            text_h_align: "Center".to_string(),
1200            text_v_align: "Center".to_string(),
1201            ..none
1202        },
1203        FormControlType::CheckBox => FormCtrlPreset {
1204            object_type: "Checkbox".to_string(),
1205            auto_fill: "True".to_string(),
1206            filled: Some("f".to_string()),
1207            fill_color: Some("window [65]".to_string()),
1208            stroked: Some("f".to_string()),
1209            stroke_color: Some("windowText [64]".to_string()),
1210            no_three_d: Some("".to_string()),
1211            text_v_align: "Center".to_string(),
1212            ..none
1213        },
1214        FormControlType::GroupBox => FormCtrlPreset {
1215            object_type: "GBox".to_string(),
1216            auto_fill: "False".to_string(),
1217            filled: Some("f".to_string()),
1218            stroked: Some("f".to_string()),
1219            stroke_color: Some("windowText [64]".to_string()),
1220            no_three_d: Some("".to_string()),
1221            ..none
1222        },
1223        FormControlType::Label => FormCtrlPreset {
1224            object_type: "Label".to_string(),
1225            auto_fill: "False".to_string(),
1226            filled: Some("f".to_string()),
1227            fill_color: Some("window [65]".to_string()),
1228            stroked: Some("f".to_string()),
1229            stroke_color: Some("windowText [64]".to_string()),
1230            ..none
1231        },
1232        FormControlType::OptionButton => FormCtrlPreset {
1233            object_type: "Radio".to_string(),
1234            auto_fill: "False".to_string(),
1235            filled: Some("f".to_string()),
1236            fill_color: Some("window [65]".to_string()),
1237            stroked: Some("f".to_string()),
1238            stroke_color: Some("windowText [64]".to_string()),
1239            no_three_d: Some("".to_string()),
1240            first_button: Some("".to_string()),
1241            text_v_align: "Center".to_string(),
1242            ..none
1243        },
1244        FormControlType::ScrollBar => FormCtrlPreset {
1245            object_type: "Scroll".to_string(),
1246            stroked: Some("f".to_string()),
1247            stroke_color: Some("windowText [64]".to_string()),
1248            ..none
1249        },
1250        FormControlType::SpinButton => FormCtrlPreset {
1251            object_type: "Spin".to_string(),
1252            auto_fill: "False".to_string(),
1253            stroked: Some("f".to_string()),
1254            stroke_color: Some("windowText [64]".to_string()),
1255            ..none
1256        },
1257    }
1258}
1259
1260fn prepare_form_ctrl_options(mut opts: VmlOptions) -> VmlOptions {
1261    if let Some(fc) = opts.form_control.as_mut() {
1262        if fc.format.scale_x == 0.0 {
1263            fc.format.scale_x = 1.0;
1264        }
1265        if fc.format.scale_y == 0.0 {
1266            fc.format.scale_y = 1.0;
1267        }
1268        if fc.width == 0 {
1269            fc.width = 140;
1270        }
1271        if fc.height == 0 {
1272            fc.height = 60;
1273        }
1274    }
1275    opts
1276}
1277
1278fn form_ctrl_text(fc: &FormControl) -> Vec<VmlFont> {
1279    let mut fonts = Vec::new();
1280    if !fc.text.is_empty() {
1281        fonts.push(VmlFont {
1282            content: fc.text.clone(),
1283            ..Default::default()
1284        });
1285    }
1286    for run in &fc.paragraph {
1287        let mut content = format!("{}<br></br>\r\n", run.text);
1288        let mut face = None;
1289        let mut size = None;
1290        let mut color = None;
1291        if let Some(font) = &run.font {
1292            face.clone_from(&font.name);
1293            if let Some(sz) = font.size {
1294                size = Some((sz * 20.0) as u32);
1295            }
1296            color.clone_from(&font.color);
1297            let mut color_str = color.clone().unwrap_or_default();
1298            if !color_str.starts_with('#') && !color_str.is_empty() {
1299                color_str = format!("#{color_str}");
1300            }
1301            color = Some(color_str);
1302            if font.underline == Some("single".to_string()) {
1303                content = format!("<u>{content}</u>");
1304            } else if font.underline == Some("double".to_string()) {
1305                content = format!("<u class=\"font1\">{content}</u>");
1306            }
1307            if font.italic == Some(true) {
1308                content = format!("<i>{content}</i>");
1309            }
1310            if font.bold == Some(true) {
1311                content = format!("<b>{content}</b>");
1312            }
1313        }
1314        fonts.push(VmlFont {
1315            face,
1316            size,
1317            color,
1318            content,
1319        });
1320    }
1321    fonts
1322}
1323
1324// ------------------------------------------------------------------
1325// Extraction helpers
1326// ------------------------------------------------------------------
1327
1328/// Wrap a `<v:shape>` inner XML fragment in a synthetic root so that it can be
1329/// parsed with quick-xml even though the namespace declarations live on the
1330/// drawing root.
1331fn wrap_vml_inner_xml(inner_xml: &str) -> String {
1332    format!(
1333        r#"<shape xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">{}</shape>"#,
1334        inner_xml
1335    )
1336}
1337
1338fn local_name(name: &[u8]) -> &[u8] {
1339    if let Some(pos) = name.iter().rposition(|&b| b == b':') {
1340        &name[pos + 1..]
1341    } else {
1342        name
1343    }
1344}
1345
1346#[derive(Debug, Default)]
1347struct ParsedClientData {
1348    object_type: String,
1349    anchor: String,
1350    fmla_macro: String,
1351    checked: u32,
1352    fmla_link: String,
1353    val: u32,
1354    min: u32,
1355    max: u32,
1356    inc: u32,
1357    page: u32,
1358    horiz: bool,
1359}
1360
1361fn parse_client_data(inner_xml: &str) -> Option<ParsedClientData> {
1362    let wrapped = wrap_vml_inner_xml(inner_xml);
1363    let mut reader = Reader::from_str(&wrapped);
1364    reader.config_mut().trim_text(true);
1365    let mut buf = Vec::new();
1366    let mut cd = ParsedClientData::default();
1367    let mut in_client_data = false;
1368    let mut current_tag = Vec::new();
1369    let mut current_text = String::new();
1370    let mut found = false;
1371
1372    loop {
1373        match reader.read_event_into(&mut buf) {
1374            Ok(Event::Start(e)) => {
1375                let name = e.name();
1376                let local = local_name(name.as_ref());
1377                if local == b"ClientData" {
1378                    in_client_data = true;
1379                    found = true;
1380                    for attr in e.attributes().flatten() {
1381                        let key = String::from_utf8_lossy(attr.key.as_ref());
1382                        if key == "ObjectType" {
1383                            cd.object_type = String::from_utf8_lossy(&attr.value).to_string();
1384                        }
1385                    }
1386                } else if in_client_data {
1387                    current_tag = local.to_vec();
1388                    current_text.clear();
1389                }
1390            }
1391            Ok(Event::Empty(e)) => {
1392                let name = e.name();
1393                let local = local_name(name.as_ref());
1394                if local == b"ClientData" {
1395                    found = true;
1396                    for attr in e.attributes().flatten() {
1397                        let key = String::from_utf8_lossy(attr.key.as_ref());
1398                        if key == "ObjectType" {
1399                            cd.object_type = String::from_utf8_lossy(&attr.value).to_string();
1400                        }
1401                    }
1402                } else if in_client_data && local == b"Horiz" {
1403                    cd.horiz = true;
1404                }
1405            }
1406            Ok(Event::Text(e)) => {
1407                if in_client_data && !current_tag.is_empty() {
1408                    let t = e.unescape().unwrap_or(Cow::Borrowed(""));
1409                    current_text.push_str(&t);
1410                }
1411            }
1412            Ok(Event::End(e)) => {
1413                let name = e.name();
1414                let local = local_name(name.as_ref());
1415                if local == b"ClientData" {
1416                    in_client_data = false;
1417                } else if in_client_data {
1418                    let text = current_text.trim();
1419                    match local {
1420                        b"Anchor" => cd.anchor = text.to_string(),
1421                        b"FmlaMacro" => cd.fmla_macro = text.to_string(),
1422                        b"Checked" => cd.checked = text.parse().unwrap_or(0),
1423                        b"FmlaLink" => cd.fmla_link = text.to_string(),
1424                        b"Val" => cd.val = text.parse().unwrap_or(0),
1425                        b"Min" => cd.min = text.parse().unwrap_or(0),
1426                        b"Max" => cd.max = text.parse().unwrap_or(0),
1427                        b"Inc" => cd.inc = text.parse().unwrap_or(0),
1428                        b"Page" => cd.page = text.parse().unwrap_or(0),
1429                        b"Horiz" => cd.horiz = true,
1430                        _ => {}
1431                    }
1432                    current_tag.clear();
1433                }
1434            }
1435            Ok(Event::Eof) => break,
1436            Err(_) => break,
1437            _ => {}
1438        }
1439        buf.clear();
1440    }
1441
1442    if found { Some(cd) } else { None }
1443}
1444
1445fn extract_object_type(inner_xml: &str) -> String {
1446    parse_client_data(inner_xml)
1447        .map(|cd| cd.object_type)
1448        .unwrap_or_default()
1449}
1450
1451fn extract_anchor(inner_xml: &str) -> String {
1452    parse_client_data(inner_xml)
1453        .map(|cd| cd.anchor)
1454        .unwrap_or_default()
1455}
1456
1457fn extract_anchor_cell(
1458    anchor: &str,
1459) -> Result<(i32, i32), Box<dyn std::error::Error + Send + Sync>> {
1460    let parts: Vec<&str> = anchor.split(',').collect();
1461    if parts.len() != 8 {
1462        return Err(Box::new(ErrParameterInvalid));
1463    }
1464    let left_col = parts[0].trim().parse::<i32>()?;
1465    let top_row = parts[2].trim().parse::<i32>()?;
1466    Ok((left_col, top_row))
1467}
1468
1469fn extract_form_control(inner_xml: &str) -> crate::errors::Result<FormControl> {
1470    let mut fc = FormControl::default();
1471    let cd = parse_client_data(inner_xml).unwrap_or_default();
1472    for (t, preset) in [
1473        (
1474            FormControlType::Note,
1475            form_ctrl_preset(FormControlType::Note),
1476        ),
1477        (
1478            FormControlType::Button,
1479            form_ctrl_preset(FormControlType::Button),
1480        ),
1481        (
1482            FormControlType::CheckBox,
1483            form_ctrl_preset(FormControlType::CheckBox),
1484        ),
1485        (
1486            FormControlType::GroupBox,
1487            form_ctrl_preset(FormControlType::GroupBox),
1488        ),
1489        (
1490            FormControlType::Label,
1491            form_ctrl_preset(FormControlType::Label),
1492        ),
1493        (
1494            FormControlType::OptionButton,
1495            form_ctrl_preset(FormControlType::OptionButton),
1496        ),
1497        (
1498            FormControlType::ScrollBar,
1499            form_ctrl_preset(FormControlType::ScrollBar),
1500        ),
1501        (
1502            FormControlType::SpinButton,
1503            form_ctrl_preset(FormControlType::SpinButton),
1504        ),
1505    ] {
1506        if preset.object_type == cd.object_type && !cd.anchor.is_empty() {
1507            fc.r#type = t;
1508            break;
1509        }
1510    }
1511    if fc.r#type == FormControlType::Note {
1512        return Ok(fc);
1513    }
1514    fc.paragraph = extract_vml_fonts(inner_xml);
1515    if !fc.paragraph.is_empty() && fc.paragraph[0].font.is_none() {
1516        fc.text.clone_from(&fc.paragraph[0].text);
1517        fc.paragraph.remove(0);
1518    }
1519    let (col, row) = extract_anchor_cell(&cd.anchor)?;
1520    fc.cell = coordinates_to_cell_name(col + 1, row + 1, false)?;
1521    fc.macro_name = cd.fmla_macro;
1522    fc.checked = cd.checked != 0;
1523    fc.cell_link = cd.fmla_link;
1524    fc.current_val = cd.val;
1525    fc.min_val = cd.min;
1526    fc.max_val = cd.max;
1527    fc.inc_change = cd.inc;
1528    fc.page_change = cd.page;
1529    fc.horizontally = cd.horiz;
1530    Ok(fc)
1531}
1532
1533fn extract_vml_fonts(inner_xml: &str) -> Vec<RichTextRun> {
1534    parse_vml_textbox(inner_xml)
1535}
1536
1537#[derive(Debug, Default)]
1538struct FontParseState {
1539    text: String,
1540    face: Option<String>,
1541    size: Option<u32>,
1542    color: Option<String>,
1543    bold: bool,
1544    italic: bool,
1545    underline: Option<String>,
1546}
1547
1548fn parse_vml_textbox(inner_xml: &str) -> Vec<RichTextRun> {
1549    let wrapped = wrap_vml_inner_xml(inner_xml);
1550    let mut reader = Reader::from_str(&wrapped);
1551    reader.config_mut().trim_text(true);
1552    let mut buf = Vec::new();
1553    let mut runs = Vec::new();
1554    let mut depth = 0usize;
1555    let mut current: Option<FontParseState> = None;
1556
1557    loop {
1558        match reader.read_event_into(&mut buf) {
1559            Ok(Event::Start(e)) => {
1560                let name = e.name();
1561                let local = local_name(name.as_ref());
1562                match local {
1563                    b"textbox" if depth == 0 => depth = 1,
1564                    b"div" if depth == 1 => depth = 2,
1565                    b"font" if depth == 2 => {
1566                        depth = 3;
1567                        let mut state = FontParseState::default();
1568                        for attr in e.attributes().flatten() {
1569                            let key = String::from_utf8_lossy(attr.key.as_ref());
1570                            let value = String::from_utf8_lossy(&attr.value).to_string();
1571                            if key == "face" || key.ends_with(":face") {
1572                                state.face = Some(value);
1573                            } else if key == "size" || key.ends_with(":size") {
1574                                state.size = value.parse().ok();
1575                            } else if key == "color" || key.ends_with(":color") {
1576                                state.color = Some(value);
1577                            }
1578                        }
1579                        current = Some(state);
1580                    }
1581                    _ if depth >= 3 => {
1582                        match local {
1583                            b"b" | b"strong" => {
1584                                if let Some(c) = current.as_mut() {
1585                                    c.bold = true;
1586                                }
1587                            }
1588                            b"i" | b"em" => {
1589                                if let Some(c) = current.as_mut() {
1590                                    c.italic = true;
1591                                }
1592                            }
1593                            b"u" => {
1594                                let class = e
1595                                    .attributes()
1596                                    .flatten()
1597                                    .find(|a| {
1598                                        let k = String::from_utf8_lossy(a.key.as_ref());
1599                                        k == "class" || k.ends_with(":class")
1600                                    })
1601                                    .map(|a| String::from_utf8_lossy(&a.value).to_string());
1602                                let kind = if class.as_deref() == Some("font1") {
1603                                    "double"
1604                                } else {
1605                                    "single"
1606                                };
1607                                if let Some(c) = current.as_mut() {
1608                                    c.underline = Some(kind.to_string());
1609                                }
1610                            }
1611                            _ => {}
1612                        }
1613                        depth += 1;
1614                    }
1615                    _ => {}
1616                }
1617            }
1618            Ok(Event::Empty(e)) => {
1619                let name = e.name();
1620                let local = local_name(name.as_ref());
1621                if depth >= 3 && local == b"br" {
1622                    // Drop <br> elements; surrounding text is preserved.
1623                }
1624            }
1625            Ok(Event::Text(e)) => {
1626                if depth >= 3 {
1627                    if let Some(c) = current.as_mut() {
1628                        let t = e.unescape().unwrap_or(Cow::Borrowed(""));
1629                        c.text.push_str(&t);
1630                    }
1631                }
1632            }
1633            Ok(Event::End(e)) => {
1634                let name = e.name();
1635                let local = local_name(name.as_ref());
1636                match local {
1637                    b"textbox" if depth == 1 => depth = 0,
1638                    b"div" if depth == 2 => depth = 1,
1639                    b"font" if depth == 3 => {
1640                        depth = 2;
1641                        if let Some(state) = current.take() {
1642                            let mut run = RichTextRun::default();
1643                            run.text = state.text;
1644                            let mut font = crate::styles::Font::default();
1645                            if let Some(face) = state.face {
1646                                font.name = Some(face);
1647                            }
1648                            if let Some(size) = state.size {
1649                                font.size = Some(size as f64 / 20.0);
1650                            }
1651                            if let Some(color) = state.color {
1652                                font.color = Some(color.trim_start_matches('#').to_string());
1653                            }
1654                            if state.bold {
1655                                font.bold = Some(true);
1656                            }
1657                            if state.italic {
1658                                font.italic = Some(true);
1659                            }
1660                            if let Some(u) = state.underline {
1661                                font.underline = Some(u);
1662                            }
1663                            if font.name.is_some()
1664                                || font.size.is_some()
1665                                || font.color.is_some()
1666                                || font.bold == Some(true)
1667                                || font.italic == Some(true)
1668                                || font.underline.is_some()
1669                            {
1670                                run.font = Some(font);
1671                            }
1672                            runs.push(run);
1673                        }
1674                    }
1675                    _ if depth > 3 => depth -= 1,
1676                    _ => {}
1677                }
1678            }
1679            Ok(Event::Eof) => break,
1680            Ok(_) => {}
1681            Err(_) => break,
1682        }
1683        buf.clear();
1684    }
1685
1686    runs
1687}
1688
1689// ------------------------------------------------------------------
1690// Font / run property conversion
1691// ------------------------------------------------------------------
1692
1693fn font_to_rpr(font: &crate::styles::Font) -> XlsxRPr {
1694    let mut rpr = XlsxRPr::default();
1695    rpr.family = Some(AttrValInt { val: Some(2) });
1696    if let Some(name) = &font.name {
1697        rpr.r_font = Some(AttrValString {
1698            val: Some(name.clone()),
1699        });
1700    }
1701    if let Some(size) = font.size {
1702        rpr.sz = Some(AttrValFloat { val: Some(size) });
1703    }
1704    if font.bold == Some(true) {
1705        rpr.b = Some(AttrValBool { val: Some(true) });
1706    }
1707    if font.italic == Some(true) {
1708        rpr.i = Some(AttrValBool { val: Some(true) });
1709    }
1710    if font.strike == Some(true) {
1711        rpr.strike = Some(AttrValBool { val: Some(true) });
1712    }
1713    if let Some(u) = &font.underline {
1714        rpr.u = Some(AttrValString {
1715            val: Some(u.clone()),
1716        });
1717    }
1718    if let Some(color) = &font.color {
1719        let mut c = color.clone();
1720        if !c.starts_with("FF") && c.len() == 6 {
1721            c = format!("FF{c}");
1722        }
1723        rpr.color = Some(XlsxColor {
1724            rgb: Some(c),
1725            ..Default::default()
1726        });
1727    }
1728    rpr
1729}
1730
1731fn rpr_to_font(rpr: &XlsxRPr) -> crate::styles::Font {
1732    let mut font = crate::styles::Font::default();
1733    if let Some(name) = rpr.r_font.as_ref().and_then(|a| a.val.clone()) {
1734        font.name = Some(name);
1735    }
1736    if let Some(sz) = rpr.sz.as_ref().and_then(|a| a.val) {
1737        font.size = Some(sz);
1738    }
1739    if rpr.b.as_ref().and_then(|a| a.val).unwrap_or(false) {
1740        font.bold = Some(true);
1741    }
1742    if rpr.i.as_ref().and_then(|a| a.val).unwrap_or(false) {
1743        font.italic = Some(true);
1744    }
1745    if rpr.strike.as_ref().and_then(|a| a.val).unwrap_or(false) {
1746        font.strike = Some(true);
1747    }
1748    if let Some(u) = rpr.u.as_ref().and_then(|a| a.val.clone()) {
1749        font.underline = Some(u);
1750    }
1751    if let Some(color) = rpr.color.as_ref().and_then(|c| c.rgb.clone()) {
1752        font.color = Some(color.trim_start_matches("FF").to_string());
1753    }
1754    font
1755}
1756
1757fn supported_image_types() -> HashMap<String, String> {
1758    [
1759        (".bmp".to_string(), ".bmp".to_string()),
1760        (".emf".to_string(), ".emf".to_string()),
1761        (".emz".to_string(), ".emz".to_string()),
1762        (".gif".to_string(), ".gif".to_string()),
1763        (".ico".to_string(), ".ico".to_string()),
1764        (".jpeg".to_string(), ".jpeg".to_string()),
1765        (".jpg".to_string(), ".jpeg".to_string()),
1766        (".png".to_string(), ".png".to_string()),
1767        (".svg".to_string(), ".svg".to_string()),
1768        (".tif".to_string(), ".tiff".to_string()),
1769        (".tiff".to_string(), ".tiff".to_string()),
1770        (".wmf".to_string(), ".wmf".to_string()),
1771        (".wmz".to_string(), ".wmz".to_string()),
1772    ]
1773    .into_iter()
1774    .collect()
1775}
1776
1777fn html_escape(s: &str) -> String {
1778    s.replace('&', "&amp;")
1779        .replace('<', "&lt;")
1780        .replace('>', "&gt;")
1781        .replace('"', "&quot;")
1782}
1783
1784// ------------------------------------------------------------------
1785// Tests
1786// ------------------------------------------------------------------
1787
1788#[cfg(test)]
1789mod tests {
1790    use super::*;
1791    use crate::xml::common::RichTextRun;
1792    use crate::{File, Options};
1793
1794    #[test]
1795    fn comment_round_trip() {
1796        let mut f = File::new_with_options(Options::default());
1797        f.add_comment(
1798            "Sheet1",
1799            Comment {
1800                cell: "A1".to_string(),
1801                author: "Excelize".to_string(),
1802                text: "Hello comment".to_string(),
1803                ..Default::default()
1804            },
1805        )
1806        .unwrap();
1807
1808        let comments = f.get_comments("Sheet1").unwrap();
1809        assert_eq!(comments.len(), 1);
1810        assert_eq!(comments[0].cell, "A1");
1811        assert_eq!(comments[0].author, "Excelize");
1812        assert_eq!(comments[0].text, "Hello comment");
1813
1814        let tmp = std::env::temp_dir().join("excelize_rust_comment.xlsx");
1815        let _ = std::fs::remove_file(&tmp);
1816        f.save_as(tmp.to_str().unwrap()).unwrap();
1817
1818        let f2 = File::open_file(tmp.to_str().unwrap(), Options::default()).unwrap();
1819        let comments2 = f2.get_comments("Sheet1").unwrap();
1820        assert_eq!(comments2.len(), 1);
1821        assert_eq!(comments2[0].cell, "A1");
1822        assert_eq!(comments2[0].text, "Hello comment");
1823        let _ = std::fs::remove_file(&tmp);
1824    }
1825
1826    #[test]
1827    fn rich_text_comment_round_trip() {
1828        let f = File::new_with_options(Options::default());
1829        f.add_comment(
1830            "Sheet1",
1831            Comment {
1832                cell: "B2".to_string(),
1833                author: "Author".to_string(),
1834                paragraph: vec![
1835                    RichTextRun {
1836                        text: "Bold: ".to_string(),
1837                        font: Some(crate::styles::Font {
1838                            bold: Some(true),
1839                            ..Default::default()
1840                        }),
1841                    },
1842                    RichTextRun {
1843                        text: "plain text".to_string(),
1844                        ..Default::default()
1845                    },
1846                ],
1847                ..Default::default()
1848            },
1849        )
1850        .unwrap();
1851
1852        let comments = f.get_comments("Sheet1").unwrap();
1853        assert_eq!(comments.len(), 1);
1854        assert_eq!(comments[0].paragraph.len(), 2);
1855        assert_eq!(comments[0].paragraph[0].text, "Bold: ");
1856        assert_eq!(
1857            comments[0].paragraph[0].font.as_ref().unwrap().bold,
1858            Some(true)
1859        );
1860    }
1861
1862    #[test]
1863    fn form_control_button_round_trip() {
1864        let f = File::new_with_options(Options::default());
1865        f.add_form_control(
1866            "Sheet1",
1867            FormControl {
1868                cell: "C3".to_string(),
1869                r#type: FormControlType::Button,
1870                text: "Click me".to_string(),
1871                ..Default::default()
1872            },
1873        )
1874        .unwrap();
1875
1876        let controls = f.get_form_controls("Sheet1").unwrap();
1877        assert_eq!(controls.len(), 1);
1878        assert_eq!(controls[0].cell, "C3");
1879        assert_eq!(controls[0].r#type, FormControlType::Button);
1880        assert_eq!(controls[0].text, "Click me");
1881    }
1882
1883    #[test]
1884    fn delete_comment() {
1885        let f = File::new_with_options(Options::default());
1886        f.add_comment(
1887            "Sheet1",
1888            Comment {
1889                cell: "D4".to_string(),
1890                text: "to delete".to_string(),
1891                ..Default::default()
1892            },
1893        )
1894        .unwrap();
1895        assert_eq!(f.get_comments("Sheet1").unwrap().len(), 1);
1896        f.delete_comment("Sheet1", "D4").unwrap();
1897        assert_eq!(f.get_comments("Sheet1").unwrap().len(), 0);
1898    }
1899
1900    #[test]
1901    fn header_footer_image_adds_vml_part() {
1902        let f = File::new_with_options(Options::default());
1903        // 1x1 red PNG
1904        let png = vec![
1905            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
1906            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
1907            0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
1908            0x99, 0x63, 0xf8, 0x0f, 0x00, 0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xd8, 0x4e, 0x00,
1909            0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
1910        ];
1911        f.add_header_footer_image(
1912            "Sheet1",
1913            &HeaderFooterImageOptions {
1914                position: HeaderFooterImagePositionType::Center,
1915                file: png,
1916                extension: ".png".to_string(),
1917                width: "100pt".to_string(),
1918                height: "50pt".to_string(),
1919                ..Default::default()
1920            },
1921        )
1922        .unwrap();
1923
1924        let ws = f.work_sheet_reader("Sheet1").unwrap();
1925        assert!(ws.legacy_drawing_hf.is_some());
1926    }
1927
1928    #[test]
1929    fn vml_image_data_serializes_fields() {
1930        let image_data = VmlImageData {
1931            rel_id: Some("rId1".to_string()),
1932            title: Some("logo".to_string()),
1933            crop_top: Some("1px".to_string()),
1934            grayscale: Some("t".to_string()),
1935            ..Default::default()
1936        };
1937        let xml = image_data.to_xml_string();
1938        assert!(xml.contains("<v:imagedata"));
1939        assert!(xml.contains("o:relid=\"rId1\""));
1940        assert!(xml.contains("o:title=\"logo\""));
1941        assert!(xml.contains("croptop=\"1px\""));
1942        assert!(xml.contains("grayscale=\"t\""));
1943    }
1944
1945    #[test]
1946    fn encode_shape_emits_image_data_and_lock() {
1947        let sp = EncodeShape {
1948            image_data: Some(VmlImageData {
1949                rel_id: Some("rId2".to_string()),
1950                ..Default::default()
1951            }),
1952            lock: Some(VmlLock {
1953                ext: "edit".to_string(),
1954                rotation: Some("t".to_string()),
1955                ..Default::default()
1956            }),
1957            ..Default::default()
1958        };
1959        let inner = build_shape_inner_xml(&sp, &FormCtrlPreset::default(), &VmlOptions::default());
1960        assert!(inner.contains("<v:imagedata o:relid=\"rId2\"/>"));
1961        assert!(inner.contains("<o:lock v:ext=\"edit\" rotation=\"t\"/>"));
1962    }
1963}