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