1use 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
31pub use crate::xml::comments::Comment;
36
37#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
39#[repr(u8)]
40pub enum FormControlType {
41 #[default]
43 Note,
44 Button,
46 OptionButton,
48 SpinButton,
50 CheckBox,
52 GroupBox,
54 Label,
56 ScrollBar,
58}
59
60#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
62pub enum HeaderFooterImagePositionType {
63 #[default]
65 Left,
66 Center,
68 Right,
70}
71
72#[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#[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
105impl File {
110 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 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 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 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 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 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 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#[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
378impl 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#[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#[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
1324fn 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 }
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
1689fn 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('&', "&")
1779 .replace('<', "<")
1780 .replace('>', ">")
1781 .replace('"', """)
1782}
1783
1784#[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 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}