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