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;
11use quick_xml::se::to_string as xml_to_string;
12
13use crate::calc::arg::FORMULA_ERROR_VALUE;
14use crate::constants::{
15    DEFAULT_DRAWING_SCALE, DEFAULT_XML_PATH_CELL_IMAGES, DEFAULT_XML_PATH_CELL_IMAGES_RELS, EMU,
16    EXT_URI_SVG, MAX_GRAPHIC_ALT_TEXT_LENGTH, MAX_GRAPHIC_NAME_LENGTH, NAMESPACE_DRAWING_2016_SVG,
17    NAMESPACE_SPREADSHEET, SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_HYPER_LINK,
18    SOURCE_RELATIONSHIP_IMAGE,
19};
20use crate::errors::Result;
21use crate::errors::{
22    ErrImgExt, ErrImgLoad, ErrMaxGraphicAltTextLength, ErrMaxGraphicNameLength, ErrParameterInvalid,
23};
24use crate::file::File;
25use crate::lib_util::{
26    cell_name_to_coordinates, coordinates_to_cell_name, count_utf16_string,
27    range_ref_to_coordinates, sort_coordinates,
28};
29use crate::xml::decode_drawing::DecodeCellImages;
30use crate::xml::drawing::{
31    XdrCellAnchor, XlsxBlip, XlsxBlipFill, XlsxCNvPicPr, XlsxCNvPr, XlsxFrom, XlsxNvPicPr, XlsxOff,
32    XlsxPic, XlsxPicLocks, XlsxPositiveSize2D, XlsxPrstGeom, XlsxSpPr, XlsxStretch, XlsxTo,
33    XlsxXfrm,
34};
35use crate::xml::worksheet::XlsxC;
36
37// ------------------------------------------------------------------
38// Re-exports of public types
39// ------------------------------------------------------------------
40
41pub use crate::xml::drawing::{GraphicOptions, Picture, PictureInsertType};
42
43// ------------------------------------------------------------------
44// Image type support
45// ------------------------------------------------------------------
46
47fn supported_image_types() -> HashMap<String, String> {
48    [
49        (".bmp".to_string(), ".bmp".to_string()),
50        (".emf".to_string(), ".emf".to_string()),
51        (".emz".to_string(), ".emz".to_string()),
52        (".gif".to_string(), ".gif".to_string()),
53        (".ico".to_string(), ".ico".to_string()),
54        (".jpeg".to_string(), ".jpeg".to_string()),
55        (".jpg".to_string(), ".jpg".to_string()),
56        (".png".to_string(), ".png".to_string()),
57        (".svg".to_string(), ".svg".to_string()),
58        (".tif".to_string(), ".tif".to_string()),
59        (".tiff".to_string(), ".tiff".to_string()),
60        (".wmf".to_string(), ".wmf".to_string()),
61        (".wmz".to_string(), ".wmz".to_string()),
62    ]
63    .into_iter()
64    .collect()
65}
66
67fn detect_image_extension(data: &[u8]) -> Option<String> {
68    if data.starts_with(b"\x89PNG\r\n\x1a\n") {
69        Some(".png".to_string())
70    } else if data.starts_with(b"\xFF\xD8\xFF") {
71        Some(".jpg".to_string())
72    } else if data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a") {
73        Some(".gif".to_string())
74    } else if data.starts_with(b"BM") {
75        Some(".bmp".to_string())
76    } else if data.starts_with(b"<?xml") || data.starts_with(b"<svg") {
77        Some(".svg".to_string())
78    } else if data.starts_with(b"II*\0") || data.starts_with(b"MM\0*") {
79        Some(".tiff".to_string())
80    } else {
81        None
82    }
83}
84
85fn image_dimensions(data: &[u8], ext: &str) -> Option<(i32, i32)> {
86    if ext.eq_ignore_ascii_case(".svg") {
87        return Some((64, 64));
88    }
89    if let Ok(reader) = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format() {
90        if let Ok(dim) = reader.into_dimensions() {
91            return Some((dim.0 as i32, dim.1 as i32));
92        }
93    }
94    None
95}
96
97// ------------------------------------------------------------------
98// Public API
99// ------------------------------------------------------------------
100
101impl File {
102    /// Add a picture to a worksheet from a file path.
103    pub fn add_picture(
104        &mut self,
105        sheet: &str,
106        cell: &str,
107        path: &str,
108        opts: Option<&GraphicOptions>,
109    ) -> Result<()> {
110        self.add_picture_from_file(sheet, cell, path, PictureInsertType::PLACE_OVER_CELLS, opts)
111    }
112
113    /// Add a picture to a worksheet from a file path with an explicit insert
114    /// type, for example [PictureInsertType::PLACE_IN_CELL] (Excel 365) or
115    /// [PictureInsertType::DISPIMG] (WPS) to embed the picture in the cell.
116    pub fn add_picture_from_file(
117        &mut self,
118        sheet: &str,
119        cell: &str,
120        path: &str,
121        insert_type: PictureInsertType,
122        opts: Option<&GraphicOptions>,
123    ) -> Result<()> {
124        let ext = std::path::Path::new(path)
125            .extension()
126            .and_then(|e| e.to_str())
127            .unwrap_or("")
128            .to_lowercase();
129        let ext = format!(".{ext}");
130        if !supported_image_types().contains_key(&ext) {
131            return Err(Box::new(ErrImgExt));
132        }
133        let file = std::fs::read(path)?;
134        let pic = Picture {
135            extension: ext,
136            file,
137            format: opts.cloned(),
138            insert_type,
139            ..Default::default()
140        };
141        self.add_picture_from_bytes(sheet, cell, &pic)
142    }
143
144    /// Add a picture to a worksheet from raw bytes.
145    pub fn add_picture_from_bytes(&mut self, sheet: &str, cell: &str, pic: &Picture) -> Result<()> {
146        if pic.insert_type == PictureInsertType::PLACE_IN_CELL {
147            return self.add_in_cell_picture(sheet, cell, pic);
148        }
149        if pic.insert_type == PictureInsertType::DISPIMG {
150            return self.add_dispimg_picture(sheet, cell, pic);
151        }
152        if pic.insert_type != PictureInsertType::PLACE_OVER_CELLS {
153            return Err(Box::new(ErrParameterInvalid));
154        }
155        let mut ext = pic.extension.clone();
156        if ext.is_empty() {
157            if let Some(detected) = detect_image_extension(&pic.file) {
158                ext = detected;
159            }
160        }
161        let types = supported_image_types();
162        let mapped_ext = types.get(&ext.to_lowercase()).cloned().unwrap_or(ext);
163        if !types.contains_key(&mapped_ext.to_lowercase()) {
164            return Err(Box::new(ErrImgExt));
165        }
166        let options = parse_picture_options(pic)?;
167        let dims = image_dimensions(&pic.file, &mapped_ext).ok_or_else(|| Box::new(ErrImgLoad))?;
168        let _ = self.work_sheet_reader(sheet)?;
169
170        let drawing_id = self.count_drawings() + 1;
171        let drawing_xml = format!("xl/drawings/drawing{drawing_id}.xml");
172        let (drawing_id, drawing_xml) = self.prepare_drawing(sheet, drawing_id, &drawing_xml)?;
173        let drawing_rels = format!("xl/drawings/_rels/drawing{drawing_id}.xml.rels");
174
175        let media = self.add_media(&pic.file, &mapped_ext);
176        let media_target = format!("..{}", media.trim_start_matches("xl"));
177        let mut drawing_rid = 0;
178        if let Ok(Some(rels)) = self.rels_reader(&drawing_rels) {
179            for rel in &rels.relationships {
180                if rel.r#type == SOURCE_RELATIONSHIP_IMAGE && rel.target == media_target {
181                    drawing_rid = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
182                    break;
183                }
184            }
185        }
186        if drawing_rid == 0 {
187            drawing_rid =
188                self.add_rels(&drawing_rels, SOURCE_RELATIONSHIP_IMAGE, &media_target, "");
189        }
190
191        let mut hyperlink_rid = 0;
192        let mut hyperlink_type = String::new();
193        if !options.hyperlink.is_empty() && !options.hyperlink_type.is_empty() {
194            if options.hyperlink_type == "External" {
195                hyperlink_type = "External".to_string();
196            }
197            hyperlink_rid = self.add_rels(
198                &drawing_rels,
199                SOURCE_RELATIONSHIP_HYPER_LINK,
200                &options.hyperlink,
201                &hyperlink_type,
202            );
203        }
204
205        self.add_drawing_picture(
206            sheet,
207            &drawing_xml,
208            cell,
209            &mapped_ext,
210            drawing_rid,
211            hyperlink_rid,
212            dims,
213            &options,
214        )?;
215        self.add_content_type_part(drawing_id, "drawings")?;
216        self.add_sheet_name_space(sheet, NAMESPACE_SPREADSHEET);
217        Ok(())
218    }
219
220    /// Embed a picture into a cell using the Microsoft rich data mechanism
221    /// (Excel 365 "Place in Cell").
222    fn add_in_cell_picture(&mut self, sheet: &str, cell: &str, pic: &Picture) -> Result<()> {
223        use crate::constants::*;
224        let mapped_ext = resolve_picture_extension(pic)?;
225        let _ = self.work_sheet_reader(sheet)?;
226        let media = self.add_media(&pic.file, &mapped_ext);
227        let media_target = format!("..{}", media.trim_start_matches("xl"));
228
229        // Reference the media part from `xl/richData/_rels/richValueRel.xml.rels`.
230        let mut embed_rid = 0;
231        if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_RD_RICH_VALUE_REL_RELS) {
232            for rel in &rels.relationships {
233                if rel.r#type == SOURCE_RELATIONSHIP_IMAGE && rel.target == media_target {
234                    embed_rid = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
235                    break;
236                }
237            }
238        }
239        if embed_rid == 0 {
240            embed_rid = self.add_rels(
241                DEFAULT_XML_PATH_RD_RICH_VALUE_REL_RELS,
242                SOURCE_RELATIONSHIP_IMAGE,
243                &media_target,
244                "",
245            );
246        }
247
248        // Append the relationship ID to `xl/richData/richValueRel.xml`.
249        let mut rvr = self.rich_value_rel_reader()?;
250        rvr.rels.push(crate::xml::metadata::XlsxRichValueRelRelationship {
251            id: format!("rId{embed_rid}"),
252        });
253        let rel_idx = rvr.rels.len() - 1;
254        let mut out = xml_to_string(&rvr).unwrap_or_default().into_bytes();
255        crate::file::replace_root_namespace_attributes(
256            &mut out,
257            &format!("xmlns=\"{NAMESPACE_RICH_DATA_2}\""),
258        )?;
259        self.save_file_list(DEFAULT_XML_PATH_RD_RICH_VALUE_REL, &out);
260
261        // Ensure the `_localImage` structure exists in `rdRichValueStructure.xml`.
262        let mut structs = self.rich_value_structures_reader()?;
263        let struct_idx = match structs.s.iter().position(|s| s.t == "_localImage") {
264            Some(idx) => idx,
265            None => {
266                structs
267                    .s
268                    .push(crate::xml::metadata::XlsxRichValueStructure {
269                        t: "_localImage".to_string(),
270                        k: vec![
271                            crate::xml::metadata::XlsxRichValueKey {
272                                n: "_rvRel:LocalImageIdentifier".to_string(),
273                                t: Some("i".to_string()),
274                            },
275                            crate::xml::metadata::XlsxRichValueKey {
276                                n: "CalcOrigin".to_string(),
277                                t: Some("i".to_string()),
278                            },
279                        ],
280                    });
281                structs.s.len() - 1
282            }
283        };
284        structs.count = Some(structs.s.len() as i64);
285        let mut out = xml_to_string(&structs).unwrap_or_default().into_bytes();
286        crate::file::replace_root_namespace_attributes(
287            &mut out,
288            &format!(
289                "xmlns=\"{NAMESPACE_RICH_DATA}\" count=\"{}\"",
290                structs.s.len()
291            ),
292        )?;
293        self.save_file_list(DEFAULT_XML_PATH_RD_RICH_VALUE_STRUCTURE, &out);
294
295        // Append the rich value to `rdrichvalue.xml`.
296        let mut rvdata = self.rich_value_reader()?;
297        rvdata.rv.push(crate::xml::metadata::XlsxRichValue {
298            s: struct_idx as i64,
299            v: vec![rel_idx.to_string(), "5".to_string()],
300            fb: None,
301        });
302        rvdata.count = Some(rvdata.rv.len() as i64);
303        let rv_idx = rvdata.rv.len() - 1;
304        let mut out = xml_to_string(&rvdata).unwrap_or_default().into_bytes();
305        crate::file::replace_root_namespace_attributes(
306            &mut out,
307            &format!(
308                "xmlns=\"{NAMESPACE_RICH_DATA}\" count=\"{}\"",
309                rvdata.rv.len()
310            ),
311        )?;
312        self.save_file_list(DEFAULT_XML_PATH_RD_RICH_VALUE, &out);
313
314        // Register the rich value in `xl/metadata.xml` and tag the cell.
315        let vm = self.upsert_in_cell_metadata(rv_idx);
316        let path = self
317            .get_sheet_xml_path(sheet)
318            .ok_or_else(|| crate::errors::ErrSheetNotExist {
319                sheet_name: sheet.to_string(),
320            })?;
321        let mut ws = self.work_sheet_reader(sheet)?;
322        {
323            let c = crate::cell::get_or_make_cell(&mut ws, cell);
324            c.t = Some("e".to_string());
325            c.v = Some(FORMULA_ERROR_VALUE.to_string());
326            c.vm = Some(vm);
327        }
328        crate::cell::update_dimension(&mut ws)?;
329        self.sheet.insert(path, ws);
330
331        // Register package-level content types and workbook relationships.
332        crate::sheet::set_content_type_part_image_extensions(self)?;
333        self.ensure_content_type_override("/xl/metadata.xml", CONTENT_TYPE_SHEET_METADATA)?;
334        self.ensure_content_type_override(
335            "/xl/richData/rdrichvalue.xml",
336            CONTENT_TYPE_RD_RICH_VALUE,
337        )?;
338        self.ensure_content_type_override(
339            "/xl/richData/rdRichValueStructure.xml",
340            CONTENT_TYPE_RD_RICH_VALUE_STRUCTURE,
341        )?;
342        self.ensure_content_type_override(
343            "/xl/richData/richValueRel.xml",
344            CONTENT_TYPE_RICH_VALUE_REL,
345        )?;
346        self.ensure_workbook_rel(SOURCE_RELATIONSHIP_SHEET_METADATA, "metadata.xml");
347        self.ensure_workbook_rel(SOURCE_RELATIONSHIP_RD_RICH_VALUE, "richData/rdrichvalue.xml");
348        self.ensure_workbook_rel(
349            SOURCE_RELATIONSHIP_RD_RICH_VALUE_STRUCTURE,
350            "richData/rdRichValueStructure.xml",
351        );
352        self.ensure_workbook_rel(SOURCE_RELATIONSHIP_RICH_VALUE_REL, "richData/richValueRel.xml");
353        Ok(())
354    }
355
356    /// Embed a picture into a cell using the Kingsoft WPS Office `DISPIMG`
357    /// mechanism.
358    fn add_dispimg_picture(&mut self, sheet: &str, cell: &str, pic: &Picture) -> Result<()> {
359        use crate::constants::*;
360        let mapped_ext = resolve_picture_extension(pic)?;
361        let dims =
362            image_dimensions(&pic.file, &mapped_ext).ok_or_else(|| Box::new(ErrImgLoad))?;
363        let _ = self.work_sheet_reader(sheet)?;
364        let media = self.add_media(&pic.file, &mapped_ext);
365        let media_target = media.trim_start_matches("xl/").to_string();
366
367        // Reference the media part from `xl/_rels/cellimages.xml.rels`.
368        let mut embed_rid = 0;
369        if let Ok(Some(rels)) = self.rels_reader(DEFAULT_XML_PATH_CELL_IMAGES_RELS) {
370            for rel in &rels.relationships {
371                if rel.r#type == SOURCE_RELATIONSHIP_IMAGE && rel.target == media_target {
372                    embed_rid = rel.id.trim_start_matches("rId").parse().unwrap_or(0);
373                    break;
374                }
375            }
376        }
377        if embed_rid == 0 {
378            embed_rid = self.add_rels(
379                DEFAULT_XML_PATH_CELL_IMAGES_RELS,
380                SOURCE_RELATIONSHIP_IMAGE,
381                &media_target,
382                "",
383            );
384        }
385
386        // Append the picture entry to `xl/cellimages.xml`.
387        let img_id = format!("ID_{:032X}", rand::random::<u128>());
388        let alt_text = pic
389            .format
390            .as_ref()
391            .map(|f| f.alt_text.clone())
392            .unwrap_or_default();
393        let mut cell_images = self.cell_images_reader()?;
394        let cnv_id = 1000 + cell_images.cell_image.len() as i32 + 1;
395        cell_images
396            .cell_image
397            .push(crate::xml::decode_drawing::DecodeCellImage {
398                pic: crate::xml::decode_drawing::DecodePic {
399                    nv_pic_pr: crate::xml::decode_drawing::DecodeNvPicPr {
400                        c_nv_pr: crate::xml::decode_drawing::DecodeCNvPr {
401                            id: cnv_id,
402                            name: img_id.clone(),
403                            descr: alt_text,
404                            ..Default::default()
405                        },
406                        ..Default::default()
407                    },
408                    blip_fill: crate::xml::decode_drawing::DecodeBlipFill {
409                        blip: crate::xml::decode_drawing::DecodeBlip {
410                            embed: format!("rId{embed_rid}"),
411                            cstate: Some("print".to_string()),
412                            ..Default::default()
413                        },
414                        ..Default::default()
415                    },
416                    sp_pr: crate::xml::decode_drawing::DecodeSpPr {
417                        xfrm: crate::xml::decode_drawing::DecodeXfrm {
418                            ext: crate::xml::decode_drawing::DecodePositiveSize2D {
419                                cx: dims.0 as i64 * EMU as i64,
420                                cy: dims.1 as i64 * EMU as i64,
421                                ..Default::default()
422                            },
423                            ..Default::default()
424                        },
425                        ..Default::default()
426                    },
427                },
428            });
429        self.save_file_list(
430            DEFAULT_XML_PATH_CELL_IMAGES,
431            build_cell_images_xml(&cell_images).as_bytes(),
432        );
433
434        // Write the DISPIMG formula into the target cell.
435        let path = self
436            .get_sheet_xml_path(sheet)
437            .ok_or_else(|| crate::errors::ErrSheetNotExist {
438                sheet_name: sheet.to_string(),
439            })?;
440        let mut ws = self.work_sheet_reader(sheet)?;
441        {
442            let c = crate::cell::get_or_make_cell(&mut ws, cell);
443            c.t = Some("str".to_string());
444            c.f = Some(crate::xml::worksheet::XlsxF {
445                content: format!("_xlfn.DISPIMG(\"{img_id}\",1)"),
446                ..Default::default()
447            });
448            c.v = Some(img_id);
449        }
450        crate::cell::update_dimension(&mut ws)?;
451        self.sheet.insert(path, ws);
452
453        // Register package-level content types and the workbook relationship.
454        crate::sheet::set_content_type_part_image_extensions(self)?;
455        self.ensure_content_type_override("/xl/cellimages.xml", CONTENT_TYPE_WPS_CELL_IMAGES)?;
456        self.ensure_workbook_rel(SOURCE_RELATIONSHIP_CELL_IMAGES, "cellimages.xml");
457        Ok(())
458    }
459
460    /// Ensure `[Content_Types].xml` contains an override for the given part.
461    fn ensure_content_type_override(&self, part_name: &str, content_type: &str) -> Result<()> {
462        let mut ct = self.content_types_reader()?;
463        let exists = ct.entries.iter().any(|e| {
464            matches!(e, crate::xml::content_types::XlsxContentTypeEntry::Override(o) if o.part_name == part_name)
465        });
466        if !exists {
467            ct.entries
468                .push(crate::xml::content_types::XlsxContentTypeEntry::Override(
469                    crate::xml::content_types::XlsxOverride {
470                        part_name: part_name.to_string(),
471                        content_type: content_type.to_string(),
472                    },
473                ));
474            *self.content_types.lock().unwrap() = Some(ct);
475        }
476        Ok(())
477    }
478
479    /// Ensure the workbook relationships part contains a relationship of the
480    /// given type.
481    fn ensure_workbook_rel(&self, rel_type: &str, target: &str) {
482        let path = self.get_workbook_rels_path();
483        let mut rels = self.rels_reader(&path).unwrap_or_default().unwrap_or_default();
484        if rels.relationships.iter().any(|r| r.r#type == rel_type) {
485            return;
486        }
487        let max_rid = rels
488            .relationships
489            .iter()
490            .filter_map(|r| r.id.trim_start_matches("rId").parse::<i32>().ok())
491            .max()
492            .unwrap_or(0);
493        rels.relationships
494            .push(crate::xml::workbook::XlsxRelationship {
495                id: format!("rId{}", max_rid + 1),
496                r#type: rel_type.to_string(),
497                target: target.to_string(),
498                target_mode: None,
499            });
500        self.relationships.insert(path, rels);
501    }
502
503    /// Add or update `xl/metadata.xml` for a new in-cell rich value, returning
504    /// the 1-based value metadata index (`vm`) for the cell.
505    fn upsert_in_cell_metadata(&self, rv_idx: usize) -> u64 {
506        use crate::constants::*;
507        let raw = self.read_xml(DEFAULT_XML_PATH_METADATA);
508        let text = String::from_utf8_lossy(&raw).into_owned();
509        let body = match text.find("?>") {
510            Some(pos) => &text[pos + 2..],
511            None => text.as_str(),
512        };
513        let rvb = format!(
514            "<bk><extLst><ext uri=\"{{3e2802c4-a4d2-4d8b-9148-e3be6c30e623}}\"><xlrd:rvb i=\"{rv_idx}\"/></ext></extLst></bk>"
515        );
516        if !body.contains("<metadata") {
517            let content = format!(
518                "<metadata xmlns=\"{NAMESPACE_SPREADSHEET}\" xmlns:xlrd=\"{NAMESPACE_RICH_DATA_2}\">\
519<metadataTypes count=\"1\"><metadataType name=\"XLRICHVALUE\" minSupportedVersion=\"120000\" copy=\"1\" pasteAll=\"1\" pasteValues=\"1\" merge=\"1\" splitFirst=\"1\" rowColShift=\"1\" clearAll=\"1\" clearFormats=\"1\" clearContents=\"1\" clearComments=\"1\" assign=\"1\" coerce=\"1\"/></metadataTypes>\
520<futureMetadata name=\"XLRICHVALUE\" count=\"1\">{rvb}</futureMetadata>\
521<valueMetadata count=\"1\"><bk><rc t=\"1\" v=\"{rv_idx}\"/></bk></valueMetadata>\
522</metadata>"
523            );
524            self.save_file_list(DEFAULT_XML_PATH_METADATA, content.as_bytes());
525            return 1;
526        }
527        let mut s = body.to_string();
528        // Append a value metadata block after the existing ones.
529        let vm = if let Some(open) = s.find("<valueMetadata") {
530            let gt = open + s[open..].find('>').unwrap_or(0);
531            let tag = s[open..=gt].to_string();
532            let count = parse_count_attr(&tag).unwrap_or(0);
533            let new_tag = set_count_attr(&tag, count + 1);
534            s.replace_range(open..=gt, &new_tag);
535            if let Some(close) = s.find("</valueMetadata>") {
536                s.insert_str(close, &format!("<bk><rc t=\"1\" v=\"{rv_idx}\"/></bk>"));
537            }
538            count + 1
539        } else {
540            if let Some(close) = s.find("</metadata>") {
541                s.insert_str(
542                    close,
543                    &format!("<valueMetadata count=\"1\"><bk><rc t=\"1\" v=\"{rv_idx}\"/></bk></valueMetadata>"),
544                );
545            }
546            1
547        };
548        // Keep the XLRICHVALUE future metadata in sync.
549        if let Some(open) = s.find("<futureMetadata name=\"XLRICHVALUE\"") {
550            let gt = open + s[open..].find('>').unwrap_or(0);
551            let tag = s[open..=gt].to_string();
552            let count = parse_count_attr(&tag).unwrap_or(0);
553            let new_tag = set_count_attr(&tag, count + 1);
554            s.replace_range(open..=gt, &new_tag);
555            if let Some(close) = s[open..].find("</futureMetadata>") {
556                s.insert_str(open + close, &rvb);
557            }
558        } else {
559            let fm = format!("<futureMetadata name=\"XLRICHVALUE\" count=\"1\">{rvb}</futureMetadata>");
560            let pos = s
561                .find("<cellMetadata")
562                .or_else(|| s.find("<valueMetadata"))
563                .or_else(|| s.find("</metadata>"));
564            if let Some(pos) = pos {
565                s.insert_str(pos, &fm);
566            }
567        }
568        self.save_file_list(DEFAULT_XML_PATH_METADATA, s.as_bytes());
569        vm as u64
570    }
571
572    /// Return all pictures anchored at a given cell in a worksheet.
573    pub fn get_pictures(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
574        let mut pics = self.get_cell_images(sheet, cell)?;
575        let ws = self.work_sheet_reader(sheet)?;
576        if ws.drawing.is_none() {
577            return Ok(pics);
578        }
579        let (col, row) = cell_name_to_coordinates(cell)?;
580        let col = col - 1;
581        let row = row - 1;
582        let drawing_xml = self
583            .get_sheet_relationships_target_by_id(
584                sheet,
585                ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
586            )
587            .replace("..", "xl")
588            .trim_start_matches('/')
589            .to_string();
590        let drawing_rels = drawing_xml
591            .replace("xl/drawings", "xl/drawings/_rels")
592            .replace(".xml", ".xml.rels");
593        for pic in self.get_picture(row, col, &drawing_xml, &drawing_rels)? {
594            if !pics.iter().any(|p| p.file == pic.file) {
595                pics.push(pic);
596            }
597        }
598        Ok(pics)
599    }
600
601    /// Return all picture cell references in a worksheet.
602    ///
603    /// Equivalent to Go `GetPictureCells`.
604    pub fn get_picture_cells(&self, sheet: &str) -> Result<Vec<String>> {
605        let mut cells = self.get_image_cells(sheet)?;
606        let ws = self.work_sheet_reader(sheet)?;
607        if ws.drawing.is_none() {
608            return Ok(cells);
609        }
610        let drawing_xml = self
611            .get_sheet_relationships_target_by_id(
612                sheet,
613                ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
614            )
615            .replace("..", "xl")
616            .trim_start_matches('/')
617            .to_string();
618        let drawing_rels = drawing_xml
619            .replace("xl/drawings", "xl/drawings/_rels")
620            .replace(".xml", ".xml.rels");
621        let (wsdr, _) = self.drawing_parser(&drawing_xml)?;
622        for anchor in wsdr
623            .one_cell_anchor
624            .iter()
625            .chain(wsdr.two_cell_anchor.iter())
626        {
627            let Some(pic) = &anchor.pic else {
628                continue;
629            };
630            let r_id = &pic.blip_fill.blip.embed;
631            if self.get_drawing_relationship(&drawing_rels, r_id).is_none() {
632                continue;
633            }
634            if let Some(from) = &anchor.from {
635                let cell =
636                    coordinates_to_cell_name(from.col as i32 + 1, from.row as i32 + 1, false)?;
637                if !cells.contains(&cell) {
638                    cells.push(cell);
639                }
640            }
641        }
642        Ok(cells)
643    }
644
645    /// Read the Kingsoft WPS Office embedded cell images part.
646    fn cell_images_reader(&self) -> Result<DecodeCellImages> {
647        let data = self.apply_charset_transcoder(&self.read_xml(DEFAULT_XML_PATH_CELL_IMAGES))?;
648        let data = crate::file::namespace_strict_to_transitional(&data);
649        if data.is_empty() {
650            return Ok(DecodeCellImages::default());
651        }
652        Ok(xml_from_reader(data.as_slice()).unwrap_or_default())
653    }
654
655    /// Return all cell images and Kingsoft WPS Office embedded image cells in a
656    /// worksheet.
657    fn get_image_cells(&self, sheet: &str) -> Result<Vec<String>> {
658        let ws = self.work_sheet_reader(sheet)?;
659        let mut cells = Vec::new();
660        for row in &ws.sheet_data.row {
661            for c in &row.c {
662                let Some(cell_ref) = c.r.as_deref() else {
663                    continue;
664                };
665                if let Some(f) = &c.f {
666                    if !f.content.is_empty() && is_dispimg_formula(&f.content) {
667                        self.calc_cell_value(sheet, cell_ref)?;
668                        cells.push(cell_ref.to_string());
669                    }
670                }
671                let mut pic = Picture {
672                    format: Some(GraphicOptions::default()),
673                    ..Default::default()
674                };
675                if self.get_image_cell_rel(c, &mut pic)?.is_some() {
676                    cells.push(cell_ref.to_string());
677                }
678            }
679        }
680        Ok(cells)
681    }
682
683    /// Return the relationship of a cell image by a rich value rel index.
684    fn get_rich_data_rich_value_rel(
685        &self,
686        val: &str,
687    ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
688        let idx = val.parse::<usize>()?;
689        let rich_value_rel = self.rich_value_rel_reader()?;
690        let r_id = match rich_value_rel.rels.get(idx) {
691            Some(rel) => rel.id.clone(),
692            None => return Ok(None),
693        };
694        let rel = self.get_rich_data_rich_value_rel_relationship(&r_id);
695        if rel
696            .as_ref()
697            .map_or(false, |r| r.r#type != SOURCE_RELATIONSHIP_IMAGE)
698        {
699            return Ok(None);
700        }
701        Ok(rel)
702    }
703
704    /// Return the relationship of a web image by a web image rich value index.
705    fn get_rich_data_web_images_rel(
706        &self,
707        val: &str,
708    ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
709        let idx = val.parse::<usize>()?;
710        let web_images = self.rich_value_web_image_reader()?;
711        let r_id = match web_images.web_image_srd.get(idx) {
712            Some(img) => img.blip.r_id.clone().unwrap_or_default(),
713            None => return Ok(None),
714        };
715        let rel = self.get_rich_value_web_image_relationship(&r_id);
716        if rel
717            .as_ref()
718            .map_or(false, |r| r.r#type != SOURCE_RELATIONSHIP_IMAGE)
719        {
720            return Ok(None);
721        }
722        Ok(rel)
723    }
724
725    /// Return the cell image relationship for a worksheet cell.
726    fn get_image_cell_rel(
727        &self,
728        c: &XlsxC,
729        pic: &mut Picture,
730    ) -> Result<Option<crate::xml::workbook::XlsxRelationship>> {
731        let vm = match c.vm {
732            Some(vm) => vm,
733            None => return Ok(None),
734        };
735        if c.v.as_deref() != Some(FORMULA_ERROR_VALUE) {
736            return Ok(None);
737        }
738        let metadata = self.metadata_reader()?;
739        let vmd = match metadata.value_metadata {
740            Some(vmd) => vmd,
741            None => return Ok(None),
742        };
743        let rc = match vmd
744            .bk
745            .get((vm as usize).saturating_sub(1))
746            .and_then(|b| b.rc.first())
747        {
748            Some(rc) => rc,
749            None => return Ok(None),
750        };
751        let rich_value_idx = rc.v as usize;
752        let rich_value = self.rich_value_reader()?;
753        let rv = match rich_value.rv.get(rich_value_idx) {
754            Some(rv) => rv,
755            None => return Ok(None),
756        };
757        let rv_structures = self.rich_value_structures_reader()?;
758        let rv_struct = match rv_structures.s.get(rv.s as usize) {
759            Some(s) => s,
760            None => return Ok(None),
761        };
762        if rv_struct.k.len() != rv.v.len() {
763            return Ok(None);
764        }
765        if let Some(idx) = rich_value_key_idx(&rv_struct.k, "Text") {
766            if let Some(fmt) = &mut pic.format {
767                fmt.alt_text = rv.v[idx].clone();
768            }
769        }
770        if let Some(idx) = rich_value_key_idx(&rv_struct.k, "_rvRel:LocalImageIdentifier") {
771            pic.insert_type = PictureInsertType::PLACE_IN_CELL;
772            return self.get_rich_data_rich_value_rel(&rv.v[idx]);
773        }
774        if let Some(idx) = rich_value_key_idx(&rv_struct.k, "WebImageIdentifier") {
775            pic.insert_type = PictureInsertType::IMAGE;
776            return self.get_rich_data_web_images_rel(&rv.v[idx]);
777        }
778        Ok(None)
779    }
780
781    /// Return cell images and Kingsoft WPS Office embedded cell images for a
782    /// given worksheet and cell reference.
783    fn get_cell_images(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
784        let mut pics = self.get_disp_images(sheet, cell)?;
785        let ws = self.work_sheet_reader(sheet)?;
786        let c = match crate::cell::find_cell(&ws, cell) {
787            Some(c) => c.clone(),
788            None => return Ok(pics),
789        };
790        let mut pic = Picture {
791            format: Some(GraphicOptions::default()),
792            insert_type: PictureInsertType::PLACE_IN_CELL,
793            ..Default::default()
794        };
795        if let Some(rel) = self.get_image_cell_rel(&c, &mut pic)? {
796            pic.extension = std::path::Path::new(&rel.target)
797                .extension()
798                .and_then(|e| e.to_str())
799                .map(|e| format!(".{e}"))
800                .unwrap_or_default();
801            let target = rel
802                .target
803                .replace("..", "xl")
804                .trim_start_matches('/')
805                .to_string();
806            if let Some(bytes) = self.pkg.get(&target) {
807                pic.file = bytes.value().clone();
808                pics.push(pic);
809            }
810        }
811        Ok(pics)
812    }
813
814    /// Return Kingsoft WPS Office embedded cell images for a given worksheet and
815    /// cell reference.
816    fn get_disp_images(&self, sheet: &str, cell: &str) -> Result<Vec<Picture>> {
817        let formula = self.get_cell_formula(sheet, cell)?;
818        if !is_dispimg_formula(&formula) {
819            return Ok(Vec::new());
820        }
821        let img_id = self.calc_cell_value(sheet, cell)?;
822        let cell_images = self.cell_images_reader()?;
823        let rels = match self.rels_reader(DEFAULT_XML_PATH_CELL_IMAGES_RELS)? {
824            Some(rels) => rels,
825            None => return Ok(Vec::new()),
826        };
827        let mut pics = Vec::new();
828        for cell_img in &cell_images.cell_image {
829            if cell_img.pic.nv_pic_pr.c_nv_pr.name != img_id {
830                continue;
831            }
832            for rel in &rels.relationships {
833                if rel.id == cell_img.pic.blip_fill.blip.embed {
834                    let mut pic = Picture {
835                        extension: std::path::Path::new(&rel.target)
836                            .extension()
837                            .and_then(|e| e.to_str())
838                            .map(|e| format!(".{e}"))
839                            .unwrap_or_default(),
840                        format: Some(GraphicOptions::default()),
841                        insert_type: PictureInsertType::DISPIMG,
842                        ..Default::default()
843                    };
844                    let target = format!("xl/{}", rel.target);
845                    if let Some(bytes) = self.pkg.get(&target) {
846                        pic.file = bytes.value().clone();
847                        if let Some(fmt) = &mut pic.format {
848                            fmt.alt_text = cell_img.pic.nv_pic_pr.c_nv_pr.descr.clone();
849                            fmt.name = cell_img.pic.nv_pic_pr.c_nv_pr.name.clone();
850                        }
851                        pics.push(pic);
852                    }
853                }
854            }
855        }
856        Ok(pics)
857    }
858}
859
860fn is_dispimg_formula(content: &str) -> bool {
861    let content = content.strip_prefix('=').unwrap_or(content);
862    let content = content.strip_prefix("_xlfn.").unwrap_or(content);
863    content.starts_with("DISPIMG")
864}
865
866/// Resolve and validate the image file extension of a picture.
867fn resolve_picture_extension(pic: &Picture) -> Result<String> {
868    let mut ext = pic.extension.clone();
869    if ext.is_empty() {
870        if let Some(detected) = detect_image_extension(&pic.file) {
871            ext = detected;
872        }
873    }
874    let types = supported_image_types();
875    let mapped_ext = types.get(&ext.to_lowercase()).cloned().unwrap_or(ext);
876    if !types.contains_key(&mapped_ext.to_lowercase()) {
877        return Err(Box::new(ErrImgExt));
878    }
879    Ok(mapped_ext)
880}
881
882/// Serialize the WPS `xl/cellimages.xml` part. The decode structs intentionally
883/// ignore namespace prefixes, so the document is built by hand to keep the
884/// conventional `etc:`, `xdr:`, `a:` and `r:` prefixes used by WPS Office.
885fn build_cell_images_xml(images: &DecodeCellImages) -> String {
886    use crate::constants::*;
887    let mut out = format!(
888        "<etc:cellImages xmlns:etc=\"{NAMESPACE_WPS_ET_CUSTOM_DATA}\" xmlns:xdr=\"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing\" xmlns:a=\"{NAMESPACE_DRAWING_ML_MAIN}\" xmlns:r=\"{SOURCE_RELATIONSHIP}\">"
889    );
890    for img in &images.cell_image {
891        let pic = &img.pic;
892        let cnv = &pic.nv_pic_pr.c_nv_pr;
893        let blip = &pic.blip_fill.blip;
894        let cstate = blip.cstate.as_deref().unwrap_or("");
895        out.push_str(&format!(
896            "<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id=\"{}\" name=\"{}\" descr=\"{}\"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed=\"{}\" cstate=\"{}\"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"{}\" cy=\"{}\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic></etc:cellImage>",
897            cnv.id,
898            escape_xml_attr(&cnv.name),
899            escape_xml_attr(&cnv.descr),
900            blip.embed,
901            cstate,
902            pic.sp_pr.xfrm.ext.cx,
903            pic.sp_pr.xfrm.ext.cy,
904        ));
905    }
906    out.push_str("</etc:cellImages>");
907    out
908}
909
910/// Escape a string for use in an XML attribute value.
911fn escape_xml_attr(s: &str) -> String {
912    s.replace('&', "&amp;")
913        .replace('<', "&lt;")
914        .replace('>', "&gt;")
915        .replace('"', "&quot;")
916}
917
918/// Return the value of the `count` attribute in an XML start tag.
919fn parse_count_attr(tag: &str) -> Option<usize> {
920    let re = regex::Regex::new(r#"count="(\d+)""#).unwrap();
921    re.captures(tag)
922        .and_then(|c| c.get(1))
923        .and_then(|m| m.as_str().parse().ok())
924}
925
926/// Replace the value of the `count` attribute in an XML start tag.
927fn set_count_attr(tag: &str, count: usize) -> String {
928    let re = regex::Regex::new(r#"count="\d+""#).unwrap();
929    re.replace(tag, format!("count=\"{count}\"")).into_owned()
930}
931
932fn cell_in_merge_range(cell: &str, range_ref: &str) -> Result<bool> {
933    let (col, row) = cell_name_to_coordinates(cell)?;
934    if !range_ref.contains(':') {
935        return Ok(false);
936    }
937    let mut coords = range_ref_to_coordinates(range_ref)?;
938    sort_coordinates(&mut coords)?;
939    Ok(col >= coords[0] && col <= coords[2] && row >= coords[1] && row <= coords[3])
940}
941
942fn rich_value_key_idx(
943    keys: &[crate::xml::metadata::XlsxRichValueKey],
944    name: &str,
945) -> Option<usize> {
946    keys.iter().position(|k| k.n == name)
947}
948
949impl File {
950    /// Delete all pictures in a cell.
951    pub fn delete_picture(&mut self, sheet: &str, cell: &str) -> Result<()> {
952        let (col, row) = cell_name_to_coordinates(cell)?;
953        let col = col - 1;
954        let row = row - 1;
955        let ws = self.work_sheet_reader(sheet)?;
956        if ws.drawing.is_none() {
957            return Ok(());
958        }
959        let drawing_xml = self
960            .get_sheet_relationships_target_by_id(
961                sheet,
962                ws.drawing.as_ref().unwrap().rid.as_deref().unwrap_or(""),
963            )
964            .replace("..", "xl")
965            .trim_start_matches('/')
966            .to_string();
967        let drawing_rels = format!(
968            "xl/drawings/_rels/{}.rels",
969            std::path::Path::new(&drawing_xml)
970                .file_name()
971                .unwrap_or_default()
972                .to_string_lossy()
973        );
974        let r_ids = self.delete_drawing(col, row, &drawing_xml, "Pic")?;
975        for r_id in r_ids {
976            if let Some(rel) = self.get_drawing_relationship(&drawing_rels, &r_id) {
977                let target = rel
978                    .target
979                    .replace("../", "xl/")
980                    .trim_start_matches('/')
981                    .to_string();
982                let mut used = false;
983                for entry in self.pkg.iter() {
984                    if entry.key().contains("xl/drawings/_rels/drawing")
985                        && *entry.key() != drawing_rels
986                    {
987                        if let Ok(Some(rels)) = self.rels_reader(entry.key()) {
988                            for r in &rels.relationships {
989                                if r.r#type == SOURCE_RELATIONSHIP_IMAGE
990                                    && std::path::Path::new(&r.target)
991                                        .file_name()
992                                        .unwrap_or_default()
993                                        == std::path::Path::new(&rel.target)
994                                            .file_name()
995                                            .unwrap_or_default()
996                                {
997                                    used = true;
998                                }
999                            }
1000                        }
1001                    }
1002                }
1003                if !used {
1004                    self.pkg.remove(&target);
1005                }
1006            }
1007            self.delete_drawing_rels(&drawing_rels, &r_id);
1008        }
1009        Ok(())
1010    }
1011}
1012
1013// ------------------------------------------------------------------
1014// Option parsing
1015// ------------------------------------------------------------------
1016
1017fn parse_picture_options(pic: &Picture) -> Result<GraphicOptions> {
1018    let mut opts = pic.format.clone().unwrap_or_default();
1019    if opts.print_object.is_none() {
1020        opts.print_object = Some(true);
1021    }
1022    if opts.locked.is_none() {
1023        opts.locked = Some(true);
1024    }
1025    if opts.scale_x == 0.0 {
1026        opts.scale_x = DEFAULT_DRAWING_SCALE;
1027    }
1028    if opts.scale_y == 0.0 {
1029        opts.scale_y = DEFAULT_DRAWING_SCALE;
1030    }
1031    if !opts.positioning.is_empty()
1032        && !["oneCell", "twoCell", "absolute"].contains(&opts.positioning.as_str())
1033    {
1034        return Err(crate::errors::new_invalid_optional_value(
1035            "Positioning",
1036            &opts.positioning,
1037            &["oneCell", "twoCell", "absolute"],
1038        )
1039        .into());
1040    }
1041    if count_utf16_string(&opts.alt_text) > MAX_GRAPHIC_ALT_TEXT_LENGTH {
1042        return Err(Box::new(ErrMaxGraphicAltTextLength));
1043    }
1044    if count_utf16_string(&opts.name) > MAX_GRAPHIC_NAME_LENGTH {
1045        return Err(Box::new(ErrMaxGraphicNameLength));
1046    }
1047    Ok(opts)
1048}
1049
1050// ------------------------------------------------------------------
1051// Media management
1052// ------------------------------------------------------------------
1053
1054impl File {
1055    fn count_media(&self) -> i32 {
1056        let mut count = 0;
1057        for entry in self.pkg.iter() {
1058            if entry.key().contains("xl/media/image") {
1059                count += 1;
1060            }
1061        }
1062        count
1063    }
1064
1065    pub(crate) fn add_media(&self, file: &[u8], ext: &str) -> String {
1066        for entry in self.pkg.iter() {
1067            if !entry.key().starts_with("xl/media/image") {
1068                continue;
1069            }
1070            if entry.value() == file {
1071                return entry.key().clone();
1072            }
1073        }
1074        let count = self.count_media();
1075        let name = format!("xl/media/image{}{}", count + 1, ext);
1076        self.pkg.insert(name.clone(), file.to_vec());
1077        name
1078    }
1079}
1080
1081// ------------------------------------------------------------------
1082// Drawing helpers
1083// ------------------------------------------------------------------
1084
1085impl File {
1086    fn drawing_resize(
1087        &self,
1088        sheet: &str,
1089        cell: &str,
1090        width: f64,
1091        height: f64,
1092        opts: &GraphicOptions,
1093    ) -> Result<(i32, i32, i32, i32)> {
1094        let (mut col, mut row) = cell_name_to_coordinates(cell)?;
1095        let mut cell_width = self.get_col_width_pixels(sheet, col)? as f64;
1096        let mut cell_height = self.get_row_height_pixels(sheet, row)? as f64;
1097        let merge_cells = self.get_merge_cells(sheet)?;
1098        let mut rng = Vec::new();
1099        let mut in_merge_cell = false;
1100        for merge in merge_cells {
1101            if in_merge_cell {
1102                break;
1103            }
1104            if let Ok(true) = cell_in_merge_range(cell, &merge) {
1105                if let Ok(mut coords) = range_ref_to_coordinates(&merge) {
1106                    sort_coordinates(&mut coords)?;
1107                    rng = coords;
1108                    in_merge_cell = true;
1109                }
1110            }
1111        }
1112        if in_merge_cell {
1113            cell_width = 0.0;
1114            cell_height = 0.0;
1115            col = rng[0];
1116            row = rng[1];
1117            for c in rng[0]..=rng[2] {
1118                cell_width += self.get_col_width_pixels(sheet, c)? as f64;
1119            }
1120            for r in rng[1]..=rng[3] {
1121                cell_height += self.get_row_height_pixels(sheet, r)? as f64;
1122            }
1123        }
1124        let (mut width, mut height) = (width, height);
1125        if cell_width < width || cell_height < height {
1126            let asp_width = cell_width / width;
1127            let asp_height = cell_height / height;
1128            let asp = asp_width.min(asp_height);
1129            width *= asp;
1130            height *= asp;
1131        }
1132        if opts.auto_fit_ignore_aspect {
1133            width = cell_width;
1134            height = cell_height;
1135        }
1136        Ok((
1137            (width * opts.scale_x) as i32,
1138            (height * opts.scale_y) as i32,
1139            col,
1140            row,
1141        ))
1142    }
1143
1144    fn add_drawing_picture(
1145        &self,
1146        sheet: &str,
1147        drawing_xml: &str,
1148        cell: &str,
1149        ext: &str,
1150        r_id: i32,
1151        hyperlink_rid: i32,
1152        dims: (i32, i32),
1153        opts: &GraphicOptions,
1154    ) -> Result<()> {
1155        let (mut col, mut row) = cell_name_to_coordinates(cell)?;
1156        let (mut width, mut height) = dims;
1157        if !opts.positioning.is_empty()
1158            && !["oneCell", "twoCell", "absolute"].contains(&opts.positioning.as_str())
1159        {
1160            return Err(crate::errors::new_invalid_optional_value(
1161                "Positioning",
1162                &opts.positioning,
1163                &["oneCell", "twoCell", "absolute"],
1164            )
1165            .into());
1166        }
1167        if opts.auto_fit {
1168            (width, height, col, row) =
1169                self.drawing_resize(sheet, cell, width as f64, height as f64, opts)?;
1170        } else {
1171            width = (width as f64 * opts.scale_x) as i32;
1172            height = (height as f64 * opts.scale_y) as i32;
1173        }
1174        let (col_start, row_start, col_end, row_end, x1, y1, x2, y2) =
1175            self.position_object_pixels(sheet, col, row, width, height, opts)?;
1176        let (mut content, c_nv_pr_id) = self.drawing_parser(drawing_xml)?;
1177
1178        let mut anchor = XdrCellAnchor {
1179            edit_as: if opts.positioning.is_empty() {
1180                None
1181            } else {
1182                Some(opts.positioning.clone())
1183            },
1184            from: Some(XlsxFrom {
1185                col: col_start as i64,
1186                col_off: x1 as i64 * EMU as i64,
1187                row: row_start as i64,
1188                row_off: y1 as i64 * EMU as i64,
1189            }),
1190            ..Default::default()
1191        };
1192        if opts.positioning != "oneCell" {
1193            anchor.to = Some(XlsxTo {
1194                col: col_end as i64,
1195                col_off: x2 as i64 * EMU as i64,
1196                row: row_end as i64,
1197                row_off: y2 as i64 * EMU as i64,
1198            });
1199        }
1200
1201        let mut pic = XlsxPic {
1202            nv_pic_pr: XlsxNvPicPr {
1203                c_nv_pr: XlsxCNvPr {
1204                    id: c_nv_pr_id,
1205                    descr: opts.alt_text.clone(),
1206                    name: if opts.name.is_empty() {
1207                        format!("Picture {c_nv_pr_id}")
1208                    } else {
1209                        opts.name.clone()
1210                    },
1211                    ..Default::default()
1212                },
1213                c_nv_pic_pr: XlsxCNvPicPr {
1214                    pic_locks: XlsxPicLocks {
1215                        no_change_aspect: opts.lock_aspect_ratio,
1216                        ..Default::default()
1217                    },
1218                },
1219            },
1220            blip_fill: XlsxBlipFill {
1221                blip: XlsxBlip {
1222                    embed: format!("rId{r_id}"),
1223                    xmlns_r: SOURCE_RELATIONSHIP.to_string(),
1224                    ..Default::default()
1225                },
1226                stretch: XlsxStretch {
1227                    fill_rect: String::new(),
1228                },
1229            },
1230            sp_pr: XlsxSpPr {
1231                xfrm: XlsxXfrm {
1232                    off: XlsxOff { x: 0, y: 0 },
1233                    ext: XlsxPositiveSize2D { cx: 0, cy: 0 },
1234                },
1235                prst_geom: XlsxPrstGeom {
1236                    prst: "rect".to_string(),
1237                },
1238                ..Default::default()
1239            },
1240        };
1241        if hyperlink_rid != 0 {
1242            pic.nv_pic_pr.c_nv_pr.hlink_click = Some(crate::xml::drawing::XlsxHlinkClick {
1243                r_id: Some(format!("rId{hyperlink_rid}")),
1244                xmlns_r: Some(SOURCE_RELATIONSHIP.to_string()),
1245                ..Default::default()
1246            });
1247        }
1248        if ext == ".svg" {
1249            pic.blip_fill.blip.ext_list = Some(crate::xml::drawing::XlsxEGOfficeArtExtensionList {
1250                ext: vec![crate::xml::drawing::XlsxCTOfficeArtExtension {
1251                    uri: EXT_URI_SVG.to_string(),
1252                    svg_blip: crate::xml::drawing::XlsxCTSVGBlip {
1253                        xmlns_asvg: NAMESPACE_DRAWING_2016_SVG.to_string(),
1254                        embed: format!("rId{r_id}"),
1255                        ..Default::default()
1256                    },
1257                }],
1258            });
1259        }
1260        pic.sp_pr.xfrm.ext = XlsxPositiveSize2D {
1261            cx: width as i64 * EMU as i64,
1262            cy: height as i64 * EMU as i64,
1263        };
1264        if opts.positioning == "oneCell" {
1265            let cx = x2 as i64 * EMU as i64;
1266            let cy = y2 as i64 * EMU as i64;
1267            anchor.ext = Some(XlsxPositiveSize2D { cx, cy });
1268            pic.sp_pr.xfrm.ext = XlsxPositiveSize2D { cx, cy };
1269        }
1270        anchor.pic = Some(pic);
1271        anchor.client_data = Some(crate::xml::drawing::XdrClientData {
1272            f_locks_with_sheet: opts.locked.unwrap_or(true),
1273            f_prints_with_sheet: opts.print_object.unwrap_or(true),
1274        });
1275
1276        if opts.positioning == "oneCell" {
1277            content.one_cell_anchor.push(anchor);
1278        } else {
1279            content.two_cell_anchor.push(anchor);
1280        }
1281        self.drawings.insert(drawing_xml.to_string(), content);
1282        Ok(())
1283    }
1284
1285    fn get_picture(
1286        &self,
1287        row: i32,
1288        col: i32,
1289        drawing_xml: &str,
1290        drawing_rels: &str,
1291    ) -> Result<Vec<Picture>> {
1292        let (wsdr, _) = self.drawing_parser(drawing_xml)?;
1293        let mut pics = Vec::new();
1294        for anchor in wsdr
1295            .one_cell_anchor
1296            .iter()
1297            .chain(wsdr.two_cell_anchor.iter())
1298        {
1299            if anchor.pic.is_none() {
1300                continue;
1301            }
1302            if let Some(from) = &anchor.from {
1303                if from.col != col as i64 || from.row != row as i64 {
1304                    continue;
1305                }
1306            }
1307            let pic = anchor.pic.as_ref().unwrap();
1308            let r_id = &pic.blip_fill.blip.embed;
1309            if let Some(rel) = self.get_drawing_relationship(drawing_rels, r_id) {
1310                let target = rel
1311                    .target
1312                    .replace("../", "xl/")
1313                    .trim_start_matches('/')
1314                    .to_string();
1315                if let Some(bytes) = self.pkg.get(&target) {
1316                    let extension = std::path::Path::new(&target)
1317                        .extension()
1318                        .and_then(|e| e.to_str())
1319                        .map(|e| format!(".{e}"))
1320                        .unwrap_or_default();
1321                    let mut format = GraphicOptions {
1322                        scale_x: DEFAULT_DRAWING_SCALE,
1323                        scale_y: DEFAULT_DRAWING_SCALE,
1324                        ..Default::default()
1325                    };
1326                    if let Some(client) = &anchor.client_data {
1327                        format.locked = Some(client.f_locks_with_sheet);
1328                        format.print_object = Some(client.f_prints_with_sheet);
1329                    }
1330                    if anchor.to.is_none() {
1331                        format.positioning = "oneCell".to_string();
1332                    }
1333                    if let Some(from) = &anchor.from {
1334                        format.offset_x = from.col_off / EMU as i64;
1335                        format.offset_y = from.row_off / EMU as i64;
1336                    }
1337                    format.lock_aspect_ratio = pic.nv_pic_pr.c_nv_pic_pr.pic_locks.no_change_aspect;
1338                    format.alt_text = pic.nv_pic_pr.c_nv_pr.descr.clone();
1339                    format.name = pic.nv_pic_pr.c_nv_pr.name.clone();
1340                    calculate_picture_scale(&mut format, bytes.value(), &pic.sp_pr.xfrm.ext);
1341                    pics.push(Picture {
1342                        extension,
1343                        file: bytes.value().clone(),
1344                        format: Some(format),
1345                        insert_type: PictureInsertType::PLACE_OVER_CELLS,
1346                    });
1347                }
1348            }
1349        }
1350        Ok(pics)
1351    }
1352
1353    fn delete_drawing_rels(&self, rels: &str, r_id: &str) {
1354        if let Ok(Some(mut rels_obj)) = self.rels_reader(rels) {
1355            rels_obj.relationships.retain(|r| r.id != r_id);
1356            self.relationships.insert(rels.to_string(), rels_obj);
1357        }
1358    }
1359}
1360
1361fn calculate_picture_scale(format: &mut GraphicOptions, file: &[u8], ext: &XlsxPositiveSize2D) {
1362    if ext.cx <= 0 || ext.cy <= 0 {
1363        return;
1364    }
1365    if let Some((w, h)) = image_dimensions(file, ".png") {
1366        if w > 0 && h > 0 {
1367            format.scale_x = ((ext.cx / EMU as i64) as f64 / w as f64 * 100.0).round() / 100.0;
1368            format.scale_y = ((ext.cy / EMU as i64) as f64 / h as f64 * 100.0).round() / 100.0;
1369        }
1370    }
1371}
1372
1373// ------------------------------------------------------------------
1374// Tests
1375// ------------------------------------------------------------------
1376
1377#[cfg(test)]
1378mod tests {
1379    use super::*;
1380    use crate::Options;
1381
1382    #[test]
1383    fn picture_round_trip() {
1384        let mut f = File::new_with_options(Options::default());
1385        // 1x1 red PNG
1386        let png = vec![
1387            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
1388            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
1389            0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
1390            0x99, 0x63, 0xf8, 0x0f, 0x00, 0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xd8, 0x4e, 0x00,
1391            0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
1392        ];
1393        let pic = Picture {
1394            extension: ".png".to_string(),
1395            file: png.clone(),
1396            format: Some(GraphicOptions {
1397                name: "RedPixel".to_string(),
1398                ..Default::default()
1399            }),
1400            ..Default::default()
1401        };
1402        f.add_picture_from_bytes("Sheet1", "B2", &pic).unwrap();
1403        let pics = f.get_pictures("Sheet1", "B2").unwrap();
1404        assert_eq!(pics.len(), 1);
1405        assert_eq!(pics[0].extension, ".png");
1406        assert_eq!(pics[0].format.as_ref().unwrap().name, "RedPixel");
1407
1408        let cells = f.get_picture_cells("Sheet1").unwrap();
1409        assert_eq!(cells, vec!["B2"]);
1410    }
1411
1412    fn red_pixel_png() -> Vec<u8> {
1413        vec![
1414            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
1415            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
1416            0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08,
1417            0x99, 0x63, 0xf8, 0x0f, 0x00, 0x00, 0x01, 0x01, 0x00, 0x05, 0x18, 0xd8, 0x4e, 0x00,
1418            0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
1419        ]
1420    }
1421
1422    #[test]
1423    fn in_cell_picture_round_trip() {
1424        let png = red_pixel_png();
1425        let path = std::env::temp_dir().join("excelize_rs_in_cell_picture.xlsx");
1426        let path_str = path.to_string_lossy().to_string();
1427        {
1428            let mut f = File::new_with_options(Options::default());
1429            f.add_picture_from_bytes(
1430                "Sheet1",
1431                "B2",
1432                &Picture {
1433                    extension: ".png".to_string(),
1434                    file: png.clone(),
1435                    insert_type: PictureInsertType::PLACE_IN_CELL,
1436                    ..Default::default()
1437                },
1438            )
1439            .unwrap();
1440            let pics = f.get_pictures("Sheet1", "B2").unwrap();
1441            assert!(
1442                pics.iter()
1443                    .any(|p| p.insert_type == PictureInsertType::PLACE_IN_CELL && p.file == png),
1444                "in-cell picture should be readable before save"
1445            );
1446            f.save_as(&path_str).unwrap();
1447            f.close().unwrap();
1448        }
1449        let mut f = File::open_file(&path_str, Options::default()).unwrap();
1450        let pics = f.get_pictures("Sheet1", "B2").unwrap();
1451        assert_eq!(pics.len(), 1);
1452        assert_eq!(pics[0].insert_type, PictureInsertType::PLACE_IN_CELL);
1453        assert_eq!(pics[0].file, png);
1454        f.close().unwrap();
1455        let _ = std::fs::remove_file(&path);
1456    }
1457
1458    #[test]
1459    fn add_picture_from_file_round_trip() {
1460        let png = red_pixel_png();
1461        let img_path = std::env::temp_dir().join("excelize_rs_add_picture_from_file.png");
1462        std::fs::write(&img_path, &png).unwrap();
1463        let img_path_str = img_path.to_string_lossy().to_string();
1464        let path = std::env::temp_dir().join("excelize_rs_add_picture_from_file.xlsx");
1465        let path_str = path.to_string_lossy().to_string();
1466        {
1467            let mut f = File::new_with_options(Options::default());
1468            f.add_picture_from_file(
1469                "Sheet1",
1470                "B2",
1471                &img_path_str,
1472                PictureInsertType::PLACE_IN_CELL,
1473                None,
1474            )
1475            .unwrap();
1476            f.add_picture_from_file(
1477                "Sheet1",
1478                "B3",
1479                &img_path_str,
1480                PictureInsertType::DISPIMG,
1481                Some(&GraphicOptions {
1482                    alt_text: "wps image".to_string(),
1483                    ..Default::default()
1484                }),
1485            )
1486            .unwrap();
1487            assert!(
1488                f.add_picture_from_file(
1489                    "Sheet1",
1490                    "B4",
1491                    "image.xyz",
1492                    PictureInsertType::PLACE_OVER_CELLS,
1493                    None,
1494                )
1495                .is_err(),
1496                "unsupported extension should be rejected"
1497            );
1498            f.save_as(&path_str).unwrap();
1499            f.close().unwrap();
1500        }
1501        let mut f = File::open_file(&path_str, Options::default()).unwrap();
1502        let pics = f.get_pictures("Sheet1", "B2").unwrap();
1503        assert!(
1504            pics.iter()
1505                .any(|p| p.insert_type == PictureInsertType::PLACE_IN_CELL && p.file == png),
1506            "expected a PLACE_IN_CELL picture, got {pics:?}"
1507        );
1508        let pics = f.get_pictures("Sheet1", "B3").unwrap();
1509        assert!(
1510            pics.iter()
1511                .any(|p| p.insert_type == PictureInsertType::DISPIMG && p.file == png),
1512            "expected a DISPIMG picture, got {pics:?}"
1513        );
1514        f.close().unwrap();
1515        let _ = std::fs::remove_file(&path);
1516        let _ = std::fs::remove_file(&img_path);
1517    }
1518
1519    #[test]
1520    fn dispimg_picture_round_trip() {
1521        let png = red_pixel_png();
1522        let path = std::env::temp_dir().join("excelize_rs_dispimg_picture.xlsx");
1523        let path_str = path.to_string_lossy().to_string();
1524        {
1525            let mut f = File::new_with_options(Options::default());
1526            f.add_picture_from_bytes(
1527                "Sheet1",
1528                "B2",
1529                &Picture {
1530                    extension: ".png".to_string(),
1531                    file: png.clone(),
1532                    insert_type: PictureInsertType::DISPIMG,
1533                    format: Some(GraphicOptions {
1534                        alt_text: "wps image".to_string(),
1535                        ..Default::default()
1536                    }),
1537                    ..Default::default()
1538                },
1539            )
1540            .unwrap();
1541            assert!(
1542                f.get_cell_formula("Sheet1", "B2")
1543                    .unwrap()
1544                    .contains("DISPIMG")
1545            );
1546            f.save_as(&path_str).unwrap();
1547            f.close().unwrap();
1548        }
1549        let mut f = File::open_file(&path_str, Options::default()).unwrap();
1550        let pics = f.get_pictures("Sheet1", "B2").unwrap();
1551        assert!(
1552            pics.iter()
1553                .any(|p| p.insert_type == PictureInsertType::DISPIMG && p.file == png),
1554            "expected a DISPIMG picture, got {pics:?}"
1555        );
1556        f.close().unwrap();
1557        let _ = std::fs::remove_file(&path);
1558    }
1559}