Skip to main content

document_svg/
reverse.rs

1//! Package SVG pages as vector images in PPTX, DOCX, or XLSX files.
2//!
3//! This preserves the rendered SVG, not the source document's semantic structure.
4
5use std::fmt::{Display, Formatter};
6use std::fs;
7use std::io::{BufWriter, Write};
8use std::path::{Path, PathBuf};
9
10use base64::Engine;
11use quick_xml::Reader;
12use quick_xml::events::Event;
13use serde::Serialize;
14use zip::ZipWriter;
15use zip::write::SimpleFileOptions;
16
17use crate::error::{Error, Result};
18use crate::ooxml::{attribute, local_name};
19
20const EMU_PER_POINT: f64 = 12_700.0;
21const FALLBACK_DPI: f64 = 96.0;
22const MAX_FALLBACK_DIMENSION: f64 = 4_096.0;
23const MAX_FALLBACK_PIXELS: f64 = 16_777_216.0;
24const MAX_NESTED_SVG_DEPTH: usize = 4;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
27#[serde(rename_all = "lowercase")]
28pub enum OpenXmlFormat {
29    Pptx,
30    Docx,
31    Xlsx,
32}
33
34impl OpenXmlFormat {
35    fn detect(path: &Path) -> Result<Self> {
36        let extension = path
37            .extension()
38            .and_then(|value| value.to_str())
39            .map(str::to_ascii_lowercase)
40            .ok_or_else(|| Error::InvalidInput("output has no file extension".into()))?;
41        match extension.as_str() {
42            "pptx" => Ok(Self::Pptx),
43            "docx" => Ok(Self::Docx),
44            "xlsx" => Ok(Self::Xlsx),
45            _ => Err(Error::Unsupported(format!(
46                "output extension .{extension}; expected PPTX, DOCX, or XLSX"
47            ))),
48        }
49    }
50}
51
52impl Display for OpenXmlFormat {
53    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
54        formatter.write_str(match self {
55            Self::Pptx => "PPTX",
56            Self::Docx => "DOCX",
57            Self::Xlsx => "XLSX",
58        })
59    }
60}
61
62#[derive(Clone, Debug)]
63pub struct ReverseOptions {
64    pub max_input_bytes: u64,
65    pub max_pages: usize,
66}
67
68impl Default for ReverseOptions {
69    fn default() -> Self {
70        Self {
71            max_input_bytes: 512 * 1024 * 1024,
72            max_pages: 10_000,
73        }
74    }
75}
76
77#[derive(Clone, Debug, Serialize)]
78pub struct ReverseReport {
79    pub converter: &'static str,
80    pub version: &'static str,
81    pub source: String,
82    pub output: String,
83    pub output_format: OpenXmlFormat,
84    pub page_count: usize,
85    pub input_bytes: u64,
86    pub warnings: Vec<String>,
87}
88
89#[derive(Clone, Debug)]
90struct SvgPage {
91    bytes: Vec<u8>,
92    fallback_png: Vec<u8>,
93    width_points: f64,
94    height_points: f64,
95}
96
97pub fn svg_to_openxml(
98    input: impl AsRef<Path>,
99    output: impl AsRef<Path>,
100    options: &ReverseOptions,
101) -> Result<ReverseReport> {
102    let input = input.as_ref();
103    let output = output.as_ref();
104    if output.exists() {
105        return Err(Error::InvalidInput(format!(
106            "output already exists: {}",
107            output.display()
108        )));
109    }
110    if options.max_pages == 0 {
111        return Err(Error::InvalidInput("max_pages must be at least 1".into()));
112    }
113    let format = OpenXmlFormat::detect(output)?;
114    let paths = collect_svg_paths(input, options.max_pages)?;
115    let mut pages = Vec::with_capacity(paths.len());
116    let mut input_bytes = 0u64;
117    let mut render_options = None;
118    for path in paths {
119        let metadata = fs::metadata(&path)?;
120        input_bytes = input_bytes.saturating_add(metadata.len());
121        if input_bytes > options.max_input_bytes {
122            return Err(Error::LimitExceeded(format!(
123                "SVG input is {input_bytes} bytes; maximum is {} bytes",
124                options.max_input_bytes
125            )));
126        }
127        let bytes = fs::read(&path)?;
128        validate_svg_document(&bytes, 0)?;
129        let (width_points, height_points) = svg_dimensions(&bytes)?;
130        let render_options = render_options.get_or_insert_with(|| {
131            let mut options = resvg::usvg::Options::default();
132            options.fontdb_mut().load_system_fonts();
133            options
134        });
135        let fallback_png =
136            render_svg_fallback(&bytes, width_points, height_points, render_options)?;
137        pages.push(SvgPage {
138            bytes,
139            fallback_png,
140            width_points,
141            height_points,
142        });
143    }
144
145    if let Some(parent) = output.parent().filter(|path| !path.as_os_str().is_empty()) {
146        fs::create_dir_all(parent)?;
147    }
148    let parent = output
149        .parent()
150        .filter(|path| !path.as_os_str().is_empty())
151        .unwrap_or(Path::new("."));
152    let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
153    let writer = BufWriter::new(temporary.as_file_mut());
154    let mut package = Package::new(writer);
155    match format {
156        OpenXmlFormat::Pptx => write_pptx(&mut package, &pages)?,
157        OpenXmlFormat::Docx => write_docx(&mut package, &pages)?,
158        OpenXmlFormat::Xlsx => write_xlsx(&mut package, &pages)?,
159    }
160    package.finish()?;
161    temporary
162        .persist_noclobber(output)
163        .map_err(|error| error.error)?;
164
165    Ok(ReverseReport {
166        converter: "document-svg",
167        version: env!("CARGO_PKG_VERSION"),
168        source: input.to_string_lossy().into_owned(),
169        output: output.to_string_lossy().into_owned(),
170        output_format: format,
171        page_count: pages.len(),
172        input_bytes,
173        warnings: vec![
174            "SVG pages are embedded as vector images; original document semantics are not reconstructed"
175                .into(),
176        ],
177    })
178}
179
180fn collect_svg_paths(input: &Path, max_pages: usize) -> Result<Vec<PathBuf>> {
181    let metadata = fs::metadata(input)?;
182    let mut paths = if metadata.is_file() {
183        if !has_svg_extension(input) {
184            return Err(Error::Unsupported(format!(
185                "input {}; expected an SVG file or directory",
186                input.display()
187            )));
188        }
189        vec![input.to_path_buf()]
190    } else if metadata.is_dir() {
191        let mut entries = Vec::new();
192        for entry in fs::read_dir(input)? {
193            let path = entry?.path();
194            if path.is_file() && has_svg_extension(&path) {
195                entries.push(path);
196            }
197        }
198        entries.sort();
199        entries
200    } else {
201        return Err(Error::InvalidInput(format!(
202            "{} is not a regular file or directory",
203            input.display()
204        )));
205    };
206    if paths.is_empty() {
207        return Err(Error::InvalidInput(format!(
208            "{} contains no SVG files",
209            input.display()
210        )));
211    }
212    if paths.len() > max_pages {
213        return Err(Error::LimitExceeded(format!(
214            "SVG input has {} pages; maximum is {max_pages}",
215            paths.len()
216        )));
217    }
218    paths.shrink_to_fit();
219    Ok(paths)
220}
221
222fn has_svg_extension(path: &Path) -> bool {
223    path.extension()
224        .and_then(|value| value.to_str())
225        .is_some_and(|value| value.eq_ignore_ascii_case("svg"))
226}
227
228fn svg_dimensions(bytes: &[u8]) -> Result<(f64, f64)> {
229    let mut reader = Reader::from_reader(bytes);
230    reader.config_mut().trim_text(true);
231    let mut buffer = Vec::new();
232    loop {
233        match reader.read_event_into(&mut buffer)? {
234            Event::Start(start) | Event::Empty(start)
235                if local_name(start.name().as_ref()) == b"svg" =>
236            {
237                let width = attribute(&start, b"width").and_then(|value| parse_svg_length(&value));
238                let height =
239                    attribute(&start, b"height").and_then(|value| parse_svg_length(&value));
240                let view_box = attribute(&start, b"viewBox").and_then(|value| {
241                    let tokens = value
242                        .split(|character: char| {
243                            character.is_ascii_whitespace() || character == ','
244                        })
245                        .filter(|value| !value.is_empty())
246                        .collect::<Vec<_>>();
247                    if tokens.len() != 4 {
248                        return None;
249                    }
250                    let values = tokens
251                        .iter()
252                        .map(|value| value.parse::<f64>())
253                        .collect::<std::result::Result<Vec<_>, _>>()
254                        .ok()?;
255                    (values.iter().all(|value| value.is_finite())
256                        && values[2] > 0.0
257                        && values[3] > 0.0)
258                        .then_some((values[2], values[3]))
259                });
260                let width = width.or_else(|| view_box.map(|value| value.0 * 0.75));
261                let height = height.or_else(|| view_box.map(|value| value.1 * 0.75));
262                let (width, height) = match (width, height) {
263                    (Some(width), Some(height)) if width > 0.0 && height > 0.0 => (width, height),
264                    _ => {
265                        return Err(Error::InvalidInput(
266                            "SVG root needs positive width/height or viewBox dimensions".into(),
267                        ));
268                    }
269                };
270                return Ok((width, height));
271            }
272            Event::Eof => {
273                return Err(Error::InvalidInput(
274                    "input does not contain an SVG root".into(),
275                ));
276            }
277            _ => {}
278        }
279        buffer.clear();
280    }
281}
282
283fn parse_svg_length(value: &str) -> Option<f64> {
284    let value = value.trim();
285    let lower = value.to_ascii_lowercase();
286    let (number, unit) = ["pt", "px", "in", "cm", "mm", "pc"]
287        .into_iter()
288        .find_map(|unit| lower.strip_suffix(unit).map(|number| (number.trim(), unit)))
289        .unwrap_or((value, ""));
290    let number = number.parse::<f64>().ok()?;
291    let points = match unit {
292        "pt" => number,
293        "px" | "" => number * 0.75,
294        "in" => number * 72.0,
295        "cm" => number * 72.0 / 2.54,
296        "mm" => number * 72.0 / 25.4,
297        "pc" => number * 12.0,
298        _ => unreachable!(),
299    };
300    points.is_finite().then_some(points)
301}
302
303fn validate_svg_document(bytes: &[u8], depth: usize) -> Result<()> {
304    if depth > MAX_NESTED_SVG_DEPTH {
305        return Err(Error::LimitExceeded(format!(
306            "nested SVG depth exceeds {MAX_NESTED_SVG_DEPTH}"
307        )));
308    }
309    let mut reader = Reader::from_reader(bytes);
310    reader.config_mut().trim_text(false);
311    let mut buffer = Vec::new();
312    let mut saw_root = false;
313    let mut root_closed = false;
314    let mut element_depth = 0usize;
315    let mut style_depth = None::<usize>;
316    loop {
317        match reader.read_event_into(&mut buffer)? {
318            Event::DocType(_) => {
319                return Err(Error::InvalidInput(
320                    "SVG document types and entities are not allowed".into(),
321                ));
322            }
323            Event::PI(_) => {
324                return Err(Error::InvalidInput(
325                    "SVG processing instructions are not allowed".into(),
326                ));
327            }
328            Event::GeneralRef(_) if style_depth.is_some() => {
329                return Err(Error::InvalidInput(
330                    "SVG CSS entity references are not allowed".into(),
331                ));
332            }
333            Event::Start(start) => {
334                if root_closed {
335                    return Err(Error::InvalidInput(
336                        "input contains content after the SVG root".into(),
337                    ));
338                }
339                if !saw_root {
340                    if local_name(start.name().as_ref()) != b"svg" {
341                        return Err(Error::InvalidInput(
342                            "input must have an <svg> root element".into(),
343                        ));
344                    }
345                    validate_svg_root_namespace(&start)?;
346                    saw_root = true;
347                }
348                validate_svg_element(&start, reader.decoder(), depth)?;
349                if local_name(start.name().as_ref()).eq_ignore_ascii_case(b"style") {
350                    style_depth = Some(element_depth + 1);
351                }
352                element_depth += 1;
353            }
354            Event::Empty(start) => {
355                if root_closed {
356                    return Err(Error::InvalidInput(
357                        "input contains content after the SVG root".into(),
358                    ));
359                }
360                if !saw_root {
361                    if local_name(start.name().as_ref()) != b"svg" {
362                        return Err(Error::InvalidInput(
363                            "input must have an <svg> root element".into(),
364                        ));
365                    }
366                    validate_svg_root_namespace(&start)?;
367                    saw_root = true;
368                    root_closed = true;
369                }
370                validate_svg_element(&start, reader.decoder(), depth)?;
371            }
372            Event::Text(text) => {
373                let value = text.decode().map_err(|error| {
374                    Error::InvalidInput(format!("invalid SVG text encoding: {error}"))
375                })?;
376                if (!saw_root || root_closed) && !value.trim().is_empty() {
377                    return Err(Error::InvalidInput(
378                        "input contains text outside the SVG root".into(),
379                    ));
380                }
381                if style_depth.is_some() {
382                    validate_css_references(&value)?;
383                }
384            }
385            Event::CData(text) => {
386                if !saw_root || root_closed {
387                    return Err(Error::InvalidInput(
388                        "input contains CDATA outside the SVG root".into(),
389                    ));
390                }
391                if style_depth.is_some() {
392                    let value = String::from_utf8_lossy(text.as_ref());
393                    validate_css_references(&value)?;
394                }
395            }
396            Event::End(end) => {
397                element_depth = element_depth.saturating_sub(1);
398                if element_depth == 0 {
399                    root_closed = true;
400                }
401                if local_name(end.name().as_ref()).eq_ignore_ascii_case(b"style") {
402                    style_depth = None;
403                }
404            }
405            Event::Eof => break,
406            _ => {}
407        }
408        buffer.clear();
409    }
410    if !saw_root || !root_closed {
411        return Err(Error::InvalidInput(
412            "input does not contain an SVG root".into(),
413        ));
414    }
415    Ok(())
416}
417
418fn validate_svg_root_namespace(start: &quick_xml::events::BytesStart<'_>) -> Result<()> {
419    let raw_name = String::from_utf8_lossy(start.name().as_ref()).into_owned();
420    let namespace_key = raw_name.split_once(':').map_or_else(
421        || "xmlns".to_owned(),
422        |(prefix, _)| format!("xmlns:{prefix}"),
423    );
424    let mut namespace = None::<String>;
425    for item in start.attributes().with_checks(true) {
426        let item = item
427            .map_err(|error| Error::InvalidInput(format!("invalid SVG root attribute: {error}")))?;
428        if item.key.as_ref() == namespace_key.as_bytes() {
429            namespace = Some(String::from_utf8_lossy(item.value.as_ref()).into_owned());
430        }
431    }
432    if raw_name.contains(':') && namespace.as_deref() != Some("http://www.w3.org/2000/svg") {
433        return Err(Error::InvalidInput(
434            "prefixed SVG root must use the SVG namespace".into(),
435        ));
436    }
437    if namespace
438        .as_deref()
439        .is_some_and(|value| value != "http://www.w3.org/2000/svg")
440    {
441        return Err(Error::InvalidInput(
442            "SVG root uses an unsupported namespace".into(),
443        ));
444    }
445    Ok(())
446}
447
448fn validate_svg_element(
449    start: &quick_xml::events::BytesStart<'_>,
450    decoder: quick_xml::encoding::Decoder,
451    depth: usize,
452) -> Result<()> {
453    let name = String::from_utf8_lossy(local_name(start.name().as_ref())).to_ascii_lowercase();
454    if matches!(
455        name.as_str(),
456        "script"
457            | "foreignobject"
458            | "iframe"
459            | "object"
460            | "embed"
461            | "animate"
462            | "animatetransform"
463            | "animatemotion"
464            | "discard"
465            | "set"
466    ) {
467        return Err(Error::InvalidInput(format!(
468            "active SVG element <{name}> is not allowed"
469        )));
470    }
471    for item in start.attributes().with_checks(true) {
472        let item =
473            item.map_err(|error| Error::InvalidInput(format!("invalid SVG attribute: {error}")))?;
474        let key = String::from_utf8_lossy(local_name(item.key.as_ref())).to_ascii_lowercase();
475        let value = item
476            .decoded_and_normalized_value(quick_xml::XmlVersion::Implicit1_0, decoder)?
477            .into_owned();
478        if key.starts_with("on") || key == "base" {
479            return Err(Error::InvalidInput(format!(
480                "SVG event attribute {key} is not allowed"
481            )));
482        }
483        if key == "href" {
484            validate_svg_href(&value, depth)?;
485        }
486        validate_css_references(&value)?;
487    }
488    Ok(())
489}
490
491fn validate_svg_href(value: &str, depth: usize) -> Result<()> {
492    let value = value.trim();
493    if value.is_empty() || value.starts_with('#') {
494        return Ok(());
495    }
496    let lower = value.to_ascii_lowercase();
497    if lower.starts_with("data:image/png;")
498        || lower.starts_with("data:image/jpeg;")
499        || lower.starts_with("data:image/gif;")
500        || lower.starts_with("data:image/webp;")
501    {
502        return Ok(());
503    }
504    if lower.starts_with("data:image/svg+xml;base64,") {
505        let encoded = value
506            .split_once(',')
507            .map(|(_, data)| data)
508            .unwrap_or_default();
509        let compact = encoded
510            .bytes()
511            .filter(|byte| !byte.is_ascii_whitespace())
512            .collect::<Vec<_>>();
513        let nested = base64::engine::general_purpose::STANDARD
514            .decode(compact)
515            .map_err(|error| {
516                Error::InvalidInput(format!("invalid nested SVG data URI: {error}"))
517            })?;
518        return validate_svg_document(&nested, depth + 1);
519    }
520    Err(Error::InvalidInput(format!(
521        "external or active SVG reference is not allowed: {}",
522        value.chars().take(80).collect::<String>()
523    )))
524}
525
526fn validate_css_references(value: &str) -> Result<()> {
527    let lower = value.to_ascii_lowercase();
528    if lower.contains('\\') || lower.contains("/*") {
529        return Err(Error::InvalidInput(
530            "SVG CSS escapes and comments are not allowed".into(),
531        ));
532    }
533    if lower.contains("@import") || lower.contains("javascript:") {
534        return Err(Error::InvalidInput(
535            "external or active SVG CSS is not allowed".into(),
536        ));
537    }
538    let mut remainder = value;
539    while let Some(index) = remainder.to_ascii_lowercase().find("url(") {
540        let after = &remainder[index + 4..];
541        let Some(end) = after.find(')') else {
542            return Err(Error::InvalidInput("unterminated SVG CSS url()".into()));
543        };
544        let target = after[..end]
545            .trim()
546            .trim_matches(|character| matches!(character, '\'' | '"'));
547        if !target.starts_with('#') {
548            return Err(Error::InvalidInput(
549                "external SVG CSS url() reference is not allowed".into(),
550            ));
551        }
552        remainder = &after[end + 1..];
553    }
554    Ok(())
555}
556
557fn render_svg_fallback(
558    bytes: &[u8],
559    width_points: f64,
560    height_points: f64,
561    options: &resvg::usvg::Options<'_>,
562) -> Result<Vec<u8>> {
563    let tree = resvg::usvg::Tree::from_data(bytes, options)
564        .map_err(|error| Error::InvalidInput(format!("SVG fallback parse failed: {error}")))?;
565    let (pixel_width, pixel_height) = fallback_pixel_size(width_points, height_points);
566    let mut pixmap = resvg::tiny_skia::Pixmap::new(pixel_width, pixel_height).ok_or_else(|| {
567        Error::LimitExceeded(format!(
568            "SVG fallback raster allocation failed for {pixel_width}x{pixel_height} pixels"
569        ))
570    })?;
571    let source = tree.size();
572    let transform = resvg::tiny_skia::Transform::from_scale(
573        pixel_width as f32 / source.width(),
574        pixel_height as f32 / source.height(),
575    );
576    resvg::render(&tree, transform, &mut pixmap.as_mut());
577    pixmap
578        .encode_png()
579        .map_err(|error| Error::InvalidInput(format!("SVG fallback PNG encoding failed: {error}")))
580}
581
582fn fallback_pixel_size(width_points: f64, height_points: f64) -> (u32, u32) {
583    let mut width = (width_points * FALLBACK_DPI / 72.0).max(1.0);
584    let mut height = (height_points * FALLBACK_DPI / 72.0).max(1.0);
585    let scale = (MAX_FALLBACK_DIMENSION / width)
586        .min(MAX_FALLBACK_DIMENSION / height)
587        .min((MAX_FALLBACK_PIXELS / (width * height)).sqrt())
588        .min(1.0);
589    width *= scale;
590    height *= scale;
591    (
592        width.round().max(1.0) as u32,
593        height.round().max(1.0) as u32,
594    )
595}
596
597struct Package<W: Write + std::io::Seek> {
598    zip: ZipWriter<W>,
599    options: SimpleFileOptions,
600}
601
602impl<W: Write + std::io::Seek> Package<W> {
603    fn new(writer: W) -> Self {
604        Self {
605            zip: ZipWriter::new(writer),
606            options: SimpleFileOptions::default()
607                .compression_method(zip::CompressionMethod::Deflated)
608                .unix_permissions(0o644),
609        }
610    }
611
612    fn part(&mut self, name: &str, bytes: impl AsRef<[u8]>) -> Result<()> {
613        self.zip.start_file(name, self.options)?;
614        self.zip.write_all(bytes.as_ref())?;
615        Ok(())
616    }
617
618    fn finish(self) -> Result<()> {
619        self.zip.finish()?;
620        Ok(())
621    }
622}
623
624fn root_relationships(target: &str, relationship_type: &str) -> String {
625    format!(
626        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
627<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/{relationship_type}" Target="{target}"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>"#
628    )
629}
630
631fn property_content_type_overrides() -> &'static str {
632    r#"<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#
633}
634
635fn write_common_properties<W: Write + std::io::Seek>(package: &mut Package<W>) -> Result<()> {
636    package.part(
637        "docProps/core.xml",
638        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
639<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>Document SVG</dc:title><dc:creator>document-svg</dc:creator><cp:lastModifiedBy>document-svg</cp:lastModifiedBy></cp:coreProperties>"#,
640    )?;
641    package.part(
642        "docProps/app.xml",
643        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
644<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>document-svg</Application><AppVersion>0.1</AppVersion></Properties>"#,
645    )?;
646    Ok(())
647}
648
649fn write_pptx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
650    let mut overrides = String::new();
651    let mut slide_ids = String::new();
652    let mut presentation_rels = String::new();
653    for (index, _) in pages.iter().enumerate() {
654        let number = index + 1;
655        overrides.push_str(&format!(r#"<Override PartName="/ppt/slides/slide{number}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>"#));
656        slide_ids.push_str(&format!(
657            r#"<p:sldId id="{}" r:id="rId{}"/>"#,
658            256 + index,
659            number + 1
660        ));
661        presentation_rels.push_str(&format!(r#"<Relationship Id="rId{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide{number}.xml"/>"#, number + 1));
662    }
663    let content_types = format!(
664        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
665<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="svg" ContentType="image/svg+xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/><Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/><Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>{property_overrides}{overrides}</Types>"#,
666        property_overrides = property_content_type_overrides(),
667    );
668    package.part("[Content_Types].xml", content_types)?;
669    package.part(
670        "_rels/.rels",
671        root_relationships("ppt/presentation.xml", "officeDocument"),
672    )?;
673    write_common_properties(package)?;
674    let first = &pages[0];
675    let slide_width = points_to_emu(first.width_points);
676    let slide_height = points_to_emu(first.height_points);
677    let presentation = format!(
678        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
679<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:sldIdLst>{slide_ids}</p:sldIdLst><p:sldSz cx="{slide_width}" cy="{slide_height}" type="custom"/><p:notesSz cx="6858000" cy="9144000"/></p:presentation>"#
680    );
681    package.part("ppt/presentation.xml", presentation)?;
682    let presentation_relationships = format!(
683        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
684<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>{presentation_rels}<Relationship Id="rId{pres_props_id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" Target="presProps.xml"/></Relationships>"#,
685        pres_props_id = pages.len() + 2,
686    );
687    package.part(
688        "ppt/_rels/presentation.xml.rels",
689        presentation_relationships,
690    )?;
691    package.part("ppt/slideMasters/slideMaster1.xml", pptx_slide_master())?;
692    package.part("ppt/slideMasters/_rels/slideMaster1.xml.rels", r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
693<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/></Relationships>"#)?;
694    package.part("ppt/slideLayouts/slideLayout1.xml", pptx_slide_layout())?;
695    package.part("ppt/slideLayouts/_rels/slideLayout1.xml.rels", r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
696<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/></Relationships>"#)?;
697    package.part("ppt/theme/theme1.xml", pptx_theme())?;
698    package.part(
699        "ppt/presProps.xml",
700        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
701<p:presentationPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:showPr useTimings="0"/></p:presentationPr>"#,
702    )?;
703    for (index, page) in pages.iter().enumerate() {
704        let number = index + 1;
705        let (x, y, cx, cy) = fit_rect(
706            page.width_points,
707            page.height_points,
708            first.width_points,
709            first.height_points,
710        );
711        let slide = format!(
712            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
713<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><mc:AlternateContent><mc:Choice Requires="asvg">{choice}</mc:Choice><mc:Fallback>{fallback}</mc:Fallback></mc:AlternateContent></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>"#,
714            choice = pptx_svg_picture(number, x, y, cx, cy, true),
715            fallback = pptx_svg_picture(number, x, y, cx, cy, false),
716        );
717        package.part(&format!("ppt/slides/slide{number}.xml"), slide)?;
718        let relationships = format!(
719            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
720<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{number}.svg"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{number}-fallback.png"/></Relationships>"#
721        );
722        package.part(
723            &format!("ppt/slides/_rels/slide{number}.xml.rels"),
724            relationships,
725        )?;
726        package.part(&format!("ppt/media/image{number}.svg"), &page.bytes)?;
727        package.part(
728            &format!("ppt/media/image{number}-fallback.png"),
729            &page.fallback_png,
730        )?;
731    }
732    Ok(())
733}
734
735fn pptx_svg_picture(number: usize, x: i64, y: i64, cx: i64, cy: i64, svg: bool) -> String {
736    let extension = if svg {
737        r#"<a:extLst><a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}"><asvg:svgBlip r:embed="rId2"/></a:ext></a:extLst>"#
738    } else {
739        ""
740    };
741    format!(
742        r#"<p:pic><p:nvPicPr><p:cNvPr id="2" name="SVG page {number}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId3">{extension}</a:blip><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="{x}" y="{y}"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln><a:noFill/></a:ln></p:spPr></p:pic>"#
743    )
744}
745
746fn pptx_slide_master() -> &'static str {
747    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
748<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMap accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" bg1="lt1" bg2="lt2" folHlink="folHlink" hlink="hlink" tx1="dk1" tx2="dk2"/><p:sldLayoutIdLst><p:sldLayoutId id="1" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>"#
749}
750
751fn pptx_slide_layout() -> &'static str {
752    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
753<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1"><p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>"#
754}
755
756fn pptx_theme() -> &'static str {
757    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
758<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Document SVG"><a:themeElements><a:clrScheme name="Document SVG"><a:dk1><a:srgbClr val="000000"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F1F1F"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Document SVG"><a:majorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Arial"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="Document SVG"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="25400"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="38100"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>"#
759}
760
761fn write_docx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
762    let content_types = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
763<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="svg" ContentType="image/svg+xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
764    package.part("[Content_Types].xml", content_types)?;
765    package.part(
766        "_rels/.rels",
767        root_relationships("word/document.xml", "officeDocument"),
768    )?;
769    write_common_properties(package)?;
770    let mut body = String::new();
771    let mut relationships = String::new();
772    for (index, page) in pages.iter().enumerate() {
773        let number = index + 1;
774        let cx = points_to_emu(page.width_points);
775        let cy = points_to_emu(page.height_points);
776        body.push_str("<w:p><w:pPr><w:spacing w:before=\"0\" w:after=\"0\"/>");
777        if number < pages.len() {
778            body.push_str(&docx_section_properties(page, true));
779        }
780        body.push_str("</w:pPr>");
781        let png_rel = number * 2 - 1;
782        let svg_rel = number * 2;
783        body.push_str(&format!(r#"<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="{cx}" cy="{cy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="{number}" name="SVG page {number}"/><wp:cNvGraphicFramePr/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="{number}" name="SVG page {number}"/><pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr></pic:nvPicPr><pic:blipFill><a:blip r:embed="rId{png_rel}"><a:extLst><a:ext uri="{{96DAC541-7B7A-43D3-8B79-37D633B846F1}}"><asvg:svgBlip r:embed="rId{svg_rel}"/></a:ext></a:extLst></a:blip><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln><a:noFill/></a:ln></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r></w:p>"#));
784        relationships.push_str(&format!(r#"<Relationship Id="rId{png_rel}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image{number}-fallback.png"/><Relationship Id="rId{svg_rel}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image{number}.svg"/>"#));
785        package.part(&format!("word/media/image{number}.svg"), &page.bytes)?;
786        package.part(
787            &format!("word/media/image{number}-fallback.png"),
788            &page.fallback_png,
789        )?;
790    }
791    let final_section = docx_section_properties(&pages[pages.len() - 1], false);
792    let document = format!(
793        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
794<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main"><w:body>{body}{final_section}</w:body></w:document>"#
795    );
796    package.part("word/document.xml", document)?;
797    package.part("word/_rels/document.xml.rels", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
798<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{relationships}</Relationships>"#))?;
799    Ok(())
800}
801
802fn docx_section_properties(page: &SvgPage, section_break: bool) -> String {
803    let page_width = points_to_twips(page.width_points);
804    let page_height = points_to_twips(page.height_points);
805    let orientation = if page.width_points > page.height_points {
806        r#" w:orient="landscape""#
807    } else {
808        ""
809    };
810    let section_type = if section_break {
811        r#"<w:type w:val="nextPage"/>"#
812    } else {
813        ""
814    };
815    format!(
816        r#"<w:sectPr>{section_type}<w:pgSz w:w="{page_width}" w:h="{page_height}"{orientation}/><w:pgMar w:top="0" w:right="0" w:bottom="0" w:left="0" w:header="0" w:footer="0" w:gutter="0"/></w:sectPr>"#
817    )
818}
819
820fn write_xlsx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
821    let mut overrides = String::new();
822    let mut sheets = String::new();
823    let mut workbook_rels = String::new();
824    for (index, _) in pages.iter().enumerate() {
825        let number = index + 1;
826        overrides.push_str(&format!(r#"<Override PartName="/xl/worksheets/sheet{number}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/drawings/drawing{number}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>"#));
827        sheets.push_str(&format!(
828            r#"<sheet name="Page {number}" sheetId="{number}" r:id="rId{number}"/>"#
829        ));
830        workbook_rels.push_str(&format!(r#"<Relationship Id="rId{number}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{number}.xml"/>"#));
831    }
832    package.part("[Content_Types].xml", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
833<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="svg" ContentType="image/svg+xml"/><Default Extension="png" ContentType="image/png"/>{property_overrides}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>{overrides}</Types>"#,
834        property_overrides = property_content_type_overrides(),
835    ))?;
836    package.part(
837        "_rels/.rels",
838        root_relationships("xl/workbook.xml", "officeDocument"),
839    )?;
840    write_common_properties(package)?;
841    package.part("xl/workbook.xml", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
842<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>{sheets}</sheets></workbook>"#))?;
843    package.part("xl/_rels/workbook.xml.rels", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
844<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{workbook_rels}</Relationships>"#))?;
845    for (index, page) in pages.iter().enumerate() {
846        let number = index + 1;
847        let orientation = if page.width_points > page.height_points {
848            "landscape"
849        } else {
850            "portrait"
851        };
852        package.part(&format!("xl/worksheets/sheet{number}.xml"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
853<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetPr><pageSetUpPr fitToPage="1"/></sheetPr><dimension ref="A1"/><sheetViews><sheetView workbookViewId="0"/></sheetViews><sheetFormatPr defaultRowHeight="15"/><sheetData/><pageMargins left="0.25" right="0.25" top="0.25" bottom="0.25" header="0" footer="0"/><pageSetup paperSize="9" orientation="{orientation}" fitToWidth="1" fitToHeight="1"/><drawing r:id="rId1"/></worksheet>"#))?;
854        package.part(&format!("xl/worksheets/_rels/sheet{number}.xml.rels"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
855<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing{number}.xml"/></Relationships>"#))?;
856        let cx = points_to_emu(page.width_points);
857        let cy = points_to_emu(page.height_points);
858        package.part(&format!("xl/drawings/drawing{number}.xml"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
859<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main"><xdr:absoluteAnchor><xdr:pos x="0" y="0"/><xdr:ext cx="{cx}" cy="{cy}"/><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="SVG page {number}"/><xdr:cNvPicPr><a:picLocks noChangeAspect="1"/></xdr:cNvPicPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"><a:extLst><a:ext uri="{{96DAC541-7B7A-43D3-8B79-37D633B846F1}}"><asvg:svgBlip r:embed="rId2"/></a:ext></a:extLst></a:blip><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{cx}" cy="{cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln><a:noFill/></a:ln></xdr:spPr></xdr:pic><xdr:clientData/></xdr:absoluteAnchor></xdr:wsDr>"#))?;
860        package.part(&format!("xl/drawings/_rels/drawing{number}.xml.rels"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
861<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{number}-fallback.png"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image{number}.svg"/></Relationships>"#))?;
862        package.part(&format!("xl/media/image{number}.svg"), &page.bytes)?;
863        package.part(
864            &format!("xl/media/image{number}-fallback.png"),
865            &page.fallback_png,
866        )?;
867    }
868    Ok(())
869}
870
871fn points_to_emu(points: f64) -> i64 {
872    (points * EMU_PER_POINT).round().clamp(1.0, i64::MAX as f64) as i64
873}
874
875fn points_to_twips(points: f64) -> i64 {
876    (points * 20.0).round().clamp(1.0, 31_680.0) as i64
877}
878
879fn fit_rect(
880    width: f64,
881    height: f64,
882    container_width: f64,
883    container_height: f64,
884) -> (i64, i64, i64, i64) {
885    let scale = (container_width / width).min(container_height / height);
886    let width = width * scale;
887    let height = height * scale;
888    let x = (container_width - width) / 2.0;
889    let y = (container_height - height) / 2.0;
890    (
891        points_to_emu(x),
892        points_to_emu(y),
893        points_to_emu(width),
894        points_to_emu(height),
895    )
896}