1use std::collections::HashMap;
9
10use quick_xml::de::from_reader as xml_from_reader;
11
12use crate::calc::arg::FORMULA_ERROR_VALUE;
13use crate::constants::{
14 DEFAULT_DRAWING_SCALE, DEFAULT_XML_PATH_CELL_IMAGES, DEFAULT_XML_PATH_CELL_IMAGES_RELS, EMU,
15 EXT_URI_SVG, MAX_GRAPHIC_ALT_TEXT_LENGTH, MAX_GRAPHIC_NAME_LENGTH, NAMESPACE_DRAWING_2016_SVG,
16 NAMESPACE_SPREADSHEET, SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_HYPER_LINK,
17 SOURCE_RELATIONSHIP_IMAGE,
18};
19use crate::errors::Result;
20use crate::errors::{
21 ErrImgExt, ErrImgLoad, ErrMaxGraphicAltTextLength, ErrMaxGraphicNameLength, ErrParameterInvalid,
22};
23use crate::file::File;
24use crate::lib_util::{
25 cell_name_to_coordinates, coordinates_to_cell_name, count_utf16_string,
26 range_ref_to_coordinates, sort_coordinates,
27};
28use crate::xml::decode_drawing::DecodeCellImages;
29use crate::xml::drawing::{
30 XdrCellAnchor, XlsxBlip, XlsxBlipFill, XlsxCNvPicPr, XlsxCNvPr, XlsxFrom, XlsxNvPicPr, XlsxOff,
31 XlsxPic, XlsxPicLocks, XlsxPositiveSize2D, XlsxPrstGeom, XlsxSpPr, XlsxStretch, XlsxTo,
32 XlsxXfrm,
33};
34use crate::xml::worksheet::XlsxC;
35
36pub use crate::xml::drawing::{GraphicOptions, Picture, PictureInsertType};
41
42fn supported_image_types() -> HashMap<String, String> {
47 [
48 (".bmp".to_string(), ".bmp".to_string()),
49 (".emf".to_string(), ".emf".to_string()),
50 (".emz".to_string(), ".emz".to_string()),
51 (".gif".to_string(), ".gif".to_string()),
52 (".ico".to_string(), ".ico".to_string()),
53 (".jpeg".to_string(), ".jpeg".to_string()),
54 (".jpg".to_string(), ".jpg".to_string()),
55 (".png".to_string(), ".png".to_string()),
56 (".svg".to_string(), ".svg".to_string()),
57 (".tif".to_string(), ".tif".to_string()),
58 (".tiff".to_string(), ".tiff".to_string()),
59 (".wmf".to_string(), ".wmf".to_string()),
60 (".wmz".to_string(), ".wmz".to_string()),
61 ]
62 .into_iter()
63 .collect()
64}
65
66fn detect_image_extension(data: &[u8]) -> Option<String> {
67 if data.starts_with(b"\x89PNG\r\n\x1a\n") {
68 Some(".png".to_string())
69 } else if data.starts_with(b"\xFF\xD8\xFF") {
70 Some(".jpg".to_string())
71 } else if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
72 Some(".gif".to_string())
73 } else if data.starts_with(b"BM") {
74 Some(".bmp".to_string())
75 } else if data.starts_with(b"<?xml") || data.starts_with(b"<svg") {
76 Some(".svg".to_string())
77 } else if data.starts_with(b"II*\0") || data.starts_with(b"MM\0*") {
78 Some(".tiff".to_string())
79 } else {
80 None
81 }
82}
83
84fn image_dimensions(data: &[u8], ext: &str) -> Option<(i32, i32)> {
85 if ext.eq_ignore_ascii_case(".svg") {
86 return Some((64, 64));
87 }
88 if let Ok(reader) = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format() {
89 if let Ok(dim) = reader.into_dimensions() {
90 return Some((dim.0 as i32, dim.1 as i32));
91 }
92 }
93 None
94}
95
96impl File {
101 pub fn add_picture(
103 &mut self,
104 sheet: &str,
105 cell: &str,
106 path: &str,
107 opts: Option<&GraphicOptions>,
108 ) -> Result<()> {
109 let ext = std::path::Path::new(path)
110 .extension()
111 .and_then(|e| e.to_str())
112 .unwrap_or("")
113 .to_lowercase();
114 let ext = format!(".{ext}");
115 if !supported_image_types().contains_key(&ext) {
116 return Err(Box::new(ErrImgExt));
117 }
118 let file = std::fs::read(path)?;
119 let pic = Picture {
120 extension: ext,
121 file,
122 format: opts.cloned(),
123 ..Default::default()
124 };
125 self.add_picture_from_bytes(sheet, cell, &pic)
126 }
127
128 pub fn add_picture_from_bytes(&mut self, sheet: &str, cell: &str, pic: &Picture) -> Result<()> {
130 if pic.insert_type != PictureInsertType::PLACE_OVER_CELLS {
131 return Err(Box::new(ErrParameterInvalid));
132 }
133 let mut ext = pic.extension.clone();
134 if ext.is_empty() {
135 if let Some(detected) = detect_image_extension(&pic.file) {
136 ext = detected;
137 }
138 }
139 let types = supported_image_types();
140 let mapped_ext = types.get(&ext.to_lowercase()).cloned().unwrap_or(ext);
141 if !types.contains_key(&mapped_ext.to_lowercase()) {
142 return Err(Box::new(ErrImgExt));
143 }
144 let options = parse_picture_options(pic)?;
145 let dims = image_dimensions(&pic.file, &mapped_ext).ok_or_else(|| Box::new(ErrImgLoad))?;
146 let _ = self.work_sheet_reader(sheet)?;
147
148 let drawing_id = self.count_drawings() + 1;
149 let drawing_xml = format!("xl/drawings/drawing{drawing_id}.xml");
150 let (drawing_id, drawing_xml) = self.prepare_drawing(sheet, drawing_id, &drawing_xml)?;
151 let drawing_rels = format!("xl/drawings/_rels/drawing{drawing_id}.xml.rels");
152
153 let media = self.add_media(&pic.file, &mapped_ext);
154 let media_target = format!("..{}", media.trim_start_matches("xl"));
155 let mut drawing_rid = 0;
156 if let Ok(Some(rels)) = self.rels_reader(&drawing_rels) {
157 for rel in &rels.relationships {
158 if rel.r#type == SOURCE_RELATIONSHIP_IMAGE && rel.target == media_target {
159 drawing_rid = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
160 break;
161 }
162 }
163 }
164 if drawing_rid == 0 {
165 drawing_rid =
166 self.add_rels(&drawing_rels, SOURCE_RELATIONSHIP_IMAGE, &media_target, "");
167 }
168
169 let mut hyperlink_rid = 0;
170 let mut hyperlink_type = String::new();
171 if !options.hyperlink.is_empty() && !options.hyperlink_type.is_empty() {
172 if options.hyperlink_type == "External" {
173 hyperlink_type = "External".to_string();
174 }
175 hyperlink_rid = self.add_rels(
176 &drawing_rels,
177 SOURCE_RELATIONSHIP_HYPER_LINK,
178 &options.hyperlink,
179 &hyperlink_type,
180 );
181 }
182
183 self.add_drawing_picture(
184 sheet,
185 &drawing_xml,
186 cell,
187 &mapped_ext,
188 drawing_rid,
189 hyperlink_rid,
190 dims,
191 &options,
192 )?;
193 self.add_content_type_part(drawing_id, "drawings")?;
194 self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET);
195 Ok(())
196 }
197
198 pub fn get_pictures(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
200 let mut pics = self.get_cell_images(sheet, cell)?;
201 let ws = self.work_sheet_reader(sheet)?;
202 if ws.drawing.is_none() {
203 return Ok(pics);
204 }
205 let (col, row) = cell_name_to_coordinates(cell)?;
206 let col = col - 1;
207 let row = row - 1;
208 let drawing_xml = self
209 .get_sheet_relationships_target_by_id(
210 sheet,
211 ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
212 )
213 .replace("..", "xl")
214 .trim_start_matches('/')
215 .to_string();
216 let drawing_rels = drawing_xml
217 .replace("xl/drawings", "xl/drawings/_rels")
218 .replace(".xml", ".xml.rels");
219 for pic in self.get_picture(row, col, &drawing_xml, &drawing_rels)? {
220 if !pics.iter().any(|p| p.file == pic.file) {
221 pics.push(pic);
222 }
223 }
224 Ok(pics)
225 }
226
227 pub fn get_picture_cells(&self, sheet: &str) -> Result<Vec<String>> {
231 let mut cells = self.get_image_cells(sheet)?;
232 let ws = self.work_sheet_reader(sheet)?;
233 if ws.drawing.is_none() {
234 return Ok(cells);
235 }
236 let drawing_xml = self
237 .get_sheet_relationships_target_by_id(
238 sheet,
239 ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
240 )
241 .replace("..", "xl")
242 .trim_start_matches('/')
243 .to_string();
244 let drawing_rels = drawing_xml
245 .replace("xl/drawings", "xl/drawings/_rels")
246 .replace(".xml", ".xml.rels");
247 let (wsdr, _) = self.drawing_parser(&drawing_xml)?;
248 for anchor in wsdr
249 .one_cell_anchor
250 .iter()
251 .chain(wsdr.two_cell_anchor.iter())
252 {
253 let Some(pic) = &anchor.pic else {
254 continue;
255 };
256 let r_id = &pic.blip_fill.blip.embed;
257 if self.get_drawing_relationship(&drawing_rels, r_id).is_none() {
258 continue;
259 }
260 if let Some(from) = &anchor.from {
261 let cell =
262 coordinates_to_cell_name(from.col as i32 + 1, from.row as i32 + 1, false)?;
263 if !cells.contains(&cell) {
264 cells.push(cell);
265 }
266 }
267 }
268 Ok(cells)
269 }
270
271 fn cell_images_reader(&self) -> Result<DecodeCellImages> {
273 let data = self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_CELL_IMAGES))?;
274 let data = crate::file::namespace_strict_to_transitional(&data);
275 if data.is_empty() {
276 return Ok(DecodeCellImages::default());
277 }
278 Ok(xml_from_reader(data.as_slice()).unwrap_or_default())
279 }
280
281 fn get_image_cells(&self, sheet: &str) -> Result<Vec<String>> {
284 let ws = self.work_sheet_reader(sheet)?;
285 let mut cells = Vec::new();
286 for row in &ws.sheet_data.row {
287 for c in &row.c {
288 let Some(cell_ref) = c.r.as_deref() else {
289 continue;
290 };
291 if let Some(f) = &c.f {
292 if !f.content.is_empty() && is_dispimg_formula(&f.content) {
293 self.calc_cell_value(sheet, cell_ref)?;
294 cells.push(cell_ref.to_string());
295 }
296 }
297 let mut pic = Picture {
298 format: Some(GraphicOptions::default()),
299 ..Default::default()
300 };
301 if self.get_image_cell_rel(c, &mut pic)?.is_some() {
302 cells.push(cell_ref.to_string());
303 }
304 }
305 }
306 Ok(cells)
307 }
308
309 fn get_rich_data_rich_value_rel(
311 &self,
312 val: &str,
313 ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
314 let idx = val.parse::<usize>()?;
315 let rich_value_rel = self.rich_value_rel_reader()?;
316 let r_id = match rich_value_rel.rels.get(idx) {
317 Some(rel) => rel.id.clone(),
318 None => return Ok(None),
319 };
320 let rel = self.get_rich_data_rich_value_rel_relationship(&r_id);
321 if rel
322 .as_ref()
323 .map_or(false, |r| r.r#type != SOURCE_RELATIONSHIP_IMAGE)
324 {
325 return Ok(None);
326 }
327 Ok(rel)
328 }
329
330 fn get_rich_data_web_images_rel(
332 &self,
333 val: &str,
334 ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
335 let idx = val.parse::<usize>()?;
336 let web_images = self.rich_value_web_image_reader()?;
337 let r_id = match web_images.web_image_srd.get(idx) {
338 Some(img) => img.blip.r_id.clone().unwrap_or_default(),
339 None => return Ok(None),
340 };
341 let rel = self.get_rich_value_web_image_relationship(&r_id);
342 if rel
343 .as_ref()
344 .map_or(false, |r| r.r#type != SOURCE_RELATIONSHIP_IMAGE)
345 {
346 return Ok(None);
347 }
348 Ok(rel)
349 }
350
351 fn get_image_cell_rel(
353 &self,
354 c: &XlsxC,
355 pic: &mut Picture,
356 ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
357 let vm = match c.vm {
358 Some(vm) => vm,
359 None => return Ok(None),
360 };
361 if c.v.as_deref() != Some(FORMULA_ERROR_VALUE) {
362 return Ok(None);
363 }
364 let metadata = self.metadata_reader()?;
365 let vmd = match metadata.value_metadata {
366 Some(vmd) => vmd,
367 None => return Ok(None),
368 };
369 let rc = match vmd
370 .bk
371 .get((vm as usize).saturating_sub(1))
372 .and_then(|b| b.rc.first())
373 {
374 Some(rc) => rc,
375 None => return Ok(None),
376 };
377 let rich_value_idx = rc.v as usize;
378 let rich_value = self.rich_value_reader()?;
379 let rv = match rich_value.rv.get(rich_value_idx) {
380 Some(rv) => rv,
381 None => return Ok(None),
382 };
383 let rv_structures = self.rich_value_structures_reader()?;
384 let rv_struct = match rv_structures.s.get(rv.s as usize) {
385 Some(s) => s,
386 None => return Ok(None),
387 };
388 if rv_struct.k.len() != rv.v.len() {
389 return Ok(None);
390 }
391 if let Some(idx) = rich_value_key_idx(&rv_struct.k, "Text") {
392 if let Some(fmt) = &mut pic.format {
393 fmt.alt_text = rv.v[idx].clone();
394 }
395 }
396 if let Some(idx) = rich_value_key_idx(&rv_struct.k, "_rvRel:LocalImageIdentifier") {
397 pic.insert_type = PictureInsertType::PLACE_IN_CELL;
398 return self.get_rich_data_rich_value_rel(&rv.v[idx]);
399 }
400 if let Some(idx) = rich_value_key_idx(&rv_struct.k, "WebImageIdentifier") {
401 pic.insert_type = PictureInsertType::IMAGE;
402 return self.get_rich_data_web_images_rel(&rv.v[idx]);
403 }
404 Ok(None)
405 }
406
407 fn get_cell_images(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
410 let mut pics = self.get_disp_images(sheet, cell)?;
411 let ws = self.work_sheet_reader(sheet)?;
412 let c = match crate::cell::find_cell(&ws, cell) {
413 Some(c) => c.clone(),
414 None => return Ok(pics),
415 };
416 let mut pic = Picture {
417 format: Some(GraphicOptions::default()),
418 insert_type: PictureInsertType::PLACE_IN_CELL,
419 ..Default::default()
420 };
421 if let Some(rel) = self.get_image_cell_rel(&c, &mut pic)? {
422 pic.extension = std::path::Path::new(&rel.target)
423 .extension()
424 .and_then(|e| e.to_str())
425 .map(|e| format!(".{e}"))
426 .unwrap_or_default();
427 let target = rel
428 .target
429 .replace("..", "xl")
430 .trim_start_matches('/')
431 .to_string();
432 if let Some(bytes) = self.pkg.get(&target) {
433 pic.file = bytes.value().clone();
434 pics.push(pic);
435 }
436 }
437 Ok(pics)
438 }
439
440 fn get_disp_images(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
443 let formula = self.get_cell_formula(sheet, cell)?;
444 if !is_dispimg_formula(&formula) {
445 return Ok(Vec::new());
446 }
447 let img_id = self.calc_cell_value(sheet, cell)?;
448 let cell_images = self.cell_images_reader()?;
449 let rels = match self.rels_reader(DEFAULT_XML_PATH_CELL_IMAGES_RELS)? {
450 Some(rels) => rels,
451 None => return Ok(Vec::new()),
452 };
453 let mut pics = Vec::new();
454 for cell_img in &cell_images.cell_image {
455 if cell_img.pic.nv_pic_pr.c_nv_pr.name != img_id {
456 continue;
457 }
458 for rel in &rels.relationships {
459 if rel.id == cell_img.pic.blip_fill.blip.embed {
460 let mut pic = Picture {
461 extension: std::path::Path::new(&rel.target)
462 .extension()
463 .and_then(|e| e.to_str())
464 .map(|e| format!(".{e}"))
465 .unwrap_or_default(),
466 format: Some(GraphicOptions::default()),
467 insert_type: PictureInsertType::DISPIMG,
468 ..Default::default()
469 };
470 let target = format!("xl/{}", rel.target);
471 if let Some(bytes) = self.pkg.get(&target) {
472 pic.file = bytes.value().clone();
473 if let Some(fmt) = &mut pic.format {
474 fmt.alt_text = cell_img.pic.nv_pic_pr.c_nv_pr.descr.clone();
475 fmt.name = cell_img.pic.nv_pic_pr.c_nv_pr.name.clone();
476 }
477 pics.push(pic);
478 }
479 }
480 }
481 }
482 Ok(pics)
483 }
484}
485
486fn is_dispimg_formula(content: &str) -> bool {
487 let content = content.strip_prefix('=').unwrap_or(content);
488 let content = content.strip_prefix("_xlfn.").unwrap_or(content);
489 content.starts_with("DISPIMG")
490}
491
492fn cell_in_merge_range(cell: &str, range_ref: &str) -> Result<bool> {
493 let (col, row) = cell_name_to_coordinates(cell)?;
494 if !range_ref.contains(':') {
495 return Ok(false);
496 }
497 let mut coords = range_ref_to_coordinates(range_ref)?;
498 sort_coordinates(&mut coords)?;
499 Ok(col >= coords[0] && col <= coords[2] && row >= coords[1] && row <= coords[3])
500}
501
502fn rich_value_key_idx(
503 keys: &[crate::xml::metadata::XlsxRichValueKey],
504 name: &str,
505) -> Option<usize> {
506 keys.iter().position(|k| k.n == name)
507}
508
509impl File {
510 pub fn delete_picture(&mut self, sheet: &str, cell: &str) -> Result<()> {
512 let (col, row) = cell_name_to_coordinates(cell)?;
513 let col = col - 1;
514 let row = row - 1;
515 let ws = self.work_sheet_reader(sheet)?;
516 if ws.drawing.is_none() {
517 return Ok(());
518 }
519 let drawing_xml = self
520 .get_sheet_relationships_target_by_id(
521 sheet,
522 ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
523 )
524 .replace("..", "xl")
525 .trim_start_matches('/')
526 .to_string();
527 let drawing_rels = format!(
528 "xl/drawings/_rels/{}.rels",
529 std::path::Path::new(&drawing_xml)
530 .file_name()
531 .unwrap_or_default()
532 .to_string_lossy()
533 );
534 let r_ids = self.delete_drawing(col, row, &drawing_xml, "Pic")?;
535 for r_id in r_ids {
536 if let Some(rel) = self.get_drawing_relationship(&drawing_rels, &r_id) {
537 let target = rel
538 .target
539 .replace("../", "xl/")
540 .trim_start_matches('/')
541 .to_string();
542 let mut used = false;
543 for entry in self.pkg.iter() {
544 if entry.key().contains("xl/drawings/_rels/drawing")
545 && *entry.key() != drawing_rels
546 {
547 if let Ok(Some(rels)) = self.rels_reader(entry.key()) {
548 for r in &rels.relationships {
549 if r.r#type == SOURCE_RELATIONSHIP_IMAGE
550 && std::path::Path::new(&r.target)
551 .file_name()
552 .unwrap_or_default()
553 == std::path::Path::new(&rel.target)
554 .file_name()
555 .unwrap_or_default()
556 {
557 used = true;
558 }
559 }
560 }
561 }
562 }
563 if !used {
564 self.pkg.remove(&target);
565 }
566 }
567 self.delete_drawing_rels(&drawing_rels, &r_id);
568 }
569 Ok(())
570 }
571}
572
573fn parse_picture_options(pic: &Picture) -> Result<GraphicOptions> {
578 let mut opts = pic.format.clone().unwrap_or_default();
579 if opts.print_object.is_none() {
580 opts.print_object = Some(true);
581 }
582 if opts.locked.is_none() {
583 opts.locked = Some(true);
584 }
585 if opts.scale_x == 0.0 {
586 opts.scale_x = DEFAULT_DRAWING_SCALE;
587 }
588 if opts.scale_y == 0.0 {
589 opts.scale_y = DEFAULT_DRAWING_SCALE;
590 }
591 if !opts.positioning.is_empty()
592 && !["oneCell", "twoCell", "absolute"].contains(&opts.positioning.as_str())
593 {
594 return Err(crate::errors::new_invalid_optional_value(
595 "Positioning",
596 &opts.positioning,
597 &["oneCell", "twoCell", "absolute"],
598 )
599 .into());
600 }
601 if count_utf16_string(&opts.alt_text) > MAX_GRAPHIC_ALT_TEXT_LENGTH {
602 return Err(Box::new(ErrMaxGraphicAltTextLength));
603 }
604 if count_utf16_string(&opts.name) > MAX_GRAPHIC_NAME_LENGTH {
605 return Err(Box::new(ErrMaxGraphicNameLength));
606 }
607 Ok(opts)
608}
609
610impl File {
615 fn count_media(&self) -> i32 {
616 let mut count = 0;
617 for entry in self.pkg.iter() {
618 if entry.key().contains("xl/media/image") {
619 count += 1;
620 }
621 }
622 count
623 }
624
625 pub(crate) fn add_media(&self, file: &[u8], ext: &str) -> String {
626 for entry in self.pkg.iter() {
627 if !entry.key().starts_with("xl/media/image") {
628 continue;
629 }
630 if entry.value() == file {
631 return entry.key().clone();
632 }
633 }
634 let count = self.count_media();
635 let name = format!("xl/media/image{}{}", count + 1, ext);
636 self.pkg.insert(name.clone(), file.to_vec());
637 name
638 }
639}
640
641impl File {
646 fn drawing_resize(
647 &self,
648 sheet: &str,
649 cell: &str,
650 width: f64,
651 height: f64,
652 opts: &GraphicOptions,
653 ) -> Result<(i32, i32, i32, i32)> {
654 let (mut col, mut row) = cell_name_to_coordinates(cell)?;
655 let mut cell_width = self.get_col_width_pixels(sheet, col)? as f64;
656 let mut cell_height = self.get_row_height_pixels(sheet, row)? as f64;
657 let merge_cells = self.get_merge_cells(sheet)?;
658 let mut rng = Vec::new();
659 let mut in_merge_cell = false;
660 for merge in merge_cells {
661 if in_merge_cell {
662 break;
663 }
664 if let Ok(true) = cell_in_merge_range(cell, &merge) {
665 if let Ok(mut coords) = range_ref_to_coordinates(&merge) {
666 sort_coordinates(&mut coords)?;
667 rng = coords;
668 in_merge_cell = true;
669 }
670 }
671 }
672 if in_merge_cell {
673 cell_width = 0.0;
674 cell_height = 0.0;
675 col = rng[0];
676 row = rng[1];
677 for c in rng[0]..=rng[2] {
678 cell_width += self.get_col_width_pixels(sheet, c)? as f64;
679 }
680 for r in rng[1]..=rng[3] {
681 cell_height += self.get_row_height_pixels(sheet, r)? as f64;
682 }
683 }
684 let (mut width, mut height) = (width, height);
685 if cell_width < width || cell_height < height {
686 let asp_width = cell_width / width;
687 let asp_height = cell_height / height;
688 let asp = asp_width.min(asp_height);
689 width *= asp;
690 height *= asp;
691 }
692 if opts.auto_fit_ignore_aspect {
693 width = cell_width;
694 height = cell_height;
695 }
696 Ok((
697 (width * opts.scale_x) as i32,
698 (height * opts.scale_y) as i32,
699 col,
700 row,
701 ))
702 }
703
704 fn add_drawing_picture(
705 &self,
706 sheet: &str,
707 drawing_xml: &str,
708 cell: &str,
709 ext: &str,
710 r_id: i32,
711 hyperlink_rid: i32,
712 dims: (i32, i32),
713 opts: &GraphicOptions,
714 ) -> Result<()> {
715 let (mut col, mut row) = cell_name_to_coordinates(cell)?;
716 let (mut width, mut height) = dims;
717 if !opts.positioning.is_empty()
718 && !["oneCell", "twoCell", "absolute"].contains(&opts.positioning.as_str())
719 {
720 return Err(crate::errors::new_invalid_optional_value(
721 "Positioning",
722 &opts.positioning,
723 &["oneCell", "twoCell", "absolute"],
724 )
725 .into());
726 }
727 if opts.auto_fit {
728 (width, height, col, row) =
729 self.drawing_resize(sheet, cell, width as f64, height as f64, opts)?;
730 } else {
731 width = (width as f64 * opts.scale_x) as i32;
732 height = (height as f64 * opts.scale_y) as i32;
733 }
734 let (col_start, row_start, col_end, row_end, x1, y1, x2, y2) =
735 self.position_object_pixels(sheet, col, row, width, height, opts)?;
736 let (mut content, c_nv_pr_id) = self.drawing_parser(drawing_xml)?;
737
738 let mut anchor = XdrCellAnchor {
739 edit_as: if opts.positioning.is_empty() {
740 None
741 } else {
742 Some(opts.positioning.clone())
743 },
744 from: Some(XlsxFrom {
745 col: col_start as i64,
746 col_off: x1 as i64 * EMU as i64,
747 row: row_start as i64,
748 row_off: y1 as i64 * EMU as i64,
749 }),
750 ..Default::default()
751 };
752 if opts.positioning != "oneCell" {
753 anchor.to = Some(XlsxTo {
754 col: col_end as i64,
755 col_off: x2 as i64 * EMU as i64,
756 row: row_end as i64,
757 row_off: y2 as i64 * EMU as i64,
758 });
759 }
760
761 let mut pic = XlsxPic {
762 nv_pic_pr: XlsxNvPicPr {
763 c_nv_pr: XlsxCNvPr {
764 id: c_nv_pr_id,
765 descr: opts.alt_text.clone(),
766 name: if opts.name.is_empty() {
767 format!("Picture {c_nv_pr_id}")
768 } else {
769 opts.name.clone()
770 },
771 ..Default::default()
772 },
773 c_nv_pic_pr: XlsxCNvPicPr {
774 pic_locks: XlsxPicLocks {
775 no_change_aspect: opts.lock_aspect_ratio,
776 ..Default::default()
777 },
778 },
779 },
780 blip_fill: XlsxBlipFill {
781 blip: XlsxBlip {
782 embed: format!("rId{r_id}"),
783 xmlns_r: SOURCE_RELATIONSHIP.to_string(),
784 ..Default::default()
785 },
786 stretch: XlsxStretch {
787 fill_rect: String::new(),
788 },
789 },
790 sp_pr: XlsxSpPr {
791 xfrm: XlsxXfrm {
792 off: XlsxOff { x: 0, y: 0 },
793 ext: XlsxPositiveSize2D { cx: 0, cy: 0 },
794 },
795 prst_geom: XlsxPrstGeom {
796 prst: "rect".to_string(),
797 },
798 ..Default::default()
799 },
800 };
801 if hyperlink_rid != 0 {
802 pic.nv_pic_pr.c_nv_pr.hlink_click = Some(crate::xml::drawing::XlsxHlinkClick {
803 r_id: Some(format!("rId{hyperlink_rid}")),
804 xmlns_r: Some(SOURCE_RELATIONSHIP.to_string()),
805 ..Default::default()
806 });
807 }
808 if ext == ".svg" {
809 pic.blip_fill.blip.ext_list = Some(crate::xml::drawing::XlsxEGOfficeArtExtensionList {
810 ext: vec![crate::xml::drawing::XlsxCTOfficeArtExtension {
811 uri: EXT_URI_SVG.to_string(),
812 svg_blip: crate::xml::drawing::XlsxCTSVGBlip {
813 xmlns_asvg: NAMESPACE_DRAWING_2016_SVG.to_string(),
814 embed: format!("rId{r_id}"),
815 ..Default::default()
816 },
817 }],
818 });
819 }
820 pic.sp_pr.xfrm.ext = XlsxPositiveSize2D {
821 cx: width as i64 * EMU as i64,
822 cy: height as i64 * EMU as i64,
823 };
824 if opts.positioning == "oneCell" {
825 let cx = x2 as i64 * EMU as i64;
826 let cy = y2 as i64 * EMU as i64;
827 anchor.ext = Some(XlsxPositiveSize2D { cx, cy });
828 pic.sp_pr.xfrm.ext = XlsxPositiveSize2D { cx, cy };
829 }
830 anchor.pic = Some(pic);
831 anchor.client_data = Some(crate::xml::drawing::XdrClientData {
832 f_locks_with_sheet: opts.locked.unwrap_or(true),
833 f_prints_with_sheet: opts.print_object.unwrap_or(true),
834 });
835
836 if opts.positioning == "oneCell" {
837 content.one_cell_anchor.push(anchor);
838 } else {
839 content.two_cell_anchor.push(anchor);
840 }
841 self.drawings.insert(drawing_xml.to_string(), content);
842 Ok(())
843 }
844
845 fn get_picture(
846 &self,
847 row: i32,
848 col: i32,
849 drawing_xml: &str,
850 drawing_rels: &str,
851 ) -> Result<Vec<Picture>> {
852 let (wsdr, _) = self.drawing_parser(drawing_xml)?;
853 let mut pics = Vec::new();
854 for anchor in wsdr
855 .one_cell_anchor
856 .iter()
857 .chain(wsdr.two_cell_anchor.iter())
858 {
859 if anchor.pic.is_none() {
860 continue;
861 }
862 if let Some(from) = &anchor.from {
863 if from.col != col as i64 || from.row != row as i64 {
864 continue;
865 }
866 }
867 let pic = anchor.pic.as_ref().unwrap();
868 let r_id = &pic.blip_fill.blip.embed;
869 if let Some(rel) = self.get_drawing_relationship(drawing_rels, r_id) {
870 let target = rel
871 .target
872 .replace("../", "xl/")
873 .trim_start_matches('/')
874 .to_string();
875 if let Some(bytes) = self.pkg.get(&target) {
876 let extension = std::path::Path::new(&target)
877 .extension()
878 .and_then(|e| e.to_str())
879 .map(|e| format!(".{e}"))
880 .unwrap_or_default();
881 let mut format = GraphicOptions {
882 scale_x: DEFAULT_DRAWING_SCALE,
883 scale_y: DEFAULT_DRAWING_SCALE,
884 ..Default::default()
885 };
886 if let Some(client) = &anchor.client_data {
887 format.locked = Some(client.f_locks_with_sheet);
888 format.print_object = Some(client.f_prints_with_sheet);
889 }
890 if anchor.to.is_none() {
891 format.positioning = "oneCell".to_string();
892 }
893 if let Some(from) = &anchor.from {
894 format.offset_x = from.col_off / EMU as i64;
895 format.offset_y = from.row_off / EMU as i64;
896 }
897 format.lock_aspect_ratio = pic.nv_pic_pr.c_nv_pic_pr.pic_locks.no_change_aspect;
898 format.alt_text = pic.nv_pic_pr.c_nv_pr.descr.clone();
899 format.name = pic.nv_pic_pr.c_nv_pr.name.clone();
900 calculate_picture_scale(&mut format, bytes.value(), &pic.sp_pr.xfrm.ext);
901 pics.push(Picture {
902 extension,
903 file: bytes.value().clone(),
904 format: Some(format),
905 insert_type: PictureInsertType::PLACE_OVER_CELLS,
906 });
907 }
908 }
909 }
910 Ok(pics)
911 }
912
913 fn delete_drawing_rels(&self, rels: &str, r_id: &str) {
914 if let Ok(Some(mut rels_obj)) = self.rels_reader(rels) {
915 rels_obj.relationships.retain(|r| r.id != r_id);
916 self.relationships.insert(rels.to_string(), rels_obj);
917 }
918 }
919}
920
921fn calculate_picture_scale(format: &mut GraphicOptions, file: &[u8], ext: &XlsxPositiveSize2D) {
922 if ext.cx <= 0 || ext.cy <= 0 {
923 return;
924 }
925 if let Some((w, h)) = image_dimensions(file, ".png") {
926 if w > 0 && h > 0 {
927 format.scale_x = ((ext.cx / EMU as i64) as f64 / w as f64 * 100.0).round() / 100.0;
928 format.scale_y = ((ext.cy / EMU as i64) as f64 / h as f64 * 100.0).round() / 100.0;
929 }
930 }
931}
932
933#[cfg(test)]
938mod tests {
939 use super::*;
940 use crate::Options;
941
942 #[test]
943 fn picture_round_trip() {
944 let mut f = File::new_with_options(Options::default());
945 let png = vec![
947 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
948 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
949 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
950 0x99, 0x63, 0xf8, 0x0f, 0x00, 0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xd8, 0x4e, 0x00,
951 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
952 ];
953 let pic = Picture {
954 extension: ".png".to_string(),
955 file: png.clone(),
956 format: Some(GraphicOptions {
957 name: "RedPixel".to_string(),
958 ..Default::default()
959 }),
960 ..Default::default()
961 };
962 f.add_picture_from_bytes("Sheet1", "B2", &pic).unwrap();
963 let pics = f.get_pictures("Sheet1", "B2").unwrap();
964 assert_eq!(pics.len(), 1);
965 assert_eq!(pics[0].extension, ".png");
966 assert_eq!(pics[0].format.as_ref().unwrap().name, "RedPixel");
967
968 let cells = f.get_picture_cells("Sheet1").unwrap();
969 assert_eq!(cells, vec!["B2"]);
970 }
971}