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