1use std::fmt::Write;
2
3use base64::Engine;
4
5use crate::xml::{
6 direct_child_tag, extract_balanced_tag_slices, extract_direct_child_tag_ranges,
7 get_attribute_ref,
8};
9
10use crate::xml::escape_xml_text;
11
12mod effects;
13mod geometry;
14mod ink;
15mod math;
16pub mod model3d;
17mod raster;
18mod shapes;
19pub mod smartart;
20pub mod text;
21mod xml;
22
23#[doc(hidden)]
24pub fn with_cached_xml<T>(parse: impl FnOnce() -> T) -> T {
25 xml::with_cached_xml(parse)
26}
27
28pub use shapes::{render_shape, render_text_fill, StyleReferenceKind, TextFill};
29pub use shapes::render_3d_path;
30pub use ink::{render_ink, parse_ink, render_ink_in_frame, InkBounds, InkBrush, InkDrawing, InkPoint, InkStroke};
31pub use math::{parse_office_math, MathAttribute, MathElement, OfficeMath};
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34#[serde(default, rename_all = "camelCase")]
35pub struct ParseOptions {
36 #[serde(rename = "include3dModels")]
37 pub include_3d_models: bool,
38}
39
40#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct PaintBounds {
43 pub x_px: f64,
44 pub y_px: f64,
45 pub width_px: f64,
46 pub height_px: f64,
47 pub layout_width_px: f64,
48 pub layout_height_px: f64,
49}
50
51#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct RenderedPicture {
54 pub svg: String,
55 pub paint_bounds: Option<PaintBounds>,
56}
57
58#[derive(Clone, Debug)]
59pub struct ResolvedColor {
60 pub color: String,
61 pub opacity: Option<f64>,
62}
63
64pub trait PictureContext {
65 fn resolve_color(&self, xml: &str) -> Option<ResolvedColor>;
66
67 fn resolve_image(&self, _relationship_id: &str) -> Option<String> {
68 None
69 }
70
71 fn resolve_style_reference(
72 &self,
73 _kind: StyleReferenceKind,
74 _index: u32,
75 _placeholder_xml: &str,
76 ) -> Option<String> {
77 None
78 }
79}
80
81pub fn normalize_raster(source: &str) -> Option<String> {
82 raster::normalize(source)
83}
84
85pub fn render_effects(
88 properties_xml: &str,
89 svg_body: &str,
90 width: f64,
91 height: f64,
92 context: &impl PictureContext,
93) -> Option<RenderedPicture> {
94 if !width.is_finite()
95 || !height.is_finite()
96 || width <= 0.0
97 || height <= 0.0
98 || width > 1_000_000.0
99 || height > 1_000_000.0
100 || properties_xml.len() > 4 * 1024 * 1024
101 || svg_body.len() > 32 * 1024 * 1024
102 {
103 return None;
104 }
105 let mut defs = String::new();
106 let (body, bounds) = apply_shape_effects(
107 properties_xml,
108 svg_body,
109 "",
110 width,
111 height,
112 0.0,
113 Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
114 context,
115 &mut defs,
116 true,
117 )?;
118 let paint_width = bounds.right - bounds.left;
119 let paint_height = bounds.bottom - bounds.top;
120 let svg = format!(
121 r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{paint_width}" height="{paint_height}" viewBox="{} {} {paint_width} {paint_height}"><defs>{defs}</defs>{body}</svg>"##,
122 bounds.left, bounds.top,
123 );
124 Some(RenderedPicture {
125 svg,
126 paint_bounds: Some(PaintBounds {
127 x_px: bounds.left,
128 y_px: bounds.top,
129 width_px: paint_width,
130 height_px: paint_height,
131 layout_width_px: width,
132 layout_height_px: height,
133 }),
134 })
135}
136
137fn resolve_drawing_color_from_xml(
138 xml: Option<&str>,
139 context: &impl PictureContext,
140) -> Option<ResolvedColor> {
141 context.resolve_color(xml?)
142}
143
144fn number(xml: &str, name: &str, default: f64) -> Option<f64> {
145 match get_attribute_ref(xml, name) {
146 Some(value) => value
147 .trim()
148 .parse::<f64>()
149 .ok()
150 .filter(|value| value.is_finite()),
151 None => Some(default),
152 }
153}
154
155fn percentage(xml: &str, name: &str, default: f64) -> Option<f64> {
156 let Some(value) = get_attribute_ref(xml, name) else {
157 return Some(default);
158 };
159 let value = value.trim();
160 let (value, divisor) = match value.strip_suffix('%') {
161 Some(value) => (value, 100.0),
162 None => (value, 100_000.0),
163 };
164 value
165 .parse::<f64>()
166 .ok()
167 .filter(|value| value.is_finite())
168 .map(|value| value / divisor)
169}
170
171fn enabled(xml: &str, name: &str) -> bool {
172 get_attribute_ref(xml, name).is_some_and(|value| matches!(value, "1" | "true" | "on"))
173}
174
175fn rectangle(xml: Option<&str>) -> Option<[f64; 4]> {
176 let xml = xml.unwrap_or("");
177 Some([
178 percentage(xml, "l", 0.0)?,
179 percentage(xml, "t", 0.0)?,
180 percentage(xml, "r", 0.0)?,
181 percentage(xml, "b", 0.0)?,
182 ])
183}
184
185fn grayscale(out: &mut String) {
186 out.push_str(r##"<feColorMatrix type="matrix" values="0.2125 0.7154 0.0721 0 0 0.2125 0.7154 0.0721 0 0 0.2125 0.7154 0.0721 0 0 0 0 0 1 0"/>"##);
187}
188
189fn threshold(out: &mut String, channels: &[&str], threshold: f64) {
190 let _ = write!(out, "<feComponentTransfer>");
191 for channel in channels {
192 let _ = write!(
193 out,
194 r##"<feFunc{channel} type="linear" slope="1" intercept="{}"/>"##,
195 0.5 - threshold
196 );
197 }
198 out.push_str("</feComponentTransfer><feComponentTransfer>");
199 for channel in channels {
200 let _ = write!(
201 out,
202 r##"<feFunc{channel} type="discrete" tableValues="0 1"/>"##
203 );
204 }
205 out.push_str("</feComponentTransfer>");
206}
207
208fn rgb(xml: &str, colors: &impl PictureContext) -> Option<[f64; 3]> {
209 let resolved = resolve_drawing_color_from_xml(Some(xml), colors)?;
210 let mut channels = [0.0; 3];
211 for (index, channel) in channels.iter_mut().enumerate() {
212 *channel = u8::from_str_radix(resolved.color.get(1 + index * 2..3 + index * 2)?, 16).ok()?
213 as f64
214 / 255.0;
215 }
216 Some(channels)
217}
218
219fn replace_color(out: &mut String, color: [f64; 3]) {
220 out.push_str("<feComponentTransfer>");
221 for (channel, value) in ["R", "G", "B"].into_iter().zip(color) {
222 let _ = write!(
223 out,
224 r##"<feFunc{channel} type="linear" slope="0" intercept="{value}"/>"##
225 );
226 }
227 out.push_str("</feComponentTransfer>");
228}
229
230fn linear_rgb(out: &mut String, slope: f64, intercept: f64) {
231 out.push_str("<feComponentTransfer>");
232 for channel in ["R", "G", "B"] {
233 let _ = write!(
234 out,
235 r##"<feFunc{channel} type="linear" slope="{slope}" intercept="{intercept}"/>"##
236 );
237 }
238 out.push_str("</feComponentTransfer>");
239}
240
241fn blur_radius(xml: &str, name: &str) -> Option<f64> {
242 let radius = number(xml, name, 0.0)? / 9525.0;
243 (0.0..=10_000.0).contains(&radius).then_some(radius)
244}
245
246fn blip_effects(blip: &str) -> Vec<xml::TaggedRange<'_>> {
247 let mut effects = extract_direct_child_tag_ranges(blip);
248 effects.sort_by_key(|effect| {
249 !matches!(
250 effect.tag_name,
251 "a:clrChange" | "a:alphaBiLevel" | "a:alphaCeiling" | "a:alphaFloor" | "a:biLevel"
252 )
253 });
254 effects
255}
256
257fn blip_filter(blip: &str, colors: &impl PictureContext) -> Option<String> {
258 let mut out = String::new();
259 for child in blip_effects(blip) {
260 let xml = &blip[child.start..child.end];
261 match child.tag_name {
262 "a:extLst" => {}
263 "a:grayscl" => grayscale(&mut out),
264 "a:biLevel" => {
265 grayscale(&mut out);
266 threshold(
267 &mut out,
268 &["R", "G", "B"],
269 percentage(xml, "thresh", 0.5)?.clamp(0.0, 1.0),
270 );
271 }
272 "a:alphaBiLevel" => threshold(
273 &mut out,
274 &["A"],
275 percentage(xml, "thresh", 0.5)?.clamp(0.0, 1.0),
276 ),
277 "a:alphaFloor" => threshold(&mut out, &["A"], 1.0),
278 "a:alphaModFix" => {
279 let amount = percentage(xml, "amt", 1.0)?.max(0.0);
280 let _ = write!(
281 out,
282 r##"<feComponentTransfer><feFuncA type="linear" slope="{amount}" intercept="0"/></feComponentTransfer>"##
283 );
284 }
285 "a:alphaRepl" => {
286 let amount = percentage(xml, "a", 1.0)?.clamp(0.0, 1.0);
287 let _ = write!(
288 out,
289 r##"<feComponentTransfer><feFuncA type="linear" slope="0" intercept="{amount}"/></feComponentTransfer>"##
290 );
291 }
292 "a:alphaInv" => {
293 out.push_str(r##"<feComponentTransfer><feFuncA type="linear" slope="-1" intercept="1"/></feComponentTransfer>"##);
294 let color = if extract_direct_child_tag_ranges(xml).is_empty() {
295 [0.0; 3]
296 } else {
297 rgb(xml, colors)?
298 };
299 replace_color(&mut out, color);
300 }
301 "a:clrRepl" => replace_color(&mut out, rgb(xml, colors)?),
302 "a:lum" => {
303 let brightness = percentage(xml, "bright", 0.0)?.clamp(-1.0, 1.0);
304 let contrast = percentage(xml, "contrast", 0.0)?.clamp(-1.0, 1.0);
305 let brightness = (brightness * 0.5) as f32;
306 let slope = (if contrast > 0.0 {
307 1.0 / (1.0 - contrast.min(0.99999))
308 } else {
309 1.0 + contrast
310 }) as f32;
311 let intercept = slope * (brightness - 0.5) + brightness + 0.5;
312 linear_rgb(&mut out, f64::from(slope), f64::from(intercept));
313 }
314 "a:blur" => {
315 let deviation = blur_radius(xml, "rad")? / 2.0;
316 let _ = write!(out, r##"<feGaussianBlur stdDeviation="{deviation}"/>"##);
317 }
318 "a:duotone" => {
319 let children = extract_direct_child_tag_ranges(xml);
320 let [dark, light] = children.as_slice() else {
321 return None;
322 };
323 let dark = rgb(&xml[dark.start..dark.end], colors)?;
324 let light = rgb(&xml[light.start..light.end], colors)?;
325 grayscale(&mut out);
326 out.push_str("<feComponentTransfer>");
327 for (index, channel) in ["R", "G", "B"].into_iter().enumerate() {
328 let _ = write!(
329 out,
330 r##"<feFunc{channel} type="table" tableValues="{} {}"/>"##,
331 dark[index], light[index]
332 );
333 }
334 out.push_str("</feComponentTransfer>");
335 }
336 _ => return None,
337 }
338 }
339 Some(out)
340}
341
342fn adjusted_value(geometry: &str, name: &str, default: f64) -> Option<f64> {
343 let Some(adjustments) = direct_child_tag(geometry, "a:avLst") else {
344 return Some(default);
345 };
346 for adjustment in extract_direct_child_tag_ranges(adjustments) {
347 let xml = &adjustments[adjustment.start..adjustment.end];
348 if get_attribute_ref(xml, "name") == Some(name) {
349 let formula = get_attribute_ref(xml, "fmla")?
350 .trim()
351 .strip_prefix("val ")?;
352 return formula
353 .trim()
354 .parse::<f64>()
355 .ok()
356 .filter(|value| value.is_finite());
357 }
358 }
359 Some(default)
360}
361
362fn geometry_path(properties: &str, width: f64, height: f64) -> Option<String> {
363 if direct_child_tag(properties, "a:custGeom").is_none() {
364 if let Some(path) = legacy_geometry_path(properties, width, height) {
365 return Some(path);
366 }
367 }
368 let paths = geometry::geometry_paths(properties, width, height)?;
369 let path = paths
370 .into_iter()
371 .filter(|path| path.fill != geometry::PathFill::None)
372 .map(|path| path.d)
373 .collect::<Vec<_>>()
374 .join(" ");
375 (!path.is_empty()).then_some(path)
376}
377
378fn legacy_geometry_path(properties: &str, width: f64, height: f64) -> Option<String> {
379 if let Some(geometry) = direct_child_tag(properties, "a:custGeom") {
380 let paths = direct_child_tag(geometry, "a:pathLst")?;
381 let paths = extract_direct_child_tag_ranges(paths);
382 if paths.len() != 1 {
383 return None;
384 }
385 let path = extract_balanced_tag_slices(geometry, "a:path")
386 .into_iter()
387 .next()?;
388 let path_width = number(path, "w", 21600.0)?;
389 let path_height = number(path, "h", 21600.0)?;
390 if path_width <= 0.0 || path_height <= 0.0 {
391 return None;
392 }
393 let mut output = String::new();
394 for command in extract_direct_child_tag_ranges(path) {
395 let (expected, operation) = match command.tag_name {
396 "a:moveTo" => (1, 'M'),
397 "a:lnTo" => (1, 'L'),
398 "a:cubicBezTo" => (3, 'C'),
399 "a:quadBezTo" => (2, 'Q'),
400 "a:close" => (0, 'Z'),
401 _ => return None,
402 };
403 let command = &path[command.start..command.end];
404 let points = extract_direct_child_tag_ranges(command);
405 if points.len() != expected {
406 return None;
407 }
408 output.push(operation);
409 for point in points {
410 if point.tag_name != "a:pt" {
411 return None;
412 }
413 let point = &command[point.start..point.end];
414 let x = get_attribute_ref(point, "x")?
415 .parse::<f64>()
416 .ok()?
417 .mul_add(width / path_width, 0.0);
418 let y = get_attribute_ref(point, "y")?
419 .parse::<f64>()
420 .ok()?
421 .mul_add(height / path_height, 0.0);
422 if !x.is_finite() || !y.is_finite() {
423 return None;
424 }
425 let _ = write!(output, "{x} {y} ");
426 }
427 }
428 return (!output.is_empty()).then_some(output);
429 }
430 let geometry = direct_child_tag(properties, "a:prstGeom").unwrap_or("");
431 let preset = get_attribute_ref(geometry, "prst").unwrap_or("rect");
432 let x_mid = width / 2.0;
433 let y_mid = height / 2.0;
434 Some(match preset {
435 "rect" => format!("M0 0 H{width} V{height} H0 Z"),
436 "ellipse" => format!(
437 "M0 {y_mid} A{x_mid} {y_mid} 0 1 0 {width} {y_mid} A{x_mid} {y_mid} 0 1 0 0 {y_mid} Z"
438 ),
439 "roundRect" => {
440 let radius = width.min(height)
441 * (adjusted_value(geometry, "adj", 16667.0)? / 100000.0).clamp(0.0, 0.5);
442 let right = width - radius;
443 let bottom = height - radius;
444 format!("M{radius} 0 H{right} A{radius} {radius} 0 0 1 {width} {radius} V{bottom} A{radius} {radius} 0 0 1 {right} {height} H{radius} A{radius} {radius} 0 0 1 0 {bottom} V{radius} A{radius} {radius} 0 0 1 {radius} 0 Z")
445 }
446 "triangle" => {
447 let apex =
448 width * (adjusted_value(geometry, "adj", 50000.0)? / 100000.0).clamp(0.0, 1.0);
449 format!("M{apex} 0 L{width} {height} L0 {height} Z")
450 }
451 "diamond" => format!("M{x_mid} 0 L{width} {y_mid} L{x_mid} {height} L0 {y_mid} Z"),
452 "rtTriangle" => format!("M0 {height} L{width} {height} L0 0 Z"),
453 "flowChartDelay" => format!("M0 0 H{x_mid} A{x_mid} {y_mid} 0 0 1 {x_mid} {height} H0 Z"),
454 _ => return None,
455 })
456}
457
458fn preset_dash_segments(preset: &str) -> Option<&'static [f64]> {
459 Some(match preset {
460 "solid" => &[],
461 "dot" => &[1.0, 3.0],
462 "dash" => &[4.0, 3.0],
463 "dashDot" => &[4.0, 3.0, 1.0, 3.0],
464 "sysDot" => &[1.0, 1.0],
465 "sysDash" => &[3.0, 1.0],
466 "sysDashDot" => &[3.0, 1.0, 1.0, 1.0],
467 "sysDashDotDot" => &[3.0, 1.0, 1.0, 1.0, 1.0, 1.0],
468 "lgDash" => &[8.0, 3.0],
469 "lgDashDot" => &[8.0, 3.0, 1.0, 3.0],
470 "lgDashDotDot" => &[8.0, 3.0, 1.0, 3.0, 1.0, 3.0],
471 _ => return None,
472 })
473}
474
475fn rectangular_picture(properties: &str) -> bool {
476 direct_child_tag(properties, "a:custGeom").is_none()
477 && direct_child_tag(properties, "a:prstGeom")
478 .and_then(|geometry| get_attribute_ref(geometry, "prst"))
479 .is_none_or(|preset| preset == "rect")
480}
481
482fn border(properties: &str, colors: &impl PictureContext) -> Option<(String, f64)> {
483 let Some(line) = direct_child_tag(properties, "a:ln") else {
484 return Some((String::new(), 0.0));
485 };
486 if direct_child_tag(line, "a:noFill").is_some() {
487 return Some((String::new(), 0.0));
488 }
489 if direct_child_tag(line, "a:gradFill").is_some()
490 || direct_child_tag(line, "a:pattFill").is_some()
491 || get_attribute_ref(line, "cmpd").is_some_and(|value| value != "sng")
492 || get_attribute_ref(line, "algn").is_some_and(|value| value != "ctr")
493 {
494 return None;
495 }
496 let width = number(line, "w", 12700.0)?.max(0.0) / 9525.0;
497 if width == 0.0 {
498 return Some((String::new(), 0.0));
499 }
500 let color = resolve_drawing_color_from_xml(Some(line), colors)?;
501 let cap = match get_attribute_ref(line, "cap").unwrap_or("flat") {
502 "rnd" => "round",
503 "sq" => "square",
504 _ => "butt",
505 };
506 let join = if direct_child_tag(line, "a:miter").is_some() {
507 "miter"
508 } else if direct_child_tag(line, "a:bevel").is_some() {
509 "bevel"
510 } else {
511 "round"
512 };
513 let miter = match direct_child_tag(line, "a:miter") {
514 Some(xml) => percentage(xml, "lim", 8.0)?,
515 None => 8.0,
516 }
517 .max(1.0);
518 let mut attributes = format!(
519 r##"fill="none" stroke="{}" stroke-opacity="{}" stroke-width="{}" stroke-linecap="{cap}" stroke-linejoin="{join}" stroke-miterlimit="{miter}""##,
520 escape_xml_text(&color.color),
521 color.opacity.unwrap_or(1.0),
522 width
523 * if rectangular_picture(properties) {
524 1.0
525 } else {
526 2.0
527 },
528 );
529 if let Some(dash) = direct_child_tag(line, "a:prstDash") {
530 let segments = preset_dash_segments(get_attribute_ref(dash, "val").unwrap_or("solid"))?;
531 if !segments.is_empty() {
532 let values = segments
533 .iter()
534 .map(|value| (value * width).to_string())
535 .collect::<Vec<_>>()
536 .join(" ");
537 let _ = write!(attributes, r##" stroke-dasharray="{values}""##);
538 }
539 }
540 if let Some(dash) = direct_child_tag(line, "a:custDash") {
541 let mut values = Vec::new();
542 for stop in extract_direct_child_tag_ranges(dash) {
543 let xml = &dash[stop.start..stop.end];
544 values.extend([
545 percentage(xml, "d", 1.0)?.max(0.0) * width,
546 percentage(xml, "sp", 1.0)?.max(0.0) * width,
547 ]);
548 }
549 if !values.is_empty() && values.iter().any(|value| *value > 0.0) {
550 let values = values
551 .iter()
552 .map(f64::to_string)
553 .collect::<Vec<_>>()
554 .join(" ");
555 let _ = write!(attributes, r##" stroke-dasharray="{values}""##);
556 }
557 }
558 let geometry = direct_child_tag(properties, "a:prstGeom")
559 .and_then(|geometry| get_attribute_ref(geometry, "prst"))
560 .unwrap_or("rect");
561 let bounded_corner = direct_child_tag(properties, "a:custGeom").is_none()
562 && matches!(geometry, "rect" | "ellipse" | "roundRect");
563 Some((
564 attributes,
565 width
566 * if join == "miter" && !bounded_corner {
567 miter
568 } else {
569 1.0
570 },
571 ))
572}
573
574#[derive(Clone, Copy)]
575struct Bounds {
576 left: f64,
577 top: f64,
578 right: f64,
579 bottom: f64,
580}
581
582impl Bounds {
583 fn rect(width: f64, height: f64) -> Self {
584 Self {
585 left: 0.0,
586 top: 0.0,
587 right: width,
588 bottom: height,
589 }
590 }
591
592 fn expanded(self, radius: f64) -> Self {
593 Self {
594 left: self.left - radius,
595 top: self.top - radius,
596 right: self.right + radius,
597 bottom: self.bottom + radius,
598 }
599 }
600
601 fn union(self, other: Self) -> Self {
602 Self {
603 left: self.left.min(other.left),
604 top: self.top.min(other.top),
605 right: self.right.max(other.right),
606 bottom: self.bottom.max(other.bottom),
607 }
608 }
609
610 fn transformed(self, matrix: Matrix) -> Self {
611 let points = [
612 (self.left, self.top),
613 (self.right, self.top),
614 (self.left, self.bottom),
615 (self.right, self.bottom),
616 ]
617 .map(|(x, y)| matrix.point(x, y));
618 Self {
619 left: points.iter().map(|p| p.0).fold(f64::INFINITY, f64::min),
620 top: points.iter().map(|p| p.1).fold(f64::INFINITY, f64::min),
621 right: points.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max),
622 bottom: points.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max),
623 }
624 }
625
626 fn attributes(self) -> String {
627 format!(
628 r##"x="{}" y="{}" width="{}" height="{}""##,
629 self.left,
630 self.top,
631 self.right - self.left,
632 self.bottom - self.top
633 )
634 }
635
636 fn valid(self) -> bool {
637 [self.left, self.top, self.right, self.bottom]
638 .into_iter()
639 .all(|value| value.is_finite() && value.abs() <= 100_000_000.0)
640 && self.right > self.left
641 && self.bottom > self.top
642 }
643}
644
645#[derive(Clone, Copy)]
646struct Matrix([f64; 6]);
647
648impl Matrix {
649 fn point(self, x: f64, y: f64) -> (f64, f64) {
650 let [a, b, c, d, e, f] = self.0;
651 (a * x + c * y + e, b * x + d * y + f)
652 }
653
654 fn composed(self, other: Self) -> Self {
655 let [a, b, c, d, e, f] = self.0;
656 let [g, h, i, j, k, l] = other.0;
657 Self([
658 a * g + c * h,
659 b * g + d * h,
660 a * i + c * j,
661 b * i + d * j,
662 a * k + c * l + e,
663 b * k + d * l + f,
664 ])
665 }
666
667 fn markup(self) -> String {
668 let [a, b, c, d, e, f] = self.0;
669 format!("matrix({a} {b} {c} {d} {e} {f})")
670 }
671}
672
673fn alignment(value: &str) -> (f64, f64) {
674 (
675 match value {
676 "tl" | "l" | "bl" => 0.0,
677 "tr" | "r" | "br" => 1.0,
678 _ => 0.5,
679 },
680 match value {
681 "tl" | "t" | "tr" => 0.0,
682 "bl" | "b" | "br" => 1.0,
683 _ => 0.5,
684 },
685 )
686}
687
688fn effect_matrix(xml: &str, bounds: Bounds) -> Option<Matrix> {
689 let sx = percentage(xml, "sx", 1.0)?;
690 let sy = percentage(xml, "sy", 1.0)?;
691 let kx = number(xml, "kx", 0.0)? / 60_000.0;
692 let ky = number(xml, "ky", 0.0)? / 60_000.0;
693 if sx.abs() > 100.0 || sy.abs() > 100.0 || kx.abs() >= 90.0 || ky.abs() >= 90.0 {
694 return None;
695 }
696 let direction = (number(xml, "dir", 0.0)? / 60_000.0).to_radians();
697 let distance = number(xml, "dist", 0.0)?.max(0.0) / 9525.0;
698 let (ax, ay) = alignment(get_attribute_ref(xml, "algn").unwrap_or("b"));
699 let ax = bounds.left + (bounds.right - bounds.left) * ax;
700 let ay = bounds.top + (bounds.bottom - bounds.top) * ay;
701 let a = sx;
702 let b = sx * libm::tan(ky.to_radians());
703 let c = sy * libm::tan(kx.to_radians());
704 let d = sy;
705 Some(Matrix([
706 a,
707 b,
708 c,
709 d,
710 ax - a * ax - c * ay + distance * libm::cos(direction),
711 ay - b * ax - d * ay + distance * libm::sin(direction),
712 ]))
713}
714
715fn color_filter(
716 defs: &mut String,
717 id: &str,
718 bounds: Bounds,
719 color_xml: &str,
720 colors: &impl PictureContext,
721 deviation: f64,
722 dilation: f64,
723) -> Option<()> {
724 let color = resolve_drawing_color_from_xml(Some(color_xml), colors)?;
725 let morphology = if dilation > 0.0 {
726 format!(
727 r##"<feMorphology in="SourceAlpha" operator="dilate" radius="{dilation}" result="silhouette"/>"##
728 )
729 } else {
730 String::new()
731 };
732 let input = if dilation > 0.0 {
733 "silhouette"
734 } else {
735 "SourceAlpha"
736 };
737 let _ = write!(
738 defs,
739 r##"<filter id="{id}" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB">{morphology}<feGaussianBlur in="{input}" stdDeviation="{deviation}" result="blur"/><feFlood flood-color="{}" flood-opacity="{}"/><feComposite in2="blur" operator="in"/></filter>"##,
740 bounds.attributes(),
741 escape_xml_text(&color.color),
742 color.opacity.unwrap_or(1.0)
743 );
744 Some(())
745}
746
747fn apply_shape_effects(
748 properties: &str,
749 body: &str,
750 path: &str,
751 width: f64,
752 height: f64,
753 margin: f64,
754 matrix: Matrix,
755 colors: &impl PictureContext,
756 defs: &mut String,
757 glow_from_source: bool,
758) -> Option<(String, Bounds)> {
759 let local = Bounds::rect(width, height).expanded(margin);
760 let mut source_bounds = local;
761 let mut source = body.to_string();
762 if let Some(graph) = direct_child_tag(properties, "a:effectDag") {
763 let filter = effects::render(graph, colors)?;
764 let _ = write!(
765 defs,
766 r##"<filter id="picture-effect-tree" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB">{filter}</filter>"##,
767 source_bounds.attributes()
768 );
769 source = format!(r##"<g filter="url(#picture-effect-tree)">{source}</g>"##);
770 }
771 let list = direct_child_tag(properties, "a:effectLst").unwrap_or("");
772 let children = extract_direct_child_tag_ranges(list);
773 for effect in &children {
774 let xml = &list[effect.start..effect.end];
775 match effect.tag_name {
776 "a:blur" => {
777 let deviation = blur_radius(xml, "rad")? / 2.0;
778 let grow = !get_attribute_ref(xml, "grow")
779 .is_some_and(|value| matches!(value, "0" | "false" | "off"));
780 if grow {
781 source_bounds = source_bounds.expanded(3.0 * deviation);
782 }
783 let _ = write!(
784 defs,
785 r##"<filter id="picture-shape-blur" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="{deviation}"/></filter>"##,
786 source_bounds.attributes()
787 );
788 source = format!(r##"<g filter="url(#picture-shape-blur)">{source}</g>"##);
789 }
790 "a:softEdge" => {
791 let radius = blur_radius(xml, "rad")?;
792 let erosion = radius;
793 let deviation = radius / 3.0;
794 let _ = write!(
795 defs,
796 r##"<filter id="picture-soft-edge" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB"><feComponentTransfer in="SourceAlpha" result="binary"><feFuncA type="linear" slope="255"/></feComponentTransfer><feMorphology in="binary" operator="erode" radius="{erosion}" result="inset"/><feGaussianBlur in="inset" stdDeviation="{deviation}" result="soft"/><feComposite in="SourceGraphic" in2="soft" operator="in"/></filter>"##,
797 source_bounds.attributes()
798 );
799 source = format!(r##"<g filter="url(#picture-soft-edge)">{source}</g>"##);
800 }
801 "a:innerShdw" | "a:outerShdw" | "a:glow" | "a:reflection" => {}
802 _ => return None,
803 }
804 }
805 if children.iter().any(|effect| {
806 matches!(effect.tag_name, "a:innerShdw" | "a:outerShdw" | "a:reflection")
807 || (glow_from_source && effect.tag_name == "a:glow")
808 }) {
809 let _ = write!(defs, r##"<g id="picture-body">{source}</g>"##);
810 }
811 let mut bounds = source_bounds.transformed(matrix);
812 let mut underlay = String::new();
813 let mut overlay = String::new();
814 for (index, effect) in children.iter().enumerate() {
815 let xml = &list[effect.start..effect.end];
816 let id = format!("picture-effect-{index}");
817 match effect.tag_name {
818 "a:glow" => {
819 let radius = blur_radius(xml, "rad")?;
820 let deviation = radius / 6.0;
821 let dilation = radius / 2.0;
822 let effect_bounds = bounds.expanded(dilation + 3.0 * deviation);
823 color_filter(defs, &id, effect_bounds, xml, colors, deviation, dilation)?;
824 if glow_from_source {
825 let _ = write!(
826 underlay,
827 r##"<g filter="url(#{id})"><use xlink:href="#picture-body" transform="{}"/></g>"##,
828 matrix.markup()
829 );
830 } else {
831 let _ = write!(
832 underlay,
833 r##"<g filter="url(#{id})"><path d="{path}" fill="white" transform="{}"/></g>"##,
834 matrix.markup()
835 );
836 }
837 bounds = bounds.union(effect_bounds);
838 }
839 "a:outerShdw" | "a:reflection" => {
840 let deviation = blur_radius(xml, "blurRad")?
841 / if effect.tag_name == "a:outerShdw" {
842 3.0
843 } else {
844 2.0
845 };
846 let rotate_with_shape = !get_attribute_ref(xml, "rotWithShape")
847 .is_some_and(|value| matches!(value, "0" | "false" | "off"));
848 let transformed = if rotate_with_shape {
849 matrix.composed(effect_matrix(xml, Bounds::rect(width, height))?)
850 } else {
851 effect_matrix(xml, Bounds::rect(width, height).transformed(matrix))?
852 .composed(matrix)
853 };
854 let raw_bounds = source_bounds.transformed(transformed);
855 let effect_bounds = raw_bounds.expanded(3.0 * deviation);
856 if effect.tag_name == "a:outerShdw" {
857 color_filter(defs, &id, effect_bounds, xml, colors, deviation, 0.0)?;
858 let _ = write!(
859 underlay,
860 r##"<g filter="url(#{id})"><use xlink:href="#picture-body" transform="{}"/></g>"##,
861 transformed.markup()
862 );
863 } else {
864 let start_alpha = percentage(xml, "stA", 1.0)?.clamp(0.0, 1.0);
865 let end_alpha = percentage(xml, "endA", 0.0)?.clamp(0.0, 1.0);
866 let start = percentage(xml, "stPos", 0.0)?.clamp(0.0, 1.0);
867 let end = percentage(xml, "endPos", 1.0)?.clamp(start, 1.0);
868 let fade = (number(xml, "fadeDir", 5_400_000.0)? / 60_000.0).to_radians();
869 let (dy, dx) = libm::sincos(fade);
870 let length = (raw_bounds.right - raw_bounds.left) * dx.abs()
871 + (raw_bounds.bottom - raw_bounds.top) * dy.abs();
872 let center_x = (raw_bounds.left + raw_bounds.right) / 2.0;
873 let center_y = (raw_bounds.top + raw_bounds.bottom) / 2.0;
874 let _ = write!(
875 defs,
876 r##"<linearGradient id="{id}-fade" gradientUnits="userSpaceOnUse" x1="{}" y1="{}" x2="{}" y2="{}"><stop offset="{start}" stop-color="white" stop-opacity="{start_alpha}"/><stop offset="{end}" stop-color="white" stop-opacity="{end_alpha}"/></linearGradient><mask id="{id}-mask" maskUnits="userSpaceOnUse" {}><rect {} fill="url(#{id}-fade)"/></mask><filter id="{id}" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="{deviation}"/></filter>"##,
877 center_x - length * dx / 2.0,
878 center_y - length * dy / 2.0,
879 center_x + length * dx / 2.0,
880 center_y + length * dy / 2.0,
881 effect_bounds.attributes(),
882 effect_bounds.attributes(),
883 effect_bounds.attributes()
884 );
885 let _ = write!(
886 underlay,
887 r##"<g data-office-art-overflow="true" mask="url(#{id}-mask)" filter="url(#{id})"><use xlink:href="#picture-body" transform="{}"/></g>"##,
888 transformed.markup()
889 );
890 }
891 bounds = bounds.union(effect_bounds);
892 }
893 "a:innerShdw" => {
894 let deviation = blur_radius(xml, "blurRad")? / 2.0;
895 let direction = (number(xml, "dir", 0.0)? / 60_000.0).to_radians();
896 let distance = number(xml, "dist", 0.0)?.max(0.0) / 9525.0;
897 let dx = -distance * libm::cos(direction);
898 let dy = -distance * libm::sin(direction);
899 let color = resolve_drawing_color_from_xml(Some(xml), colors)?;
900 let _ = write!(
901 defs,
902 r##"<filter id="{id}" filterUnits="userSpaceOnUse" {} color-interpolation-filters="sRGB"><feOffset in="SourceAlpha" dx="{dx}" dy="{dy}" result="offset"/><feGaussianBlur in="offset" stdDeviation="{deviation}" result="blur"/><feComposite in="SourceAlpha" in2="blur" operator="out" result="edge"/><feFlood flood-color="{}" flood-opacity="{}"/><feComposite in2="edge" operator="in"/></filter>"##,
903 local.attributes(),
904 escape_xml_text(&color.color),
905 color.opacity.unwrap_or(1.0)
906 );
907 let _ = write!(
908 overlay,
909 r##"<g transform="{}"><use xlink:href="#picture-body" filter="url(#{id})"/></g>"##,
910 matrix.markup()
911 );
912 }
913 _ => {}
914 }
915 }
916 if !bounds.valid() {
917 return None;
918 }
919 Some((
920 format!(
921 r##"<g data-office-art-effect="true" pointer-events="none">{underlay}</g><g transform="{}">{source}</g><g data-office-art-effect="true" pointer-events="none">{overlay}</g>"##,
922 matrix.markup()
923 ),
924 bounds,
925 ))
926}
927
928fn raster_metrics(source: &str) -> Option<(f64, f64, f64, f64)> {
929 let (header, payload) = source.split_once(',')?;
930 if !header.starts_with("data:image/") || !header.ends_with(";base64") {
931 return None;
932 }
933 let length = payload.len().min(262_144) / 4 * 4;
934 let bytes = base64::engine::general_purpose::STANDARD
935 .decode(&payload[..length])
936 .ok()?;
937 let be_u32 = |offset: usize| -> Option<u32> {
938 Some(u32::from_be_bytes(
939 bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
940 ))
941 };
942 let le_u32 = |offset: usize| -> Option<u32> {
943 Some(u32::from_le_bytes(
944 bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
945 ))
946 };
947 let le_u16 = |offset: usize| -> Option<u16> {
948 Some(u16::from_le_bytes(
949 bytes.get(offset..offset.checked_add(2)?)?.try_into().ok()?,
950 ))
951 };
952 let mut dpi = (72.0, 72.0);
953 let (width, height) = if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
954 let dimensions = (be_u32(16)? as f64, be_u32(20)? as f64);
955 let mut offset = 8usize;
956 while offset.checked_add(12)? <= bytes.len() {
957 let length = be_u32(offset)? as usize;
958 let kind = bytes.get(offset + 4..offset + 8)?;
959 if kind == b"pHYs" && length == 9 && bytes.get(offset + 16) == Some(&1) {
960 let x = be_u32(offset + 8)?;
961 let y = be_u32(offset + 12)?;
962 if x > 0 {
963 dpi.0 = x as f64 * 0.0254;
964 }
965 if y > 0 {
966 dpi.1 = y as f64 * 0.0254;
967 }
968 }
969 if kind == b"IDAT" {
970 break;
971 }
972 offset = offset.checked_add(length)?.checked_add(12)?;
973 }
974 dimensions
975 } else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
976 (le_u16(6)? as f64, le_u16(8)? as f64)
977 } else if bytes.starts_with(b"BM") {
978 if le_u32(14)? == 12 {
979 (le_u16(18)? as f64, le_u16(20)? as f64)
980 } else {
981 let x = le_u32(38)? as i32;
982 let y = le_u32(42)? as i32;
983 if x > 0 && y > 0 {
984 dpi = (x as f64 * 0.0254, y as f64 * 0.0254);
985 }
986 (
987 (le_u32(18)? as i32).checked_abs()? as f64,
988 (le_u32(22)? as i32).checked_abs()? as f64,
989 )
990 }
991 } else if bytes.starts_with(&[0xff, 0xd8]) {
992 let mut offset = 2usize;
993 let mut dimensions = None;
994 while offset + 4 <= bytes.len() {
995 if bytes[offset] != 0xff {
996 return None;
997 }
998 while bytes.get(offset + 1) == Some(&0xff) {
999 offset += 1;
1000 }
1001 let marker = *bytes.get(offset + 1)?;
1002 if marker == 0xda || marker == 0xd9 {
1003 break;
1004 }
1005 if marker == 0x01 || (0xd0..=0xd7).contains(&marker) {
1006 offset += 2;
1007 continue;
1008 }
1009 let length =
1010 u16::from_be_bytes(bytes.get(offset + 2..offset + 4)?.try_into().ok()?) as usize;
1011 if length < 2 {
1012 return None;
1013 }
1014 let data = bytes.get(offset + 4..offset.checked_add(length)?.checked_add(2)?)?;
1015 if marker == 0xe0 && data.starts_with(b"JFIF\0") && data.len() >= 12 {
1016 let x = u16::from_be_bytes(data[8..10].try_into().ok()?) as f64;
1017 let y = u16::from_be_bytes(data[10..12].try_into().ok()?) as f64;
1018 if x > 0.0 && y > 0.0 {
1019 match data[7] {
1020 1 => dpi = (x, y),
1021 2 => dpi = (x * 2.54, y * 2.54),
1022 _ => {}
1023 }
1024 }
1025 }
1026 if matches!(marker, 0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf)
1027 && data.len() >= 5
1028 {
1029 dimensions = Some((
1030 u16::from_be_bytes(data[3..5].try_into().ok()?) as f64,
1031 u16::from_be_bytes(data[1..3].try_into().ok()?) as f64,
1032 ));
1033 }
1034 offset = offset.checked_add(length)?.checked_add(2)?;
1035 }
1036 dimensions?
1037 } else {
1038 return None;
1039 };
1040 if width <= 0.0 || height <= 0.0 || dpi.0 <= 0.0 || dpi.1 <= 0.0 {
1041 return None;
1042 }
1043 Some((width, height, dpi.0, dpi.1))
1044}
1045
1046fn tile_picture(
1047 fill: &str,
1048 source: &str,
1049 intrinsic_source: &str,
1050 width: f64,
1051 height: f64,
1052 crop: [f64; 4],
1053 filter: &str,
1054 defs: &mut String,
1055) -> Option<String> {
1056 let tile = direct_child_tag(fill, "a:tile")?;
1057 let (pixel_width, pixel_height, dpi_x, dpi_y) = raster_metrics(intrinsic_source)?;
1058 let sx = percentage(tile, "sx", 1.0)?;
1059 let sy = percentage(tile, "sy", 1.0)?;
1060 let image_width = pixel_width * 96.0 / dpi_x * sx;
1061 let image_height = pixel_height * 96.0 / dpi_y * sy;
1062 let [left, top, right, bottom] = crop;
1063 let tile_width = image_width * (1.0 - left - right);
1064 let tile_height = image_height * (1.0 - top - bottom);
1065 if [tile_width, tile_height]
1066 .into_iter()
1067 .any(|value| !value.is_finite() || !(0.001..=1_000_000.0).contains(&value))
1068 {
1069 return None;
1070 }
1071 let (align_x, align_y) = alignment(get_attribute_ref(tile, "algn").unwrap_or("tl"));
1072 let x = (width - tile_width) * align_x + number(tile, "tx", 0.0)? / 9525.0;
1073 let y = (height - tile_height) * align_y + number(tile, "ty", 0.0)? / 9525.0;
1074 let flip = get_attribute_ref(tile, "flip").unwrap_or("none");
1075 let flip_x = matches!(flip, "x" | "xy");
1076 let flip_y = matches!(flip, "y" | "xy");
1077 let pattern_width = tile_width * if flip_x { 2.0 } else { 1.0 };
1078 let pattern_height = tile_height * if flip_y { 2.0 } else { 1.0 };
1079 let image_x = -left * image_width;
1080 let image_y = -top * image_height;
1081 let _ = write!(
1082 defs,
1083 r##"<clipPath id="picture-tile-clip"><rect width="{tile_width}" height="{tile_height}"/></clipPath><g id="picture-tile" clip-path="url(#picture-tile-clip)"><image xlink:href="{}" x="{image_x}" y="{image_y}" width="{image_width}" height="{image_height}" preserveAspectRatio="none"{filter}/></g><pattern id="picture-tile-pattern" x="{x}" y="{y}" width="{pattern_width}" height="{pattern_height}" patternUnits="userSpaceOnUse"><use xlink:href="#picture-tile"/>"##,
1084 escape_xml_text(source)
1085 );
1086 if flip_x {
1087 let _ = write!(
1088 defs,
1089 r##"<use xlink:href="#picture-tile" transform="translate({pattern_width} 0) scale(-1 1)"/>"##
1090 );
1091 }
1092 if flip_y {
1093 let _ = write!(
1094 defs,
1095 r##"<use xlink:href="#picture-tile" transform="translate(0 {pattern_height}) scale(1 -1)"/>"##
1096 );
1097 }
1098 if flip_x && flip_y {
1099 let _ = write!(
1100 defs,
1101 r##"<use xlink:href="#picture-tile" transform="translate({pattern_width} {pattern_height}) scale(-1 -1)"/>"##
1102 );
1103 }
1104 defs.push_str("</pattern>");
1105 Some(r##"fill="url(#picture-tile-pattern)""##.to_string())
1106}
1107
1108pub fn render_picture(
1109 xml: &str,
1110 source: &str,
1111 width: f64,
1112 height: f64,
1113 context: &impl PictureContext,
1114) -> Option<RenderedPicture> {
1115 if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 {
1116 return None;
1117 }
1118 let picture = extract_balanced_tag_slices(xml, "pic:pic")
1119 .into_iter()
1120 .next()?;
1121 let properties = direct_child_tag(picture, "pic:spPr").unwrap_or("");
1122 let fill = direct_child_tag(picture, "pic:blipFill")?;
1123 if direct_child_tag(properties, "a:scene3d").is_some()
1124 || direct_child_tag(properties, "a:sp3d").is_some()
1125 {
1126 return None;
1127 }
1128 let transform = direct_child_tag(properties, "a:xfrm").unwrap_or("");
1129 let rotation = (number(transform, "rot", 0.0)? / 60000.0).rem_euclid(360.0);
1130 let flip_h = enabled(transform, "flipH");
1131 let flip_v = enabled(transform, "flipV");
1132 if (rotation != 0.0 || flip_h || flip_v)
1133 && get_attribute_ref(fill, "rotWithShape")
1134 .is_some_and(|value| matches!(value, "0" | "false" | "off"))
1135 {
1136 return None;
1137 }
1138 let crop = rectangle(direct_child_tag(fill, "a:srcRect"))?;
1139 let destination = rectangle(
1140 direct_child_tag(fill, "a:stretch")
1141 .and_then(|stretch| direct_child_tag(stretch, "a:fillRect")),
1142 )?;
1143 let blip = direct_child_tag(fill, "a:blip")?;
1144 let intrinsic_source = source;
1145 let raster_source = if raster::required(blip, source) {
1146 Some(raster::render(blip, source, context)?)
1147 } else {
1148 None
1149 };
1150 let source = raster_source.as_deref().unwrap_or(source);
1151 let filter = if raster_source.is_some() {
1152 String::new()
1153 } else {
1154 blip_filter(blip, context)?
1155 };
1156 let (border, margin) = border(properties, context)?;
1157 let geometry = direct_child_tag(properties, "a:prstGeom")
1158 .and_then(|geometry| get_attribute_ref(geometry, "prst"));
1159 let shaped = direct_child_tag(properties, "a:custGeom").is_some()
1160 || geometry.is_some_and(|geometry| geometry != "rect");
1161 if !shaped
1162 && rotation == 0.0
1163 && !flip_h
1164 && !flip_v
1165 && border.is_empty()
1166 && filter.is_empty()
1167 && crop == [0.0; 4]
1168 && destination == [0.0; 4]
1169 && raster_source.is_none()
1170 && direct_child_tag(fill, "a:tile").is_none()
1171 && direct_child_tag(properties, "a:effectDag").is_none()
1172 && direct_child_tag(properties, "a:effectLst")
1173 .is_none_or(|effects| extract_direct_child_tag_ranges(effects).is_empty())
1174 {
1175 return None;
1176 }
1177 let path = geometry_path(properties, width, height)?;
1178 let [left, top, right, bottom] = crop;
1179 let [fill_left, fill_top, fill_right, fill_bottom] = destination;
1180 let source_width = 1.0 - left - right;
1181 let source_height = 1.0 - top - bottom;
1182 let fill_width = width * (1.0 - fill_left - fill_right);
1183 let fill_height = height * (1.0 - fill_top - fill_bottom);
1184 if source_width <= 0.0 || source_height <= 0.0 || fill_width <= 0.0 || fill_height <= 0.0 {
1185 return None;
1186 }
1187 let image_width = fill_width / source_width;
1188 let image_height = fill_height / source_height;
1189 let image_x = width * fill_left - left * image_width;
1190 let image_y = height * fill_top - top * image_height;
1191 if [image_width, image_height, image_x, image_y]
1192 .iter()
1193 .any(|value| !value.is_finite() || value.abs() > 100_000_000.0)
1194 {
1195 return None;
1196 }
1197 let mut defs = format!(
1198 r##"<clipPath id="picture-clip"><path d="{path}"/></clipPath><clipPath id="picture-fill"><rect x="{}" y="{}" width="{fill_width}" height="{fill_height}"/></clipPath>"##,
1199 width * fill_left,
1200 height * fill_top
1201 );
1202 let filter_attribute = if filter.is_empty() {
1203 String::new()
1204 } else {
1205 let _ = write!(
1206 defs,
1207 r##"<filter id="picture-filter" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">{filter}</filter>"##
1208 );
1209 r##" filter="url(#picture-filter)""##.to_string()
1210 };
1211 let border_element = if border.is_empty() {
1212 String::new()
1213 } else if rectangular_picture(properties) {
1214 let line_width =
1215 number(direct_child_tag(properties, "a:ln")?, "w", 12700.0)?.max(0.0) / 9525.0;
1216 let outset = line_width / 2.0;
1217 format!(
1218 r##"<rect x="{}" y="{}" width="{}" height="{}" {border}/>"##,
1219 -outset,
1220 -outset,
1221 width + line_width,
1222 height + line_width
1223 )
1224 } else {
1225 let bounds = Bounds::rect(width, height).expanded(margin);
1226 let _ = write!(
1227 defs,
1228 r##"<mask id="picture-border-mask" maskUnits="userSpaceOnUse" {}><rect {} fill="white"/><path d="{path}" fill="black"/></mask>"##,
1229 bounds.attributes(),
1230 bounds.attributes()
1231 );
1232 format!(r##"<path d="{path}" {border} mask="url(#picture-border-mask)"/>"##)
1233 };
1234 let fill_element = if direct_child_tag(fill, "a:tile").is_some() {
1235 let tiled = tile_picture(
1236 fill,
1237 source,
1238 intrinsic_source,
1239 width,
1240 height,
1241 crop,
1242 &filter_attribute,
1243 &mut defs,
1244 )?;
1245 format!(r##"<path d="{path}" {tiled}/>"##)
1246 } else {
1247 format!(
1248 r##"<g clip-path="url(#picture-clip)"><g clip-path="url(#picture-fill)"><image xlink:href="{}" x="{image_x}" y="{image_y}" width="{image_width}" height="{image_height}" preserveAspectRatio="none"{filter_attribute}/></g></g>"##,
1249 escape_xml_text(source)
1250 )
1251 };
1252 let (sin, cos) = libm::sincos(rotation.to_radians());
1253 let sx = if flip_h { -1.0 } else { 1.0 };
1254 let sy = if flip_v { -1.0 } else { 1.0 };
1255 let [a, b, c, d] = [cos * sx, sin * sx, -sin * sy, cos * sy];
1256 let matrix = Matrix([
1257 a,
1258 b,
1259 c,
1260 d,
1261 width / 2.0 - a * width / 2.0 - c * height / 2.0,
1262 height / 2.0 - b * width / 2.0 - d * height / 2.0,
1263 ]);
1264 let (body, paint) = apply_shape_effects(
1265 properties,
1266 &format!("{fill_element}{border_element}"),
1267 &path,
1268 width,
1269 height,
1270 margin,
1271 matrix,
1272 context,
1273 &mut defs,
1274 false,
1275 )?;
1276 let paint_width = paint.right - paint.left;
1277 let paint_height = paint.bottom - paint.top;
1278 let paint_x = paint.left;
1279 let paint_y = paint.top;
1280 let bounds = ((paint_x.abs()
1281 + paint_y.abs()
1282 + (paint_width - width).abs()
1283 + (paint_height - height).abs())
1284 > 0.000001)
1285 .then(|| PaintBounds {
1286 x_px: paint_x,
1287 y_px: paint_y,
1288 width_px: paint_width,
1289 height_px: paint_height,
1290 layout_width_px: width,
1291 layout_height_px: height,
1292 });
1293 let svg = format!(
1294 r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{paint_width}" height="{paint_height}" viewBox="{paint_x} {paint_y} {paint_width} {paint_height}" preserveAspectRatio="none"><defs>{defs}</defs>{body}</svg>"##,
1295 );
1296 Some(RenderedPicture {
1297 svg,
1298 paint_bounds: bounds,
1299 })
1300}