Skip to main content

excelize_rs/
picture.rs

1//! Picture API.
2//!
3//! Ported from Go `picture.go` and `drawing.go`.
4//!
5//! This is a functional subset supporting the most common image formats
6//! (PNG, JPEG, GIF, BMP, SVG, TIFF, WMF, EMF) placed over cells.
7
8use 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
36// ------------------------------------------------------------------
37// Re-exports of public types
38// ------------------------------------------------------------------
39
40pub use crate::xml::drawing::{GraphicOptions, Picture, PictureInsertType};
41
42// ------------------------------------------------------------------
43// Image type support
44// ------------------------------------------------------------------
45
46fn 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
96// ------------------------------------------------------------------
97// Public API
98// ------------------------------------------------------------------
99
100impl File {
101    /// Add a picture to a worksheet from a file path.
102    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    /// Add a picture to a worksheet from raw bytes.
129    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    /// Return all pictures anchored at a given cell in a worksheet.
199    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    /// Return all picture cell references in a worksheet.
228    ///
229    /// Equivalent to Go `GetPictureCells`.
230    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    /// Read the Kingsoft WPS Office embedded cell images part.
272    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    /// Return all cell images and Kingsoft WPS Office embedded image cells in a
282    /// worksheet.
283    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    /// Return the relationship of a cell image by a rich value rel index.
310    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    /// Return the relationship of a web image by a web image rich value index.
331    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    /// Return the cell image relationship for a worksheet cell.
352    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    /// Return cell images and Kingsoft WPS Office embedded cell images for a
408    /// given worksheet and cell reference.
409    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    /// Return Kingsoft WPS Office embedded cell images for a given worksheet and
441    /// cell reference.
442    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    /// Delete all pictures in a cell.
511    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
573// ------------------------------------------------------------------
574// Option parsing
575// ------------------------------------------------------------------
576
577fn 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
610// ------------------------------------------------------------------
611// Media management
612// ------------------------------------------------------------------
613
614impl 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
641// ------------------------------------------------------------------
642// Drawing helpers
643// ------------------------------------------------------------------
644
645impl 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// ------------------------------------------------------------------
934// Tests
935// ------------------------------------------------------------------
936
937#[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        // 1x1 red PNG
946        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}