Skip to main content

excelize_rs/
picture.rs

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