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 self.add_picture_from_file(sheet, cell, path, PictureInsertType::PLACE_OVER_CELLS, opts)
111 }
112
113 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
867fn 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
883fn 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
911fn escape_xml_attr(s: &str) -> String {
913 s.replace('&', "&")
914 .replace('<', "<")
915 .replace('>', ">")
916 .replace('"', """)
917}
918
919fn 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
927fn 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 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
1014fn 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
1051impl 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
1082impl 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#[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 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 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}