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