1use 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}