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