1use 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
37pub use crate::xml::drawing::{GraphicOptions, Picture, PictureInsertType};
42
43fn 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
97impl File {
102 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
851fn 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
867fn 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
895fn escape_xml_attr(s: &str) -> String {
897 s.replace('&', "&")
898 .replace('<', "<")
899 .replace('>', ">")
900 .replace('"', """)
901}
902
903fn 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
911fn 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 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
998fn 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
1035impl 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
1066impl 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#[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 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}