1use crate::File;
6use crate::constants::{
7 DEFAULT_LINE_WIDTH, DEFAULT_SHAPE_SIZE, EMU, SOURCE_RELATIONSHIP_DRAWING_ML,
8};
9use crate::errors::{ErrParameterInvalid, ErrTransparency, Result};
10use crate::lib_util::{cell_name_to_coordinates, column_number_to_name};
11use crate::styles::Font;
12use crate::xml::common::{AttrValInt, AttrValString, RichTextRun};
13use crate::xml::drawing::{
14 ABodyPr, AEndParaRPr, AFontRef, AP, AR, ARPr, ARef, ASolidFill, ASrgbClr, GraphicOptions,
15 LineOptions, LineType, Shape, XdrCNvSpPr, XdrCellAnchor, XdrClientData, XdrNvSpPr, XdrSp,
16 XdrStyle, XdrTxBody, XlsxCNvPr, XlsxCTTextFont, XlsxFrom, XlsxOff, XlsxPositiveSize2D,
17 XlsxPrstGeom, XlsxSpPr, XlsxTo, XlsxXfrm,
18};
19
20const SUPPORTED_DRAWING_UNDERLINE_TYPES: &[&str] = &[
21 "none",
22 "sng",
23 "dbl",
24 "heavy",
25 "dotted",
26 "dottedHeavy",
27 "dash",
28 "dashHeavy",
29 "dashLong",
30 "dashLongHeavy",
31 "dotDash",
32 "dotDashHeavy",
33 "dotDotDash",
34 "dotDotDashHeavy",
35 "wavy",
36 "wavyHeavy",
37 "wavyDbl",
38];
39
40impl File {
41 pub fn add_shape(&self, sheet: &str, opts: &Shape) -> Result<()> {
43 let mut options = opts.clone();
44 parse_shape_options(&mut options)?;
45
46 let mut ws = self.work_sheet_reader(sheet)?;
47
48 let drawing_id = self.count_drawings() + 1;
50 let mut drawing_xml = format!("xl/drawings/drawing{drawing_id}.xml");
51 let mut sheet_rels_drawing_xml = format!("../drawings/drawing{drawing_id}.xml");
52
53 if let Some(ref drawing) = ws.drawing {
54 sheet_rels_drawing_xml = self
55 .get_sheet_relationships_target_by_id(sheet, drawing.rid.as_deref().unwrap_or(""));
56 drawing_xml = sheet_rels_drawing_xml.replace("..", "xl");
57 } else {
58 let sheet_xml_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
59 let sheet_rels = format!(
60 "xl/worksheets/_rels/{}{}.rels",
61 sheet_xml_path.strip_prefix("xl/worksheets/").unwrap_or(""),
62 ""
63 );
64 let r_id = self.add_rels(
65 &sheet_rels,
66 SOURCE_RELATIONSHIP_DRAWING_ML,
67 &sheet_rels_drawing_xml,
68 "",
69 );
70 self.add_sheet_drawing(sheet, r_id)?;
71 self.add_sheet_name_space(sheet, "r");
72 }
73
74 self.add_drawing_shape(sheet, &drawing_xml, &opts.cell, &options)?;
75 self.add_content_type_part(drawing_id, "drawings")?;
76 Ok(())
77 }
78
79 fn add_drawing_shape(
81 &self,
82 sheet: &str,
83 drawing_xml: &str,
84 cell: &str,
85 opts: &Shape,
86 ) -> Result<()> {
87 let (mut ws_dr, mut anchor, c_nv_pr_id) = self.shape_cell_anchor(
88 sheet,
89 drawing_xml,
90 cell,
91 opts.width as i32,
92 opts.height as i32,
93 &opts.format,
94 )?;
95
96 let shape_color = opts
97 .fill
98 .color
99 .first()
100 .map(|c| c.trim_start_matches('#').to_uppercase())
101 .unwrap_or_default();
102 let line_color = opts
103 .line
104 .fill
105 .color
106 .first()
107 .map(|c| c.trim_start_matches('#').to_uppercase())
108 .unwrap_or_default();
109
110 let mut shape = XdrSp {
111 macro_name: opts.macro_name.clone(),
112 text_link: String::new(),
113 nv_sp_pr: Some(XdrNvSpPr {
114 c_nv_pr: Some(XlsxCNvPr {
115 id: c_nv_pr_id,
116 name: if opts.format.name.is_empty() {
117 format!("Shape {c_nv_pr_id}")
118 } else {
119 opts.format.name.clone()
120 },
121 descr: opts.format.alt_text.clone(),
122 title: None,
123 hlink_click: None,
124 }),
125 c_nv_sp_pr: Some(XdrCNvSpPr { tx_box: true }),
126 }),
127 sp_pr: Some(XlsxSpPr {
128 xfrm: XlsxXfrm {
129 off: XlsxOff { x: 0, y: 0 },
130 ext: XlsxPositiveSize2D {
131 cx: opts.width as i64 * EMU as i64,
132 cy: opts.height as i64 * EMU as i64,
133 },
134 },
135 prst_geom: XlsxPrstGeom {
136 prst: opts.r#type.clone(),
137 },
138 ln: crate::chart::draw_chart_ln(&opts.line),
139 ..Default::default()
140 }),
141 style: Some(XdrStyle {
142 ln_ref: Some(shape_ref(&line_color, 2)),
143 fill_ref: Some(shape_ref(&shape_color, 1)),
144 effect_ref: Some(shape_ref("", 0)),
145 font_ref: Some(AFontRef {
146 idx: "minor".to_string(),
147 scheme_clr: Some(AttrValString {
148 val: Some("tx1".to_string()),
149 }),
150 }),
151 }),
152 tx_body: Some(XdrTxBody {
153 body_pr: Some(ABodyPr {
154 vert_overflow: Some("clip".to_string()),
155 horz_overflow: Some("clip".to_string()),
156 wrap: Some("none".to_string()),
157 rtl_col: Some(false),
158 anchor: Some("t".to_string()),
159 ..Default::default()
160 }),
161 p: Vec::new(),
162 }),
163 };
164
165 if opts.fill.transparency > 0 {
166 let val = (100 - opts.fill.transparency) * 1000;
167 if let Some(ref mut sp_pr) = shape.sp_pr {
168 sp_pr.solid_fill = Some(ASolidFill {
169 srgb_clr: Some(ASrgbClr {
170 val: Some(shape_color.clone()),
171 alpha: Some(AttrValInt {
172 val: Some(val as i64),
173 }),
174 ..Default::default()
175 }),
176 ..Default::default()
177 });
178 }
179 }
180
181 let default_font = self
182 .get_default_font()
183 .unwrap_or_else(|_| "Calibri".to_string());
184 let paragraphs = if opts.paragraph.is_empty() {
185 vec![RichTextRun {
186 font: Some(Font {
187 bold: Some(false),
188 italic: Some(false),
189 underline: Some("none".to_string()),
190 name: Some(default_font.clone()),
191 size: Some(11.0),
192 color: Some("000000".to_string()),
193 ..Default::default()
194 }),
195 text: " ".to_string(),
196 }]
197 } else {
198 opts.paragraph.clone()
199 };
200
201 if let Some(ref mut tx_body) = shape.tx_body {
202 for p in paragraphs {
203 let font = p.font.unwrap_or_default();
204 let u = if SUPPORTED_DRAWING_UNDERLINE_TYPES
205 .iter()
206 .any(|&t| t == font.underline.as_deref().unwrap_or("none"))
207 {
208 font.underline.unwrap_or_else(|| "none".to_string())
209 } else {
210 "none".to_string()
211 };
212 let text = if p.text.is_empty() {
213 " ".to_string()
214 } else {
215 p.text
216 };
217 let srgb_clr = font
218 .color
219 .as_deref()
220 .unwrap_or("")
221 .trim_start_matches('#')
222 .to_uppercase();
223 let mut r_pr = ARPr {
224 i: font.italic.unwrap_or(false),
225 b: font.bold.unwrap_or(false),
226 lang: Some("en-US".to_string()),
227 alt_lang: Some("en-US".to_string()),
228 u: Some(u),
229 sz: font.size.map(|s| s * 100.0),
230 latin: Some(XlsxCTTextFont {
231 typeface: font.name.clone().unwrap_or_else(|| default_font.clone()),
232 charset: None,
233 panose: None,
234 pitch_family: None,
235 }),
236 ea: Some(XlsxCTTextFont {
237 typeface: font.name.clone().unwrap_or_else(|| default_font.clone()),
238 charset: None,
239 panose: None,
240 pitch_family: None,
241 }),
242 cs: Some(XlsxCTTextFont {
243 typeface: font.name.clone().unwrap_or_else(|| default_font.clone()),
244 charset: None,
245 panose: None,
246 pitch_family: None,
247 }),
248 ..Default::default()
249 };
250 if srgb_clr.len() == 6 {
251 r_pr.solid_fill = Some(ASolidFill {
252 srgb_clr: Some(ASrgbClr {
253 val: Some(srgb_clr),
254 ..Default::default()
255 }),
256 ..Default::default()
257 });
258 }
259 tx_body.p.push(AP {
260 r: Some(AR {
261 r_pr: Some(r_pr),
262 t: Some(text),
263 }),
264 end_para_r_pr: Some(AEndParaRPr {
265 lang: "en-US".to_string(),
266 ..Default::default()
267 }),
268 ..Default::default()
269 });
270 }
271 }
272
273 anchor.sp = Some(shape);
274 anchor.client_data = Some(XdrClientData {
275 f_locks_with_sheet: opts.format.locked.unwrap_or(true),
276 f_prints_with_sheet: opts.format.print_object.unwrap_or(true),
277 });
278
279 if opts.format.positioning == "oneCell" {
280 ws_dr.one_cell_anchor.push(anchor);
281 } else {
282 ws_dr.two_cell_anchor.push(anchor);
283 }
284 self.drawings.insert(drawing_xml.to_string(), ws_dr);
285 Ok(())
286 }
287
288 fn shape_cell_anchor(
290 &self,
291 sheet: &str,
292 drawing_xml: &str,
293 cell: &str,
294 width: i32,
295 height: i32,
296 format: &GraphicOptions,
297 ) -> Result<(crate::xml::drawing::XlsxWsDr, XdrCellAnchor, i64)> {
298 let (from_col, from_row) = cell_name_to_coordinates(cell)?;
299 let w = (width as f64 * format.scale_x) as i32;
300 let h = (height as f64 * format.scale_y) as i32;
301 let (col_start, row_start, col_end, row_end, x1, y1, x2, y2) =
302 self.position_object_pixels(sheet, from_col, from_row, w, h, format)?;
303
304 let (ws_dr, c_nv_pr_id) = self.drawing_parser(drawing_xml)?;
305 let mut anchor = XdrCellAnchor::default();
306 anchor.from = Some(XlsxFrom {
307 col: col_start as i64,
308 col_off: x1 as i64 * EMU as i64,
309 row: row_start as i64,
310 row_off: y1 as i64 * EMU as i64,
311 });
312
313 if format.positioning != "oneCell" {
314 anchor.to = Some(XlsxTo {
315 col: col_end as i64,
316 col_off: x2 as i64 * EMU as i64,
317 row: row_end as i64,
318 row_off: y2 as i64 * EMU as i64,
319 });
320 anchor.edit_as = Some(format.positioning.clone());
321 } else {
322 anchor.ext = Some(XlsxPositiveSize2D {
323 cx: x2 as i64 * EMU as i64,
324 cy: y2 as i64 * EMU as i64,
325 });
326 }
327
328 Ok((ws_dr, anchor, c_nv_pr_id))
329 }
330}
331
332fn parse_shape_options(opts: &mut Shape) -> Result<()> {
333 if opts.r#type.is_empty() {
334 return Err(Box::new(ErrParameterInvalid));
335 }
336 if opts.width == 0 {
337 opts.width = DEFAULT_SHAPE_SIZE as u64;
338 }
339 if opts.height == 0 {
340 opts.height = DEFAULT_SHAPE_SIZE as u64;
341 }
342 if opts.line.width == 0.0 {
343 opts.line.width = DEFAULT_LINE_WIDTH as f64;
344 }
345 if !(0..=100).contains(&opts.fill.transparency) {
346 return Err(Box::new(ErrTransparency));
347 }
348 if opts.format.scale_x == 0.0 {
349 opts.format.scale_x = 1.0;
350 }
351 if opts.format.scale_y == 0.0 {
352 opts.format.scale_y = 1.0;
353 }
354 Ok(())
355}
356
357fn shape_ref(color: &str, idx: i64) -> ARef {
358 if color.is_empty() {
359 ARef {
360 idx: 0,
361 scrgb_clr: Some(crate::xml::drawing::AScrgbClr {
362 r: 0.0,
363 g: 0.0,
364 b: 0.0,
365 }),
366 ..Default::default()
367 }
368 } else {
369 ARef {
370 idx,
371 srgb_clr: Some(AttrValString {
372 val: Some(color.to_string()),
373 }),
374 ..Default::default()
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::options::Options;
383 use crate::xml::drawing::Fill;
384
385 #[test]
386 fn add_shape_creates_drawing_part() {
387 let f = File::new_with_options(Options::default());
388 f.add_shape(
389 "Sheet1",
390 &Shape {
391 cell: "G6".to_string(),
392 r#type: "rect".to_string(),
393 width: 180,
394 height: 40,
395 fill: Fill {
396 r#type: "pattern".to_string(),
397 pattern: 1,
398 color: vec!["8EB9FF".to_string()],
399 ..Default::default()
400 },
401 line: LineOptions {
402 width: 1.2,
403 fill: Fill {
404 r#type: "pattern".to_string(),
405 pattern: 1,
406 color: vec!["4286F4".to_string()],
407 ..Default::default()
408 },
409 ..Default::default()
410 },
411 paragraph: vec![RichTextRun {
412 text: "Rectangle Shape".to_string(),
413 font: Some(Font {
414 bold: Some(true),
415 italic: Some(true),
416 name: Some("Times New Roman".to_string()),
417 size: Some(18.0),
418 color: Some("777777".to_string()),
419 underline: Some("sng".to_string()),
420 ..Default::default()
421 }),
422 }],
423 ..Default::default()
424 },
425 )
426 .unwrap();
427
428 let ws = f.work_sheet_reader("Sheet1").unwrap();
429 assert!(ws.drawing.is_some());
430
431 let target = f.get_sheet_relationships_target_by_id(
432 "Sheet1",
433 ws.drawing.as_ref().unwrap().rid.as_deref().unwrap(),
434 );
435 let drawing_xml = target.replace("..", "xl");
436 let (ws_dr, _) = f.drawing_parser(&drawing_xml).unwrap();
437 assert_eq!(ws_dr.two_cell_anchor.len(), 1);
438
439 let anchor = &ws_dr.two_cell_anchor[0];
440 assert_eq!(anchor.from.as_ref().unwrap().col, 6); assert_eq!(anchor.from.as_ref().unwrap().row, 5); let sp = anchor.sp.as_ref().unwrap();
443 assert_eq!(sp.sp_pr.as_ref().unwrap().prst_geom.prst, "rect");
444 }
445}