1use std::fmt::{Display, Formatter};
9use std::fs;
10use std::io::{BufWriter, Cursor, Write};
11use std::path::{Path, PathBuf};
12use std::sync::OnceLock;
13
14static SYSTEM_FONTDB: OnceLock<resvg::usvg::fontdb::Database> = OnceLock::new();
15
16use base64::Engine;
17use flate2::Compression;
18use flate2::write::ZlibEncoder;
19use image_webp::WebPEncoder;
20use lopdf::{Document, Object, Stream, dictionary};
21use quick_xml::Reader;
22use quick_xml::events::Event;
23use serde::Serialize;
24use zip::ZipWriter;
25use zip::write::SimpleFileOptions;
26
27use crate::convert::read_limited_file;
28use crate::error::{Error, Result};
29use crate::ooxml::{attribute, decode_xml_reference, local_name};
30
31const EMU_PER_POINT: f64 = 12_700.0;
32const FALLBACK_DPI: f64 = 96.0;
33const MAX_FALLBACK_DIMENSION: f64 = 4_096.0;
34const MAX_FALLBACK_PIXELS: f64 = 16_777_216.0;
35const MAX_NESTED_SVG_DEPTH: usize = 4;
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum ReverseFormat {
40 Pptx,
41 Docx,
42 Xlsx,
43 Pdf,
44 Drawio,
45 Dxf,
46 Dot,
47 Mermaid,
48 Markdown,
49 Csv,
50 Tex,
51 Jsx,
52 Tsx,
53 Vue,
54 DataUri,
55 Png,
56 Gcode,
57 Gerber,
58 Hpgl,
59 Excellon,
60 Stl,
61 Obj,
62 Ply,
63 Step,
64 Gmsh,
65 Vtk,
66 ThreeMf,
67 Iges,
68 Svelte,
69 PathData,
70 Html,
71 Webp,
72}
73
74impl ReverseFormat {
75 fn detect(path: &Path) -> Result<Self> {
76 let filename = path
77 .file_name()
78 .and_then(|value| value.to_str())
79 .unwrap_or("")
80 .to_ascii_lowercase();
81 if filename.ends_with(".chart.json") {
82 return Ok(Self::Csv);
83 }
84 let extension = path
85 .extension()
86 .and_then(|value| value.to_str())
87 .map(str::to_ascii_lowercase)
88 .ok_or_else(|| Error::InvalidInput("output has no file extension".into()))?;
89 match extension.as_str() {
90 "pptx" | "pptm" | "potx" | "potm" | "ppsx" | "ppam" | "sldx" | "sldm" => Ok(Self::Pptx),
91 "docx" | "docm" | "dotx" | "dotm" => Ok(Self::Docx),
92 "xlsx" | "xlsm" | "xltx" | "xltm" | "xlam" => Ok(Self::Xlsx),
93 "drawio" | "dio" => Ok(Self::Drawio),
94 "dxf" => Ok(Self::Dxf),
95 "gcode" | "nc" | "ngc" | "tap" | "gco" | "cnc" => Ok(Self::Gcode),
96 "gbr" | "gerber" | "gtl" | "gbl" | "gts" | "gbs" | "gto" | "gbo" | "gko" | "gm1"
97 | "gm2" | "art" | "pho" => Ok(Self::Gerber),
98 "plt" | "hpgl" | "hpg" | "gl2" | "prn" => Ok(Self::Hpgl),
99 "drl" | "drd" | "xln" | "exc" => Ok(Self::Excellon),
100 "stl" => Ok(Self::Stl),
101 "obj" => Ok(Self::Obj),
102 "ply" => Ok(Self::Ply),
103 "3mf" => Ok(Self::ThreeMf),
104 "step" | "stp" | "p21" | "stpnc" => Ok(Self::Step),
105 "iges" | "igs" => Ok(Self::Iges),
106 "msh" => Ok(Self::Gmsh),
107 "vtk" | "vtu" => Ok(Self::Vtk),
108 "dot" | "gv" => Ok(Self::Dot),
109 "mmd" | "mermaid" | "puml" | "plantuml" | "pu" | "wsd" => Ok(Self::Mermaid),
110 "md" | "markdown" | "mdown" | "mkd" | "mdx" | "txt" => Ok(Self::Markdown),
111 "csv" | "tsv" | "tab" | "chart" => Ok(Self::Csv),
112 "tex" | "latex" | "ltx" => Ok(Self::Tex),
113 "jsx" => Ok(Self::Jsx),
114 "tsx" => Ok(Self::Tsx),
115 "vue" => Ok(Self::Vue),
116 "svelte" => Ok(Self::Svelte),
117 "path" | "icon" => Ok(Self::PathData),
118 "datauri" => Ok(Self::DataUri),
119 "png" => Ok(Self::Png),
120 "pdf" => Ok(Self::Pdf),
121 "html" | "htm" => Ok(Self::Html),
122 "webp" => Ok(Self::Webp),
123 _ => Err(Error::Unsupported(format!(
124 "output extension .{extension}; expected PPTX, DOCX, XLSX, PDF, DRAWIO, DXF, GCODE, GERBER, HPGL, EXCELLON, STL, OBJ, PLY, 3MF, STEP, IGES, MSH, VTK, DOT, MERMAID, MD, CSV, TEX, JSX, TSX, VUE, SVELTE, PATH/ICON, DATAURI, PNG, HTML, or WEBP"
125 ))),
126 }
127 }
128
129 const fn needs_raster_fallback(self) -> bool {
132 matches!(
133 self,
134 Self::Pptx | Self::Docx | Self::Xlsx | Self::Pdf | Self::Png | Self::Webp
135 )
136 }
137}
138
139impl Display for ReverseFormat {
140 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
141 formatter.write_str(match self {
142 Self::Pptx => "PPTX",
143 Self::Docx => "DOCX",
144 Self::Xlsx => "XLSX",
145 Self::Pdf => "PDF",
146 Self::Drawio => "DRAWIO",
147 Self::Dxf => "DXF",
148 Self::Dot => "DOT",
149 Self::Mermaid => "MERMAID",
150 Self::Markdown => "MARKDOWN",
151 Self::Csv => "CSV",
152 Self::Tex => "TEX",
153 Self::Jsx => "JSX",
154 Self::Tsx => "TSX",
155 Self::Vue => "VUE",
156 Self::DataUri => "DATAURI",
157 Self::Png => "PNG",
158 Self::Gcode => "GCODE",
159 Self::Gerber => "GERBER",
160 Self::Hpgl => "HPGL",
161 Self::Excellon => "EXCELLON",
162 Self::Stl => "STL",
163 Self::Obj => "OBJ",
164 Self::Ply => "PLY",
165 Self::Step => "STEP",
166 Self::Gmsh => "GMSH",
167 Self::Vtk => "VTK",
168 Self::ThreeMf => "3MF",
169 Self::Iges => "IGES",
170 Self::Svelte => "SVELTE",
171 Self::PathData => "PATH",
172 Self::Html => "HTML",
173 Self::Webp => "WEBP",
174 })
175 }
176}
177
178#[derive(Clone, Debug)]
179pub struct ReverseOptions {
180 pub max_input_bytes: u64,
181 pub max_pages: usize,
182}
183
184impl Default for ReverseOptions {
185 fn default() -> Self {
186 Self {
187 max_input_bytes: 512 * 1024 * 1024,
188 max_pages: 10_000,
189 }
190 }
191}
192
193#[derive(Clone, Debug, Serialize)]
194pub struct ReverseReport {
195 pub converter: &'static str,
196 pub version: &'static str,
197 pub source: String,
198 pub output: String,
199 pub output_format: ReverseFormat,
200 pub page_count: usize,
201 pub input_bytes: u64,
202 pub warnings: Vec<String>,
203}
204
205#[derive(Clone, Debug)]
206struct SvgPage {
207 bytes: Vec<u8>,
208 fallback_png: Vec<u8>,
209 fallback_webp: Vec<u8>,
210 width_points: f64,
211 height_points: f64,
212 diagrams: Vec<String>,
215}
216
217pub fn svg_to_document(
218 input: impl AsRef<Path>,
219 output: impl AsRef<Path>,
220 options: &ReverseOptions,
221) -> Result<ReverseReport> {
222 let input = input.as_ref();
223 let output = output.as_ref();
224 if output.exists() {
225 return Err(Error::InvalidInput(format!(
226 "output already exists: {}",
227 output.display()
228 )));
229 }
230 if options.max_pages == 0 {
231 return Err(Error::InvalidInput("max_pages must be at least 1".into()));
232 }
233 let format = ReverseFormat::detect(output)?;
234 let paths = collect_svg_paths(input, options.max_pages)?;
235 let mut pages = Vec::with_capacity(paths.len());
236 let mut input_bytes = 0u64;
237 let mut render_options = None;
238 for path in paths {
239 let metadata = fs::metadata(&path)?;
240 let previous_bytes = input_bytes;
241 input_bytes = input_bytes.saturating_add(metadata.len());
242 if input_bytes > options.max_input_bytes {
243 return Err(Error::LimitExceeded(format!(
244 "SVG input is {input_bytes} bytes; maximum is {} bytes",
245 options.max_input_bytes
246 )));
247 }
248 let bytes = read_limited_file(
249 &path,
250 options.max_input_bytes.saturating_sub(previous_bytes),
251 "SVG input",
252 )?;
253 input_bytes = previous_bytes.saturating_add(bytes.len() as u64);
254 let diagrams = if format == ReverseFormat::Drawio {
255 embedded_diagrams(&bytes)?
256 } else {
257 Vec::new()
258 };
259 if diagrams.is_empty() {
265 validate_svg_document(&bytes, 0)?;
266 }
267 let (width_points, height_points) = svg_dimensions(&bytes)?;
268 let fallback_png = if format.needs_raster_fallback() && format != ReverseFormat::Webp {
269 let render_options = render_options.get_or_insert_with(|| {
270 let mut options = resvg::usvg::Options::default();
271 let db = SYSTEM_FONTDB.get_or_init(|| {
272 let mut db = resvg::usvg::fontdb::Database::new();
273 db.load_system_fonts();
274 db
275 });
276 *options.fontdb_mut() = db.clone();
277 options
278 });
279 render_svg_fallback(&bytes, width_points, height_points, render_options)?
280 } else {
281 Vec::new()
282 };
283 let fallback_webp = if format == ReverseFormat::Webp {
284 let render_options = render_options.get_or_insert_with(|| {
285 let mut options = resvg::usvg::Options::default();
286 let db = SYSTEM_FONTDB.get_or_init(|| {
287 let mut db = resvg::usvg::fontdb::Database::new();
288 db.load_system_fonts();
289 db
290 });
291 *options.fontdb_mut() = db.clone();
292 options
293 });
294 render_svg_webp(&bytes, width_points, height_points, render_options)?
295 } else {
296 Vec::new()
297 };
298 pages.push(SvgPage {
299 bytes,
300 fallback_png,
301 fallback_webp,
302 width_points,
303 height_points,
304 diagrams,
305 });
306 }
307
308 if let Some(parent) = output.parent().filter(|path| !path.as_os_str().is_empty()) {
309 fs::create_dir_all(parent)?;
310 }
311 let parent = output
312 .parent()
313 .filter(|path| !path.as_os_str().is_empty())
314 .unwrap_or(Path::new("."));
315 let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
316 let restored = pages
317 .iter()
318 .filter(|page| !page.diagrams.is_empty())
319 .count();
320 if format == ReverseFormat::Drawio {
321 let mut writer = BufWriter::new(temporary.as_file_mut());
322 writer.write_all(write_drawio(&pages).as_bytes())?;
323 writer.flush()?;
324 } else if format == ReverseFormat::Dxf {
325 let mut writer = BufWriter::new(temporary.as_file_mut());
326 for page in &pages {
327 let svg_str = std::str::from_utf8(&page.bytes)
328 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
329 crate::cad::dxf::writer::write_svg_to_dxf(svg_str, &mut writer)?;
330 }
331 writer.flush()?;
332 } else if format == ReverseFormat::Gcode {
333 let mut writer = BufWriter::new(temporary.as_file_mut());
334 for page in &pages {
335 let svg_str = std::str::from_utf8(&page.bytes)
336 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
337 crate::cad::gcode::writer::write_svg_to_gcode(svg_str, &mut writer)?;
338 }
339 writer.flush()?;
340 } else if format == ReverseFormat::Gerber {
341 let mut writer = BufWriter::new(temporary.as_file_mut());
342 for page in &pages {
343 let svg_str = std::str::from_utf8(&page.bytes)
344 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
345 crate::cad::gerber::writer::write_svg_to_gerber(svg_str, &mut writer)?;
346 }
347 writer.flush()?;
348 } else if format == ReverseFormat::Hpgl {
349 let mut writer = BufWriter::new(temporary.as_file_mut());
350 for page in &pages {
351 let svg_str = std::str::from_utf8(&page.bytes)
352 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
353 crate::cad::hpgl::writer::write_svg_to_hpgl(svg_str, &mut writer)?;
354 }
355 writer.flush()?;
356 } else if format == ReverseFormat::Excellon {
357 let mut writer = BufWriter::new(temporary.as_file_mut());
358 for page in &pages {
359 let svg_str = std::str::from_utf8(&page.bytes)
360 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
361 crate::cad::excellon::writer::write_svg_to_excellon(svg_str, &mut writer)?;
362 }
363 writer.flush()?;
364 } else if format == ReverseFormat::Stl {
365 let mut writer = BufWriter::new(temporary.as_file_mut());
366 for page in &pages {
367 let svg_str = std::str::from_utf8(&page.bytes)
368 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
369 crate::cad::stl::writer::write_svg_to_stl(svg_str, &mut writer)?;
370 }
371 writer.flush()?;
372 } else if format == ReverseFormat::Obj {
373 let mut writer = BufWriter::new(temporary.as_file_mut());
374 for page in &pages {
375 let svg_str = std::str::from_utf8(&page.bytes)
376 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
377 crate::cad::obj::writer::write_svg_to_obj(svg_str, &mut writer)?;
378 }
379 writer.flush()?;
380 } else if format == ReverseFormat::Ply {
381 let mut writer = BufWriter::new(temporary.as_file_mut());
382 for page in &pages {
383 let svg_str = std::str::from_utf8(&page.bytes)
384 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
385 crate::cad::ply::writer::write_svg_to_ply(svg_str, &mut writer)?;
386 }
387 writer.flush()?;
388 } else if format == ReverseFormat::Step {
389 let mut writer = BufWriter::new(temporary.as_file_mut());
390 for page in &pages {
391 let svg_str = std::str::from_utf8(&page.bytes)
392 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
393 crate::cad::step::writer::write_svg_to_step(svg_str, &mut writer)?;
394 }
395 writer.flush()?;
396 } else if format == ReverseFormat::ThreeMf {
397 for page in &pages {
398 let svg_str = std::str::from_utf8(&page.bytes)
399 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
400 crate::cad::threemf::write_svg_to_threemf(svg_str, temporary.as_file_mut())?;
401 }
402 } else if format == ReverseFormat::Iges {
403 let mut writer = BufWriter::new(temporary.as_file_mut());
404 for page in &pages {
405 let svg_str = std::str::from_utf8(&page.bytes)
406 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
407 crate::cad::iges::write_svg_to_iges(svg_str, &mut writer)?;
408 }
409 writer.flush()?;
410 } else if format == ReverseFormat::Gmsh {
411 let mut writer = BufWriter::new(temporary.as_file_mut());
412 for page in &pages {
413 let svg_str = std::str::from_utf8(&page.bytes)
414 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
415 crate::cad::simulation::writer::write_svg_to_gmsh(svg_str, &mut writer)?;
416 }
417 writer.flush()?;
418 } else if format == ReverseFormat::Vtk {
419 let mut writer = BufWriter::new(temporary.as_file_mut());
420 for page in &pages {
421 let svg_str = std::str::from_utf8(&page.bytes)
422 .map_err(|e| Error::InvalidInput(format!("SVG is not valid UTF-8: {e}")))?;
423 crate::cad::simulation::writer::write_svg_to_vtk(svg_str, &mut writer)?;
424 }
425 writer.flush()?;
426 } else if matches!(format, ReverseFormat::Dot | ReverseFormat::Mermaid) {
427 let mut writer = BufWriter::new(temporary.as_file_mut());
428 for page in &pages {
429 let dot = crate::diagram::extract_diagram_from_svg(&page.bytes)?;
430 writer.write_all(dot.as_bytes())?;
431 }
432 writer.flush()?;
433 } else if format == ReverseFormat::Markdown {
434 let mut writer = BufWriter::new(temporary.as_file_mut());
435 for page in &pages {
436 let md = crate::table::extract_markdown_table_from_svg(&page.bytes)?;
437 writer.write_all(md.as_bytes())?;
438 }
439 writer.flush()?;
440 } else if format == ReverseFormat::Csv {
441 let mut writer = BufWriter::new(temporary.as_file_mut());
442 let embedded_csv = pages.iter().find_map(|page| {
443 (crate::svg::reader::extract_source_format(&page.bytes).as_deref() == Some("csv"))
444 .then(|| crate::svg::reader::extract_embedded_source(&page.bytes))
445 .flatten()
446 });
447 if let Some(csv) = embedded_csv {
448 writer.write_all(csv.as_bytes())?;
449 } else {
450 for page in &pages {
451 let csv = crate::chart::extract_csv_from_chart_svg(&page.bytes)?;
452 writer.write_all(csv.as_bytes())?;
453 }
454 }
455 writer.flush()?;
456 } else if format == ReverseFormat::Tex {
457 let mut writer = BufWriter::new(temporary.as_file_mut());
458 for page in &pages {
459 let tex = crate::math::extract_latex_from_svg(&page.bytes)?;
460 writer.write_all(tex.as_bytes())?;
461 }
462 writer.flush()?;
463 } else if matches!(format, ReverseFormat::Jsx | ReverseFormat::Tsx) {
464 let mut writer = BufWriter::new(temporary.as_file_mut());
465 let comp_name = output
466 .file_stem()
467 .and_then(|s| s.to_str())
468 .unwrap_or("SvgIcon");
469 let mut comp = String::new();
470 let mut capitalize = true;
471 for c in comp_name.chars() {
472 if c == '-' || c == '_' {
473 capitalize = true;
474 } else if capitalize {
475 comp.extend(c.to_uppercase());
476 capitalize = false;
477 } else {
478 comp.push(c);
479 }
480 }
481 if comp.is_empty() || comp.chars().next().is_some_and(|c| !c.is_alphabetic()) {
482 comp = format!("Icon{comp}");
483 }
484 for page in &pages {
485 let jsx = crate::code::svg_to_jsx(&page.bytes, &comp, format == ReverseFormat::Tsx)?;
486 writer.write_all(jsx.as_bytes())?;
487 }
488 writer.flush()?;
489 } else if format == ReverseFormat::Vue {
490 let mut writer = BufWriter::new(temporary.as_file_mut());
491 for page in &pages {
492 let vue = crate::code::svg_to_vue(&page.bytes)?;
493 writer.write_all(vue.as_bytes())?;
494 }
495 writer.flush()?;
496 } else if format == ReverseFormat::Svelte {
497 let mut writer = BufWriter::new(temporary.as_file_mut());
498 for page in &pages {
499 let svelte = crate::code::svg_to_svelte(&page.bytes)?;
500 writer.write_all(svelte.as_bytes())?;
501 }
502 writer.flush()?;
503 } else if format == ReverseFormat::PathData {
504 let mut writer = BufWriter::new(temporary.as_file_mut());
505 for page in &pages {
506 let path_data = crate::code::svg_to_path_data(&page.bytes)?;
507 writer.write_all(path_data.as_bytes())?;
508 }
509 writer.flush()?;
510 } else if format == ReverseFormat::DataUri {
511 let mut writer = BufWriter::new(temporary.as_file_mut());
512 for page in &pages {
513 let uri = crate::code::svg_to_data_uri(&page.bytes)?;
514 writer.write_all(uri.as_bytes())?;
515 }
516 writer.flush()?;
517 } else if format == ReverseFormat::Png {
518 let mut writer = BufWriter::new(temporary.as_file_mut());
519 if let Some(page) = pages.first() {
520 writer.write_all(&page.fallback_png)?;
521 }
522 writer.flush()?;
523 } else if format == ReverseFormat::Html {
524 let mut writer = BufWriter::new(temporary.as_file_mut());
525 let doc_title = output
526 .file_stem()
527 .and_then(|s| s.to_str())
528 .unwrap_or("Document SVG");
529 for page in &pages {
530 let html = crate::code::svg_to_html(&page.bytes, doc_title)?;
531 writer.write_all(html.as_bytes())?;
532 }
533 writer.flush()?;
534 } else if format == ReverseFormat::Pdf {
535 write_pdf(&pages, temporary.as_file_mut())?;
536 } else if format == ReverseFormat::Webp {
537 let mut writer = BufWriter::new(temporary.as_file_mut());
538 if let Some(page) = pages.first() {
539 writer.write_all(&page.fallback_webp)?;
540 }
541 writer.flush()?;
542 } else {
543 let writer = BufWriter::new(temporary.as_file_mut());
544 let mut package = Package::new(writer);
545 match format {
546 ReverseFormat::Pptx => write_pptx(&mut package, &pages)?,
547 ReverseFormat::Docx => write_docx(&mut package, &pages)?,
548 ReverseFormat::Xlsx => write_xlsx(&mut package, &pages)?,
549 ReverseFormat::Drawio
550 | ReverseFormat::Dxf
551 | ReverseFormat::Dot
552 | ReverseFormat::Mermaid
553 | ReverseFormat::Markdown
554 | ReverseFormat::Csv
555 | ReverseFormat::Tex
556 | ReverseFormat::Jsx
557 | ReverseFormat::Tsx
558 | ReverseFormat::Vue
559 | ReverseFormat::DataUri
560 | ReverseFormat::Png
561 | ReverseFormat::Pdf
562 | ReverseFormat::Html
563 | ReverseFormat::Webp
564 | ReverseFormat::Gcode
565 | ReverseFormat::Gerber
566 | ReverseFormat::Hpgl
567 | ReverseFormat::Excellon
568 | ReverseFormat::Stl
569 | ReverseFormat::Obj
570 | ReverseFormat::Ply
571 | ReverseFormat::Step
572 | ReverseFormat::Gmsh
573 | ReverseFormat::Vtk
574 | ReverseFormat::ThreeMf
575 | ReverseFormat::Iges
576 | ReverseFormat::Svelte
577 | ReverseFormat::PathData => {
578 unreachable!("non-package formats are written directly")
579 }
580 }
581 package.finish()?;
582 }
583 temporary
584 .persist_noclobber(output)
585 .map_err(|error| error.error)?;
586
587 let warnings = match (format, restored, pages.len()) {
588 (ReverseFormat::Drawio, restored, total) if restored == total => vec![format!(
589 "{restored} page(s) were restored from the diagram source the SVG carries; they are editable shapes again"
590 )],
591 (ReverseFormat::Drawio, 0, _) => vec![
592 "SVG pages are embedded as pictures in the diagram; they are not editable shapes, because the SVG carries no diagram source"
593 .into(),
594 ],
595 (ReverseFormat::Drawio, restored, total) => vec![format!(
596 "{restored} of {total} page(s) were restored from the diagram source the SVG carries; the rest are embedded as pictures"
597 )],
598 (ReverseFormat::Dxf, _, _) => vec![
599 "SVG vector shapes and text were converted to AutoCAD DXF entities"
600 .into(),
601 ],
602 (ReverseFormat::Gcode, _, _) => vec![
603 "SVG vector paths were converted to CNC G-code toolpath commands"
604 .into(),
605 ],
606 (ReverseFormat::Gerber, _, _) => vec![
607 "SVG pads, lines, and filled polygons were converted to Gerber RS-274X PCB artwork"
608 .into(),
609 ],
610 (ReverseFormat::Hpgl, _, _) => vec![
611 "SVG vector paths and circles were converted to HP-GL plotter commands"
612 .into(),
613 ],
614 (ReverseFormat::Excellon, _, _) => vec![
615 "SVG circles and drill pads were converted to Excellon NC drill commands"
616 .into(),
617 ],
618 (ReverseFormat::Stl, _, _) => vec![
619 "SVG vector contours were extruded into a 3D printable STL triangle mesh"
620 .into(),
621 ],
622 (ReverseFormat::Obj, _, _) => vec![
623 "SVG vector contours were extruded into a 3D Wavefront OBJ polygonal mesh"
624 .into(),
625 ],
626 (ReverseFormat::Ply, _, _) => vec![
627 "SVG vector contours were extruded into a Stanford PLY 3D mesh"
628 .into(),
629 ],
630 (ReverseFormat::Step, _, _) => vec![
631 "SVG vector elements were converted to ISO 10303-21 STEP mechanical CAD wireframe entities"
632 .into(),
633 ],
634 (ReverseFormat::Gmsh, _, _) => vec![
635 "SVG vector geometry was converted to a Gmsh 2.2 finite element mesh"
636 .into(),
637 ],
638 (ReverseFormat::Vtk, _, _) => vec![
639 "SVG vector polygons were converted to a VTK Legacy polygonal dataset"
640 .into(),
641 ],
642 (ReverseFormat::ThreeMf, _, _) => vec![
643 "SVG vector contours were extruded into a 3MF 3D manufacturing package"
644 .into(),
645 ],
646 (ReverseFormat::Iges, _, _) => vec![
647 "SVG vector elements were converted to ANSI IGES mechanical CAD entities"
648 .into(),
649 ],
650 (ReverseFormat::Dot | ReverseFormat::Mermaid, _, _) => vec![
651 "SVG shapes and text were extracted into a graph structure; topology is approximated from geometric elements"
652 .into(),
653 ],
654 (ReverseFormat::Markdown, _, _) => vec![
655 "SVG table grid lines and cell text were extracted into a Markdown table"
656 .into(),
657 ],
658 (ReverseFormat::Csv, _, _) => vec![
659 "SVG chart elements and text were extracted into tabular CSV data"
660 .into(),
661 ],
662 (ReverseFormat::Tex, _, _) => vec![
663 "SVG mathematical elements were reconstructed into LaTeX math expressions"
664 .into(),
665 ],
666 (ReverseFormat::Jsx | ReverseFormat::Tsx, _, _) => vec![
667 "SVG element structure was compiled into a React component"
668 .into(),
669 ],
670 (ReverseFormat::Vue, _, _) => vec![
671 "SVG element structure was compiled into a Vue 3 component template"
672 .into(),
673 ],
674 (ReverseFormat::DataUri, _, _) => vec![
675 "SVG was base64-encoded into a Data URI"
676 .into(),
677 ],
678 (ReverseFormat::Png, _, _) => vec![
679 "SVG was rendered directly to a raster PNG image"
680 .into(),
681 ],
682 (ReverseFormat::Pdf, _, _) => vec![
683 "SVG pages were packaged into a PDF document with exact page dimensions and raster imagery"
684 .into(),
685 ],
686 _ => vec![
687 "SVG pages are embedded as vector images; original document semantics are not reconstructed"
688 .into(),
689 ],
690 };
691 Ok(ReverseReport {
692 converter: "document-svg",
693 version: env!("CARGO_PKG_VERSION"),
694 source: input.to_string_lossy().into_owned(),
695 output: output.to_string_lossy().into_owned(),
696 output_format: format,
697 page_count: pages.len(),
698 input_bytes,
699 warnings,
700 })
701}
702
703fn collect_svg_paths(input: &Path, max_pages: usize) -> Result<Vec<PathBuf>> {
704 let metadata = fs::metadata(input)?;
705 let mut paths = if metadata.is_file() {
706 if !has_svg_extension(input) {
707 return Err(Error::Unsupported(format!(
708 "input {}; expected an SVG file or directory",
709 input.display()
710 )));
711 }
712 vec![input.to_path_buf()]
713 } else if metadata.is_dir() {
714 let mut entries = Vec::new();
715 for entry in fs::read_dir(input)? {
716 let path = entry?.path();
717 if path.is_file() && has_svg_extension(&path) {
718 entries.push(path);
719 }
720 }
721 entries.sort();
722 entries
723 } else {
724 return Err(Error::InvalidInput(format!(
725 "{} is not a regular file or directory",
726 input.display()
727 )));
728 };
729 if paths.is_empty() {
730 return Err(Error::InvalidInput(format!(
731 "{} contains no SVG files",
732 input.display()
733 )));
734 }
735 if paths.len() > max_pages {
736 return Err(Error::LimitExceeded(format!(
737 "SVG input has {} pages; maximum is {max_pages}",
738 paths.len()
739 )));
740 }
741 paths.shrink_to_fit();
742 Ok(paths)
743}
744
745fn has_svg_extension(path: &Path) -> bool {
746 path.extension()
747 .and_then(|value| value.to_str())
748 .is_some_and(|value| value.eq_ignore_ascii_case("svg"))
749}
750
751fn svg_dimensions(bytes: &[u8]) -> Result<(f64, f64)> {
752 let mut reader = Reader::from_reader(bytes);
753 reader.config_mut().trim_text(true);
754 let mut buffer = Vec::new();
755 loop {
756 match reader.read_event_into(&mut buffer)? {
757 Event::Start(start) | Event::Empty(start)
758 if local_name(start.name().as_ref()) == b"svg" =>
759 {
760 let width = attribute(&start, b"width").and_then(|value| parse_svg_length(&value));
761 let height =
762 attribute(&start, b"height").and_then(|value| parse_svg_length(&value));
763 let view_box = attribute(&start, b"viewBox").and_then(|value| {
764 let tokens = value
765 .split(|character: char| {
766 character.is_ascii_whitespace() || character == ','
767 })
768 .filter(|value| !value.is_empty())
769 .collect::<Vec<_>>();
770 if tokens.len() != 4 {
771 return None;
772 }
773 let values = tokens
774 .iter()
775 .map(|value| value.parse::<f64>())
776 .collect::<std::result::Result<Vec<_>, _>>()
777 .ok()?;
778 (values.iter().all(|value| value.is_finite())
779 && values[2] > 0.0
780 && values[3] > 0.0)
781 .then_some((values[2], values[3]))
782 });
783 let width = width.or_else(|| view_box.map(|value| value.0 * 0.75));
784 let height = height.or_else(|| view_box.map(|value| value.1 * 0.75));
785 let (width, height) = match (width, height) {
786 (Some(width), Some(height)) if width > 0.0 && height > 0.0 => (width, height),
787 _ => {
788 return Err(Error::InvalidInput(
789 "SVG root needs positive width/height or viewBox dimensions".into(),
790 ));
791 }
792 };
793 return Ok((width, height));
794 }
795 Event::Eof => {
796 return Err(Error::InvalidInput(
797 "input does not contain an SVG root".into(),
798 ));
799 }
800 _ => {}
801 }
802 buffer.clear();
803 }
804}
805
806fn parse_svg_length(value: &str) -> Option<f64> {
807 let value = value.trim();
808 let lower = value.to_ascii_lowercase();
809 let (number, unit) = ["pt", "px", "in", "cm", "mm", "pc"]
810 .into_iter()
811 .find_map(|unit| lower.strip_suffix(unit).map(|number| (number.trim(), unit)))
812 .unwrap_or((value, ""));
813 let number = number.parse::<f64>().ok()?;
814 let points = match unit {
815 "pt" => number,
816 "px" | "" => number * 0.75,
817 "in" => number * 72.0,
818 "cm" => number * 72.0 / 2.54,
819 "mm" => number * 72.0 / 25.4,
820 "pc" => number * 12.0,
821 _ => unreachable!(),
822 };
823 points.is_finite().then_some(points)
824}
825
826pub(crate) fn validate_svg_document(bytes: &[u8], depth: usize) -> Result<()> {
830 if depth > MAX_NESTED_SVG_DEPTH {
831 return Err(Error::LimitExceeded(format!(
832 "nested SVG depth exceeds {MAX_NESTED_SVG_DEPTH}"
833 )));
834 }
835 let mut reader = Reader::from_reader(bytes);
836 reader.config_mut().trim_text(false);
837 let mut buffer = Vec::new();
838 let mut saw_root = false;
839 let mut root_closed = false;
840 let mut element_depth = 0usize;
841 let mut style_depth = None::<usize>;
842 let mut style_text = String::new();
843 loop {
844 match reader.read_event_into(&mut buffer)? {
845 Event::DocType(doctype) => {
846 let text = doctype
853 .decode()
854 .map_err(|error| {
855 Error::InvalidInput(format!("invalid SVG document type: {error}"))
856 })?
857 .to_ascii_uppercase();
858 if text.contains('[') || text.contains("ENTITY") {
859 return Err(Error::InvalidInput(
860 "SVG document type entities are not allowed".into(),
861 ));
862 }
863 }
864 Event::PI(_) => {
865 return Err(Error::InvalidInput(
866 "SVG processing instructions are not allowed".into(),
867 ));
868 }
869 Event::GeneralRef(reference) if style_depth.is_some() => {
875 push_style_text(
876 &mut style_text,
877 &decode_xml_reference(&reference, "SVG CSS")?,
878 )?;
879 }
880 Event::Start(start) => {
881 if root_closed {
882 return Err(Error::InvalidInput(
883 "input contains content after the SVG root".into(),
884 ));
885 }
886 if !saw_root {
887 if local_name(start.name().as_ref()) != b"svg" {
888 return Err(Error::InvalidInput(
889 "input must have an <svg> root element".into(),
890 ));
891 }
892 validate_svg_root_namespace(&start)?;
893 saw_root = true;
894 }
895 validate_svg_element(&start, reader.decoder(), depth)?;
896 if local_name(start.name().as_ref()).eq_ignore_ascii_case(b"style") {
897 style_depth = Some(element_depth + 1);
898 }
899 element_depth += 1;
900 }
901 Event::Empty(start) => {
902 if root_closed {
903 return Err(Error::InvalidInput(
904 "input contains content after the SVG root".into(),
905 ));
906 }
907 if !saw_root {
908 if local_name(start.name().as_ref()) != b"svg" {
909 return Err(Error::InvalidInput(
910 "input must have an <svg> root element".into(),
911 ));
912 }
913 validate_svg_root_namespace(&start)?;
914 saw_root = true;
915 root_closed = true;
916 }
917 validate_svg_element(&start, reader.decoder(), depth)?;
918 }
919 Event::Text(text) => {
920 let value = text.decode().map_err(|error| {
921 Error::InvalidInput(format!("invalid SVG text encoding: {error}"))
922 })?;
923 if (!saw_root || root_closed) && !value.trim().is_empty() {
924 return Err(Error::InvalidInput(
925 "input contains text outside the SVG root".into(),
926 ));
927 }
928 if style_depth.is_some() {
929 push_style_text(&mut style_text, &value)?;
930 }
931 }
932 Event::CData(text) => {
933 if !saw_root || root_closed {
934 return Err(Error::InvalidInput(
935 "input contains CDATA outside the SVG root".into(),
936 ));
937 }
938 if style_depth.is_some() {
939 let value = String::from_utf8_lossy(text.as_ref());
940 push_style_text(&mut style_text, &value)?;
941 }
942 }
943 Event::End(end) => {
944 element_depth = element_depth.saturating_sub(1);
945 if element_depth == 0 {
946 root_closed = true;
947 }
948 if local_name(end.name().as_ref()).eq_ignore_ascii_case(b"style") {
949 style_depth = None;
950 validate_css_references(&style_text)?;
951 style_text.clear();
952 }
953 }
954 Event::Eof => {
955 if !style_text.is_empty() {
956 validate_css_references(&style_text)?;
957 }
958 break;
959 }
960 _ => {}
961 }
962 buffer.clear();
963 }
964 if !saw_root || !root_closed {
965 return Err(Error::InvalidInput(
966 "input does not contain an SVG root".into(),
967 ));
968 }
969 Ok(())
970}
971
972fn validate_svg_root_namespace(start: &quick_xml::events::BytesStart<'_>) -> Result<()> {
973 let raw_name = String::from_utf8_lossy(start.name().as_ref()).into_owned();
974 let namespace_key = raw_name.split_once(':').map_or_else(
975 || "xmlns".to_owned(),
976 |(prefix, _)| format!("xmlns:{prefix}"),
977 );
978 let mut namespace = None::<String>;
979 for item in start.attributes().with_checks(true) {
980 let item = item
981 .map_err(|error| Error::InvalidInput(format!("invalid SVG root attribute: {error}")))?;
982 if item.key.as_ref() == namespace_key.as_bytes() {
983 namespace = Some(String::from_utf8_lossy(item.value.as_ref()).into_owned());
984 }
985 }
986 if raw_name.contains(':') && namespace.as_deref() != Some("http://www.w3.org/2000/svg") {
987 return Err(Error::InvalidInput(
988 "prefixed SVG root must use the SVG namespace".into(),
989 ));
990 }
991 if namespace
992 .as_deref()
993 .is_some_and(|value| value != "http://www.w3.org/2000/svg")
994 {
995 return Err(Error::InvalidInput(
996 "SVG root uses an unsupported namespace".into(),
997 ));
998 }
999 Ok(())
1000}
1001
1002fn validate_svg_element(
1003 start: &quick_xml::events::BytesStart<'_>,
1004 decoder: quick_xml::encoding::Decoder,
1005 depth: usize,
1006) -> Result<()> {
1007 let name = String::from_utf8_lossy(local_name(start.name().as_ref())).to_ascii_lowercase();
1008 if matches!(
1009 name.as_str(),
1010 "script"
1011 | "foreignobject"
1012 | "iframe"
1013 | "object"
1014 | "embed"
1015 | "animate"
1016 | "animatetransform"
1017 | "animatemotion"
1018 | "discard"
1019 | "set"
1020 ) {
1021 return Err(Error::InvalidInput(format!(
1022 "active SVG element <{name}> is not allowed"
1023 )));
1024 }
1025 for item in start.attributes().with_checks(true) {
1026 let item =
1027 item.map_err(|error| Error::InvalidInput(format!("invalid SVG attribute: {error}")))?;
1028 let key = String::from_utf8_lossy(local_name(item.key.as_ref())).to_ascii_lowercase();
1029 let value = item
1030 .decoded_and_normalized_value(quick_xml::XmlVersion::Implicit1_0, decoder)?
1031 .into_owned();
1032 if key.starts_with("on") || key == "base" {
1033 return Err(Error::InvalidInput(format!(
1034 "SVG event attribute {key} is not allowed"
1035 )));
1036 }
1037 if key == "href" {
1038 validate_svg_href(&value, depth)?;
1039 }
1040 if key != "content" {
1045 validate_css_references(&value)?;
1046 }
1047 }
1048 Ok(())
1049}
1050
1051fn validate_svg_href(value: &str, depth: usize) -> Result<()> {
1052 let value = value.trim();
1053 if value.is_empty() || value.starts_with('#') {
1054 return Ok(());
1055 }
1056 let lower = value.to_ascii_lowercase();
1057 if lower.starts_with("data:image/png;")
1058 || lower.starts_with("data:image/jpeg;")
1059 || lower.starts_with("data:image/gif;")
1060 || lower.starts_with("data:image/webp;")
1061 {
1062 return Ok(());
1063 }
1064 if lower.starts_with("data:image/svg+xml;base64,") {
1065 let encoded = value
1066 .split_once(',')
1067 .map(|(_, data)| data)
1068 .unwrap_or_default();
1069 let compact = encoded
1070 .bytes()
1071 .filter(|byte| !byte.is_ascii_whitespace())
1072 .collect::<Vec<_>>();
1073 let nested = base64::engine::general_purpose::STANDARD
1074 .decode(compact)
1075 .map_err(|error| {
1076 Error::InvalidInput(format!("invalid nested SVG data URI: {error}"))
1077 })?;
1078 return validate_svg_document(&nested, depth + 1);
1079 }
1080 Err(Error::InvalidInput(format!(
1081 "external or active SVG reference is not allowed: {}",
1082 value.chars().take(80).collect::<String>()
1083 )))
1084}
1085
1086fn validate_css_references(value: &str) -> Result<()> {
1087 let lower = value.to_ascii_lowercase();
1088 if lower.contains('\\') || lower.contains("/*") {
1089 return Err(Error::InvalidInput(
1090 "SVG CSS escapes and comments are not allowed".into(),
1091 ));
1092 }
1093 if lower.contains("@import") || lower.contains("javascript:") {
1094 return Err(Error::InvalidInput(
1095 "external or active SVG CSS is not allowed".into(),
1096 ));
1097 }
1098 let mut remainder = value;
1099 while let Some(index) = remainder.to_ascii_lowercase().find("url(") {
1100 let after = &remainder[index + 4..];
1101 let Some(end) = after.find(')') else {
1102 return Err(Error::InvalidInput("unterminated SVG CSS url()".into()));
1103 };
1104 let target = after[..end]
1105 .trim()
1106 .trim_matches(|character| matches!(character, '\'' | '"'));
1107 if !target.starts_with('#') {
1108 return Err(Error::InvalidInput(
1109 "external SVG CSS url() reference is not allowed".into(),
1110 ));
1111 }
1112 remainder = &after[end + 1..];
1113 }
1114 Ok(())
1115}
1116
1117fn render_svg_to_pixmap(
1118 bytes: &[u8],
1119 width_points: f64,
1120 height_points: f64,
1121 options: &resvg::usvg::Options<'_>,
1122) -> Result<resvg::tiny_skia::Pixmap> {
1123 let tree = resvg::usvg::Tree::from_data(bytes, options)
1124 .map_err(|error| Error::InvalidInput(format!("SVG fallback parse failed: {error}")))?;
1125 let (pixel_width, pixel_height) = fallback_pixel_size(width_points, height_points);
1126 let mut pixmap = resvg::tiny_skia::Pixmap::new(pixel_width, pixel_height).ok_or_else(|| {
1127 Error::LimitExceeded(format!(
1128 "SVG fallback raster allocation failed for {pixel_width}x{pixel_height} pixels"
1129 ))
1130 })?;
1131 let source = tree.size();
1132 let transform = resvg::tiny_skia::Transform::from_scale(
1133 pixel_width as f32 / source.width(),
1134 pixel_height as f32 / source.height(),
1135 );
1136 resvg::render(&tree, transform, &mut pixmap.as_mut());
1137 Ok(pixmap)
1138}
1139
1140fn render_svg_fallback(
1141 bytes: &[u8],
1142 width_points: f64,
1143 height_points: f64,
1144 options: &resvg::usvg::Options<'_>,
1145) -> Result<Vec<u8>> {
1146 let pixmap = render_svg_to_pixmap(bytes, width_points, height_points, options)?;
1147 pixmap
1148 .encode_png()
1149 .map_err(|error| Error::InvalidInput(format!("SVG fallback PNG encoding failed: {error}")))
1150}
1151
1152fn render_svg_webp(
1153 bytes: &[u8],
1154 width_points: f64,
1155 height_points: f64,
1156 options: &resvg::usvg::Options<'_>,
1157) -> Result<Vec<u8>> {
1158 let pixmap = render_svg_to_pixmap(bytes, width_points, height_points, options)?;
1159 let w = pixmap.width();
1160 let h = pixmap.height();
1161 let raw = pixmap.data();
1162 let mut unpremul = Vec::with_capacity(raw.len());
1163 for chunk in raw.chunks_exact(4) {
1164 let r = chunk[0];
1165 let g = chunk[1];
1166 let b = chunk[2];
1167 let a = chunk[3];
1168 if a == 0 {
1169 unpremul.extend_from_slice(&[0, 0, 0, 0]);
1170 } else if a == 255 {
1171 unpremul.extend_from_slice(&[r, g, b, 255]);
1172 } else {
1173 let fa = a as u32;
1174 let ur = ((r as u32 * 255 + fa / 2) / fa).min(255) as u8;
1175 let ug = ((g as u32 * 255 + fa / 2) / fa).min(255) as u8;
1176 let ub = ((b as u32 * 255 + fa / 2) / fa).min(255) as u8;
1177 unpremul.extend_from_slice(&[ur, ug, ub, a]);
1178 }
1179 }
1180 let mut out = Vec::new();
1181 let encoder = WebPEncoder::new(&mut out);
1182 encoder
1183 .encode(&unpremul, w, h, image_webp::ColorType::Rgba8)
1184 .map_err(|error| {
1185 Error::InvalidInput(format!("SVG fallback WebP encoding failed: {error}"))
1186 })?;
1187 Ok(out)
1188}
1189
1190fn fallback_pixel_size(width_points: f64, height_points: f64) -> (u32, u32) {
1191 let mut width = (width_points * FALLBACK_DPI / 72.0).max(1.0);
1192 let mut height = (height_points * FALLBACK_DPI / 72.0).max(1.0);
1193 let scale = (MAX_FALLBACK_DIMENSION / width)
1194 .min(MAX_FALLBACK_DIMENSION / height)
1195 .min((MAX_FALLBACK_PIXELS / (width * height)).sqrt())
1196 .min(1.0);
1197 width *= scale;
1198 height *= scale;
1199 (
1200 width.round().max(1.0) as u32,
1201 height.round().max(1.0) as u32,
1202 )
1203}
1204
1205struct Package<W: Write + std::io::Seek> {
1206 zip: ZipWriter<W>,
1207 options: SimpleFileOptions,
1208}
1209
1210impl<W: Write + std::io::Seek> Package<W> {
1211 fn new(writer: W) -> Self {
1212 Self {
1213 zip: ZipWriter::new(writer),
1214 options: SimpleFileOptions::default()
1215 .compression_method(zip::CompressionMethod::Deflated)
1216 .unix_permissions(0o644),
1217 }
1218 }
1219
1220 fn part(&mut self, name: &str, bytes: impl AsRef<[u8]>) -> Result<()> {
1221 self.zip.start_file(name, self.options)?;
1222 self.zip.write_all(bytes.as_ref())?;
1223 Ok(())
1224 }
1225
1226 fn finish(self) -> Result<()> {
1227 self.zip.finish()?;
1228 Ok(())
1229 }
1230}
1231
1232fn root_relationships(target: &str, relationship_type: &str) -> String {
1233 format!(
1234 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1235<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>"#
1236 )
1237}
1238
1239fn property_content_type_overrides() -> &'static str {
1240 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"/>"#
1241}
1242
1243fn write_common_properties<W: Write + std::io::Seek>(package: &mut Package<W>) -> Result<()> {
1244 package.part(
1245 "docProps/core.xml",
1246 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1247<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>"#,
1248 )?;
1249 package.part(
1250 "docProps/app.xml",
1251 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1252<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>"#,
1253 )?;
1254 Ok(())
1255}
1256
1257fn write_pptx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
1258 let mut overrides = String::new();
1259 let mut slide_ids = String::new();
1260 let mut presentation_rels = String::new();
1261 for (index, _) in pages.iter().enumerate() {
1262 let number = index + 1;
1263 overrides.push_str(&format!(r#"<Override PartName="/ppt/slides/slide{number}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>"#));
1264 slide_ids.push_str(&format!(
1265 r#"<p:sldId id="{}" r:id="rId{}"/>"#,
1266 256 + index,
1267 number + 1
1268 ));
1269 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));
1270 }
1271 let content_types = format!(
1272 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1273<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>"#,
1274 property_overrides = property_content_type_overrides(),
1275 );
1276 package.part("[Content_Types].xml", content_types)?;
1277 package.part(
1278 "_rels/.rels",
1279 root_relationships("ppt/presentation.xml", "officeDocument"),
1280 )?;
1281 write_common_properties(package)?;
1282 let first = &pages[0];
1283 let slide_width = points_to_emu(first.width_points);
1284 let slide_height = points_to_emu(first.height_points);
1285 let presentation = format!(
1286 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1287<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>"#
1288 );
1289 package.part("ppt/presentation.xml", presentation)?;
1290 let presentation_relationships = format!(
1291 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1292<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>"#,
1293 pres_props_id = pages.len() + 2,
1294 );
1295 package.part(
1296 "ppt/_rels/presentation.xml.rels",
1297 presentation_relationships,
1298 )?;
1299 package.part("ppt/slideMasters/slideMaster1.xml", pptx_slide_master())?;
1300 package.part("ppt/slideMasters/_rels/slideMaster1.xml.rels", r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1301<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>"#)?;
1302 package.part("ppt/slideLayouts/slideLayout1.xml", pptx_slide_layout())?;
1303 package.part("ppt/slideLayouts/_rels/slideLayout1.xml.rels", r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1304<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>"#)?;
1305 package.part("ppt/theme/theme1.xml", pptx_theme())?;
1306 package.part(
1307 "ppt/presProps.xml",
1308 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1309<p:presentationPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:showPr useTimings="0"/></p:presentationPr>"#,
1310 )?;
1311 for (index, page) in pages.iter().enumerate() {
1312 let number = index + 1;
1313 let (x, y, cx, cy) = fit_rect(
1314 page.width_points,
1315 page.height_points,
1316 first.width_points,
1317 first.height_points,
1318 );
1319 let slide = format!(
1320 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1321<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>"#,
1322 choice = pptx_svg_picture(number, x, y, cx, cy, true),
1323 fallback = pptx_svg_picture(number, x, y, cx, cy, false),
1324 );
1325 package.part(&format!("ppt/slides/slide{number}.xml"), slide)?;
1326 let relationships = format!(
1327 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1328<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>"#
1329 );
1330 package.part(
1331 &format!("ppt/slides/_rels/slide{number}.xml.rels"),
1332 relationships,
1333 )?;
1334 package.part(&format!("ppt/media/image{number}.svg"), &page.bytes)?;
1335 package.part(
1336 &format!("ppt/media/image{number}-fallback.png"),
1337 &page.fallback_png,
1338 )?;
1339 }
1340 Ok(())
1341}
1342
1343fn pptx_svg_picture(number: usize, x: i64, y: i64, cx: i64, cy: i64, svg: bool) -> String {
1344 let extension = if svg {
1345 r#"<a:extLst><a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}"><asvg:svgBlip r:embed="rId2"/></a:ext></a:extLst>"#
1346 } else {
1347 ""
1348 };
1349 format!(
1350 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>"#
1351 )
1352}
1353
1354fn pptx_slide_master() -> &'static str {
1355 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1356<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>"#
1357}
1358
1359fn pptx_slide_layout() -> &'static str {
1360 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1361<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>"#
1362}
1363
1364fn pptx_theme() -> &'static str {
1365 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1366<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>"#
1367}
1368
1369fn write_docx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
1370 let content_types = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1371<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>"#;
1372 package.part("[Content_Types].xml", content_types)?;
1373 package.part(
1374 "_rels/.rels",
1375 root_relationships("word/document.xml", "officeDocument"),
1376 )?;
1377 write_common_properties(package)?;
1378 let mut body = String::new();
1379 let mut relationships = String::new();
1380 for (index, page) in pages.iter().enumerate() {
1381 let number = index + 1;
1382 let cx = points_to_emu(page.width_points);
1383 let cy = points_to_emu(page.height_points);
1384 body.push_str("<w:p><w:pPr><w:spacing w:before=\"0\" w:after=\"0\"/>");
1385 if number < pages.len() {
1386 body.push_str(&docx_section_properties(page, true));
1387 }
1388 body.push_str("</w:pPr>");
1389 let png_rel = number * 2 - 1;
1390 let svg_rel = number * 2;
1391 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>"#));
1392 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"/>"#));
1393 package.part(&format!("word/media/image{number}.svg"), &page.bytes)?;
1394 package.part(
1395 &format!("word/media/image{number}-fallback.png"),
1396 &page.fallback_png,
1397 )?;
1398 }
1399 let final_section = docx_section_properties(&pages[pages.len() - 1], false);
1400 let document = format!(
1401 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1402<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>"#
1403 );
1404 package.part("word/document.xml", document)?;
1405 package.part("word/_rels/document.xml.rels", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1406<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{relationships}</Relationships>"#))?;
1407 Ok(())
1408}
1409
1410fn docx_section_properties(page: &SvgPage, section_break: bool) -> String {
1411 let page_width = points_to_twips(page.width_points);
1412 let page_height = points_to_twips(page.height_points);
1413 let orientation = if page.width_points > page.height_points {
1414 r#" w:orient="landscape""#
1415 } else {
1416 ""
1417 };
1418 let section_type = if section_break {
1419 r#"<w:type w:val="nextPage"/>"#
1420 } else {
1421 ""
1422 };
1423 format!(
1424 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>"#
1425 )
1426}
1427
1428fn write_xlsx<W: Write + std::io::Seek>(package: &mut Package<W>, pages: &[SvgPage]) -> Result<()> {
1429 let mut overrides = String::new();
1430 let mut sheets = String::new();
1431 let mut workbook_rels = String::new();
1432 for (index, _) in pages.iter().enumerate() {
1433 let number = index + 1;
1434 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"/>"#));
1435 sheets.push_str(&format!(
1436 r#"<sheet name="Page {number}" sheetId="{number}" r:id="rId{number}"/>"#
1437 ));
1438 workbook_rels.push_str(&format!(r#"<Relationship Id="rId{number}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{number}.xml"/>"#));
1439 }
1440 package.part("[Content_Types].xml", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1441<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>"#,
1442 property_overrides = property_content_type_overrides(),
1443 ))?;
1444 package.part(
1445 "_rels/.rels",
1446 root_relationships("xl/workbook.xml", "officeDocument"),
1447 )?;
1448 write_common_properties(package)?;
1449 package.part("xl/workbook.xml", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1450<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>{sheets}</sheets></workbook>"#))?;
1451 package.part("xl/_rels/workbook.xml.rels", format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1452<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{workbook_rels}</Relationships>"#))?;
1453 for (index, page) in pages.iter().enumerate() {
1454 let number = index + 1;
1455 let orientation = if page.width_points > page.height_points {
1456 "landscape"
1457 } else {
1458 "portrait"
1459 };
1460 package.part(&format!("xl/worksheets/sheet{number}.xml"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1461<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>"#))?;
1462 package.part(&format!("xl/worksheets/_rels/sheet{number}.xml.rels"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1463<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>"#))?;
1464 let cx = points_to_emu(page.width_points);
1465 let cy = points_to_emu(page.height_points);
1466 package.part(&format!("xl/drawings/drawing{number}.xml"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1467<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>"#))?;
1468 package.part(&format!("xl/drawings/_rels/drawing{number}.xml.rels"), format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1469<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>"#))?;
1470 package.part(&format!("xl/media/image{number}.svg"), &page.bytes)?;
1471 package.part(
1472 &format!("xl/media/image{number}-fallback.png"),
1473 &page.fallback_png,
1474 )?;
1475 }
1476 Ok(())
1477}
1478
1479fn points_to_emu(points: f64) -> i64 {
1480 (points * EMU_PER_POINT).round().clamp(1.0, i64::MAX as f64) as i64
1481}
1482
1483fn points_to_twips(points: f64) -> i64 {
1484 (points * 20.0).round().clamp(1.0, 31_680.0) as i64
1485}
1486
1487fn fit_rect(
1488 width: f64,
1489 height: f64,
1490 container_width: f64,
1491 container_height: f64,
1492) -> (i64, i64, i64, i64) {
1493 let scale = (container_width / width).min(container_height / height);
1494 let width = width * scale;
1495 let height = height * scale;
1496 let x = (container_width - width) / 2.0;
1497 let y = (container_height - height) / 2.0;
1498 (
1499 points_to_emu(x),
1500 points_to_emu(y),
1501 points_to_emu(width),
1502 points_to_emu(height),
1503 )
1504}
1505
1506const PIXELS_PER_POINT: f64 = 4.0 / 3.0;
1512const MAX_EMBEDDED_SOURCE_BYTES: usize = 32 * 1024 * 1024;
1514
1515fn embedded_diagrams(bytes: &[u8]) -> Result<Vec<String>> {
1522 let mut reader = Reader::from_reader(bytes);
1523 reader.config_mut().trim_text(false);
1524 let mut buffer = Vec::new();
1525 let source = loop {
1526 match reader.read_event_into(&mut buffer)? {
1527 Event::Start(start) | Event::Empty(start) => {
1528 if local_name(start.name().as_ref()) != b"svg" {
1529 return Ok(Vec::new());
1530 }
1531 break attribute(&start, b"content");
1532 }
1533 Event::Eof => return Ok(Vec::new()),
1534 _ => {}
1535 }
1536 };
1537 let Some(source) = source.filter(|value| !value.trim().is_empty()) else {
1538 return Ok(Vec::new());
1539 };
1540 if source.len() > MAX_EMBEDDED_SOURCE_BYTES {
1541 return Err(Error::LimitExceeded(format!(
1542 "embedded diagram source is {} bytes; maximum is {MAX_EMBEDDED_SOURCE_BYTES} bytes",
1543 source.len()
1544 )));
1545 }
1546 let source = if source.trim_start().starts_with('<') {
1550 source
1551 } else {
1552 crate::drawio::percent_decode(&source)?
1553 };
1554 split_embedded_diagrams(&source)
1555}
1556
1557fn split_embedded_diagrams(source: &str) -> Result<Vec<String>> {
1564 let bytes = source.as_bytes();
1565 let mut reader = Reader::from_reader(bytes);
1566 reader.config_mut().trim_text(false);
1567 let mut buffer = Vec::new();
1568 let mut diagrams = Vec::new();
1569 let mut open = None::<usize>;
1570 let mut depth = 0usize;
1571 let mut saw_mxfile = false;
1572 loop {
1573 let position = usize::try_from(reader.buffer_position()).unwrap_or(usize::MAX);
1574 match reader.read_event_into(&mut buffer).map_err(|error| {
1575 Error::InvalidInput(format!("embedded diagram source is not valid XML: {error}"))
1576 })? {
1577 Event::DocType(_) | Event::PI(_) => {
1578 return Err(Error::InvalidInput(
1579 "embedded diagram source may not declare a document type".into(),
1580 ));
1581 }
1582 Event::Start(start) => {
1583 let name = local_name(start.name().as_ref()).to_vec();
1584 if name == b"mxfile" {
1585 saw_mxfile = true;
1586 } else if name == b"diagram" && open.is_none() {
1587 open = Some(position);
1588 depth = 0;
1589 } else if open.is_some() {
1590 depth += 1;
1591 }
1592 }
1593 Event::End(end) => {
1594 if local_name(end.name().as_ref()) == b"diagram" && depth == 0 {
1595 if let Some(start_offset) = open.take() {
1596 let element = source
1597 .get(
1598 start_offset
1599 ..usize::try_from(reader.buffer_position())
1600 .unwrap_or(usize::MAX),
1601 )
1602 .unwrap_or_default();
1603 diagrams.push(element.to_owned());
1604 }
1605 } else if open.is_some() {
1606 depth = depth.saturating_sub(1);
1607 }
1608 }
1609 Event::Eof => break,
1610 _ => {}
1611 }
1612 buffer.clear();
1613 }
1614 if !saw_mxfile || diagrams.is_empty() {
1615 return Err(Error::InvalidInput(
1616 "embedded diagram source is not an mxfile with diagrams".into(),
1617 ));
1618 }
1619 Ok(diagrams)
1620}
1621
1622fn write_drawio(pages: &[SvgPage]) -> String {
1623 let mut diagrams = String::new();
1624 let mut count = 0usize;
1625 for (index, page) in pages.iter().enumerate() {
1626 let number = index + 1;
1627 if page.diagrams.is_empty() {
1628 diagrams.push_str(&drawio_picture_diagram(page, number));
1629 count += 1;
1630 } else {
1631 for diagram in &page.diagrams {
1632 diagrams.push_str(diagram);
1633 count += 1;
1634 }
1635 }
1636 }
1637 format!(
1638 "<mxfile host=\"document-svg\" agent=\"document-svg {}\" type=\"device\" pages=\"{count}\">{diagrams}</mxfile>\n",
1639 env!("CARGO_PKG_VERSION")
1640 )
1641}
1642
1643fn drawio_picture_diagram(page: &SvgPage, number: usize) -> String {
1648 let width = (page.width_points * PIXELS_PER_POINT).round().max(1.0);
1649 let height = (page.height_points * PIXELS_PER_POINT).round().max(1.0);
1650 let encoded = base64::engine::general_purpose::STANDARD.encode(&page.bytes);
1651 format!(
1652 "<diagram id=\"page-{number}\" name=\"Page {number}\">\
1653 <mxGraphModel dx=\"{width}\" dy=\"{height}\" grid=\"0\" gridSize=\"10\" guides=\"1\" \
1654 tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" \
1655 pageWidth=\"{width}\" pageHeight=\"{height}\" math=\"0\" shadow=\"0\">\
1656 <root><mxCell id=\"0\"/><mxCell id=\"1\" parent=\"0\"/>\
1657 <mxCell id=\"page-{number}-image\" value=\"\" \
1658 style=\"shape=image;verticalLabelPosition=bottom;verticalAlign=top;imageAspect=0;aspect=fixed;image=data:image/svg+xml,{encoded}\" \
1659 vertex=\"1\" parent=\"1\">\
1660 <mxGeometry x=\"0\" y=\"0\" width=\"{width}\" height=\"{height}\" as=\"geometry\"/>\
1661 </mxCell></root></mxGraphModel></diagram>"
1662 )
1663}
1664
1665fn push_style_text(buffer: &mut String, value: &str) -> Result<()> {
1668 const MAX_STYLE_BYTES: usize = 4 * 1024 * 1024;
1669 if buffer.len().saturating_add(value.len()) > MAX_STYLE_BYTES {
1670 return Err(Error::LimitExceeded(format!(
1671 "SVG style content exceeds {MAX_STYLE_BYTES} bytes"
1672 )));
1673 }
1674 buffer.push_str(value);
1675 Ok(())
1676}
1677
1678fn write_pdf(pages: &[SvgPage], writer: &mut std::fs::File) -> Result<()> {
1679 let mut document = Document::with_version("1.4");
1680 let pages_id = document.add_object(lopdf::Dictionary::new());
1681 let mut page_ids = Vec::with_capacity(pages.len());
1682
1683 for page in pages {
1684 let width_pts = if page.width_points > 0.0 {
1685 page.width_points
1686 } else {
1687 612.0
1688 };
1689 let height_pts = if page.height_points > 0.0 {
1690 page.height_points
1691 } else {
1692 792.0
1693 };
1694
1695 if page.fallback_png.is_empty() {
1696 return Err(Error::InvalidInput(
1697 "missing raster fallback for PDF page".into(),
1698 ));
1699 }
1700
1701 let decoder = png::Decoder::new(Cursor::new(page.fallback_png.as_slice()));
1702 let mut reader = decoder.read_info().map_err(|e| {
1703 Error::InvalidInput(format!("failed to read fallback PNG info for PDF: {e}"))
1704 })?;
1705 let output_size = reader
1706 .output_buffer_size()
1707 .ok_or_else(|| Error::InvalidInput("PNG output buffer size overflow".into()))?;
1708 let mut img_buf = vec![0u8; output_size];
1709 let info = reader.next_frame(&mut img_buf).map_err(|e| {
1710 Error::InvalidInput(format!("failed to decode fallback PNG frame for PDF: {e}"))
1711 })?;
1712 let img_bytes = &img_buf[..info.buffer_size()];
1713
1714 let mut rgb = Vec::with_capacity((info.width * info.height * 3) as usize);
1715 let mut alpha = Vec::with_capacity((info.width * info.height) as usize);
1716 let mut has_transparency = false;
1717
1718 match info.color_type {
1719 png::ColorType::Rgba => {
1720 for chunk in img_bytes.chunks_exact(4) {
1721 rgb.push(chunk[0]);
1722 rgb.push(chunk[1]);
1723 rgb.push(chunk[2]);
1724 let a = chunk[3];
1725 if a < 255 {
1726 has_transparency = true;
1727 }
1728 alpha.push(a);
1729 }
1730 }
1731 png::ColorType::Rgb => {
1732 rgb.extend_from_slice(img_bytes);
1733 }
1734 png::ColorType::GrayscaleAlpha => {
1735 for chunk in img_bytes.chunks_exact(2) {
1736 let g = chunk[0];
1737 rgb.push(g);
1738 rgb.push(g);
1739 rgb.push(g);
1740 let a = chunk[1];
1741 if a < 255 {
1742 has_transparency = true;
1743 }
1744 alpha.push(a);
1745 }
1746 }
1747 png::ColorType::Grayscale => {
1748 for &g in img_bytes {
1749 rgb.push(g);
1750 rgb.push(g);
1751 rgb.push(g);
1752 }
1753 }
1754 _ => {
1755 rgb.extend_from_slice(img_bytes);
1756 }
1757 }
1758
1759 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1760 encoder.write_all(&rgb)?;
1761 let compressed_rgb = encoder.finish()?;
1762
1763 let mut image_dict = lopdf::dictionary! {
1764 "Type" => "XObject",
1765 "Subtype" => "Image",
1766 "Width" => info.width as i64,
1767 "Height" => info.height as i64,
1768 "ColorSpace" => "DeviceRGB",
1769 "BitsPerComponent" => 8,
1770 "Filter" => "FlateDecode",
1771 };
1772
1773 if has_transparency {
1774 let mut smask_encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1775 smask_encoder.write_all(&alpha)?;
1776 let compressed_alpha = smask_encoder.finish()?;
1777
1778 let smask_dict = lopdf::dictionary! {
1779 "Type" => "XObject",
1780 "Subtype" => "Image",
1781 "Width" => info.width as i64,
1782 "Height" => info.height as i64,
1783 "ColorSpace" => "DeviceGray",
1784 "BitsPerComponent" => 8,
1785 "Filter" => "FlateDecode",
1786 };
1787 let smask_stream = Stream::new(smask_dict, compressed_alpha);
1788 let smask_id = document.add_object(smask_stream);
1789 image_dict.set("SMask", Object::Reference(smask_id));
1790 }
1791
1792 let image_stream = Stream::new(image_dict, compressed_rgb);
1793 let image_id = document.add_object(image_stream);
1794
1795 let content_ops = format!(
1796 "q {:.2} 0 0 {:.2} 0 0 cm /Im1 Do Q\n",
1797 width_pts, height_pts
1798 );
1799 let content_stream = Stream::new(lopdf::Dictionary::new(), content_ops.into_bytes());
1800 let content_id = document.add_object(content_stream);
1801
1802 let page_dict = lopdf::dictionary! {
1803 "Type" => "Page",
1804 "Parent" => Object::Reference(pages_id),
1805 "MediaBox" => vec![0.into(), 0.into(), width_pts.into(), height_pts.into()],
1806 "Contents" => Object::Reference(content_id),
1807 "Resources" => lopdf::dictionary! {
1808 "XObject" => lopdf::dictionary! {
1809 "Im1" => Object::Reference(image_id),
1810 },
1811 },
1812 };
1813 let page_id = document.add_object(page_dict);
1814 page_ids.push(page_id);
1815 }
1816
1817 let pages_dict = lopdf::dictionary! {
1818 "Type" => "Pages",
1819 "Kids" => page_ids.into_iter().map(Object::Reference).collect::<Vec<_>>(),
1820 "Count" => pages.len() as i64,
1821 };
1822 document.set_object(pages_id, pages_dict);
1823
1824 let catalog_dict = lopdf::dictionary! {
1825 "Type" => "Catalog",
1826 "Pages" => Object::Reference(pages_id),
1827 };
1828 let catalog_id = document.add_object(catalog_dict);
1829 document.trailer.set("Root", Object::Reference(catalog_id));
1830
1831 document.save_to(writer)?;
1832
1833 Ok(())
1834}