1use std::fs;
2use std::io::{BufWriter, Cursor, Read, Write};
3use std::path::Path;
4#[cfg(not(target_arch = "wasm32"))]
5use std::time::Instant;
6#[cfg(target_arch = "wasm32")]
7use web_time::Instant;
8
9use serde::Serialize;
10
11use crate::error::{Error, Result};
12use crate::ir::{IDENTITY, Matrix, Node, Page, TextAnchor, compose};
13use crate::svg::{SvgOptions, write_page};
14
15const FORMAT_SNIFF_BYTES: u64 = 4096;
16
17pub(crate) fn read_limited_file(path: &Path, max_bytes: u64, context: &str) -> Result<Vec<u8>> {
18 let mut file = fs::File::open(path)?;
19 let mut bytes = Vec::new();
20 Read::take(&mut file, max_bytes.saturating_add(1)).read_to_end(&mut bytes)?;
21 if bytes.len() as u64 > max_bytes {
22 return Err(Error::LimitExceeded(format!(
23 "{context} exceeds maximum bytes ({max_bytes})"
24 )));
25 }
26 Ok(bytes)
27}
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
30#[serde(rename_all = "lowercase")]
31pub enum SourceFormat {
32 Pdf,
33 #[serde(rename = "fdf")]
34 Fdf,
35 #[serde(rename = "xfdf")]
36 Xfdf,
37 #[serde(rename = "landxml")]
38 LandXml,
39 #[serde(rename = "mathml")]
40 Mathml,
41 #[serde(rename = "xmp")]
42 Xmp,
43 #[serde(rename = "xdp")]
44 Xdp,
45 #[serde(rename = "spreadsheetml")]
46 Spreadsheetml,
47 ProjectXml,
48 Xml,
49 Bpmn,
50 Dmn,
51 Cmmn,
52 Reqif,
53 Xmi,
54 Ppt,
55 Pptx,
56 Xls,
57 Xlsb,
58 Xlsx,
59 Doc,
60 GeoJson,
61 GeoJsonSeq,
62 TopoJson,
63 Geopackage,
64 GeoRss,
65 Feed,
66 Gml,
67 CityGml,
68 CityJson,
69 GraphGml,
70 Gpx,
71 Wkt,
72 Kml,
73 Kmz,
74 Shapefile,
75 Dbf,
76 #[serde(rename = "esri_ascii_grid")]
77 EsriAsciiGrid,
78 #[serde(rename = "properties")]
79 Properties,
80 #[serde(rename = "plist")]
81 Plist,
82 #[serde(rename = "premis")]
83 Premis,
84 Docx,
85 Drawio,
86 Dxf,
87 Gerber,
88 #[serde(rename = "kicad_pcb")]
89 KicadPcb,
90 #[serde(rename = "kicad_sch_legacy")]
91 KicadSchLegacy,
92 #[serde(rename = "kicad_sch")]
93 KicadSch,
94 #[serde(rename = "ltspice_asc")]
95 LtspiceAsc,
96 #[serde(rename = "eagle_sch")]
97 EagleSch,
98 Hpgl,
99 Dot,
100 Mermaid,
101 Markdown,
102 Chart,
103 Json,
104 JsonLd,
105 #[serde(rename = "iiif")]
106 Iiif,
107 JsonSeq,
108 Graphml,
109 Gexf,
110 Xgmml,
111 Toml,
112 Yaml,
113 Bib,
114 Tex,
115 Qr,
116 Raster,
117 Jpeg2000,
118 Gcode,
119 Excellon,
120 Stl,
121 Simulation,
122 Gltf,
123 Collada,
124 X3d,
125 Vrml,
126 Step,
127 Obj,
128 Ply,
129 Pcd,
130 Pts,
131 Ptx,
132 Xyz,
133 E57,
134 Las,
135 Unv,
136 Csv,
137 Emf,
138 Excalidraw,
139 PlantUml,
140 D2,
141 ThreeMf,
142 Iges,
143 Ifc,
144 IfcXml,
145 IfcZip,
146 Html,
147 Docbook,
148 #[serde(rename = "dublin-core")]
149 DublinCore,
150 #[serde(rename = "iso19115")]
151 Iso19115,
152 #[serde(rename = "ubl")]
153 Ubl,
154 #[serde(rename = "xbrl")]
155 Xbrl,
156 Dif,
157 Dita,
158 #[serde(rename = "ead")]
159 Ead,
160 #[serde(rename = "eac-cpf")]
161 EacCpf,
162 Fasta,
163 Fastq,
164 Pdb,
165 Hwpx,
166 Xmind,
167 Nifti,
168 Fits,
169 Gff3,
170 Gtf,
171 Mrc,
172 Netcdf,
173 Sqlite,
174 Sylk,
175 Cif,
176 #[serde(rename = "cml")]
177 Cml,
178 #[serde(rename = "rdf-xml")]
179 RdfXml,
180 #[serde(rename = "bcfzip")]
181 Bcfzip,
182 #[serde(rename = "flat-opc")]
183 FlatOpc,
184 #[serde(rename = "aasx")]
185 Aasx,
186 #[serde(rename = "openscad")]
187 OpenScad,
188 #[serde(rename = "amf")]
189 Amf,
190 #[serde(rename = "plmxml")]
191 PlmXml,
192 #[serde(rename = "stepxml")]
193 StepXml,
194 #[serde(rename = "qif")]
195 Qif,
196 #[serde(rename = "b2mml")]
197 B2mml,
198 #[serde(rename = "jdf")]
199 Jdf,
200 #[serde(rename = "xjdf")]
201 Xjdf,
202 #[serde(rename = "tmx")]
203 Tmx,
204 #[serde(rename = "tbx")]
205 Tbx,
206 #[serde(rename = "gbxml")]
207 GbXml,
208 #[serde(rename = "idml")]
209 Idml,
210 #[serde(rename = "oaipmh")]
211 OaiPmh,
212 #[serde(rename = "onix")]
213 Onix,
214 #[serde(rename = "xpdl")]
215 Xpdl,
216 Mol2,
217 Turtle,
218 Eps,
219 Epub,
220 Fb2,
221 Mobi,
222 Eml,
223 Emlx,
224 Mbox,
225 Mhtml,
226 #[serde(rename = "mets")]
227 Mets,
228 #[serde(rename = "mods")]
229 Mods,
230 Msg,
231 Ical,
232 Vcalendar,
233 Vcard,
234 Vcf,
235 Wig,
236 Odt,
237 Ods,
238 Odp,
239 Odg,
240 Visio,
241 Rtf,
242 Xps,
243 Tiff,
244 Dicom,
245 #[serde(rename = "dicom-sr")]
246 DicomSr,
247 DicomDir,
248 Rxn,
249 Mol,
250 Sdf,
251 Cbz,
252 Abaqus,
253 Nastran,
254 #[serde(rename = "op2")]
255 Op2,
256 LsDyna,
257 Jupyter,
258 Jats,
259 #[serde(rename = "tei")]
260 Tei,
261 Quarto,
262 Medit,
263 Off,
264 Asciidoc,
265 Arff,
266 Bed,
267 BedGraph,
268 Rst,
269 Org,
270 Sam,
271 #[serde(rename = "s1000d")]
272 S1000d,
273 Maf,
274 #[serde(rename = "marcxml")]
275 Marcxml,
276 #[serde(rename = "marc21")]
277 Marc,
278 Po,
279 Srt,
280 Ttml,
281 Vtt,
282 Xliff,
283 Svg,
284 Su2,
285 OpenFoam,
286 OpenDrive,
287 OpenCrg,
288 OpenScenario,
289 #[serde(rename = "openlabel")]
290 OpenLabel,
291 #[serde(rename = "openfoam-field")]
292 OpenFoamField,
293 Tecplot,
294 Ensight,
295 Plot3d,
296 Newick,
297 Stockholm,
298 Clustal,
299 Nexus,
300 Genbank,
301 Embl,
302 Uniprot,
303 Ris,
304 Spice,
305 #[serde(rename = "openapi")]
306 Openapi,
307 #[serde(rename = "asyncapi")]
308 Asyncapi,
309 #[serde(rename = "alto")]
310 Alto,
311 Wsdl,
312 #[serde(rename = "opml")]
313 Opml,
314 #[serde(rename = "jsonschema")]
315 JsonSchema,
316 Cdb,
317 Har,
318 Warc,
319 Wacz,
320 Postman,
321 Graphql,
322 Protobuf,
323 Kubernetes,
324 Compose,
325 #[serde(rename = "github-actions")]
326 GithubActions,
327 Junit,
328 Sarif,
329 #[serde(rename = "terraform-plan")]
330 TerraformPlan,
331 CycloneDx,
332 Spdx,
333 #[serde(rename = "stix-json")]
334 StixJson,
335 #[serde(rename = "taxii-json")]
336 TaxiiJson,
337 Coverage,
338 #[serde(rename = "csl-json")]
339 CslJson,
340 Lcov,
341 JsonPatch,
342 JsonMergePatch,
343 JsonFeed,
344 #[serde(rename = "cloudevents")]
345 CloudEvents,
346 #[serde(rename = "fhir-json")]
347 FhirJson,
348 #[serde(rename = "fhir-xml")]
349 FhirXml,
350 #[serde(rename = "cda")]
351 Cda,
352 #[serde(rename = "iso20022")]
353 Iso20022,
354 #[serde(rename = "sbml")]
355 Sbml,
356 #[serde(rename = "cellml")]
357 Cellml,
358 #[serde(rename = "ocel-xml")]
359 OcelXml,
360 #[serde(rename = "energyplus-idf")]
361 EnergyPlusIdf,
362 #[serde(rename = "energyplus-epw")]
363 EnergyPlusEpw,
364 #[serde(rename = "rinex")]
365 Rinex,
366 #[serde(rename = "sat")]
367 Sat,
368 #[serde(rename = "sedml")]
369 Sedml,
370 #[serde(rename = "sbgnml")]
371 Sbgnml,
372 #[serde(rename = "omex")]
373 Omex,
374 #[serde(rename = "xdmf")]
375 Xdmf,
376 #[serde(rename = "pvd")]
377 Pvd,
378 #[serde(rename = "fds")]
379 Fds,
380 #[serde(rename = "abiword")]
381 Abiword,
382 #[serde(rename = "neuroml")]
383 Neuroml,
384 #[serde(rename = "biopax")]
385 Biopax,
386 #[serde(rename = "xsd")]
387 Xsd,
388 #[serde(rename = "xslt")]
389 Xslt,
390 #[serde(rename = "xsl-fo")]
391 XslFo,
392 #[serde(rename = "hdf5")]
393 Hdf5,
394 #[serde(rename = "cgns")]
395 Cgns,
396 #[serde(rename = "exodus")]
397 Exodus,
398 #[serde(rename = "iwork")]
399 Iwork,
400 #[serde(rename = "dwg")]
401 Dwg,
402 #[serde(rename = "3dm")]
403 Rhino3dm,
404 #[serde(rename = "access")]
405 Access,
406 #[serde(rename = "3dxml")]
407 ThreeDXml,
408 #[serde(rename = "ipc2581")]
409 Ipc2581,
410 #[serde(rename = "jt")]
411 Jt,
412 #[serde(rename = "vsd")]
413 Vsd,
414 #[serde(rename = "xproc")]
415 Xproc,
416 #[serde(rename = "wadl")]
417 Wadl,
418 #[serde(rename = "opensearch")]
419 OpenSearch,
420 #[serde(rename = "saml")]
421 Saml,
422 #[serde(rename = "xacml")]
423 Xacml,
424 Avro,
425 #[serde(rename = "otlp-json")]
426 OtlpJson,
427 #[serde(rename = "ocel-json")]
428 OcelJson,
429 #[serde(rename = "json-api")]
430 JsonApi,
431}
432
433#[cfg(not(target_arch = "wasm32"))]
434impl SourceFormat {
435 pub fn detect(path: &Path) -> Result<Self> {
436 let filename = path
437 .file_name()
438 .and_then(|name| name.to_str())
439 .unwrap_or("")
440 .to_ascii_lowercase();
441
442 if filename.ends_with(".xjdf") {
443 return Ok(Self::Xjdf);
444 }
445 if filename.ends_with(".idml") {
446 return Ok(Self::Idml);
447 }
448 if filename.ends_with(".tmx") || filename.ends_with(".tmx.xml") {
449 return Ok(Self::Tmx);
450 }
451 if filename.ends_with(".tbx") || filename.ends_with(".tbx.xml") {
452 return Ok(Self::Tbx);
453 }
454 if filename.ends_with(".gbxml") || filename.ends_with(".gbxml.xml") {
455 return Ok(Self::GbXml);
456 }
457 if filename.ends_with(".fhir.xml") || filename.ends_with(".fhirxml") {
458 return Ok(Self::FhirXml);
459 }
460 if filename.ends_with(".oaipmh")
461 || filename.ends_with(".oai-pmh")
462 || filename.ends_with(".oai.xml")
463 {
464 return Ok(Self::OaiPmh);
465 }
466 if filename.ends_with(".onix")
467 || filename.ends_with(".onix.xml")
468 || filename.ends_with(".onix3")
469 {
470 return Ok(Self::Onix);
471 }
472 if filename.ends_with(".xpdl") || filename.ends_with(".xpdl.xml") {
473 return Ok(Self::Xpdl);
474 }
475 if filename.ends_with(".cda")
476 || filename.ends_with(".cda.xml")
477 || filename.ends_with(".ccd.xml")
478 {
479 return Ok(Self::Cda);
480 }
481 if filename.ends_with(".iso20022.xml")
482 || filename.ends_with(".pain.xml")
483 || filename.ends_with(".pacs.xml")
484 || filename.ends_with(".camt.xml")
485 {
486 return Ok(Self::Iso20022);
487 }
488 if filename.ends_with(".sbml") || filename.ends_with(".sbml.xml") {
489 return Ok(Self::Sbml);
490 }
491 if filename.ends_with(".cellml") || filename.ends_with(".cellml.xml") {
492 return Ok(Self::Cellml);
493 }
494 if filename.ends_with(".xmlocel") || filename.ends_with(".ocel.xml") {
495 return Ok(Self::OcelXml);
496 }
497 if filename.ends_with(".idf") || filename.ends_with(".energyplus.idf") {
498 return Ok(Self::EnergyPlusIdf);
499 }
500 if filename.ends_with(".epw") || filename.ends_with(".energyplus.epw") {
501 return Ok(Self::EnergyPlusEpw);
502 }
503 if filename.ends_with(".rnx")
504 || filename.ends_with(".rinex")
505 || filename.ends_with(".obs")
506 || filename.ends_with(".nav")
507 || filename.ends_with(".gnav")
508 {
509 return Ok(Self::Rinex);
510 }
511 if filename.ends_with(".sat") {
512 return Ok(Self::Sat);
513 }
514 if filename.ends_with(".sedml")
515 || filename.ends_with(".sedml.xml")
516 || filename.ends_with(".sed-ml.xml")
517 {
518 return Ok(Self::Sedml);
519 }
520 if filename.ends_with(".sbgn")
521 || filename.ends_with(".sbgnml")
522 || filename.ends_with(".sbgn.xml")
523 {
524 return Ok(Self::Sbgnml);
525 }
526 if filename.ends_with(".omex")
527 || filename.ends_with(".omex.zip")
528 || filename.ends_with(".combine")
529 {
530 return Ok(Self::Omex);
531 }
532 if filename.ends_with(".xdmf") || filename.ends_with(".xmf") {
533 return Ok(Self::Xdmf);
534 }
535 if filename.ends_with(".pvd") {
536 return Ok(Self::Pvd);
537 }
538 if filename.ends_with(".fds") {
539 return Ok(Self::Fds);
540 }
541 if filename.ends_with(".abw") {
542 return Ok(Self::Abiword);
543 }
544 if filename.ends_with(".nml")
545 || filename.ends_with(".neuroml")
546 || filename.ends_with(".neuroml.xml")
547 {
548 return Ok(Self::Neuroml);
549 }
550 if filename.ends_with(".biopax") || filename.ends_with(".biopax.xml") {
551 return Ok(Self::Biopax);
552 }
553 if filename.ends_with(".xsd")
554 || filename.ends_with(".xsd.xml")
555 || filename.ends_with(".schema.xsd")
556 {
557 return Ok(Self::Xsd);
558 }
559 if filename.ends_with(".xsl")
560 || filename.ends_with(".xslt")
561 || filename.ends_with(".xsl.xml")
562 {
563 return Ok(Self::Xslt);
564 }
565 if filename.ends_with(".fo")
566 || filename.ends_with(".xslfo")
567 || filename.ends_with(".xsl-fo.xml")
568 || filename.ends_with(".fo.xml")
569 {
570 return Ok(Self::XslFo);
571 }
572 if filename.ends_with(".xpl")
573 || filename.ends_with(".xproc")
574 || filename.ends_with(".xproc.xml")
575 {
576 return Ok(Self::Xproc);
577 }
578 if filename.ends_with(".wadl") || filename.ends_with(".wadl.xml") {
579 return Ok(Self::Wadl);
580 }
581 if filename.ends_with(".osdd")
582 || filename.ends_with(".opensearch")
583 || filename.ends_with(".opensearchdescription.xml")
584 {
585 return Ok(Self::OpenSearch);
586 }
587 if filename.ends_with(".saml")
588 || filename.ends_with(".saml.xml")
589 || filename.ends_with(".saml-metadata.xml")
590 {
591 return Ok(Self::Saml);
592 }
593 if filename.ends_with(".xacml")
594 || filename.ends_with(".xacml.xml")
595 || filename.ends_with(".policy.xml")
596 {
597 return Ok(Self::Xacml);
598 }
599
600 let in_github_workflows = path
601 .parent()
602 .and_then(Path::file_name)
603 .and_then(|name| name.to_str())
604 .is_some_and(|name| name.eq_ignore_ascii_case("workflows"))
605 && path
606 .parent()
607 .and_then(Path::parent)
608 .and_then(Path::file_name)
609 .and_then(|name| name.to_str())
610 .is_some_and(|name| name.eq_ignore_ascii_case(".github"));
611 if in_github_workflows
612 && path
613 .extension()
614 .and_then(|value| value.to_str())
615 .is_some_and(|value| {
616 value.eq_ignore_ascii_case("yaml") || value.eq_ignore_ascii_case("yml")
617 })
618 {
619 return Ok(Self::GithubActions);
620 }
621 if filename == "junit.xml"
622 || filename == "test-results.xml"
623 || filename == "test-report.xml"
624 || filename.ends_with(".junit.xml")
625 || filename.ends_with(".junit-report.xml")
626 || filename.ends_with(".test-results.xml")
627 || (filename.starts_with("test-") && filename.ends_with(".xml"))
628 {
629 return Ok(Self::Junit);
630 }
631 if filename == "jacoco.xml"
632 || filename == "cobertura.xml"
633 || filename == "coverage.xml"
634 || filename.ends_with(".jacoco.xml")
635 || filename.ends_with(".cobertura.xml")
636 || filename.ends_with(".coverage.xml")
637 {
638 return Ok(Self::Coverage);
639 }
640 if filename == "lcov.info"
641 || filename == "coverage.info"
642 || filename.ends_with(".lcov.info")
643 {
644 return Ok(Self::Lcov);
645 }
646 if filename.ends_with(".foamfield") || filename.ends_with(".foam-field") {
647 return Ok(Self::OpenFoamField);
648 }
649 if filename.ends_with(".csl.json")
650 || filename.ends_with(".csl-json")
651 || filename.ends_with(".cite.json")
652 {
653 return Ok(Self::CslJson);
654 }
655 if filename.ends_with(".jsonpatch")
656 || filename.ends_with(".json-patch")
657 || filename.ends_with(".patch.json")
658 {
659 return Ok(Self::JsonPatch);
660 }
661 if filename.ends_with(".mergepatch")
662 || filename.ends_with(".json-merge-patch")
663 || filename.ends_with(".merge-patch.json")
664 {
665 return Ok(Self::JsonMergePatch);
666 }
667 if filename.ends_with(".jsonfeed")
668 || filename.ends_with(".json-feed")
669 || filename.ends_with(".feed.json")
670 {
671 return Ok(Self::JsonFeed);
672 }
673 if filename.ends_with(".iiif.json")
674 || filename.ends_with(".iiif-manifest.json")
675 || filename.ends_with(".manifest.json")
676 {
677 return Ok(Self::Iiif);
678 }
679 if filename.ends_with(".cloudevent.json")
680 || filename.ends_with(".cloud-event.json")
681 || filename.ends_with(".cloudevents.json")
682 || filename.ends_with(".ce.json")
683 || filename.ends_with(".cloudevent")
684 || filename.ends_with(".cloudevents")
685 {
686 return Ok(Self::CloudEvents);
687 }
688 if filename.ends_with(".fhir.json")
689 || filename.ends_with(".fhirjson")
690 || filename.ends_with(".fhir")
691 || filename.ends_with(".bundle.fhir.json")
692 || filename.ends_with(".fhir-bundle.json")
693 {
694 return Ok(Self::FhirJson);
695 }
696 if filename.ends_with(".avsc")
697 || filename.ends_with(".avpr")
698 || filename.ends_with(".avro.json")
699 || filename.ends_with(".avro-schema.json")
700 {
701 return Ok(Self::Avro);
702 }
703 if filename.ends_with(".otlp.json")
704 || filename.ends_with(".otlp-json")
705 || filename.ends_with(".otel.json")
706 || filename.ends_with(".otlp.trace.json")
707 || filename.ends_with(".otlp.metrics.json")
708 || filename.ends_with(".otlp.logs.json")
709 {
710 return Ok(Self::OtlpJson);
711 }
712 if filename.ends_with(".jsonocel")
713 || filename.ends_with(".ocel.json")
714 || filename.ends_with(".ocel-json")
715 {
716 return Ok(Self::OcelJson);
717 }
718 if filename.ends_with(".jsonapi")
719 || filename.ends_with(".json-api.json")
720 || filename.ends_with(".jsonapi.json")
721 {
722 return Ok(Self::JsonApi);
723 }
724 if filename.ends_with(".sarif") || filename.ends_with(".sarif.json") {
725 return Ok(Self::Sarif);
726 }
727 if filename == "tfplan.json"
728 || filename == "terraform-plan.json"
729 || filename.ends_with(".tfplan.json")
730 || filename.ends_with(".terraform-plan.json")
731 {
732 return Ok(Self::TerraformPlan);
733 }
734 if filename == "bom.json"
735 || filename == "bom.xml"
736 || filename.ends_with(".cdx.json")
737 || filename.ends_with(".cdx.xml")
738 {
739 return Ok(Self::CycloneDx);
740 }
741 if filename == "spdx.json"
742 || filename == "sbom.spdx.json"
743 || filename.ends_with(".spdx.json")
744 || filename == "spdx"
745 || filename.ends_with(".spdx")
746 || filename.ends_with(".spdx.txt")
747 {
748 return Ok(Self::Spdx);
749 }
750 if filename.ends_with(".stix.json")
751 || filename.ends_with(".stix-json")
752 || filename.ends_with(".stix")
753 {
754 return Ok(Self::StixJson);
755 }
756 if filename.ends_with(".taxii.json")
757 || filename.ends_with(".taxii-json")
758 || filename.ends_with(".taxii")
759 {
760 return Ok(Self::TaxiiJson);
761 }
762 if filename.ends_with(".opendrive.xml") || filename.ends_with(".open-drive.xml") {
763 return Ok(Self::OpenDrive);
764 }
765 if filename.ends_with(".crg") || filename.ends_with(".opencrg") {
766 return Ok(Self::OpenCrg);
767 }
768 if filename.ends_with(".openscenario.xml") || filename.ends_with(".open-scenario.xml") {
769 return Ok(Self::OpenScenario);
770 }
771 if filename.ends_with(".openlabel.json")
772 || filename.ends_with(".openlabel")
773 || filename.ends_with(".open-label.json")
774 {
775 return Ok(Self::OpenLabel);
776 }
777 if filename.ends_with(".wsdl") || filename.ends_with(".wsdl.xml") {
778 return Ok(Self::Wsdl);
779 }
780 if filename.ends_with(".opml") || filename.ends_with(".opml.xml") {
781 return Ok(Self::Opml);
782 }
783 if filename.ends_with(".plist") || filename.ends_with(".plist.xml") {
784 return Ok(Self::Plist);
785 }
786 if filename.ends_with(".premis") || filename.ends_with(".premis.xml") {
787 return Ok(Self::Premis);
788 }
789 if filename.ends_with(".tei") || filename.ends_with(".tei.xml") {
790 return Ok(Self::Tei);
791 }
792 if filename.ends_with(".ead") || filename.ends_with(".ead.xml") {
793 return Ok(Self::Ead);
794 }
795 if filename.ends_with(".eac-cpf")
796 || filename.ends_with(".eac-cpf.xml")
797 || filename.ends_with(".eac")
798 {
799 return Ok(Self::EacCpf);
800 }
801 if filename.ends_with(".dublin-core.xml")
802 || filename.ends_with(".dublin.xml")
803 || filename.ends_with(".dc.xml")
804 {
805 return Ok(Self::DublinCore);
806 }
807 if filename.ends_with(".iso19115")
808 || filename.ends_with(".iso19115.xml")
809 || filename.ends_with(".iso19139")
810 || filename.ends_with(".iso19139.xml")
811 || filename.ends_with(".gmd.xml")
812 {
813 return Ok(Self::Iso19115);
814 }
815 if filename.ends_with(".ubl") || filename.ends_with(".ubl.xml") {
816 return Ok(Self::Ubl);
817 }
818 if filename.ends_with(".xbrl")
819 || filename.ends_with(".xbrl.xml")
820 || filename.ends_with(".xbrli")
821 {
822 return Ok(Self::Xbrl);
823 }
824 if filename.ends_with(".fdf") {
825 return Ok(Self::Fdf);
826 }
827 if filename.ends_with(".xfdf") || filename.ends_with(".xfdf.xml") {
828 return Ok(Self::Xfdf);
829 }
830 if filename.ends_with(".landxml") || filename.ends_with(".landxml.xml") {
831 return Ok(Self::LandXml);
832 }
833 if filename.ends_with(".mathml")
834 || filename.ends_with(".mathml.xml")
835 || filename.ends_with(".mml")
836 {
837 return Ok(Self::Mathml);
838 }
839 if filename.ends_with(".s1000d")
840 || filename.ends_with(".dmodule")
841 || filename.ends_with(".dmodule.xml")
842 {
843 return Ok(Self::S1000d);
844 }
845 if filename.ends_with(".alto") || filename.ends_with(".alto.xml") {
846 return Ok(Self::Alto);
847 }
848 if filename.ends_with(".mets") || filename.ends_with(".mets.xml") {
849 return Ok(Self::Mets);
850 }
851 if filename.ends_with(".marcxml") || filename.ends_with(".marc.xml") {
852 return Ok(Self::Marcxml);
853 }
854 if filename.ends_with(".mods") || filename.ends_with(".mods.xml") {
855 return Ok(Self::Mods);
856 }
857 if filename.ends_with(".marc") || filename.ends_with(".iso2709") {
858 return Ok(Self::Marc);
859 }
860 if filename.ends_with(".citygml")
861 || filename.ends_with(".citygml.xml")
862 || filename.ends_with(".citygml.gml")
863 {
864 return Ok(Self::CityGml);
865 }
866 if filename.ends_with(".cityjson")
867 || filename.ends_with(".city.json")
868 || filename.ends_with(".cityjson.json")
869 {
870 return Ok(Self::CityJson);
871 }
872
873 if filename.ends_with(".chart.json") {
874 return Ok(Self::Chart);
875 } else if filename.ends_with(".fb2.zip") {
876 return Ok(Self::Fb2);
877 } else if filename.ends_with(".nii.gz") {
878 return Ok(Self::Nifti);
879 } else if filename.ends_with(".fits.gz") {
880 return Ok(Self::Fits);
881 } else if filename.ends_with(".mrc.gz") {
882 return Ok(Self::Mrc);
883 } else if filename.ends_with(".excalidraw.json") {
884 return Ok(Self::Excalidraw);
885 } else if filename.ends_with(".drawio.xml") || filename.ends_with(".dio.xml") {
886 return Ok(Self::Drawio);
887 } else if filename.ends_with(".openapi.json")
888 || filename.ends_with(".swagger.json")
889 || filename.ends_with(".openapi.yaml")
890 || filename.ends_with(".openapi.yml")
891 || filename.ends_with(".swagger.yaml")
892 || filename.ends_with(".swagger.yml")
893 {
894 return Ok(Self::Openapi);
895 } else if filename.ends_with(".asyncapi.json")
896 || filename.ends_with(".asyncapi.yaml")
897 || filename.ends_with(".asyncapi.yml")
898 {
899 return Ok(Self::Asyncapi);
900 } else if filename.ends_with(".warc") || filename.ends_with(".warc.gz") {
901 return Ok(Self::Warc);
902 } else if filename.ends_with(".postman_collection.json") {
903 return Ok(Self::Postman);
904 } else if filename.ends_with(".graphql") || filename.ends_with(".graphqls") {
905 return Ok(Self::Graphql);
906 } else if filename.ends_with(".proto") {
907 return Ok(Self::Protobuf);
908 } else if filename.ends_with(".k8s.yaml")
909 || filename.ends_with(".k8s.yml")
910 || filename.ends_with(".kubernetes.yaml")
911 || filename.ends_with(".kubernetes.yml")
912 || filename.ends_with(".kube.yaml")
913 || filename.ends_with(".kube.yml")
914 {
915 return Ok(Self::Kubernetes);
916 } else if filename == "compose.yaml"
917 || filename == "compose.yml"
918 || filename == "docker-compose.yaml"
919 || filename == "docker-compose.yml"
920 || filename.ends_with(".compose.yaml")
921 || filename.ends_with(".compose.yml")
922 {
923 return Ok(Self::Compose);
924 } else if filename.ends_with(".schema.json")
925 || filename.ends_with(".jsonschema")
926 || filename.ends_with(".schema.yaml")
927 || filename.ends_with(".schema.yml")
928 || filename.ends_with(".jsonschema.yaml")
929 || filename.ends_with(".jsonschema.yml")
930 {
931 return Ok(Self::JsonSchema);
932 }
933 if filename == "dicomdir" || filename.ends_with(".dicomdir") {
934 return Ok(Self::DicomDir);
935 }
936 if filename.ends_with(".dicom-sr")
937 || filename.ends_with(".dicom-sr.dcm")
938 || filename.ends_with(".sr.dcm")
939 {
940 return Ok(Self::DicomSr);
941 }
942
943 let extension = path
944 .extension()
945 .and_then(|value| value.to_str())
946 .map(str::to_ascii_lowercase);
947
948 if let Some(ref ext) = extension {
949 match ext.as_str() {
950 "pdf" => return Ok(Self::Pdf),
951 "mspdi" => return Ok(Self::ProjectXml),
952 "ppt" => return Ok(Self::Ppt),
953 "pptx" | "pptm" | "potx" | "potm" | "ppsx" | "ppam" | "sldx" | "sldm" => {
954 return Ok(Self::Pptx);
955 }
956 "xlsx" | "xlsm" | "xltx" | "xltm" | "xlam" => return Ok(Self::Xlsx),
957 "xlsb" => return Ok(Self::Xlsb),
958 "xls" | "xlt" | "xla" => return Ok(Self::Xls),
959 "accdb" | "accde" | "accdr" | "mdb" | "mde" | "mda" | "mdt" | "accdt" | "ade" => {
960 return Ok(Self::Access);
961 }
962 "doc" => return Ok(Self::Doc),
963 "dot" => {
966 return Ok(
967 if crate::document::legacy_doc::looks_like_legacy_doc(path) {
968 Self::Doc
969 } else {
970 Self::Dot
971 },
972 );
973 }
974 "geojson" => {
975 let prefix = read_format_prefix(path)?;
976 return Ok(
977 if crate::geospatial::geojson_seq::looks_like_rfc8142_prefix(&prefix) {
978 Self::GeoJsonSeq
979 } else {
980 Self::GeoJson
981 },
982 );
983 }
984 "geojsons" | "geojsonseq" | "geojson-seq" | "geojsonl" => {
985 return Ok(Self::GeoJsonSeq);
986 }
987 "jsons" | "jsonseq" | "jsonl" => return Ok(Self::JsonSeq),
988 "toml" => return Ok(Self::Toml),
989 "yaml" | "yml" => {
990 let prefix = read_format_prefix(path)?;
991 return Ok(
992 if crate::document::kubernetes::looks_like_yaml_prefix(&prefix) {
993 Self::Kubernetes
994 } else if crate::document::compose::looks_like_yaml_prefix(&prefix) {
995 Self::Compose
996 } else if crate::document::github_actions::looks_like_yaml_prefix(&prefix) {
997 Self::GithubActions
998 } else if crate::document::asyncapi::looks_like_yaml_prefix(&prefix) {
999 Self::Asyncapi
1000 } else if crate::document::jsonschema::looks_like_yaml_prefix(&prefix) {
1001 Self::JsonSchema
1002 } else if crate::document::openapi::looks_like_yaml_prefix(&prefix) {
1003 Self::Openapi
1004 } else {
1005 Self::Yaml
1006 },
1007 );
1008 }
1009 "properties" => return Ok(Self::Properties),
1010 "opml" => return Ok(Self::Opml),
1011 "plist" => return Ok(Self::Plist),
1012 "premis" => return Ok(Self::Premis),
1013 "topojson" => return Ok(Self::TopoJson),
1014 "gpkg" => return Ok(Self::Geopackage),
1015 "rss" | "atom" | "georss" => {
1016 let prefix = read_format_prefix(path)?;
1017 return Ok(
1018 if crate::geospatial::georss::looks_like_georss_prefix(&prefix) {
1019 Self::GeoRss
1020 } else {
1021 Self::Feed
1022 },
1023 );
1024 }
1025 "gml" => {
1026 let prefix = read_format_prefix(path)?;
1027 return Ok(if crate::document::citygml::looks_like_prefix(&prefix) {
1028 Self::CityGml
1029 } else if crate::document::gml_graph::looks_like_prefix(&prefix) {
1030 Self::GraphGml
1031 } else {
1032 Self::Gml
1033 });
1034 }
1035 "gpx" => return Ok(Self::Gpx),
1036 "wkt" | "ewkt" => return Ok(Self::Wkt),
1037 "kml" => return Ok(Self::Kml),
1038 "kmz" => return Ok(Self::Kmz),
1039 "shp" => return Ok(Self::Shapefile),
1040 "dbf" => return Ok(Self::Dbf),
1041 "asc" => {
1042 let prefix = read_format_prefix(path)?;
1043 if crate::geospatial::ascii_grid::looks_like_prefix(&prefix) {
1044 return Ok(Self::EsriAsciiGrid);
1045 }
1046 if crate::cad::ltspice::looks_like_prefix(&prefix) {
1047 return Ok(Self::LtspiceAsc);
1048 }
1049 return Ok(Self::Asciidoc);
1050 }
1051 "docx" | "docm" | "dotx" | "dotm" => return Ok(Self::Docx),
1052 "vsd" => {
1053 if crate::document::legacy_visio::looks_like_legacy_visio(path) {
1054 return Ok(Self::Vsd);
1055 }
1056 return Err(Error::InvalidInput(
1057 "VSD input is not a legacy Visio Compound File Binary document".into(),
1058 ));
1059 }
1060 "vss" | "vst" | "vsw" => return Ok(Self::Vsd),
1061 "vsdx" | "vsdm" | "vstx" | "vstm" => return Ok(Self::Visio),
1062 "drawio" | "dio" => return Ok(Self::Drawio),
1063 "xml" => {
1064 let prefix = read_format_prefix(path)?;
1065 let filename = path
1066 .file_name()
1067 .and_then(|name| name.to_str())
1068 .map(str::to_ascii_lowercase)
1069 .unwrap_or_default();
1070 if filename.ends_with(".reqif.xml") {
1071 return Ok(Self::Reqif);
1072 }
1073 if crate::document::ipc2581::looks_like_prefix(&prefix) {
1074 return Ok(Self::Ipc2581);
1075 }
1076 if crate::document::flatopc::looks_like_prefix(&prefix) {
1077 return Ok(Self::FlatOpc);
1078 }
1079 if crate::document::fhir_xml::looks_like_prefix(&prefix) {
1080 return Ok(Self::FhirXml);
1081 }
1082 if crate::document::tmx::looks_like_prefix(&prefix) {
1083 return Ok(Self::Tmx);
1084 }
1085 if crate::document::tbx::looks_like_prefix(&prefix) {
1086 return Ok(Self::Tbx);
1087 }
1088 if crate::document::gbxml::looks_like_prefix(&prefix) {
1089 return Ok(Self::GbXml);
1090 }
1091 if crate::document::oaipmh::looks_like_prefix(&prefix) {
1092 return Ok(Self::OaiPmh);
1093 }
1094 if crate::document::onix::looks_like_prefix(&prefix) {
1095 return Ok(Self::Onix);
1096 }
1097 if crate::document::xpdl::looks_like_prefix(&prefix) {
1098 return Ok(Self::Xpdl);
1099 }
1100 if crate::document::cda::looks_like_prefix(&prefix) {
1101 return Ok(Self::Cda);
1102 }
1103 if crate::document::iso20022::looks_like_prefix(&prefix) {
1104 return Ok(Self::Iso20022);
1105 }
1106 if crate::document::sbml::looks_like_prefix(&prefix) {
1107 return Ok(Self::Sbml);
1108 }
1109 if crate::document::cellml::looks_like_prefix(&prefix) {
1110 return Ok(Self::Cellml);
1111 }
1112 if crate::document::ocel_xml::looks_like_prefix(&prefix) {
1113 return Ok(Self::OcelXml);
1114 }
1115 if crate::document::sedml::looks_like_prefix(&prefix) {
1116 return Ok(Self::Sedml);
1117 }
1118 if crate::document::sbgnml::looks_like_prefix(&prefix) {
1119 return Ok(Self::Sbgnml);
1120 }
1121 if crate::document::xdmf::looks_like_prefix(&prefix) {
1122 return Ok(Self::Xdmf);
1123 }
1124 if crate::document::pvd::looks_like_prefix(&prefix) {
1125 return Ok(Self::Pvd);
1126 }
1127 if crate::document::abiword::looks_like_prefix(&prefix) {
1128 return Ok(Self::Abiword);
1129 }
1130 if crate::document::neuroml::looks_like_prefix(&prefix) {
1131 return Ok(Self::Neuroml);
1132 }
1133 if crate::document::biopax::looks_like_prefix(&prefix) {
1134 return Ok(Self::Biopax);
1135 }
1136 if crate::document::xsd::looks_like_prefix(&prefix) {
1137 return Ok(Self::Xsd);
1138 }
1139 if crate::document::xslt::looks_like_prefix(&prefix) {
1140 return Ok(Self::Xslt);
1141 }
1142 if crate::document::xslfo::looks_like_prefix(&prefix) {
1143 return Ok(Self::XslFo);
1144 }
1145 if crate::document::xproc::looks_like_prefix(&prefix) {
1146 return Ok(Self::Xproc);
1147 }
1148 if crate::document::wadl::looks_like_prefix(&prefix) {
1149 return Ok(Self::Wadl);
1150 }
1151 if crate::document::opensearch::looks_like_prefix(&prefix) {
1152 return Ok(Self::OpenSearch);
1153 }
1154 if crate::document::saml::looks_like_prefix(&prefix) {
1155 return Ok(Self::Saml);
1156 }
1157 if crate::document::xacml::looks_like_prefix(&prefix) {
1158 return Ok(Self::Xacml);
1159 }
1160 if crate::document::cyclonedx::looks_like_xml_prefix(&prefix) {
1161 return Ok(Self::CycloneDx);
1162 }
1163 if crate::document::msproject::looks_like_project_xml_prefix(&prefix) {
1164 return Ok(Self::ProjectXml);
1165 }
1166 if crate::document::junit::looks_like_prefix(&prefix) {
1167 return Ok(Self::Junit);
1168 }
1169 if crate::document::coverage::looks_like_prefix(&prefix) {
1170 return Ok(Self::Coverage);
1171 }
1172 return Self::detect_from_content(path);
1173 }
1174 "info" | "lcov" => {
1175 let prefix = read_format_prefix(path)?;
1176 if crate::document::lcov::looks_like_prefix(&prefix) {
1177 return Ok(Self::Lcov);
1178 }
1179 }
1180 "bpmn" | "bpmn2" => return Ok(Self::Bpmn),
1181 "dmn" => return Ok(Self::Dmn),
1182 "cmmn" => return Ok(Self::Cmmn),
1183 "reqif" => return Ok(Self::Reqif),
1184 "xmi" => return Ok(Self::Xmi),
1185 "vdx" => return Ok(Self::Visio),
1186 "dxf" => return Ok(Self::Dxf),
1187 "cir" | "sp" | "spice" | "ckt" | "net" => return Ok(Self::Spice),
1188 "kicad_pcb" => return Ok(Self::KicadPcb),
1189 "kicad_sch" => return Ok(Self::KicadSch),
1190 "sch" => {
1191 let prefix = read_format_prefix(path)?;
1192 if crate::cad::kicad_sch::looks_like_prefix(&prefix) {
1193 return Ok(Self::KicadSchLegacy);
1194 }
1195 if crate::cad::eagle::looks_like_prefix(&prefix) {
1196 return Ok(Self::EagleSch);
1197 }
1198 }
1199 "gbr" | "gerber" | "gtl" | "gbl" | "gts" | "gbs" | "gto" | "gbo" | "gko"
1200 | "gm1" | "gm2" | "art" | "pho" | "cmp" | "sol" | "sts" => {
1201 return Ok(Self::Gerber);
1202 }
1203 "stc" => {
1204 return Ok(
1205 if crate::document::ods::looks_like_legacy_calc_archive(path) {
1206 Self::Ods
1207 } else {
1208 Self::Gerber
1209 },
1210 );
1211 }
1212 "plt" | "hpgl" | "hpg" | "gl2" | "prn" => return Ok(Self::Hpgl),
1213 "gcode" | "ngc" | "tap" | "gco" | "cnc" => return Ok(Self::Gcode),
1214 "drl" | "drd" | "xln" | "exc" => return Ok(Self::Excellon),
1215 "stl" => return Ok(Self::Stl),
1216 "gltf" | "glb" => return Ok(Self::Gltf),
1217 "3dm" => return Ok(Self::Rhino3dm),
1218 "jt" => return Ok(Self::Jt),
1219 "3dxml" => return Ok(Self::ThreeDXml),
1220 "ipc2581" | "ipc-2581" | "cvg" => return Ok(Self::Ipc2581),
1221 "dae" => return Ok(Self::Collada),
1222 "x3d" => return Ok(Self::X3d),
1223 "wrl" | "vrml" => return Ok(Self::Vrml),
1224 "ply" => return Ok(Self::Ply),
1225 "pcd" => return Ok(Self::Pcd),
1226 "pts" => return Ok(Self::Pts),
1227 "ptx" => return Ok(Self::Ptx),
1228 "xyz" => return Ok(Self::Xyz),
1229 "e57" => return Ok(Self::E57),
1230 "las" | "laz" => return Ok(Self::Las),
1231 "3mf" => return Ok(Self::ThreeMf),
1232 "ifc" => {
1233 let prefix = read_format_prefix(path)?;
1234 return Ok(if crate::cad::ifc::looks_like_ifcxml_prefix(&prefix) {
1235 Self::IfcXml
1236 } else {
1237 Self::Ifc
1238 });
1239 }
1240 "ifcxml" => return Ok(Self::IfcXml),
1241 "ifczip" => return Ok(Self::IfcZip),
1242 "step" | "stp" | "p21" | "stpnc" => return Ok(Self::Step),
1243 "iges" | "igs" => return Ok(Self::Iges),
1244 "obj" => return Ok(Self::Obj),
1245 "unv" => return Ok(Self::Unv),
1246 "su2" => return Ok(Self::Su2),
1247 "foam" => return Ok(Self::OpenFoam),
1248 "case" => return Ok(Self::Ensight),
1249 "geo" => {
1250 let prefix = read_format_prefix(path)?;
1251 if crate::cad::simulation::looks_like_ensight_prefix(&prefix) {
1252 return Ok(Self::Ensight);
1253 }
1254 }
1255 "dat" => {
1258 let prefix = read_format_prefix(path)?;
1259 if crate::document::uniprot::looks_like_prefix(&prefix) {
1260 return Ok(Self::Uniprot);
1261 }
1262 if crate::cad::simulation::looks_like_tecplot_prefix(&prefix) {
1263 return Ok(Self::Tecplot);
1264 }
1265 }
1266 "tec" | "tecplot" | "tp" => return Ok(Self::Tecplot),
1267 "p3d" | "plot3d" | "p3" => return Ok(Self::Plot3d),
1268 "msh" | "vtk" | "vtu" | "vtp" | "vti" | "vtr" | "vts" => {
1269 return Ok(Self::Simulation);
1270 }
1271 "cgns" => return Ok(Self::Cgns),
1272 "h5" | "hdf5" | "hdf" | "h5part" => return Ok(Self::Hdf5),
1273 "exo" | "ex2" | "ex2m" | "exii" => return Ok(Self::Exodus),
1274 "dwg" => return Ok(Self::Dwg),
1275 "e" => {
1276 let prefix = read_format_prefix(path)?;
1277 if crate::document::netcdf::looks_like_prefix(&prefix) {
1278 return Ok(Self::Exodus);
1279 }
1280 }
1281 "gv" => return Ok(Self::Dot),
1282 "mmd" | "mermaid" => return Ok(Self::Mermaid),
1283 "puml" | "plantuml" | "iuml" | "pu" | "wsd" => return Ok(Self::PlantUml),
1284 "d2" => return Ok(Self::D2),
1285 "excalidraw" => return Ok(Self::Excalidraw),
1286 "html" | "htm" | "xhtml" => return Ok(Self::Html),
1287 "dbk" | "docbook" => return Ok(Self::Docbook),
1288 "dc" | "dublin" | "dublincore" => return Ok(Self::DublinCore),
1289 "dif" => return Ok(Self::Dif),
1290 "fa" | "fasta" | "fna" | "faa" => return Ok(Self::Fasta),
1291 "fq" | "fastq" => return Ok(Self::Fastq),
1292 "dita" | "ditamap" => return Ok(Self::Dita),
1293 "ead" => return Ok(Self::Ead),
1294 "eac" | "eac-cpf" => return Ok(Self::EacCpf),
1295 "pdb" | "ent" => return Ok(Self::Pdb),
1296 "hwpx" => return Ok(Self::Hwpx),
1297 "xmind" => return Ok(Self::Xmind),
1298 "nii" => return Ok(Self::Nifti),
1299 "fits" | "fit" | "fts" => return Ok(Self::Fits),
1300 "gff" | "gff3" => return Ok(Self::Gff3),
1301 "gtf" => return Ok(Self::Gtf),
1302 "mrc" | "map" => return Ok(Self::Mrc),
1303 "nc3" | "cdf" => return Ok(Self::Netcdf),
1304 "nc" => {
1305 let prefix = read_format_prefix(path)?;
1306 return Ok(if crate::document::netcdf::looks_like_prefix(&prefix) {
1307 Self::Netcdf
1308 } else {
1309 Self::Gcode
1310 });
1311 }
1312 "sqlite" | "sqlite3" | "db" => return Ok(Self::Sqlite),
1313 "slk" => return Ok(Self::Sylk),
1314 "cif" | "mmcif" => return Ok(Self::Cif),
1315 "cml" => return Ok(Self::Cml),
1316 "rdf" => return Ok(Self::RdfXml),
1317 "bcfzip" => return Ok(Self::Bcfzip),
1318 "flatopc" | "fopc" => return Ok(Self::FlatOpc),
1319 "aasx" => return Ok(Self::Aasx),
1320 "scad" => return Ok(Self::OpenScad),
1321 "amf" => return Ok(Self::Amf),
1322 "plmxml" => return Ok(Self::PlmXml),
1323 "stepxml" | "stpx" => return Ok(Self::StepXml),
1324 "qif" => return Ok(Self::Qif),
1325 "b2mml" => return Ok(Self::B2mml),
1326 "jdf" | "jmf" => return Ok(Self::Jdf),
1327 "xjdf" => return Ok(Self::Xjdf),
1328 "mol2" => return Ok(Self::Mol2),
1329 "ttl" | "nt" | "nq" => return Ok(Self::Turtle),
1330 "eps" | "ps" => return Ok(Self::Eps),
1331 "eml" => return Ok(Self::Eml),
1332 "emlx" => return Ok(Self::Emlx),
1333 "mbox" => return Ok(Self::Mbox),
1334 "mht" | "mhtml" => return Ok(Self::Mhtml),
1335 "mets" => return Ok(Self::Mets),
1336 "msg" => return Ok(Self::Msg),
1337 "ics" => return Ok(Self::Ical),
1338 "vcs" => return Ok(Self::Vcalendar),
1339 "vcf" => {
1340 let prefix = read_format_prefix(path)?;
1341 return Ok(if crate::document::vcf::looks_like_prefix(&prefix) {
1342 Self::Vcf
1343 } else {
1344 Self::Vcard
1345 });
1346 }
1347 "wig" | "wiggle" => return Ok(Self::Wig),
1348 "vcard" => return Ok(Self::Vcard),
1349 "rtf" => return Ok(Self::Rtf),
1350 "xps" | "oxps" | "dwfx" => return Ok(Self::Xps),
1351 "tif" | "tiff" => return Ok(Self::Tiff),
1352 "jp2" | "j2k" | "j2c" | "jpc" | "jpx" => return Ok(Self::Jpeg2000),
1353 "dicom-sr" | "sr" => return Ok(Self::DicomSr),
1354 "dcm" | "dicom" => {
1355 let prefix = read_format_prefix(path)?;
1356 return Ok(
1357 if crate::document::dicom::looks_like_dicomdir_prefix(&prefix) {
1358 Self::DicomDir
1359 } else if crate::document::dicom::looks_like_dicom_sr_prefix(&prefix) {
1360 Self::DicomSr
1361 } else {
1362 Self::Dicom
1363 },
1364 );
1365 }
1366 "mol" => return Ok(Self::Mol),
1367 "sdf" | "sd" => return Ok(Self::Sdf),
1368 "rxn" => return Ok(Self::Rxn),
1369 "cbz" => return Ok(Self::Cbz),
1370 "cdb" => return Ok(Self::Cdb),
1371 "har" => return Ok(Self::Har),
1372 "warc" => return Ok(Self::Warc),
1373 "wacz" => return Ok(Self::Wacz),
1374 "postman_collection" => return Ok(Self::Postman),
1375 "graphql" | "graphqls" | "gql" => return Ok(Self::Graphql),
1376 "proto" => return Ok(Self::Protobuf),
1377 "inp" => return Ok(Self::Abaqus),
1378 "op2" => return Ok(Self::Op2),
1379 "bdf" | "nas" | "nastran" => return Ok(Self::Nastran),
1380 "k" => return Ok(Self::LsDyna),
1381 "key" => {
1382 let prefix = read_format_prefix(path)?;
1383 return Ok(if prefix.starts_with(b"PK\x03\x04") {
1384 Self::Iwork
1385 } else {
1386 Self::LsDyna
1387 });
1388 }
1389 "pages" | "numbers" => return Ok(Self::Iwork),
1390 "ipynb" => return Ok(Self::Jupyter),
1391 "jats" | "nxml" => return Ok(Self::Jats),
1392 "tei" => return Ok(Self::Tei),
1393 "alto" => return Ok(Self::Alto),
1394 "qmd" | "rmd" | "rmarkdown" => return Ok(Self::Quarto),
1395 "meshb" => return Ok(Self::Medit),
1396 "medit" => return Ok(Self::Medit),
1397 "off" => return Ok(Self::Off),
1398 "epub" => return Ok(Self::Epub),
1399 "fb2" => return Ok(Self::Fb2),
1400 "mobi" | "prc" | "azw" => return Ok(Self::Mobi),
1401 "odt" | "ott" | "fodt" | "sxw" | "stw" | "sxg" => return Ok(Self::Odt),
1402 "ods" | "ots" | "fods" | "sxc" => return Ok(Self::Ods),
1403 "odp" | "otp" | "fodp" | "sxi" | "sti" => return Ok(Self::Odp),
1404 "odg" | "otg" | "fodg" | "sxd" | "std" => return Ok(Self::Odg),
1405 "md" | "markdown" | "mdown" | "mkd" | "mkdn" | "mdwn" | "mdx" | "txt" | "text" => {
1406 return Ok(Self::Markdown);
1407 }
1408 "adoc" | "asciidoc" => return Ok(Self::Asciidoc),
1409 "arff" => return Ok(Self::Arff),
1410 "bed" | "bed3" | "bed4" | "bed5" | "bed6" | "bed9" | "bed12" => {
1411 return Ok(Self::Bed);
1412 }
1413 "bedgraph" | "bg" => return Ok(Self::BedGraph),
1414 "rst" | "rest" => return Ok(Self::Rst),
1415 "sam" => return Ok(Self::Sam),
1416 "iso19115" | "iso19139" | "gmd" => return Ok(Self::Iso19115),
1417 "ubl" => return Ok(Self::Ubl),
1418 "xbrl" | "xbrli" => return Ok(Self::Xbrl),
1419 "fdf" => return Ok(Self::Fdf),
1420 "xfdf" => return Ok(Self::Xfdf),
1421 "landxml" => return Ok(Self::LandXml),
1422 "mathml" | "mml" => return Ok(Self::Mathml),
1423 "xmp" => return Ok(Self::Xmp),
1424 "xdp" => return Ok(Self::Xdp),
1425 "spreadsheetml" | "xmlss" => return Ok(Self::Spreadsheetml),
1426 "s1000d" | "dmodule" => return Ok(Self::S1000d),
1427 "maf" => return Ok(Self::Maf),
1428 "marcxml" => return Ok(Self::Marcxml),
1429 "marc" | "iso2709" => return Ok(Self::Marc),
1430 "mods" => return Ok(Self::Mods),
1431 "nwk" | "newick" | "tree" => return Ok(Self::Newick),
1432 "sto" | "stockholm" => return Ok(Self::Stockholm),
1433 "aln" | "clustal" | "clustalw" => return Ok(Self::Clustal),
1434 "nex" | "nexus" => return Ok(Self::Nexus),
1435 "gb" | "gbk" | "genbank" => return Ok(Self::Genbank),
1436 "embl" | "emb" => return Ok(Self::Embl),
1437 "uniprot" | "swissprot" => return Ok(Self::Uniprot),
1438 "ris" => return Ok(Self::Ris),
1439 "org" => return Ok(Self::Org),
1440 "po" | "pot" => return Ok(Self::Po),
1441 "bib" | "bibtex" => return Ok(Self::Bib),
1442 "srt" => return Ok(Self::Srt),
1443 "vtt" => return Ok(Self::Vtt),
1444 "ttml" | "dfxp" => return Ok(Self::Ttml),
1445 "xlf" | "xliff" => return Ok(Self::Xliff),
1446 "csv" | "tsv" | "tab" => return Ok(Self::Csv),
1447 "emf" | "wmf" => return Ok(Self::Emf),
1448 "svgz" => return Ok(Self::Svg),
1449 "chart" => return Ok(Self::Chart),
1450 "tex" | "latex" | "ltx" => return Ok(Self::Tex),
1451 "qr" | "qrcode" => return Ok(Self::Qr),
1452 "png" | "jpg" | "jpeg" | "jpe" | "jfif" | "bmp" | "dib" | "gif" | "webp"
1453 | "pbm" | "pgm" | "ppm" | "pnm" | "pam" => {
1454 return Ok(Self::Raster);
1455 }
1456 "svg" => return Ok(Self::Svg),
1457 "xodr" | "opendrive" => return Ok(Self::OpenDrive),
1458 "crg" | "opencrg" => return Ok(Self::OpenCrg),
1459 "xosc" | "openscenario" => return Ok(Self::OpenScenario),
1460 "openlabel" => return Ok(Self::OpenLabel),
1461 "wsdl" => return Ok(Self::Wsdl),
1462 "json" => {
1463 let bytes = read_format_prefix(path)?;
1464 let text = String::from_utf8_lossy(&bytes);
1465 if crate::document::postman::looks_like_prefix(&bytes) {
1466 return Ok(Self::Postman);
1467 }
1468 if crate::document::kubernetes::looks_like_json_prefix(&bytes) {
1469 return Ok(Self::Kubernetes);
1470 }
1471 if crate::document::compose::looks_like_json_prefix(&bytes) {
1472 return Ok(Self::Compose);
1473 }
1474 if crate::document::sarif::looks_like_prefix(&bytes) {
1475 return Ok(Self::Sarif);
1476 }
1477 if crate::document::terraform::looks_like_prefix(&bytes) {
1478 return Ok(Self::TerraformPlan);
1479 }
1480 if crate::document::cyclonedx::looks_like_json_prefix(&bytes) {
1481 return Ok(Self::CycloneDx);
1482 }
1483 if crate::document::spdx::looks_like_prefix(&bytes) {
1484 return Ok(Self::Spdx);
1485 }
1486 if crate::document::stix::looks_like_prefix(&bytes) {
1487 return Ok(Self::StixJson);
1488 }
1489 if crate::document::taxii::looks_like_prefix(&bytes) {
1490 return Ok(Self::TaxiiJson);
1491 }
1492 if crate::document::jsonpatch::looks_like_prefix(&bytes) {
1493 return Ok(Self::JsonPatch);
1494 }
1495 if crate::document::jsonfeed::looks_like_prefix(&bytes) {
1496 return Ok(Self::JsonFeed);
1497 }
1498 if crate::document::iiif::looks_like_prefix(&bytes) {
1499 return Ok(Self::Iiif);
1500 }
1501 if crate::document::csl_json::looks_like_prefix(&bytes) {
1502 return Ok(Self::CslJson);
1503 }
1504 if crate::document::cloudevents::looks_like_prefix(&bytes) {
1505 return Ok(Self::CloudEvents);
1506 }
1507 if crate::document::fhir::looks_like_prefix(&bytes) {
1508 return Ok(Self::FhirJson);
1509 }
1510 if crate::document::avro::looks_like_prefix(&bytes) {
1511 return Ok(Self::Avro);
1512 }
1513 if crate::document::otlp::looks_like_prefix(&bytes) {
1514 return Ok(Self::OtlpJson);
1515 }
1516 if crate::document::ocel::looks_like_prefix(&bytes) {
1517 return Ok(Self::OcelJson);
1518 }
1519 if crate::document::jsonapi::looks_like_prefix(&bytes) {
1520 return Ok(Self::JsonApi);
1521 }
1522 if crate::document::openlabel::looks_like_prefix(&bytes) {
1523 return Ok(Self::OpenLabel);
1524 }
1525 if crate::document::cityjson::looks_like_prefix(&bytes) {
1526 return Ok(Self::CityJson);
1527 }
1528 if crate::document::har::looks_like_prefix(&bytes) {
1529 return Ok(Self::Har);
1530 }
1531 if crate::document::asyncapi::looks_like_json_prefix(&bytes) {
1532 return Ok(Self::Asyncapi);
1533 }
1534 if crate::document::jsonschema::looks_like_json_prefix(&bytes) {
1535 return Ok(Self::JsonSchema);
1536 }
1537 if crate::document::openapi::looks_like_json_prefix(&bytes) {
1538 return Ok(Self::Openapi);
1539 }
1540 if crate::geospatial::topojson::looks_like_topojson_prefix(&bytes) {
1541 return Ok(Self::TopoJson);
1542 }
1543 if crate::geospatial::geojson_seq::looks_like_rfc8142_prefix(&bytes) {
1544 return Ok(Self::GeoJsonSeq);
1545 }
1546 if crate::document::json_seq::looks_like_prefix(&bytes) {
1547 return Ok(Self::JsonSeq);
1548 }
1549 if crate::document::jsonld::looks_like_prefix(&bytes) {
1550 return Ok(Self::JsonLd);
1551 }
1552 if looks_like_geojson(&bytes) {
1553 return Ok(Self::GeoJson);
1554 }
1555 if looks_like_chart_json_text(&text) {
1556 return Ok(Self::Chart);
1557 }
1558 if looks_like_excalidraw_json_text(&text) {
1559 return Ok(Self::Excalidraw);
1560 }
1561 return Ok(Self::Json);
1562 }
1563 "jsonld" | "json-ld" => return Ok(Self::JsonLd),
1564 "iiif" => return Ok(Self::Iiif),
1565 "graphml" => return Ok(Self::Graphml),
1566 "gexf" => return Ok(Self::Gexf),
1567 "xgmml" => return Ok(Self::Xgmml),
1568 _ => {}
1569 }
1570 }
1571
1572 Self::detect_from_content(path)
1574 }
1575
1576 fn detect_from_content(path: &Path) -> Result<Self> {
1577 if crate::document::hwpx::looks_like_hwpx_archive(path) {
1578 return Ok(Self::Hwpx);
1579 }
1580 if crate::document::bcf::looks_like_archive(path) {
1581 return Ok(Self::Bcfzip);
1582 }
1583 if crate::document::aasx::looks_like_archive(path) {
1584 return Ok(Self::Aasx);
1585 }
1586 if crate::document::idml::looks_like_archive(path) {
1587 return Ok(Self::Idml);
1588 }
1589 if crate::document::iwork::looks_like_archive(path) {
1590 return Ok(Self::Iwork);
1591 }
1592 if crate::document::threedxml::looks_like_archive(path) {
1593 return Ok(Self::ThreeDXml);
1594 }
1595 if crate::document::omex::looks_like_archive(path) {
1596 return Ok(Self::Omex);
1597 }
1598 if crate::document::xmind::looks_like_xmind_archive(path) {
1599 return Ok(Self::Xmind);
1600 }
1601 if crate::document::wacz::looks_like_archive(path) {
1602 return Ok(Self::Wacz);
1603 }
1604 let buf = read_format_prefix(path)?;
1605
1606 if crate::document::hdf5::looks_like_file(path) {
1607 return Ok(Self::Hdf5);
1608 }
1609 if crate::document::dwg::looks_like_prefix(&buf) {
1610 return Ok(Self::Dwg);
1611 }
1612 if crate::document::rhino3dm::looks_like_prefix(&buf) {
1613 return Ok(Self::Rhino3dm);
1614 }
1615 if crate::document::jt::looks_like_prefix(&buf) {
1616 return Ok(Self::Jt);
1617 }
1618 if crate::document::access::looks_like_prefix(&buf) {
1619 return Ok(Self::Access);
1620 }
1621 if crate::document::flatopc::looks_like_prefix(&buf) {
1622 return Ok(Self::FlatOpc);
1623 }
1624 if crate::document::ipc2581::looks_like_prefix(&buf) {
1625 return Ok(Self::Ipc2581);
1626 }
1627 if crate::document::openscad::looks_like_prefix(&buf) {
1628 return Ok(Self::OpenScad);
1629 }
1630 if crate::cad::amf::looks_like_prefix(&buf) {
1631 return Ok(Self::Amf);
1632 }
1633 if crate::document::plmxml::looks_like_prefix(&buf) {
1634 return Ok(Self::PlmXml);
1635 }
1636 if crate::document::stepxml::looks_like_prefix(&buf) {
1637 return Ok(Self::StepXml);
1638 }
1639 if crate::document::qif::looks_like_prefix(&buf) {
1640 return Ok(Self::Qif);
1641 }
1642 if crate::document::b2mml::looks_like_prefix(&buf) {
1643 return Ok(Self::B2mml);
1644 }
1645 if crate::document::jdf::looks_like_prefix(&buf) {
1646 return Ok(if crate::document::jdf::looks_like_xjdf_prefix(&buf) {
1647 Self::Xjdf
1648 } else {
1649 Self::Jdf
1650 });
1651 }
1652 if crate::document::fhir_xml::looks_like_prefix(&buf) {
1653 return Ok(Self::FhirXml);
1654 }
1655 if crate::document::tmx::looks_like_prefix(&buf) {
1656 return Ok(Self::Tmx);
1657 }
1658 if crate::document::tbx::looks_like_prefix(&buf) {
1659 return Ok(Self::Tbx);
1660 }
1661 if crate::document::gbxml::looks_like_prefix(&buf) {
1662 return Ok(Self::GbXml);
1663 }
1664 if crate::document::oaipmh::looks_like_prefix(&buf) {
1665 return Ok(Self::OaiPmh);
1666 }
1667 if crate::document::onix::looks_like_prefix(&buf) {
1668 return Ok(Self::Onix);
1669 }
1670 if crate::document::xpdl::looks_like_prefix(&buf) {
1671 return Ok(Self::Xpdl);
1672 }
1673 if crate::document::cda::looks_like_prefix(&buf) {
1674 return Ok(Self::Cda);
1675 }
1676 if crate::document::iso20022::looks_like_prefix(&buf) {
1677 return Ok(Self::Iso20022);
1678 }
1679 if crate::document::sbml::looks_like_prefix(&buf) {
1680 return Ok(Self::Sbml);
1681 }
1682 if crate::document::cellml::looks_like_prefix(&buf) {
1683 return Ok(Self::Cellml);
1684 }
1685 if crate::document::ocel_xml::looks_like_prefix(&buf) {
1686 return Ok(Self::OcelXml);
1687 }
1688 if crate::document::energyplus::looks_like_idf_prefix(&buf) {
1689 return Ok(Self::EnergyPlusIdf);
1690 }
1691 if crate::document::energyplus::looks_like_epw_prefix(&buf) {
1692 return Ok(Self::EnergyPlusEpw);
1693 }
1694 if crate::document::rinex::looks_like_prefix(&buf) {
1695 return Ok(Self::Rinex);
1696 }
1697 if crate::cad::sat::looks_like_prefix(&buf) {
1698 return Ok(Self::Sat);
1699 }
1700 if crate::document::sedml::looks_like_prefix(&buf) {
1701 return Ok(Self::Sedml);
1702 }
1703 if crate::document::sbgnml::looks_like_prefix(&buf) {
1704 return Ok(Self::Sbgnml);
1705 }
1706 if crate::document::xdmf::looks_like_prefix(&buf) {
1707 return Ok(Self::Xdmf);
1708 }
1709 if crate::document::pvd::looks_like_prefix(&buf) {
1710 return Ok(Self::Pvd);
1711 }
1712 if crate::document::abiword::looks_like_prefix(&buf) {
1713 return Ok(Self::Abiword);
1714 }
1715 if crate::document::neuroml::looks_like_prefix(&buf) {
1716 return Ok(Self::Neuroml);
1717 }
1718 if crate::document::biopax::looks_like_prefix(&buf) {
1719 return Ok(Self::Biopax);
1720 }
1721 if crate::document::xsd::looks_like_prefix(&buf) {
1722 return Ok(Self::Xsd);
1723 }
1724 if crate::document::xslt::looks_like_prefix(&buf) {
1725 return Ok(Self::Xslt);
1726 }
1727 if crate::document::xslfo::looks_like_prefix(&buf) {
1728 return Ok(Self::XslFo);
1729 }
1730 if crate::document::xproc::looks_like_prefix(&buf) {
1731 return Ok(Self::Xproc);
1732 }
1733 if crate::document::wadl::looks_like_prefix(&buf) {
1734 return Ok(Self::Wadl);
1735 }
1736 if crate::document::opensearch::looks_like_prefix(&buf) {
1737 return Ok(Self::OpenSearch);
1738 }
1739 if crate::document::saml::looks_like_prefix(&buf) {
1740 return Ok(Self::Saml);
1741 }
1742 if crate::document::xacml::looks_like_prefix(&buf) {
1743 return Ok(Self::Xacml);
1744 }
1745 if crate::document::fds::looks_like_prefix(&buf) {
1746 return Ok(Self::Fds);
1747 }
1748
1749 if crate::document::plist::looks_like_prefix(&buf) {
1750 return Ok(Self::Plist);
1751 }
1752 if crate::document::premis::looks_like_prefix(&buf) {
1753 return Ok(Self::Premis);
1754 }
1755 if crate::cad::openfoam_field::looks_like_prefix(&buf) {
1756 return Ok(Self::OpenFoamField);
1757 }
1758 if crate::document::nifti::looks_like_prefix(&buf) {
1759 return Ok(Self::Nifti);
1760 }
1761 if crate::document::fits::looks_like_prefix(&buf) {
1762 return Ok(Self::Fits);
1763 }
1764 if crate::document::gff::looks_like_gff3_prefix(&buf) {
1765 return Ok(Self::Gff3);
1766 }
1767 if crate::document::gff::looks_like_gtf_prefix(&buf) {
1768 return Ok(Self::Gtf);
1769 }
1770 if crate::document::sam::looks_like_prefix(&buf) {
1771 return Ok(Self::Sam);
1772 }
1773 if crate::document::maf::looks_like_prefix(&buf) {
1774 return Ok(Self::Maf);
1775 }
1776 if crate::document::newick::looks_like_prefix(&buf) {
1777 return Ok(Self::Newick);
1778 }
1779 if crate::document::stockholm::looks_like_prefix(&buf) {
1780 return Ok(Self::Stockholm);
1781 }
1782 if crate::document::clustal::looks_like_prefix(&buf) {
1783 return Ok(Self::Clustal);
1784 }
1785 if crate::document::nexus::looks_like_prefix(&buf) {
1786 return Ok(Self::Nexus);
1787 }
1788 if crate::document::genbank::looks_like_prefix(&buf) {
1789 return Ok(Self::Genbank);
1790 }
1791 if crate::document::uniprot::looks_like_prefix(&buf) {
1792 return Ok(Self::Uniprot);
1793 }
1794 if crate::document::ris::looks_like_prefix(&buf) {
1795 return Ok(Self::Ris);
1796 }
1797 if crate::cad::spice::looks_like_prefix(&buf) {
1798 return Ok(Self::Spice);
1799 }
1800 if crate::document::embl::looks_like_prefix(&buf) {
1801 return Ok(Self::Embl);
1802 }
1803 if crate::document::mrc::looks_like_prefix(&buf) {
1804 return Ok(Self::Mrc);
1805 }
1806 if crate::document::netcdf::looks_like_prefix(&buf) {
1807 return Ok(Self::Netcdf);
1808 }
1809 if crate::geospatial::geopackage::looks_like_geopackage_prefix(&buf) {
1810 return Ok(Self::Geopackage);
1811 }
1812 if crate::document::sqlite::looks_like_prefix(&buf) {
1813 return Ok(Self::Sqlite);
1814 }
1815 if crate::document::sylk::looks_like_prefix(&buf) {
1816 return Ok(Self::Sylk);
1817 }
1818 if crate::document::dif::looks_like_prefix(&buf) {
1819 return Ok(Self::Dif);
1820 }
1821 if crate::document::fastx::looks_like_fastq_prefix(&buf) {
1822 return Ok(Self::Fastq);
1823 }
1824 if crate::document::fastx::looks_like_fasta_prefix(&buf) {
1825 return Ok(Self::Fasta);
1826 }
1827 if crate::document::vcf::looks_like_prefix(&buf) {
1828 return Ok(Self::Vcf);
1829 }
1830 if crate::document::wig::looks_like_prefix(&buf) {
1831 return Ok(Self::Wig);
1832 }
1833 if crate::document::cif::looks_like_prefix(&buf) {
1834 return Ok(Self::Cif);
1835 }
1836 if crate::document::cml::looks_like_prefix(&buf) {
1837 return Ok(Self::Cml);
1838 }
1839 if crate::document::mol2::looks_like_prefix(&buf) {
1840 return Ok(Self::Mol2);
1841 }
1842 if crate::document::turtle::looks_like_prefix(&buf) {
1843 return Ok(Self::Turtle);
1844 }
1845 if crate::document::eps::looks_like_prefix(&buf) {
1846 return Ok(Self::Eps);
1847 }
1848
1849 if crate::document::molfile::looks_like_rxn_prefix(&buf) {
1850 return Ok(Self::Rxn);
1851 }
1852
1853 if let Some(format) = crate::document::molfile::looks_like_prefix(&buf) {
1854 return Ok(format);
1855 }
1856
1857 if crate::cad::collada::looks_like_prefix(&buf) {
1858 return Ok(Self::Collada);
1859 }
1860 if crate::cad::x3d::looks_like_prefix(&buf) {
1861 return Ok(Self::X3d);
1862 }
1863 if crate::cad::vrml::looks_like_prefix(&buf) {
1864 return Ok(Self::Vrml);
1865 }
1866
1867 if crate::geospatial::topojson::looks_like_topojson_prefix(&buf) {
1868 return Ok(Self::TopoJson);
1869 }
1870 if crate::geospatial::geojson_seq::looks_like_rfc8142_prefix(&buf) {
1871 return Ok(Self::GeoJsonSeq);
1872 }
1873
1874 if crate::document::json_seq::looks_like_prefix(&buf) {
1875 return Ok(Self::JsonSeq);
1876 }
1877 if crate::document::har::looks_like_prefix(&buf) {
1878 return Ok(Self::Har);
1879 }
1880 if crate::document::warc::looks_like_prefix(&buf)
1881 || (buf.starts_with(b"\x1f\x8b") && crate::document::warc::looks_like_gzip_file(path))
1882 {
1883 return Ok(Self::Warc);
1884 }
1885 if crate::document::postman::looks_like_prefix(&buf) {
1886 return Ok(Self::Postman);
1887 }
1888 if crate::document::compose::looks_like_json_prefix(&buf) {
1889 return Ok(Self::Compose);
1890 }
1891 if crate::document::sarif::looks_like_prefix(&buf) {
1892 return Ok(Self::Sarif);
1893 }
1894 if crate::document::terraform::looks_like_prefix(&buf) {
1895 return Ok(Self::TerraformPlan);
1896 }
1897 if crate::document::cyclonedx::looks_like_json_prefix(&buf) {
1898 return Ok(Self::CycloneDx);
1899 }
1900 if crate::document::spdx::looks_like_prefix(&buf) {
1901 return Ok(Self::Spdx);
1902 }
1903 if crate::document::stix::looks_like_prefix(&buf) {
1904 return Ok(Self::StixJson);
1905 }
1906 if crate::document::taxii::looks_like_prefix(&buf) {
1907 return Ok(Self::TaxiiJson);
1908 }
1909 if crate::document::spdx::looks_like_tag_prefix(&buf) {
1910 return Ok(Self::Spdx);
1911 }
1912 if crate::document::protobuf::looks_like_prefix(&buf) {
1913 return Ok(Self::Protobuf);
1914 }
1915 if crate::document::graphql::looks_like_prefix(&buf) {
1916 return Ok(Self::Graphql);
1917 }
1918 if crate::document::openapi::looks_like_json_prefix(&buf) {
1919 return Ok(Self::Openapi);
1920 }
1921 if crate::document::asyncapi::looks_like_json_prefix(&buf) {
1922 return Ok(Self::Asyncapi);
1923 }
1924 if crate::document::jsonschema::looks_like_json_prefix(&buf) {
1925 return Ok(Self::JsonSchema);
1926 }
1927 if crate::document::arff::looks_like_prefix(&buf) {
1928 return Ok(Self::Arff);
1929 }
1930 if crate::document::bed::looks_like_bedgraph_prefix(&buf) {
1931 return Ok(Self::BedGraph);
1932 }
1933 if crate::document::bed::looks_like_bed_prefix(&buf) {
1934 return Ok(Self::Bed);
1935 }
1936 if crate::document::jsonld::looks_like_prefix(&buf) {
1937 return Ok(Self::JsonLd);
1938 }
1939 if crate::document::graphml::looks_like_prefix(&buf) {
1940 return Ok(Self::Graphml);
1941 }
1942 if crate::document::gexf::looks_like_prefix(&buf) {
1943 return Ok(Self::Gexf);
1944 }
1945 if crate::document::xgmml::looks_like_prefix(&buf) {
1946 return Ok(Self::Xgmml);
1947 }
1948
1949 if crate::document::msproject::looks_like_project_xml_prefix(&buf) {
1950 return Ok(Self::ProjectXml);
1951 }
1952
1953 if crate::document::bpmn::looks_like_prefix(&buf) {
1954 return Ok(Self::Bpmn);
1955 }
1956 if crate::document::dmn::looks_like_prefix(&buf) {
1957 return Ok(Self::Dmn);
1958 }
1959 if crate::document::cmmn::looks_like_prefix(&buf) {
1960 return Ok(Self::Cmmn);
1961 }
1962 if crate::document::reqif::looks_like_prefix(&buf) {
1963 return Ok(Self::Reqif);
1964 }
1965 if crate::document::xmi::looks_like_prefix(&buf) {
1966 return Ok(Self::Xmi);
1967 }
1968
1969 if crate::cad::ifc::looks_like_ifcxml_prefix(&buf) {
1970 return Ok(Self::IfcXml);
1971 }
1972
1973 if looks_like_geojson(&buf) {
1974 return Ok(Self::GeoJson);
1975 }
1976
1977 if buf.starts_with(b"\x1f\x8b")
1978 && let Ok(file) = std::fs::File::open(path)
1979 {
1980 let mut decoder = flate2::read::MultiGzDecoder::new(file);
1981 let mut prefix = Vec::with_capacity(FORMAT_SNIFF_BYTES as usize);
1982 if std::io::Read::take(&mut decoder, FORMAT_SNIFF_BYTES)
1983 .read_to_end(&mut prefix)
1984 .is_ok()
1985 {
1986 let text = String::from_utf8_lossy(&prefix);
1987 let trimmed = text.trim_start();
1988 if trimmed.starts_with("<svg")
1989 || (trimmed.starts_with("<?xml") && trimmed.contains("<svg"))
1990 {
1991 return Ok(Self::Svg);
1992 }
1993 }
1994 }
1995
1996 if buf.starts_with(b"%PDF-") {
1997 return Ok(Self::Pdf);
1998 }
1999 if crate::document::fdf::looks_like_prefix(&buf) {
2000 return Ok(Self::Fdf);
2001 }
2002 if crate::document::xfdf::looks_like_prefix(&buf) {
2003 return Ok(Self::Xfdf);
2004 }
2005 if crate::document::landxml::looks_like_prefix(&buf) {
2006 return Ok(Self::LandXml);
2007 }
2008 if crate::document::mathml::looks_like_prefix(&buf) {
2009 return Ok(Self::Mathml);
2010 }
2011 if crate::document::xmp::looks_like_prefix(&buf) {
2012 return Ok(Self::Xmp);
2013 }
2014 if crate::document::xdp::looks_like_prefix(&buf) {
2015 return Ok(Self::Xdp);
2016 }
2017 if crate::document::spreadsheetml::looks_like_prefix(&buf) {
2018 return Ok(Self::Spreadsheetml);
2019 }
2020 if buf.starts_with(b"glTF") {
2021 return Ok(Self::Gltf);
2022 }
2023 if buf.starts_with(b"LASF") {
2024 return Ok(Self::Las);
2025 }
2026 if crate::document::dicom::looks_like_dicomdir_prefix(&buf) {
2027 return Ok(Self::DicomDir);
2028 }
2029 if crate::document::dicom::looks_like_dicom_sr_prefix(&buf) {
2030 return Ok(Self::DicomSr);
2031 }
2032 if crate::document::dicom::looks_like_dicom_prefix(&buf) {
2033 return Ok(Self::Dicom);
2034 }
2035 if crate::geospatial::shapefile::looks_like_shapefile_prefix(&buf) {
2036 return Ok(Self::Shapefile);
2037 }
2038 if crate::geospatial::dbase::looks_like_prefix(&buf) {
2039 return Ok(Self::Dbf);
2040 }
2041 if crate::geospatial::ascii_grid::looks_like_prefix(&buf) {
2042 return Ok(Self::EsriAsciiGrid);
2043 }
2044 if crate::cad::ltspice::looks_like_prefix(&buf) {
2045 return Ok(Self::LtspiceAsc);
2046 }
2047 if crate::cad::simulation::looks_like_cdb_prefix(&buf) {
2048 return Ok(Self::Cdb);
2049 }
2050 if crate::cad::simulation::looks_like_su2_prefix(&buf) {
2051 return Ok(Self::Su2);
2052 }
2053 if crate::cad::simulation::looks_like_tecplot_prefix(&buf) {
2054 return Ok(Self::Tecplot);
2055 }
2056 if crate::cad::simulation::looks_like_ensight_prefix(&buf) {
2057 return Ok(Self::Ensight);
2058 }
2059 if crate::cad::simulation::looks_like_plot3d_prefix(&buf) {
2060 return Ok(Self::Plot3d);
2061 }
2062 if buf.starts_with(crate::cad::dxf::binary::BINARY_DXF_SENTINEL) {
2063 return Ok(Self::Dxf);
2064 }
2065 if buf.starts_with(b"II*\0")
2066 || buf.starts_with(b"MM\0*")
2067 || buf.starts_with(b"II+\0\x08\0\0\0")
2068 || buf.starts_with(b"MM\0+\0\x08\0\0")
2069 {
2070 return Ok(Self::Tiff);
2071 }
2072 if crate::jpeg2000::looks_like_prefix(&buf) {
2073 return Ok(Self::Jpeg2000);
2074 }
2075 if crate::vectorize::is_pnm(&buf) {
2076 return Ok(Self::Raster);
2077 }
2078 if buf.starts_with(b"\x89PNG\r\n\x1a\n")
2079 || buf.starts_with(b"\xff\xd8\xff")
2080 || buf.starts_with(b"BM")
2081 || crate::vectorize::is_gif(&buf)
2082 || crate::vectorize::is_webp(&buf)
2083 {
2084 return Ok(Self::Raster);
2085 }
2086 if buf.starts_with(b"PK\x03\x04")
2087 && let Ok(mut archive) =
2088 zip::ZipArchive::new(std::io::BufReader::new(std::fs::File::open(path)?))
2089 {
2090 if archive.len() > crate::ooxml::MAX_ZIP_PACKAGE_ENTRIES {
2091 return Err(Error::LimitExceeded(format!(
2092 "ZIP package contains {} entries; maximum is {}",
2093 archive.len(),
2094 crate::ooxml::MAX_ZIP_PACKAGE_ENTRIES
2095 )));
2096 }
2097 let mut has_word = false;
2098 let mut has_ppt = false;
2099 let mut has_xl = false;
2100 let mut has_xlsb = false;
2101 let mut has_visio = false;
2102 let mut has_fb2 = false;
2103 let mut has_3d = false;
2104 let mut has_epub = false;
2105 let mut has_kml = false;
2106 let mut has_ifczip = false;
2107 let has_xps = if let Ok(mut relationships) = archive.by_name("_rels/.rels") {
2108 let mut bytes = Vec::new();
2109 std::io::Read::take(&mut relationships, 1024 * 1024 + 1)
2110 .read_to_end(&mut bytes)
2111 .is_ok()
2112 && bytes.len() <= 1024 * 1024
2113 && String::from_utf8_lossy(&bytes)
2114 .to_ascii_lowercase()
2115 .contains("fixedrepresentation")
2116 } else {
2117 false
2118 };
2119 if has_xps {
2120 return Ok(Self::Xps);
2121 }
2122 for i in 0..archive.len().min(10_000) {
2123 if let Ok(entry) = archive.by_index(i) {
2124 let name = entry.name();
2125 if name == "word/document.xml" {
2126 has_word = true;
2127 } else if name == "ppt/presentation.xml" {
2128 has_ppt = true;
2129 } else if name == "xl/workbook.xml" {
2130 has_xl = true;
2131 } else if name == "xl/workbook.bin" {
2132 has_xlsb = true;
2133 } else if name == "visio/document.xml" {
2134 has_visio = true;
2135 } else if !entry.is_dir() && name.to_ascii_lowercase().ends_with(".fb2") {
2136 has_fb2 = true;
2137 } else if name.starts_with("3D/") && name.ends_with(".model") {
2138 has_3d = true;
2139 } else if name == "META-INF/container.xml" {
2140 has_epub = true;
2141 } else if !entry.is_dir() && name.to_ascii_lowercase().ends_with(".kml") {
2142 has_kml = true;
2143 } else if !entry.is_dir()
2144 && !name.contains('/')
2145 && !name.contains('\\')
2146 && name.to_ascii_lowercase().ends_with(".ifc")
2147 {
2148 has_ifczip = true;
2149 }
2150 }
2151 }
2152 if has_word {
2153 return Ok(Self::Docx);
2154 }
2155 if has_ppt {
2156 return Ok(Self::Pptx);
2157 }
2158 if has_xl {
2159 return Ok(Self::Xlsx);
2160 }
2161 if has_xlsb {
2162 return Ok(Self::Xlsb);
2163 }
2164 if has_visio {
2165 return Ok(Self::Visio);
2166 }
2167 if has_fb2 {
2168 return Ok(Self::Fb2);
2169 }
2170 if has_3d {
2171 return Ok(Self::ThreeMf);
2172 }
2173 if has_epub {
2174 return Ok(Self::Epub);
2175 }
2176 if has_kml {
2177 return Ok(Self::Kmz);
2178 }
2179 if has_ifczip {
2180 return Ok(Self::IfcZip);
2181 }
2182 }
2183 if crate::cad::ifc::looks_like_ifc_prefix(&buf) {
2184 return Ok(Self::Ifc);
2185 }
2186 if buf.starts_with(b"ISO-10303-21;") || buf.windows(13).any(|w| w == b"ISO-10303-21;") {
2187 return Ok(Self::Step);
2188 }
2189 if crate::cad::kicad::looks_like_prefix(&buf) {
2190 return Ok(Self::KicadPcb);
2191 }
2192 if crate::cad::kicad_sch_modern::looks_like_prefix(&buf) {
2193 return Ok(Self::KicadSch);
2194 }
2195 if crate::cad::kicad_sch::looks_like_prefix(&buf) {
2196 return Ok(Self::KicadSchLegacy);
2197 }
2198 if crate::cad::eagle::looks_like_prefix(&buf) {
2199 return Ok(Self::EagleSch);
2200 }
2201 if buf.starts_with(b"0\nSECTION")
2202 || buf.starts_with(b" 0\r\nSECTION")
2203 || buf.starts_with(b" 0\nSECTION")
2204 || buf.starts_with(b"0\r\nSECTION")
2205 {
2206 return Ok(Self::Dxf);
2207 }
2208 if buf.starts_with(b"%FSLA") || buf.starts_with(b"%MO") || buf.starts_with(b"G04") {
2209 return Ok(Self::Gerber);
2210 }
2211 if buf.starts_with(b"IN;")
2212 || buf.starts_with(b"SP")
2213 || buf.starts_with(b"PU")
2214 || buf.starts_with(b"PD")
2215 {
2216 return Ok(Self::Hpgl);
2217 }
2218 if buf.starts_with(b"M48") || buf.starts_with(b"T01") || buf.starts_with(b"FMAT,2") {
2219 return Ok(Self::Excellon);
2220 }
2221 if buf.starts_with(b"G0 ")
2222 || buf.starts_with(b"G1 ")
2223 || buf.starts_with(b"G21")
2224 || buf.starts_with(b"G90")
2225 || buf.starts_with(b"(Generated")
2226 || buf.starts_with(b";Generated")
2227 {
2228 return Ok(Self::Gcode);
2229 }
2230 if buf.starts_with(b"solid ") {
2231 return Ok(Self::Stl);
2232 }
2233 if buf.starts_with(b"ply\n") || buf.starts_with(b"ply\r\n") {
2234 return Ok(Self::Ply);
2235 }
2236 if crate::cad::pcd::looks_like_pcd_prefix(&buf) {
2237 return Ok(Self::Pcd);
2238 }
2239 if crate::cad::ptx::looks_like_prefix(&buf) {
2240 return Ok(Self::Ptx);
2241 }
2242 if crate::cad::pts::looks_like_prefix(&buf) {
2243 return Ok(Self::Pts);
2244 }
2245 if crate::cad::e57::looks_like_prefix(&buf) {
2246 return Ok(Self::E57);
2247 }
2248 if crate::cad::xyz::looks_like_prefix(&buf) {
2249 return Ok(Self::Xyz);
2250 }
2251 if buf.starts_with(b"$MeshFormat")
2252 || buf.starts_with(b"# vtk DataFile Version")
2253 || buf.starts_with(b"<VTKFile")
2254 {
2255 return Ok(Self::Simulation);
2256 }
2257 if crate::cad::simulation::looks_like_unv_prefix(&buf) {
2258 return Ok(Self::Unv);
2259 }
2260 if crate::document::op2::looks_like_prefix(&buf) {
2261 return Ok(Self::Op2);
2262 }
2263 if buf
2264 .windows(8)
2265 .any(|w| w == b"S 1" || w == b"S0000001" || w == b"S 1")
2266 {
2267 return Ok(Self::Iges);
2268 }
2269 if buf.len() >= 44 && buf[0..4] == [0x01, 0x00, 0x00, 0x00] && &buf[40..44] == b" EMF" {
2271 return Ok(Self::Emf);
2272 }
2273 if (buf.len() >= 4 && buf[0..4] == [0xD7, 0xCD, 0xC6, 0x9A])
2275 || (buf.len() >= 10
2276 && (buf[0..4] == [0x01, 0x00, 0x09, 0x00] || buf[0..4] == [0x02, 0x00, 0x09, 0x00])
2277 && (buf[6..10] == [0x00, 0x03, 0x00, 0x00]
2278 || buf[6..10] == [0x00, 0x01, 0x00, 0x00]))
2279 {
2280 return Ok(Self::Emf);
2281 }
2282
2283 let text = String::from_utf8_lossy(&buf);
2285 let trimmed = text.trim();
2286 if crate::cad::opencrg::looks_like_prefix(&buf) {
2287 return Ok(Self::OpenCrg);
2288 }
2289 if looks_like_legacy_visio_xml(trimmed) {
2290 return Ok(Self::Visio);
2291 }
2292 if crate::document::ttml::looks_like_ttml_prefix(&buf) {
2293 return Ok(Self::Ttml);
2294 }
2295 if crate::document::xliff::looks_like_xliff_prefix(&buf) {
2296 return Ok(Self::Xliff);
2297 }
2298 if looks_like_vcalendar(&buf) {
2299 return Ok(Self::Vcalendar);
2300 }
2301 if crate::geospatial::kml::looks_like_kml_prefix(&buf) {
2302 return Ok(Self::Kml);
2303 }
2304 if crate::geospatial::gpx::looks_like_gpx_prefix(&buf) {
2305 return Ok(Self::Gpx);
2306 }
2307 if crate::geospatial::gml::looks_like_gml_prefix(&buf) {
2308 return Ok(Self::Gml);
2309 }
2310 if crate::document::gml_graph::looks_like_prefix(&buf) {
2311 return Ok(Self::GraphGml);
2312 }
2313 if crate::geospatial::georss::looks_like_georss_prefix(&buf) {
2314 return Ok(Self::GeoRss);
2315 }
2316 if crate::document::rdfxml::looks_like_prefix(&buf) {
2317 return Ok(Self::RdfXml);
2318 }
2319 if crate::document::feed::looks_like_prefix(&buf) {
2320 return Ok(Self::Feed);
2321 }
2322 if crate::geospatial::wkt::looks_like_wkt_prefix(&buf) {
2323 return Ok(Self::Wkt);
2324 }
2325 if looks_like_ical(&buf) {
2326 return Ok(Self::Ical);
2327 }
2328 if crate::document::vcard::looks_like_vcard_prefix(&buf) {
2329 return Ok(Self::Vcard);
2330 }
2331 if crate::document::fb2::looks_like_prefix(&buf) {
2332 return Ok(Self::Fb2);
2333 }
2334 if crate::document::jats::looks_like_prefix(&buf) {
2335 return Ok(Self::Jats);
2336 }
2337 if crate::document::tei::looks_like_prefix(&buf) {
2338 return Ok(Self::Tei);
2339 }
2340 if crate::document::ead::looks_like_prefix(&buf) {
2341 return Ok(Self::Ead);
2342 }
2343 if crate::document::eac_cpf::looks_like_prefix(&buf) {
2344 return Ok(Self::EacCpf);
2345 }
2346 if crate::document::dc::looks_like_prefix(&buf) {
2347 return Ok(Self::DublinCore);
2348 }
2349 if crate::document::iso19115::looks_like_prefix(&buf) {
2350 return Ok(Self::Iso19115);
2351 }
2352 if crate::document::ubl::looks_like_prefix(&buf) {
2353 return Ok(Self::Ubl);
2354 }
2355 if crate::document::xbrl::looks_like_prefix(&buf) {
2356 return Ok(Self::Xbrl);
2357 }
2358 if crate::document::s1000d::looks_like_prefix(&buf) {
2359 return Ok(Self::S1000d);
2360 }
2361 if crate::document::alto::looks_like_prefix(&buf) {
2362 return Ok(Self::Alto);
2363 }
2364 if crate::document::mets::looks_like_prefix(&buf) {
2365 return Ok(Self::Mets);
2366 }
2367 if crate::document::marcxml::looks_like_prefix(&buf) {
2368 return Ok(Self::Marcxml);
2369 }
2370 if crate::document::marc::looks_like_prefix(&buf) {
2371 return Ok(Self::Marc);
2372 }
2373 if crate::document::mods::looks_like_prefix(&buf) {
2374 return Ok(Self::Mods);
2375 }
2376 if crate::document::docbook::looks_like_prefix(&buf) {
2377 return Ok(Self::Docbook);
2378 }
2379 if crate::document::dita::looks_like_prefix(&buf) {
2380 return Ok(Self::Dita);
2381 }
2382 if crate::document::pdb::looks_like_prefix(&buf) {
2383 return Ok(Self::Pdb);
2384 }
2385 if crate::document::mobi::looks_like_prefix(&buf) {
2386 return Ok(Self::Mobi);
2387 }
2388 if crate::document::legacy_visio::looks_like_legacy_visio(path) {
2389 return Ok(Self::Vsd);
2390 }
2391 if crate::document::legacy_doc::looks_like_legacy_doc(path) {
2392 return Ok(Self::Doc);
2393 }
2394 if crate::document::legacy_ppt::looks_like_legacy_ppt(path) {
2395 return Ok(Self::Ppt);
2396 }
2397 if crate::document::msg::looks_like_msg_file(path) {
2398 return Ok(Self::Msg);
2399 }
2400 if crate::document::mbox::looks_like_mbox_prefix(&buf) {
2401 return Ok(Self::Mbox);
2402 }
2403 if looks_like_mhtml(&buf) {
2404 return Ok(Self::Mhtml);
2405 }
2406 if crate::document::emlx::looks_like_emlx_prefix(&buf) {
2407 return Ok(Self::Emlx);
2408 }
2409 if looks_like_eml(&buf) {
2410 return Ok(Self::Eml);
2411 }
2412 if looks_like_jupyter_notebook(&buf) {
2413 return Ok(Self::Jupyter);
2414 }
2415 if looks_like_medit_mesh(&buf) {
2416 return Ok(Self::Medit);
2417 }
2418 if looks_like_medit_binary(&buf) {
2419 return Ok(Self::Medit);
2420 }
2421 if looks_like_off_mesh(&buf) {
2422 return Ok(Self::Off);
2423 }
2424 if looks_like_lsdyna_keyword(&buf) {
2425 return Ok(Self::LsDyna);
2426 }
2427 if crate::document::rst::looks_like_rst_prefix(&buf) {
2428 return Ok(Self::Rst);
2429 }
2430 if crate::document::orgmode::looks_like_org_prefix(&buf) {
2431 return Ok(Self::Org);
2432 }
2433 if crate::document::po::looks_like_po_prefix(&buf) {
2434 return Ok(Self::Po);
2435 }
2436 if crate::document::bib::looks_like_bibtex_prefix(&buf) {
2437 return Ok(Self::Bib);
2438 }
2439 if let Some(kind) = crate::document::subtitle::looks_like_subtitle_prefix(&buf) {
2440 return Ok(match kind {
2441 crate::document::subtitle::SubtitleKind::Srt => Self::Srt,
2442 crate::document::subtitle::SubtitleKind::Vtt => Self::Vtt,
2443 });
2444 }
2445 if looks_like_nastran_bdf(&buf) {
2446 return Ok(Self::Nastran);
2447 }
2448 if looks_like_abaqus_inp(&buf) {
2449 return Ok(Self::Abaqus);
2450 }
2451 if trimmed.contains("<mxfile")
2452 || trimmed.contains("<mxGraphModel")
2453 || trimmed.contains("<diagram")
2454 {
2455 return Ok(Self::Drawio);
2456 }
2457 if crate::geospatial::xml_tree::looks_like_root(&buf, b"mxlibrary", None)
2458 || crate::geospatial::xml_tree::looks_like_root(&buf, b"shapes", None)
2459 {
2460 return Ok(Self::Drawio);
2461 }
2462 if (trimmed.starts_with("<?xml") && (trimmed.contains("<svg") || trimmed.contains("<SVG")))
2463 || trimmed.starts_with("<svg")
2464 || trimmed.starts_with("<SVG")
2465 {
2466 return Ok(Self::Svg);
2467 }
2468 if trimmed.contains("<!DOCTYPE html") || trimmed.contains("<html") {
2469 return Ok(Self::Html);
2470 }
2471 if crate::document::xml::looks_like_xml_prefix(&buf) {
2472 if crate::cad::opendrive::looks_like_prefix(&buf) {
2473 return Ok(Self::OpenDrive);
2474 }
2475 if crate::cad::openscenario::looks_like_prefix(&buf) {
2476 return Ok(Self::OpenScenario);
2477 }
2478 if crate::document::citygml::looks_like_prefix(&buf) {
2479 return Ok(Self::CityGml);
2480 }
2481 if crate::document::wsdl::looks_like_prefix(&buf) {
2482 return Ok(Self::Wsdl);
2483 }
2484 if crate::document::opml::looks_like_prefix(&buf) {
2485 return Ok(Self::Opml);
2486 }
2487 if crate::document::plist::looks_like_prefix(&buf) {
2488 return Ok(Self::Plist);
2489 }
2490 if crate::document::cyclonedx::looks_like_xml_prefix(&buf) {
2491 return Ok(Self::CycloneDx);
2492 }
2493 if crate::document::junit::looks_like_prefix(&buf) {
2494 return Ok(Self::Junit);
2495 }
2496 if crate::document::coverage::looks_like_prefix(&buf) {
2497 return Ok(Self::Coverage);
2498 }
2499 return Ok(Self::Xml);
2500 }
2501 if crate::document::kubernetes::looks_like_yaml_prefix(&buf) {
2502 return Ok(Self::Kubernetes);
2503 }
2504 if crate::document::compose::looks_like_yaml_prefix(&buf) {
2505 return Ok(Self::Compose);
2506 }
2507 if crate::document::github_actions::looks_like_yaml_prefix(&buf) {
2508 return Ok(Self::GithubActions);
2509 }
2510 if crate::document::openapi::looks_like_yaml_prefix(&buf) {
2511 return Ok(Self::Openapi);
2512 }
2513 if crate::document::asyncapi::looks_like_yaml_prefix(&buf) {
2514 return Ok(Self::Asyncapi);
2515 }
2516 if crate::document::jsonschema::looks_like_yaml_prefix(&buf) {
2517 return Ok(Self::JsonSchema);
2518 }
2519 if trimmed.contains("@startuml") {
2520 return Ok(Self::PlantUml);
2521 }
2522 if trimmed.starts_with("digraph")
2523 || trimmed.starts_with("strict digraph")
2524 || trimmed.starts_with("graph ")
2525 {
2526 return Ok(Self::Dot);
2527 }
2528 if trimmed.starts_with("sequenceDiagram")
2529 || trimmed.starts_with("flowchart")
2530 || trimmed.starts_with("graph TD")
2531 || trimmed.starts_with("graph LR")
2532 || trimmed.starts_with("classDiagram")
2533 || trimmed.starts_with("stateDiagram")
2534 || trimmed.starts_with("erDiagram")
2535 || trimmed.starts_with("gantt")
2536 || trimmed.starts_with("pie")
2537 {
2538 return Ok(Self::Mermaid);
2539 }
2540 if trimmed.starts_with("\\documentclass")
2541 || trimmed.starts_with("\\begin{equation}")
2542 || trimmed.starts_with("$$")
2543 {
2544 return Ok(Self::Tex);
2545 }
2546 if crate::document::jsonpatch::looks_like_prefix(&buf) {
2547 return Ok(Self::JsonPatch);
2548 }
2549 if crate::document::jsonfeed::looks_like_prefix(&buf) {
2550 return Ok(Self::JsonFeed);
2551 }
2552 if crate::document::iiif::looks_like_prefix(&buf) {
2553 return Ok(Self::Iiif);
2554 }
2555 if crate::document::csl_json::looks_like_prefix(&buf) {
2556 return Ok(Self::CslJson);
2557 }
2558 if crate::document::cloudevents::looks_like_prefix(&buf) {
2559 return Ok(Self::CloudEvents);
2560 }
2561 if crate::document::fhir::looks_like_prefix(&buf) {
2562 return Ok(Self::FhirJson);
2563 }
2564 if crate::document::avro::looks_like_prefix(&buf) {
2565 return Ok(Self::Avro);
2566 }
2567 if crate::document::otlp::looks_like_prefix(&buf) {
2568 return Ok(Self::OtlpJson);
2569 }
2570 if crate::document::ocel::looks_like_prefix(&buf) {
2571 return Ok(Self::OcelJson);
2572 }
2573 if crate::document::jsonapi::looks_like_prefix(&buf) {
2574 return Ok(Self::JsonApi);
2575 }
2576 if crate::document::openlabel::looks_like_prefix(&buf) {
2577 return Ok(Self::OpenLabel);
2578 }
2579 if crate::document::cityjson::looks_like_prefix(&buf) {
2580 return Ok(Self::CityJson);
2581 }
2582 if crate::document::json::looks_like_json_prefix(&buf) {
2583 if looks_like_chart_json_text(trimmed) {
2584 return Ok(Self::Chart);
2585 }
2586 if looks_like_excalidraw_json_text(trimmed) {
2587 return Ok(Self::Excalidraw);
2588 }
2589 return Ok(Self::Json);
2590 }
2591 if crate::document::lcov::looks_like_prefix(&buf) {
2592 return Ok(Self::Lcov);
2593 }
2594 if trimmed.starts_with("= ") || trimmed.starts_with("== ") {
2595 return Ok(Self::Asciidoc);
2596 }
2597 if trimmed.starts_with("# ")
2598 || trimmed.contains("\n## ")
2599 || trimmed.contains("\n| ---")
2600 || (trimmed.starts_with('|') && trimmed.contains(" | ") && trimmed.contains("\n|"))
2601 {
2602 return Ok(Self::Markdown);
2603 }
2604 if trimmed.contains("->")
2605 && (trimmed.contains(": {")
2606 || trimmed.contains("shape:")
2607 || trimmed.contains("direction:"))
2608 {
2609 return Ok(Self::D2);
2610 }
2611 if (trimmed.starts_with("v ") || trimmed.starts_with("#"))
2612 && (trimmed.contains("\nv ") || trimmed.contains("\r\nv "))
2613 && (trimmed.contains("\nf ") || trimmed.contains("\r\nf ") || trimmed.contains("\nl "))
2614 {
2615 return Ok(Self::Obj);
2616 }
2617 let sample_lines: Vec<&str> = trimmed
2618 .lines()
2619 .filter(|l| !l.trim().is_empty())
2620 .take(10)
2621 .collect();
2622 if sample_lines.len() >= 2 {
2623 let comma_count = sample_lines[0].matches(',').count();
2624 if comma_count >= 1
2625 && sample_lines
2626 .iter()
2627 .all(|l| l.matches(',').count() == comma_count)
2628 {
2629 return Ok(Self::Csv);
2630 }
2631 let tab_count = sample_lines[0].matches('\t').count();
2632 if tab_count >= 1
2633 && sample_lines
2634 .iter()
2635 .all(|l| l.matches('\t').count() == tab_count)
2636 {
2637 return Ok(Self::Csv);
2638 }
2639 }
2640
2641 let ext_info = path
2642 .extension()
2643 .and_then(|v| v.to_str())
2644 .map(|s| format!(".{s}"))
2645 .unwrap_or_else(|| "no extension".into());
2646 Err(Error::Unsupported(format!(
2647 "cannot determine format for {} ({ext_info}); specify a recognized file extension or document header",
2648 path.display()
2649 )))
2650 }
2651}
2652
2653pub(crate) fn looks_like_geojson(bytes: &[u8]) -> bool {
2654 let text = String::from_utf8_lossy(bytes);
2655 let text = text.trim_start_matches('\u{feff}').trim_start();
2656 if !text.starts_with('{') {
2657 return false;
2658 }
2659 let has_coordinates = text.contains("\"coordinates\"");
2660 let has_geometry_type = [
2661 "\"Point\"",
2662 "\"MultiPoint\"",
2663 "\"LineString\"",
2664 "\"MultiLineString\"",
2665 "\"Polygon\"",
2666 "\"MultiPolygon\"",
2667 "\"GeometryCollection\"",
2668 ]
2669 .iter()
2670 .any(|kind| text.contains(kind));
2671 (text.contains("\"FeatureCollection\"") && text.contains("\"features\""))
2672 || (text.contains("\"Feature\"") && text.contains("\"geometry\""))
2673 || (has_coordinates && has_geometry_type)
2674}
2675
2676fn looks_like_chart_json_text(text: &str) -> bool {
2677 text.contains("\"series\"")
2678 && text.contains("\"labels\"")
2679 && [
2680 "\"bar\"",
2681 "\"line\"",
2682 "\"pie\"",
2683 "\"doughnut\"",
2684 "\"area\"",
2685 "\"scatter\"",
2686 ]
2687 .iter()
2688 .any(|kind| text.contains(kind))
2689}
2690
2691fn looks_like_excalidraw_json_text(text: &str) -> bool {
2692 (text.contains("\"type\"") && text.contains("\"excalidraw\""))
2693 || (text.contains("\"elements\"")
2694 && (text.contains("\"appState\"") || text.contains("\"files\"")))
2695}
2696
2697fn read_format_prefix(path: &Path) -> Result<Vec<u8>> {
2698 use std::io::Read;
2699
2700 let mut file = std::fs::File::open(path)?;
2701 let mut bytes = Vec::with_capacity(FORMAT_SNIFF_BYTES as usize);
2702 std::io::Read::take(&mut file, FORMAT_SNIFF_BYTES).read_to_end(&mut bytes)?;
2703 Ok(bytes)
2704}
2705
2706fn looks_like_legacy_visio_xml(text: &str) -> bool {
2707 let mut remaining = text.trim_start();
2708 loop {
2709 if let Some(after_declaration) = remaining.strip_prefix("<?") {
2710 let Some((_, tail)) = after_declaration.split_once("?>") else {
2711 return false;
2712 };
2713 remaining = tail.trim_start();
2714 continue;
2715 }
2716 if let Some(after_comment) = remaining.strip_prefix("<!--") {
2717 let Some((_, tail)) = after_comment.split_once("-->") else {
2718 return false;
2719 };
2720 remaining = tail.trim_start();
2721 continue;
2722 }
2723 break;
2724 }
2725 let Some(tag) = remaining.strip_prefix('<') else {
2726 return false;
2727 };
2728 let name = tag
2729 .split(|character: char| {
2730 character.is_ascii_whitespace() || character == '/' || character == '>'
2731 })
2732 .next()
2733 .unwrap_or_default();
2734 name.rsplit(':').next() == Some("VisioDocument")
2735}
2736
2737fn looks_like_eml(bytes: &[u8]) -> bool {
2738 let mut has_from = false;
2739 let mut has_subject_or_date = false;
2740 let mut saw_header_body_separator = false;
2741 for raw_line in bytes.split(|byte| *byte == b'\n').take(64) {
2742 let line = raw_line.strip_suffix(b"\r").unwrap_or(raw_line);
2743 if line.is_empty() {
2744 saw_header_body_separator = true;
2745 break;
2746 }
2747 if line.first().is_some_and(u8::is_ascii_whitespace) {
2748 continue;
2749 }
2750 let Some(colon) = line.iter().position(|byte| *byte == b':') else {
2751 return false;
2752 };
2753 let name = &line[..colon];
2754 has_from |= name.eq_ignore_ascii_case(b"From");
2755 has_subject_or_date |=
2756 name.eq_ignore_ascii_case(b"Subject") || name.eq_ignore_ascii_case(b"Date");
2757 }
2758 saw_header_body_separator && has_from && has_subject_or_date
2759}
2760
2761fn looks_like_mhtml(bytes: &[u8]) -> bool {
2762 let prefix = String::from_utf8_lossy(bytes).to_ascii_lowercase();
2763 let header = prefix.split("\r\n\r\n").next().unwrap_or(&prefix);
2764 header.contains("multipart/related") && header.contains("text/html")
2765}
2766
2767fn looks_like_ical(bytes: &[u8]) -> bool {
2768 let text = String::from_utf8_lossy(bytes)
2769 .trim_start_matches('\u{feff}')
2770 .to_ascii_uppercase();
2771 text.contains("BEGIN:VCALENDAR")
2772 && (text.contains("BEGIN:VEVENT")
2773 || text.contains("BEGIN:VTODO")
2774 || text.contains("BEGIN:VJOURNAL")
2775 || text.contains("BEGIN:VFREEBUSY"))
2776}
2777
2778fn looks_like_vcalendar(bytes: &[u8]) -> bool {
2779 let text = String::from_utf8_lossy(bytes).to_ascii_uppercase();
2780 text.contains("BEGIN:VCALENDAR")
2781 && text.contains("VERSION:1.0")
2782 && (text.contains("BEGIN:VEVENT") || text.contains("BEGIN:VTODO"))
2783}
2784
2785fn looks_like_nastran_bdf(bytes: &[u8]) -> bool {
2786 bytes.split(|byte| *byte == b'\n').any(|raw_line| {
2787 let line = String::from_utf8_lossy(raw_line);
2788 let line = line.trim_end_matches('\r');
2789 if line.trim().is_empty() || line.trim_start().starts_with('$') {
2790 return false;
2791 }
2792 let first = if line.contains(',') {
2793 line.split(',').next().unwrap_or_default().trim()
2794 } else {
2795 let first_fixed = line.get(..8).unwrap_or_default().trim();
2796 if first_fixed.split_whitespace().count() > 1 {
2797 line.split_whitespace().next().unwrap_or_default()
2798 } else {
2799 first_fixed
2800 }
2801 };
2802 first.eq_ignore_ascii_case("GRID") || first.eq_ignore_ascii_case("GRID*")
2803 })
2804}
2805
2806fn looks_like_lsdyna_keyword(bytes: &[u8]) -> bool {
2807 let mut has_keyword = false;
2808 let mut has_nodes = false;
2809 let mut has_elements = false;
2810 for raw_line in bytes.split(|byte| *byte == b'\n') {
2811 let line = String::from_utf8_lossy(raw_line);
2812 let line = line.trim().trim_start_matches('\u{feff}').trim();
2813 if line.is_empty() || line.starts_with('$') {
2814 continue;
2815 }
2816 let upper = line.to_ascii_uppercase();
2817 has_keyword |= upper == "*KEYWORD" || upper.starts_with("*KEYWORD ");
2818 has_nodes |= upper == "*NODE" || upper.starts_with("*NODE ") || upper.starts_with("*NODE,");
2819 has_elements |= upper.starts_with("*ELEMENT_");
2820 }
2821 has_keyword || (has_nodes && has_elements)
2822}
2823
2824fn looks_like_jupyter_notebook(bytes: &[u8]) -> bool {
2825 let text = String::from_utf8_lossy(bytes);
2826 let text = text.trim_start_matches('\u{feff}').trim_start();
2827 text.starts_with('{')
2828 && text.contains("\"nbformat\"")
2829 && text.contains("\"cells\"")
2830 && text.contains("\"cell_type\"")
2831}
2832
2833fn looks_like_medit_mesh(bytes: &[u8]) -> bool {
2834 for raw_line in bytes.split(|byte| *byte == b'\n') {
2835 let line = String::from_utf8_lossy(raw_line);
2836 let line = line.trim();
2837 if line.is_empty() || line.starts_with('#') {
2838 continue;
2839 }
2840 return line
2841 .split_whitespace()
2842 .next()
2843 .is_some_and(|keyword| keyword.eq_ignore_ascii_case("MeshVersionFormatted"));
2844 }
2845 false
2846}
2847
2848fn looks_like_medit_binary(bytes: &[u8]) -> bool {
2849 if bytes.len() < 20 {
2850 return false;
2851 }
2852 let order = match bytes.get(..4) {
2853 Some([1, 0, 0, 0]) => true,
2854 Some([0, 0, 0, 1]) => false,
2855 _ => return false,
2856 };
2857 let read_i32 = |offset: usize| -> Option<i32> {
2858 let raw: [u8; 4] = bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?;
2859 Some(if order {
2860 i32::from_le_bytes(raw)
2861 } else {
2862 i32::from_be_bytes(raw)
2863 })
2864 };
2865 let Some(version) = read_i32(4) else {
2866 return false;
2867 };
2868 if !(1..=4).contains(&version) || read_i32(8) != Some(3) {
2869 return false;
2870 }
2871 let dimension_offset = if version >= 3 { 20 } else { 16 };
2872 read_i32(dimension_offset).is_some_and(|dimension| matches!(dimension, 2 | 3))
2873}
2874
2875fn looks_like_off_mesh(bytes: &[u8]) -> bool {
2876 for raw_line in bytes.split(|byte| *byte == b'\n') {
2877 let line = String::from_utf8_lossy(raw_line);
2878 let line = line.trim();
2879 if line.is_empty() || line.starts_with('#') {
2880 continue;
2881 }
2882 let header = line.split_whitespace().next().unwrap_or_default();
2883 return ["OFF", "COFF", "NOFF", "CNOFF"]
2884 .iter()
2885 .any(|supported| header.eq_ignore_ascii_case(supported));
2886 }
2887 false
2888}
2889
2890fn looks_like_abaqus_inp(bytes: &[u8]) -> bool {
2891 let text = String::from_utf8_lossy(bytes);
2892 let mut has_heading = false;
2893 let mut has_nodes = false;
2894 let mut has_elements = false;
2895 for line in text
2896 .lines()
2897 .map(str::trim)
2898 .map(|line| line.trim_start_matches('\u{feff}').trim())
2899 {
2900 if line.starts_with("**") || !line.starts_with('*') {
2901 continue;
2902 }
2903 let keyword = line
2904 .split(',')
2905 .next()
2906 .unwrap_or_default()
2907 .trim()
2908 .to_ascii_uppercase();
2909 match keyword.as_str() {
2910 "*HEADING" => has_heading = true,
2911 "*NODE" => has_nodes = true,
2912 "*ELEMENT" => has_elements = true,
2913 _ => {}
2914 }
2915 }
2916 has_nodes && (has_heading || has_elements)
2917}
2918
2919impl std::fmt::Display for SourceFormat {
2920 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2921 formatter.write_str(match self {
2922 Self::Pdf => "PDF",
2923 Self::Fdf => "FDF",
2924 Self::Xfdf => "XFDF",
2925 Self::LandXml => "LANDXML",
2926 Self::Mathml => "MATHML",
2927 Self::Xmp => "XMP",
2928 Self::Xdp => "XDP",
2929 Self::Spreadsheetml => "SPREADSHEETML",
2930 Self::ProjectXml => "MS PROJECT XML",
2931 Self::Xml => "XML",
2932 Self::Bpmn => "BPMN",
2933 Self::Dmn => "DMN",
2934 Self::Cmmn => "CMMN",
2935 Self::Reqif => "REQIF",
2936 Self::Xmi => "XMI",
2937 Self::Ppt => "PPT",
2938 Self::Pptx => "PPTX",
2939 Self::Xls => "XLS",
2940 Self::Xlsb => "XLSB",
2941 Self::Xlsx => "XLSX",
2942 Self::Doc => "DOC",
2943 Self::GeoJson => "GEOJSON",
2944 Self::GeoJsonSeq => "GEOJSONSEQ",
2945 Self::TopoJson => "TOPOJSON",
2946 Self::Geopackage => "GEOPACKAGE",
2947 Self::GeoRss => "GEORSS",
2948 Self::Feed => "FEED",
2949 Self::Gml => "GML",
2950 Self::CityGml => "CITYGML",
2951 Self::CityJson => "CITYJSON",
2952 Self::GraphGml => "GRAPH GML",
2953 Self::Gpx => "GPX",
2954 Self::Wkt => "WKT",
2955 Self::Kml => "KML",
2956 Self::Kmz => "KMZ",
2957 Self::Shapefile => "SHAPEFILE",
2958 Self::Dbf => "DBF",
2959 Self::EsriAsciiGrid => "ESRI ASCII GRID",
2960 Self::Properties => "JAVA PROPERTIES",
2961 Self::Plist => "PLIST",
2962 Self::Premis => "PREMIS",
2963 Self::Docx => "DOCX",
2964 Self::Drawio => "DRAWIO",
2965 Self::Dxf => "DXF",
2966 Self::Gerber => "GERBER",
2967 Self::KicadPcb => "KICAD PCB",
2968 Self::KicadSchLegacy => "KICAD LEGACY SCH",
2969 Self::KicadSch => "KICAD SCH",
2970 Self::LtspiceAsc => "LTSPICE ASC",
2971 Self::EagleSch => "EAGLE SCH",
2972 Self::Hpgl => "HPGL",
2973 Self::Gcode => "GCODE",
2974 Self::Excellon => "EXCELLON",
2975 Self::Stl => "STL",
2976 Self::Step => "STEP",
2977 Self::Obj => "OBJ",
2978 Self::Ply => "PLY",
2979 Self::Pcd => "PCD",
2980 Self::Pts => "PTS",
2981 Self::Ptx => "PTX",
2982 Self::Xyz => "XYZ",
2983 Self::E57 => "E57",
2984 Self::Las => "LAS",
2985 Self::Unv => "UNV",
2986 Self::ThreeMf => "3MF",
2987 Self::Gltf => "GLTF",
2988 Self::Collada => "COLLADA",
2989 Self::X3d => "X3D",
2990 Self::Vrml => "VRML",
2991 Self::Iges => "IGES",
2992 Self::Ifc => "IFC",
2993 Self::IfcXml => "IFCXML",
2994 Self::IfcZip => "IFCZIP",
2995 Self::Su2 => "SU2",
2996 Self::OpenFoam => "OPENFOAM",
2997 Self::OpenDrive => "OPENDRIVE",
2998 Self::OpenCrg => "OPENCRG",
2999 Self::OpenScenario => "OPENSCENARIO",
3000 Self::OpenLabel => "OPENLABEL",
3001 Self::OpenFoamField => "OPENFOAM-FIELD",
3002 Self::Tecplot => "TECPLOT",
3003 Self::Ensight => "ENSIGHT",
3004 Self::Plot3d => "PLOT3D",
3005 Self::Newick => "NEWICK",
3006 Self::Stockholm => "STOCKHOLM",
3007 Self::Clustal => "CLUSTAL",
3008 Self::Nexus => "NEXUS",
3009 Self::Genbank => "GENBANK",
3010 Self::Embl => "EMBL",
3011 Self::Uniprot => "UNIPROT",
3012 Self::Ris => "RIS",
3013 Self::Spice => "SPICE",
3014 Self::Openapi => "OPENAPI",
3015 Self::Asyncapi => "ASYNCAPI",
3016 Self::Alto => "ALTO",
3017 Self::Wsdl => "WSDL",
3018 Self::Opml => "OPML",
3019 Self::JsonSchema => "JSONSCHEMA",
3020 Self::Simulation => "SIMULATION",
3021 Self::Dot => "DOT",
3022 Self::Mermaid => "MERMAID",
3023 Self::PlantUml => "PLANTUML",
3024 Self::D2 => "D2",
3025 Self::Excalidraw => "EXCALIDRAW",
3026 Self::Html => "HTML",
3027 Self::Docbook => "DOCBOOK",
3028 Self::DublinCore => "DUBLIN-CORE",
3029 Self::Iso19115 => "ISO 19115",
3030 Self::Ubl => "UBL",
3031 Self::Xbrl => "XBRL",
3032 Self::Dif => "DIF",
3033 Self::Fasta => "FASTA",
3034 Self::Fastq => "FASTQ",
3035 Self::Dita => "DITA",
3036 Self::Ead => "EAD",
3037 Self::EacCpf => "EAC-CPF",
3038 Self::Pdb => "PDB",
3039 Self::Hwpx => "HWPX",
3040 Self::Xmind => "XMIND",
3041 Self::Nifti => "NIFTI",
3042 Self::Fits => "FITS",
3043 Self::Gff3 => "GFF3",
3044 Self::Gtf => "GTF",
3045 Self::Mrc => "MRC",
3046 Self::Netcdf => "NETCDF",
3047 Self::Sqlite => "SQLITE",
3048 Self::Sylk => "SYLK",
3049 Self::Cif => "CIF",
3050 Self::Cml => "CML",
3051 Self::RdfXml => "RDF-XML",
3052 Self::Bcfzip => "BCFZIP",
3053 Self::FlatOpc => "FLAT-OPC",
3054 Self::Aasx => "AASX",
3055 Self::OpenScad => "OPENSCAD",
3056 Self::Amf => "AMF",
3057 Self::PlmXml => "PLMXML",
3058 Self::StepXml => "STEPXML",
3059 Self::Qif => "QIF",
3060 Self::B2mml => "B2MML",
3061 Self::Jdf => "JDF",
3062 Self::Xjdf => "XJDF",
3063 Self::Tmx => "TMX",
3064 Self::Tbx => "TBX",
3065 Self::GbXml => "GBXML",
3066 Self::Idml => "IDML",
3067 Self::OaiPmh => "OAIPMH",
3068 Self::Onix => "ONIX",
3069 Self::Xpdl => "XPDL",
3070 Self::Cda => "CDA",
3071 Self::Iso20022 => "ISO20022",
3072 Self::Sbml => "SBML",
3073 Self::Cellml => "CELLML",
3074 Self::OcelXml => "OCEL-XML",
3075 Self::EnergyPlusIdf => "ENERGYPLUS-IDF",
3076 Self::EnergyPlusEpw => "ENERGYPLUS-EPW",
3077 Self::Rinex => "RINEX",
3078 Self::Sat => "SAT",
3079 Self::Sedml => "SEDML",
3080 Self::Sbgnml => "SBGNML",
3081 Self::Omex => "OMEX",
3082 Self::Xdmf => "XDMF",
3083 Self::Pvd => "PVD",
3084 Self::Fds => "FDS",
3085 Self::Abiword => "ABIWORD",
3086 Self::Neuroml => "NEUROML",
3087 Self::Biopax => "BIOPAX",
3088 Self::Xsd => "XSD",
3089 Self::Xslt => "XSLT",
3090 Self::XslFo => "XSL-FO",
3091 Self::Hdf5 => "HDF5",
3092 Self::Cgns => "CGNS",
3093 Self::Exodus => "EXODUS",
3094 Self::Iwork => "IWORK",
3095 Self::Dwg => "DWG",
3096 Self::Rhino3dm => "3DM",
3097 Self::Access => "ACCESS",
3098 Self::ThreeDXml => "3DXML",
3099 Self::Ipc2581 => "IPC2581",
3100 Self::Jt => "JT",
3101 Self::Xproc => "XPROC",
3102 Self::Wadl => "WADL",
3103 Self::OpenSearch => "OPENSEARCH",
3104 Self::Saml => "SAML",
3105 Self::Xacml => "XACML",
3106 Self::Mol2 => "MOL2",
3107 Self::Turtle => "TURTLE",
3108 Self::Eps => "EPS",
3109 Self::Epub => "EPUB",
3110 Self::Fb2 => "FB2",
3111 Self::Mobi => "MOBI",
3112 Self::Eml => "EML",
3113 Self::Emlx => "EMLX",
3114 Self::Mbox => "MBOX",
3115 Self::Mhtml => "MHTML",
3116 Self::Mets => "METS",
3117 Self::Msg => "MSG",
3118 Self::Ical => "ICAL",
3119 Self::Vcalendar => "VCALENDAR",
3120 Self::Vcard => "VCARD",
3121 Self::Vcf => "VCF",
3122 Self::Wig => "WIG",
3123 Self::Odt => "ODT",
3124 Self::Ods => "ODS",
3125 Self::Odp => "ODP",
3126 Self::Odg => "ODG",
3127 Self::Vsd => "VSD",
3128 Self::Visio => "VISIO",
3129 Self::Rtf => "RTF",
3130 Self::Xps => "XPS",
3131 Self::Tiff => "TIFF",
3132 Self::Jpeg2000 => "JPEG2000",
3133 Self::Dicom => "DICOM",
3134 Self::DicomSr => "DICOM SR",
3135 Self::DicomDir => "DICOMDIR",
3136 Self::Rxn => "RXN",
3137 Self::Mol => "MOL",
3138 Self::Sdf => "SDF",
3139 Self::Cbz => "CBZ",
3140 Self::Cdb => "CDB",
3141 Self::Har => "HAR",
3142 Self::Warc => "WARC",
3143 Self::Wacz => "WACZ",
3144 Self::Postman => "POSTMAN",
3145 Self::Graphql => "GRAPHQL",
3146 Self::Protobuf => "PROTOBUF",
3147 Self::Kubernetes => "KUBERNETES",
3148 Self::Compose => "COMPOSE",
3149 Self::GithubActions => "GITHUB-ACTIONS",
3150 Self::Junit => "JUNIT",
3151 Self::Sarif => "SARIF",
3152 Self::TerraformPlan => "TERRAFORM-PLAN",
3153 Self::CycloneDx => "CYCLONEDX",
3154 Self::Spdx => "SPDX",
3155 Self::StixJson => "STIX-JSON",
3156 Self::TaxiiJson => "TAXII-JSON",
3157 Self::Coverage => "COVERAGE",
3158 Self::Lcov => "LCOV",
3159 Self::JsonPatch => "JSONPATCH",
3160 Self::JsonMergePatch => "JSONMERGEPATCH",
3161 Self::JsonFeed => "JSONFEED",
3162 Self::CloudEvents => "CLOUDEVENTS",
3163 Self::FhirJson => "FHIR-JSON",
3164 Self::FhirXml => "FHIR-XML",
3165 Self::Avro => "AVRO",
3166 Self::OtlpJson => "OTLP-JSON",
3167 Self::OcelJson => "OCEL-JSON",
3168 Self::JsonApi => "JSON-API",
3169 Self::CslJson => "CSL-JSON",
3170 Self::Abaqus => "ABAQUS",
3171 Self::Nastran => "NASTRAN",
3172 Self::Op2 => "OP2",
3173 Self::LsDyna => "LSDYNA",
3174 Self::Jupyter => "JUPYTER",
3175 Self::Jats => "JATS",
3176 Self::Tei => "TEI",
3177 Self::Quarto => "QUARTO",
3178 Self::Medit => "MEDIT",
3179 Self::Off => "OFF",
3180 Self::Markdown => "MARKDOWN",
3181 Self::Asciidoc => "ASCIIDOC",
3182 Self::Arff => "ARFF",
3183 Self::Bed => "BED",
3184 Self::BedGraph => "BEDGRAPH",
3185 Self::Rst => "RST",
3186 Self::Sam => "SAM",
3187 Self::S1000d => "S1000D",
3188 Self::Maf => "MAF",
3189 Self::Marcxml => "MARCXML",
3190 Self::Marc => "MARC21",
3191 Self::Mods => "MODS",
3192 Self::Org => "ORG",
3193 Self::Po => "PO",
3194 Self::Srt => "SRT",
3195 Self::Ttml => "TTML",
3196 Self::Vtt => "VTT",
3197 Self::Xliff => "XLIFF",
3198 Self::Csv => "CSV",
3199 Self::Emf => "EMF",
3200 Self::Chart => "CHART",
3201 Self::Json => "JSON",
3202 Self::JsonLd => "JSON-LD",
3203 Self::Iiif => "IIIF",
3204 Self::JsonSeq => "JSONSEQ",
3205 Self::Graphml => "GRAPHML",
3206 Self::Gexf => "GEXF",
3207 Self::Xgmml => "XGMML",
3208 Self::Toml => "TOML",
3209 Self::Yaml => "YAML",
3210 Self::Bib => "BIB",
3211 Self::Tex => "TEX",
3212 Self::Qr => "QR",
3213 Self::Raster => "RASTER",
3214 Self::Svg => "SVG",
3215 })
3216 }
3217}
3218
3219#[derive(Clone, Debug)]
3220pub struct ConvertOptions {
3221 pub max_input_bytes: u64,
3222 pub max_zip_entry_bytes: u64,
3224 pub max_pages: usize,
3225 pub max_xml_events: usize,
3226 pub include_metadata: bool,
3227 pub precision: usize,
3228 pub jobs: usize,
3229 pub outline_embedded_pdf_text: bool,
3233 pub stencil_paths: Vec<std::path::PathBuf>,
3241 pub embed_drawio_source: bool,
3249}
3250
3251impl Default for ConvertOptions {
3252 fn default() -> Self {
3253 Self {
3254 max_input_bytes: 512 * 1024 * 1024,
3255 max_zip_entry_bytes: 128 * 1024 * 1024,
3256 max_pages: 10_000,
3257 max_xml_events: 5_000_000,
3258 include_metadata: true,
3259 precision: 5,
3260 jobs: 1,
3261 outline_embedded_pdf_text: false,
3262 stencil_paths: Vec::new(),
3263 embed_drawio_source: false,
3264 }
3265 }
3266}
3267
3268#[derive(Clone, Debug, Serialize)]
3269pub struct PageReport {
3270 pub number: usize,
3271 pub svg: String,
3272 pub width_points: f64,
3273 pub height_points: f64,
3274 pub node_count: usize,
3275 pub warning_count: usize,
3276 pub warnings: Vec<String>,
3277 pub estimated_ir_bytes: usize,
3278}
3279
3280#[derive(Clone, Debug, Serialize)]
3281pub struct ConversionReport {
3282 pub converter: &'static str,
3283 pub version: &'static str,
3284 pub source: String,
3285 pub source_format: SourceFormat,
3286 pub output_directory: String,
3287 pub elapsed_ms: u128,
3288 pub input_bytes: u64,
3289 pub page_count: usize,
3290 pub largest_page_ir_bytes: usize,
3291 pub pages: Vec<PageReport>,
3292 pub warnings: Vec<String>,
3293}
3294
3295#[cfg(not(target_arch = "wasm32"))]
3296pub fn convert_path(
3297 input: impl AsRef<Path>,
3298 output_directory: impl AsRef<Path>,
3299 options: &ConvertOptions,
3300) -> Result<ConversionReport> {
3301 let started = Instant::now();
3302 let input = input.as_ref();
3303 let output_directory = output_directory.as_ref();
3304 let metadata = fs::metadata(input)?;
3305 if !metadata.is_file() {
3306 return Err(Error::InvalidInput(format!(
3307 "{} is not a regular file",
3308 input.display()
3309 )));
3310 }
3311 if metadata.len() > options.max_input_bytes {
3312 return Err(Error::LimitExceeded(format!(
3313 "input is {} bytes; maximum is {} bytes",
3314 metadata.len(),
3315 options.max_input_bytes
3316 )));
3317 }
3318 if options.jobs == 0 {
3319 return Err(Error::InvalidInput("jobs must be at least 1".into()));
3320 }
3321 fs::create_dir_all(output_directory)?;
3322 if fs::read_dir(output_directory)?.next().is_some() {
3323 return Err(Error::InvalidInput("output directory must be empty".into()));
3324 }
3325 let format = SourceFormat::detect(input)?;
3326 let mut sink = PageSink::new(output_directory, options);
3327 let warnings = match format {
3328 SourceFormat::Pdf => crate::pdf::convert(input, options, &mut sink)?,
3329 SourceFormat::Fdf => crate::document::fdf::convert(input, options, &mut sink)?,
3330 SourceFormat::Xfdf => crate::document::xfdf::convert(input, options, &mut sink)?,
3331 SourceFormat::LandXml => crate::document::landxml::convert(input, options, &mut sink)?,
3332 SourceFormat::Mathml => crate::document::mathml::convert(input, options, &mut sink)?,
3333 SourceFormat::Xmp => crate::document::xmp::convert(input, options, &mut sink)?,
3334 SourceFormat::Xdp => crate::document::xdp::convert(input, options, &mut sink)?,
3335 SourceFormat::Spreadsheetml => {
3336 crate::document::spreadsheetml::convert(input, options, &mut sink)?
3337 }
3338 SourceFormat::ProjectXml => crate::document::msproject::convert(input, options, &mut sink)?,
3339 SourceFormat::Xml => crate::document::xml::convert(input, options, &mut sink)?,
3340 SourceFormat::Bpmn => crate::document::bpmn::convert(input, options, &mut sink)?,
3341 SourceFormat::Dmn => crate::document::dmn::convert(input, options, &mut sink)?,
3342 SourceFormat::Cmmn => crate::document::cmmn::convert(input, options, &mut sink)?,
3343 SourceFormat::Reqif => crate::document::reqif::convert(input, options, &mut sink)?,
3344 SourceFormat::Xmi => crate::document::xmi::convert(input, options, &mut sink)?,
3345 SourceFormat::Pptx => crate::ooxml::pptx::convert(input, options, &mut sink)?,
3346 SourceFormat::Xls => crate::ooxml::xls::convert(input, options, &mut sink)?,
3347 SourceFormat::Xlsb => crate::ooxml::xlsb::convert(input, options, &mut sink)?,
3348 SourceFormat::GeoJson => crate::geospatial::geojson::convert(input, options, &mut sink)?,
3349 SourceFormat::GeoJsonSeq => {
3350 crate::geospatial::geojson_seq::convert(input, options, &mut sink)?
3351 }
3352 SourceFormat::TopoJson => crate::geospatial::topojson::convert(input, options, &mut sink)?,
3353 SourceFormat::Geopackage => {
3354 crate::geospatial::geopackage::convert(input, options, &mut sink)?
3355 }
3356 SourceFormat::Gml => crate::geospatial::gml::convert(input, options, &mut sink)?,
3357 SourceFormat::CityGml => crate::document::citygml::convert(input, options, &mut sink)?,
3358 SourceFormat::CityJson => crate::document::cityjson::convert(input, options, &mut sink)?,
3359 SourceFormat::GraphGml => crate::document::gml_graph::convert(input, options, &mut sink)?,
3360 SourceFormat::GeoRss => crate::geospatial::georss::convert(input, options, &mut sink)?,
3361 SourceFormat::Feed => crate::document::feed::convert(input, options, &mut sink)?,
3362 SourceFormat::Gpx => crate::geospatial::gpx::convert(input, options, &mut sink)?,
3363 SourceFormat::Wkt => crate::geospatial::wkt::convert(input, options, &mut sink)?,
3364 SourceFormat::Kml => crate::geospatial::kml::convert_kml(input, options, &mut sink)?,
3365 SourceFormat::Kmz => crate::geospatial::kml::convert_kmz(input, options, &mut sink)?,
3366 SourceFormat::Shapefile => {
3367 crate::geospatial::shapefile::convert(input, options, &mut sink)?
3368 }
3369 SourceFormat::Dbf => crate::geospatial::dbase::convert(input, options, &mut sink)?,
3370 SourceFormat::EsriAsciiGrid => {
3371 crate::geospatial::ascii_grid::convert(input, options, &mut sink)?
3372 }
3373 SourceFormat::Xlsx => crate::ooxml::xlsx::convert(input, options, &mut sink)?,
3374 SourceFormat::Docx => crate::ooxml::docx::convert(input, options, &mut sink)?,
3375 SourceFormat::Ppt => crate::document::legacy_ppt::convert(input, options, &mut sink)?,
3376 SourceFormat::Doc => {
3377 crate::document::legacy_doc::convert(input, options, "doc", &mut sink)?
3378 }
3379 SourceFormat::Vsd => crate::document::legacy_visio::convert(input, options, &mut sink)?,
3380 SourceFormat::Drawio => crate::drawio::convert(input, options, &mut sink)?,
3381 SourceFormat::KicadPcb => crate::cad::kicad::convert(input, options, &mut sink)?,
3382 SourceFormat::KicadSchLegacy => crate::cad::kicad_sch::convert(input, options, &mut sink)?,
3383 SourceFormat::KicadSch => crate::cad::kicad_sch_modern::convert(input, options, &mut sink)?,
3384 SourceFormat::LtspiceAsc => crate::cad::ltspice::convert(input, options, &mut sink)?,
3385 SourceFormat::EagleSch => crate::cad::eagle::convert(input, options, &mut sink)?,
3386 SourceFormat::Ifc => {
3387 let file = std::fs::File::open(input)?;
3388 crate::cad::ifc::convert(file, options, &mut sink)?
3389 }
3390 SourceFormat::IfcXml => {
3391 let file = std::fs::File::open(input)?;
3392 crate::cad::ifc::convert_ifcxml(file, options, &mut sink)?
3393 }
3394 SourceFormat::IfcZip => crate::cad::ifc::convert_ifczip(input, options, &mut sink)?,
3395 SourceFormat::Dxf
3396 | SourceFormat::Gerber
3397 | SourceFormat::Hpgl
3398 | SourceFormat::Gcode
3399 | SourceFormat::Excellon
3400 | SourceFormat::Stl
3401 | SourceFormat::Ply
3402 | SourceFormat::Pcd
3403 | SourceFormat::Las
3404 | SourceFormat::ThreeMf
3405 | SourceFormat::Gltf
3406 | SourceFormat::Collada
3407 | SourceFormat::X3d
3408 | SourceFormat::Step
3409 | SourceFormat::Iges
3410 | SourceFormat::Obj
3411 | SourceFormat::Simulation => crate::cad::convert(input, options, &mut sink)?,
3412 SourceFormat::Unv => {
3413 let file = std::fs::File::open(input)?;
3414 crate::cad::simulation::convert_unv(file, options, &mut sink)?
3415 }
3416 SourceFormat::Su2 => {
3417 let file = std::fs::File::open(input)?;
3418 crate::cad::simulation::convert_su2(file, options, &mut sink)?
3419 }
3420 SourceFormat::OpenFoam => {
3421 crate::cad::simulation::convert_openfoam(input, options, &mut sink)?
3422 }
3423 SourceFormat::OpenDrive => crate::cad::opendrive::convert(input, options, &mut sink)?,
3424 SourceFormat::OpenCrg => crate::cad::opencrg::convert(input, options, &mut sink)?,
3425 SourceFormat::OpenScenario => crate::cad::openscenario::convert(input, options, &mut sink)?,
3426 SourceFormat::OpenLabel => crate::document::openlabel::convert(input, options, &mut sink)?,
3427 SourceFormat::OpenFoamField => {
3428 crate::cad::openfoam_field::convert(input, options, &mut sink)?
3429 }
3430 SourceFormat::Vrml => {
3431 let file = std::fs::File::open(input)?;
3432 crate::cad::vrml::convert(file, options, &mut sink)?
3433 }
3434 SourceFormat::Tecplot => {
3435 let file = std::fs::File::open(input)?;
3436 crate::cad::simulation::convert_tecplot(file, options, &mut sink)?
3437 }
3438 SourceFormat::Ensight => {
3439 let extension = input
3440 .extension()
3441 .and_then(|value| value.to_str())
3442 .unwrap_or_default();
3443 if extension.eq_ignore_ascii_case("case") {
3444 crate::cad::simulation::convert_ensight_case(input, options, &mut sink)?
3445 } else {
3446 let file = std::fs::File::open(input)?;
3447 crate::cad::simulation::convert_ensight_geometry(file, options, &mut sink)?
3448 }
3449 }
3450 SourceFormat::Plot3d => {
3451 let file = std::fs::File::open(input)?;
3452 crate::cad::simulation::convert_plot3d(file, options, &mut sink)?
3453 }
3454 SourceFormat::Ptx => {
3455 let file = std::fs::File::open(input)?;
3456 crate::cad::ptx::convert(file, options, &mut sink)?
3457 }
3458 SourceFormat::Pts => {
3459 let file = std::fs::File::open(input)?;
3460 crate::cad::pts::convert(file, options, &mut sink)?
3461 }
3462 SourceFormat::E57 => crate::cad::e57::convert(input, options, &mut sink)?,
3463 SourceFormat::Xyz => {
3464 let file = std::fs::File::open(input)?;
3465 crate::cad::xyz::convert(file, options, &mut sink)?
3466 }
3467 SourceFormat::Dot | SourceFormat::Mermaid => {
3468 crate::diagram::convert(input, options, &mut sink)?
3469 }
3470 SourceFormat::PlantUml => crate::diagram::plantuml::convert(input, options, &mut sink)?,
3471 SourceFormat::D2 => crate::diagram::d2::convert(input, options, &mut sink)?,
3472 SourceFormat::Excalidraw => crate::diagram::excalidraw::convert(input, options, &mut sink)?,
3473 SourceFormat::Html => crate::document::html::convert(input, options, &mut sink)?,
3474 SourceFormat::Docbook => crate::document::docbook::convert(input, options, &mut sink)?,
3475 SourceFormat::DublinCore => crate::document::dc::convert(input, options, &mut sink)?,
3476 SourceFormat::Iso19115 => crate::document::iso19115::convert(input, options, &mut sink)?,
3477 SourceFormat::Ubl => crate::document::ubl::convert(input, options, &mut sink)?,
3478 SourceFormat::Xbrl => crate::document::xbrl::convert(input, options, &mut sink)?,
3479 SourceFormat::Dif => crate::document::dif::convert(input, options, &mut sink)?,
3480 SourceFormat::Fasta => crate::document::fastx::convert_fasta(input, options, &mut sink)?,
3481 SourceFormat::Fastq => crate::document::fastx::convert_fastq(input, options, &mut sink)?,
3482 SourceFormat::Dita => crate::document::dita::convert(input, options, &mut sink)?,
3483 SourceFormat::Ead => crate::document::ead::convert(input, options, &mut sink)?,
3484 SourceFormat::EacCpf => crate::document::eac_cpf::convert(input, options, &mut sink)?,
3485 SourceFormat::Pdb => crate::document::pdb::convert(input, options, &mut sink)?,
3486 SourceFormat::Hwpx => crate::document::hwpx::convert(input, options, &mut sink)?,
3487 SourceFormat::Xmind => crate::document::xmind::convert(input, options, &mut sink)?,
3488 SourceFormat::Nifti => crate::document::nifti::convert(input, options, &mut sink)?,
3489 SourceFormat::Fits => crate::document::fits::convert(input, options, &mut sink)?,
3490 SourceFormat::Gff3 => crate::document::gff::convert(
3491 input,
3492 options,
3493 &mut sink,
3494 crate::document::gff::FeatureFormat::Gff3,
3495 )?,
3496 SourceFormat::Gtf => crate::document::gff::convert(
3497 input,
3498 options,
3499 &mut sink,
3500 crate::document::gff::FeatureFormat::Gtf,
3501 )?,
3502 SourceFormat::Mrc => crate::document::mrc::convert(input, options, &mut sink)?,
3503 SourceFormat::Netcdf => crate::document::netcdf::convert(input, options, &mut sink)?,
3504 SourceFormat::Exodus => crate::document::netcdf::convert_exodus(input, options, &mut sink)?,
3505 SourceFormat::Sqlite => crate::document::sqlite::convert(input, options, &mut sink)?,
3506 SourceFormat::Sylk => crate::document::sylk::convert(input, options, &mut sink)?,
3507 SourceFormat::Cif => crate::document::cif::convert(input, options, &mut sink)?,
3508 SourceFormat::Cml => crate::document::cml::convert(input, options, &mut sink)?,
3509 SourceFormat::RdfXml => crate::document::rdfxml::convert(input, options, &mut sink)?,
3510 SourceFormat::Bcfzip => crate::document::bcf::convert(input, options, &mut sink)?,
3511 SourceFormat::FlatOpc => crate::document::flatopc::convert(input, options, &mut sink)?,
3512 SourceFormat::Aasx => crate::document::aasx::convert(input, options, &mut sink)?,
3513 SourceFormat::OpenScad => crate::document::openscad::convert(input, options, &mut sink)?,
3514 SourceFormat::Amf => crate::cad::amf::convert(input, options, &mut sink)?,
3515 SourceFormat::PlmXml => crate::document::plmxml::convert(input, options, &mut sink)?,
3516 SourceFormat::StepXml => crate::document::stepxml::convert(input, options, &mut sink)?,
3517 SourceFormat::Qif => crate::document::qif::convert(input, options, &mut sink)?,
3518 SourceFormat::B2mml => crate::document::b2mml::convert(input, options, &mut sink)?,
3519 SourceFormat::Jdf => crate::document::jdf::convert(input, options, &mut sink)?,
3520 SourceFormat::Xjdf => crate::document::jdf::convert_xjdf(input, options, &mut sink)?,
3521 SourceFormat::Tmx => crate::document::tmx::convert(input, options, &mut sink)?,
3522 SourceFormat::Tbx => crate::document::tbx::convert(input, options, &mut sink)?,
3523 SourceFormat::GbXml => crate::document::gbxml::convert(input, options, &mut sink)?,
3524 SourceFormat::Idml => crate::document::idml::convert(input, options, &mut sink)?,
3525 SourceFormat::OaiPmh => crate::document::oaipmh::convert(input, options, &mut sink)?,
3526 SourceFormat::Onix => crate::document::onix::convert(input, options, &mut sink)?,
3527 SourceFormat::Xpdl => crate::document::xpdl::convert(input, options, &mut sink)?,
3528 SourceFormat::Cda => crate::document::cda::convert(input, options, &mut sink)?,
3529 SourceFormat::Iso20022 => crate::document::iso20022::convert(input, options, &mut sink)?,
3530 SourceFormat::Sbml => crate::document::sbml::convert(input, options, &mut sink)?,
3531 SourceFormat::Cellml => crate::document::cellml::convert(input, options, &mut sink)?,
3532 SourceFormat::OcelXml => crate::document::ocel_xml::convert(input, options, &mut sink)?,
3533 SourceFormat::EnergyPlusIdf => {
3534 crate::document::energyplus::convert_idf(input, options, &mut sink)?
3535 }
3536 SourceFormat::EnergyPlusEpw => {
3537 crate::document::energyplus::convert_epw(input, options, &mut sink)?
3538 }
3539 SourceFormat::Rinex => crate::document::rinex::convert(input, options, &mut sink)?,
3540 SourceFormat::Sat => crate::cad::sat::convert(input, options, &mut sink)?,
3541 SourceFormat::Sedml => crate::document::sedml::convert(input, options, &mut sink)?,
3542 SourceFormat::Sbgnml => crate::document::sbgnml::convert(input, options, &mut sink)?,
3543 SourceFormat::Omex => crate::document::omex::convert(input, options, &mut sink)?,
3544 SourceFormat::Xdmf => crate::document::xdmf::convert(input, options, &mut sink)?,
3545 SourceFormat::Pvd => crate::document::pvd::convert(input, options, &mut sink)?,
3546 SourceFormat::Fds => crate::document::fds::convert(input, options, &mut sink)?,
3547 SourceFormat::Abiword => crate::document::abiword::convert(input, options, &mut sink)?,
3548 SourceFormat::Neuroml => crate::document::neuroml::convert(input, options, &mut sink)?,
3549 SourceFormat::Biopax => crate::document::biopax::convert(input, options, &mut sink)?,
3550 SourceFormat::Xsd => crate::document::xsd::convert(input, options, &mut sink)?,
3551 SourceFormat::Xslt => crate::document::xslt::convert(input, options, &mut sink)?,
3552 SourceFormat::XslFo => crate::document::xslfo::convert(input, options, &mut sink)?,
3553 SourceFormat::Hdf5 => crate::document::hdf5::convert_hdf5(input, options, &mut sink)?,
3554 SourceFormat::Cgns => crate::document::hdf5::convert_cgns(input, options, &mut sink)?,
3555 SourceFormat::Xproc => crate::document::xproc::convert(input, options, &mut sink)?,
3556 SourceFormat::Wadl => crate::document::wadl::convert(input, options, &mut sink)?,
3557 SourceFormat::OpenSearch => {
3558 crate::document::opensearch::convert(input, options, &mut sink)?
3559 }
3560 SourceFormat::Saml => crate::document::saml::convert(input, options, &mut sink)?,
3561 SourceFormat::Xacml => crate::document::xacml::convert(input, options, &mut sink)?,
3562 SourceFormat::Mol2 => crate::document::mol2::convert(input, options, &mut sink)?,
3563 SourceFormat::Turtle => crate::document::turtle::convert(input, options, &mut sink)?,
3564 SourceFormat::Eps => crate::document::eps::convert(input, options, &mut sink)?,
3565 SourceFormat::Epub => crate::document::epub::convert(input, options, &mut sink)?,
3566 SourceFormat::Fb2 => crate::document::fb2::convert(input, options, &mut sink)?,
3567 SourceFormat::Mobi => crate::document::mobi::convert(input, options, &mut sink)?,
3568 SourceFormat::Eml => crate::document::eml::convert(input, options, &mut sink)?,
3569 SourceFormat::Emlx => crate::document::emlx::convert(input, options, &mut sink)?,
3570 SourceFormat::Mbox => crate::document::mbox::convert(input, options, &mut sink)?,
3571 SourceFormat::Mhtml => crate::document::mhtml::convert(input, options, &mut sink)?,
3572 SourceFormat::Mets => crate::document::mets::convert(input, options, &mut sink)?,
3573 SourceFormat::Msg => crate::document::msg::convert(input, options, &mut sink)?,
3574 SourceFormat::Ical => crate::document::ical::convert(input, options, &mut sink)?,
3575 SourceFormat::Vcalendar => {
3576 crate::document::ical::convert_vcalendar(input, options, &mut sink)?
3577 }
3578 SourceFormat::Vcard => crate::document::vcard::convert(input, options, &mut sink)?,
3579 SourceFormat::Vcf => crate::document::vcf::convert(input, options, &mut sink)?,
3580 SourceFormat::Wig => crate::document::wig::convert(input, options, &mut sink)?,
3581 SourceFormat::Odt => crate::document::odt::convert(input, options, &mut sink)?,
3582 SourceFormat::Ods => crate::document::ods::convert(input, options, &mut sink)?,
3583 SourceFormat::Odp => crate::document::odp::convert(input, options, &mut sink)?,
3584 SourceFormat::Odg => crate::document::odg::convert(input, options, &mut sink)?,
3585 SourceFormat::Iwork => crate::document::iwork::convert(input, options, &mut sink)?,
3586 SourceFormat::Dwg => crate::document::dwg::convert(input, options, &mut sink)?,
3587 SourceFormat::Rhino3dm => crate::document::rhino3dm::convert(input, options, &mut sink)?,
3588 SourceFormat::Access => crate::document::access::convert(input, options, &mut sink)?,
3589 SourceFormat::ThreeDXml => crate::document::threedxml::convert(input, options, &mut sink)?,
3590 SourceFormat::Ipc2581 => crate::document::ipc2581::convert(input, options, &mut sink)?,
3591 SourceFormat::Jt => crate::document::jt::convert(input, options, &mut sink)?,
3592 SourceFormat::Visio => {
3593 if input
3594 .extension()
3595 .and_then(|extension| extension.to_str())
3596 .is_some_and(|extension| extension.eq_ignore_ascii_case("vdx"))
3597 {
3598 crate::document::vdx::convert(input, options, &mut sink)?
3599 } else {
3600 crate::document::vsdx::convert(input, options, &mut sink)?
3601 }
3602 }
3603 SourceFormat::Rtf => crate::document::rtf::convert(input, options, &mut sink)?,
3604 SourceFormat::Xps => crate::document::xps::convert(input, options, &mut sink)?,
3605 SourceFormat::Tiff => crate::document::tiff::convert(input, options, &mut sink)?,
3606 SourceFormat::Jpeg2000 => crate::document::jpeg2000::convert(input, options, &mut sink)?,
3607 SourceFormat::Dicom | SourceFormat::DicomSr => {
3608 crate::document::dicom::convert(input, options, &mut sink)?
3609 }
3610 SourceFormat::DicomDir => {
3611 crate::document::dicom::convert_directory(input, options, &mut sink)?
3612 }
3613 SourceFormat::Mol | SourceFormat::Sdf => {
3614 crate::document::molfile::convert(input, options, format, &mut sink)?
3615 }
3616 SourceFormat::Rxn => crate::document::molfile::convert_rxn(input, options, &mut sink)?,
3617 SourceFormat::Cbz => crate::document::cbz::convert(input, options, &mut sink)?,
3618 SourceFormat::Cdb => {
3619 let file = std::fs::File::open(input)?;
3620 crate::cad::simulation::convert_cdb(file, options, &mut sink)?
3621 }
3622 SourceFormat::Op2 => crate::document::op2::convert(input, options, &mut sink)?,
3623 SourceFormat::Har => crate::document::har::convert(input, options, &mut sink)?,
3624 SourceFormat::Warc => crate::document::warc::convert(input, options, &mut sink)?,
3625 SourceFormat::Wacz => crate::document::wacz::convert(input, options, &mut sink)?,
3626 SourceFormat::Postman => crate::document::postman::convert(input, options, &mut sink)?,
3627 SourceFormat::Graphql => crate::document::graphql::convert(input, options, &mut sink)?,
3628 SourceFormat::Protobuf => crate::document::protobuf::convert(input, options, &mut sink)?,
3629 SourceFormat::Abaqus => {
3630 let file = std::fs::File::open(input)?;
3631 crate::cad::simulation::convert_abaqus(file, options, &mut sink)?
3632 }
3633 SourceFormat::Nastran => {
3634 let file = std::fs::File::open(input)?;
3635 crate::cad::simulation::convert_nastran(file, options, &mut sink)?
3636 }
3637 SourceFormat::LsDyna => {
3638 let file = std::fs::File::open(input)?;
3639 crate::cad::simulation::convert_lsdyna(file, options, &mut sink)?
3640 }
3641 SourceFormat::Jupyter => {
3642 let file = std::fs::File::open(input)?;
3643 crate::document::jupyter::convert(file, options, &mut sink)?
3644 }
3645 SourceFormat::Jats => crate::document::jats::convert(input, options, &mut sink)?,
3646 SourceFormat::Tei => crate::document::tei::convert(input, options, &mut sink)?,
3647 SourceFormat::Quarto => crate::document::quarto::convert(input, options, &mut sink)?,
3648 SourceFormat::Medit => {
3649 let file = std::fs::File::open(input)?;
3650 crate::cad::simulation::convert_medit(file, options, &mut sink)?
3651 }
3652 SourceFormat::Off => {
3653 let file = std::fs::File::open(input)?;
3654 crate::cad::off::convert(file, options, &mut sink)?
3655 }
3656 SourceFormat::Markdown => crate::document::markdown::convert(input, options, &mut sink)?,
3657 SourceFormat::Asciidoc => crate::document::asciidoc::convert(input, options, &mut sink)?,
3658 SourceFormat::Arff => crate::document::arff::convert(input, options, &mut sink)?,
3659 SourceFormat::Bed => crate::document::bed::convert(
3660 input,
3661 options,
3662 &mut sink,
3663 crate::document::bed::BedFormat::Bed,
3664 )?,
3665 SourceFormat::BedGraph => crate::document::bed::convert(
3666 input,
3667 options,
3668 &mut sink,
3669 crate::document::bed::BedFormat::BedGraph,
3670 )?,
3671 SourceFormat::Rst => crate::document::rst::convert(input, options, &mut sink)?,
3672 SourceFormat::Sam => crate::document::sam::convert(input, options, &mut sink)?,
3673 SourceFormat::S1000d => crate::document::s1000d::convert(input, options, &mut sink)?,
3674 SourceFormat::Maf => crate::document::maf::convert(input, options, &mut sink)?,
3675 SourceFormat::Marcxml => crate::document::marcxml::convert(input, options, &mut sink)?,
3676 SourceFormat::Marc => crate::document::marc::convert(input, options, &mut sink)?,
3677 SourceFormat::Mods => crate::document::mods::convert(input, options, &mut sink)?,
3678 SourceFormat::Newick => crate::document::newick::convert(input, options, &mut sink)?,
3679 SourceFormat::Stockholm => crate::document::stockholm::convert(input, options, &mut sink)?,
3680 SourceFormat::Clustal => crate::document::clustal::convert(input, options, &mut sink)?,
3681 SourceFormat::Nexus => crate::document::nexus::convert(input, options, &mut sink)?,
3682 SourceFormat::Genbank => crate::document::genbank::convert(input, options, &mut sink)?,
3683 SourceFormat::Embl => crate::document::embl::convert(input, options, &mut sink)?,
3684 SourceFormat::Uniprot => crate::document::uniprot::convert(input, options, &mut sink)?,
3685 SourceFormat::Ris => crate::document::ris::convert(input, options, &mut sink)?,
3686 SourceFormat::Spice => crate::cad::spice::convert(input, options, &mut sink)?,
3687 SourceFormat::Openapi => crate::document::openapi::convert(input, options, &mut sink)?,
3688 SourceFormat::Asyncapi => crate::document::asyncapi::convert(input, options, &mut sink)?,
3689 SourceFormat::Alto => crate::document::alto::convert(input, options, &mut sink)?,
3690 SourceFormat::Wsdl => crate::document::wsdl::convert(input, options, &mut sink)?,
3691 SourceFormat::Opml => crate::document::opml::convert(input, options, &mut sink)?,
3692 SourceFormat::JsonSchema => {
3693 crate::document::jsonschema::convert(input, options, &mut sink)?
3694 }
3695 SourceFormat::Kubernetes => {
3696 crate::document::kubernetes::convert(input, options, &mut sink)?
3697 }
3698 SourceFormat::Compose => crate::document::compose::convert(input, options, &mut sink)?,
3699 SourceFormat::GithubActions => {
3700 crate::document::github_actions::convert(input, options, &mut sink)?
3701 }
3702 SourceFormat::Junit => crate::document::junit::convert(input, options, &mut sink)?,
3703 SourceFormat::Sarif => crate::document::sarif::convert(input, options, &mut sink)?,
3704 SourceFormat::TerraformPlan => {
3705 crate::document::terraform::convert(input, options, &mut sink)?
3706 }
3707 SourceFormat::CycloneDx => crate::document::cyclonedx::convert(input, options, &mut sink)?,
3708 SourceFormat::Spdx => crate::document::spdx::convert(input, options, &mut sink)?,
3709 SourceFormat::StixJson => crate::document::stix::convert(input, options, &mut sink)?,
3710 SourceFormat::TaxiiJson => crate::document::taxii::convert(input, options, &mut sink)?,
3711 SourceFormat::Coverage => crate::document::coverage::convert(input, options, &mut sink)?,
3712 SourceFormat::Lcov => crate::document::lcov::convert(input, options, &mut sink)?,
3713 SourceFormat::JsonPatch => crate::document::jsonpatch::convert(input, options, &mut sink)?,
3714 SourceFormat::JsonMergePatch => {
3715 crate::document::jsonmergepatch::convert(input, options, &mut sink)?
3716 }
3717 SourceFormat::JsonFeed => crate::document::jsonfeed::convert(input, options, &mut sink)?,
3718 SourceFormat::CloudEvents => {
3719 crate::document::cloudevents::convert(input, options, &mut sink)?
3720 }
3721 SourceFormat::FhirJson => crate::document::fhir::convert(input, options, &mut sink)?,
3722 SourceFormat::FhirXml => crate::document::fhir_xml::convert(input, options, &mut sink)?,
3723 SourceFormat::Avro => crate::document::avro::convert(input, options, &mut sink)?,
3724 SourceFormat::OtlpJson => crate::document::otlp::convert(input, options, &mut sink)?,
3725 SourceFormat::OcelJson => crate::document::ocel::convert(input, options, &mut sink)?,
3726 SourceFormat::JsonApi => crate::document::jsonapi::convert(input, options, &mut sink)?,
3727 SourceFormat::CslJson => crate::document::csl_json::convert(input, options, &mut sink)?,
3728 SourceFormat::Org => crate::document::orgmode::convert(input, options, &mut sink)?,
3729 SourceFormat::Po => crate::document::po::convert(input, options, &mut sink)?,
3730 SourceFormat::Bib => crate::document::bib::convert(input, options, &mut sink)?,
3731 SourceFormat::Srt => crate::document::subtitle::convert(
3732 input,
3733 options,
3734 &mut sink,
3735 crate::document::subtitle::SubtitleKind::Srt,
3736 )?,
3737 SourceFormat::Ttml => crate::document::ttml::convert(input, options, &mut sink)?,
3738 SourceFormat::Xliff => crate::document::xliff::convert(input, options, &mut sink)?,
3739 SourceFormat::Vtt => crate::document::subtitle::convert(
3740 input,
3741 options,
3742 &mut sink,
3743 crate::document::subtitle::SubtitleKind::Vtt,
3744 )?,
3745 SourceFormat::Csv => crate::table::convert(input, options, &mut sink)?,
3746 SourceFormat::Chart => crate::chart::convert(input, options, &mut sink)?,
3747 SourceFormat::Json => crate::document::json::convert(input, options, &mut sink)?,
3748 SourceFormat::JsonLd => crate::document::jsonld::convert(input, options, &mut sink)?,
3749 SourceFormat::Iiif => crate::document::iiif::convert(input, options, &mut sink)?,
3750 SourceFormat::JsonSeq => crate::document::json_seq::convert(input, options, &mut sink)?,
3751 SourceFormat::Graphml => crate::document::graphml::convert(input, options, &mut sink)?,
3752 SourceFormat::Gexf => crate::document::gexf::convert(input, options, &mut sink)?,
3753 SourceFormat::Xgmml => crate::document::xgmml::convert(input, options, &mut sink)?,
3754 SourceFormat::Toml => crate::document::toml::convert(input, options, &mut sink)?,
3755 SourceFormat::Yaml => crate::document::yaml::convert(input, options, &mut sink)?,
3756 SourceFormat::Properties => {
3757 crate::document::properties::convert(input, options, &mut sink)?
3758 }
3759 SourceFormat::Plist => crate::document::plist::convert(input, options, &mut sink)?,
3760 SourceFormat::Premis => crate::document::premis::convert(input, options, &mut sink)?,
3761 SourceFormat::Tex => crate::math::convert(input, options, &mut sink)?,
3762 SourceFormat::Qr => crate::qr::convert(input, options, &mut sink)?,
3763 SourceFormat::Raster => crate::vectorize::convert(input, options, &mut sink)?,
3764 SourceFormat::Emf => crate::metafile::convert(input, options, &mut sink)?,
3765 SourceFormat::Svg => crate::svg::convert(input, options, &mut sink)?,
3766 };
3767 let pages = sink.finish()?;
3768 let largest_page_ir_bytes = pages
3769 .iter()
3770 .map(|page| page.estimated_ir_bytes)
3771 .max()
3772 .unwrap_or(0);
3773 let report = ConversionReport {
3774 converter: "document-svg",
3775 version: env!("CARGO_PKG_VERSION"),
3776 source: input.to_string_lossy().into_owned(),
3777 source_format: format,
3778 output_directory: output_directory.to_string_lossy().into_owned(),
3779 elapsed_ms: started.elapsed().as_millis(),
3780 input_bytes: metadata.len(),
3781 page_count: pages.len(),
3782 largest_page_ir_bytes,
3783 pages,
3784 warnings,
3785 };
3786 let manifest_path = output_directory.join("conversion.json");
3787 let mut temporary = tempfile::NamedTempFile::new_in(output_directory)?;
3788 {
3789 let mut writer = BufWriter::new(temporary.as_file_mut());
3790 serde_json::to_writer_pretty(&mut writer, &report)?;
3791 writer.write_all(b"\n")?;
3792 writer.flush()?;
3793 }
3794 temporary
3795 .persist_noclobber(manifest_path)
3796 .map_err(|error| error.error)?;
3797 Ok(report)
3798}
3799
3800#[derive(Clone, Copy, Debug)]
3804pub struct ByteConvertLimits {
3805 pub max_page_svg_bytes: usize,
3806 pub max_total_svg_bytes: usize,
3807 pub max_page_text_spans: usize,
3808 pub max_total_text_spans: usize,
3809 pub max_total_text_bytes: usize,
3810}
3811
3812impl Default for ByteConvertLimits {
3813 fn default() -> Self {
3814 Self {
3815 max_page_svg_bytes: 64 * 1024 * 1024,
3816 max_total_svg_bytes: 256 * 1024 * 1024,
3817 max_page_text_spans: 5_000,
3818 max_total_text_spans: 50_000,
3819 max_total_text_bytes: 8 * 1024 * 1024,
3820 }
3821 }
3822}
3823
3824#[derive(Clone, Debug, Serialize)]
3826pub struct SvgPage {
3827 pub number: usize,
3828 pub svg: String,
3829 pub width_points: f64,
3830 pub height_points: f64,
3831 pub node_count: usize,
3832 pub warning_count: usize,
3833 pub warnings: Vec<String>,
3834 pub estimated_ir_bytes: usize,
3835 pub text_spans: Vec<TextSpan>,
3836}
3837
3838#[derive(Clone, Debug, Serialize)]
3841pub struct TextSpan {
3842 pub text: String,
3843 pub x: f64,
3844 pub y: f64,
3845 pub width: f64,
3846 pub height: f64,
3847 pub font_size: f64,
3848 pub transform: Matrix,
3849}
3850
3851#[derive(Clone, Debug, Serialize)]
3853pub struct ByteConversionReport {
3854 pub converter: &'static str,
3855 pub version: &'static str,
3856 pub source: String,
3857 pub source_format: SourceFormat,
3858 pub elapsed_ms: u128,
3859 pub input_bytes: u64,
3860 pub page_count: usize,
3861 pub largest_page_ir_bytes: usize,
3862 pub warnings: Vec<String>,
3863}
3864
3865pub fn convert_bytes<F>(
3869 file_name: &str,
3870 bytes: &[u8],
3871 options: &ConvertOptions,
3872 limits: ByteConvertLimits,
3873 mut on_page: F,
3874) -> Result<ByteConversionReport>
3875where
3876 F: FnMut(SvgPage) -> Result<()>,
3877{
3878 let started = Instant::now();
3879 if bytes.len() as u64 > options.max_input_bytes {
3880 return Err(Error::LimitExceeded(format!(
3881 "input is {} bytes; maximum is {} bytes",
3882 bytes.len(),
3883 options.max_input_bytes
3884 )));
3885 }
3886 if options.jobs == 0 {
3887 return Err(Error::InvalidInput("jobs must be at least 1".into()));
3888 }
3889 if limits.max_page_svg_bytes == 0
3890 || limits.max_total_svg_bytes == 0
3891 || limits.max_page_text_spans == 0
3892 || limits.max_total_text_spans == 0
3893 || limits.max_total_text_bytes == 0
3894 {
3895 return Err(Error::InvalidInput(
3896 "SVG and text-layer output limits must be greater than zero".into(),
3897 ));
3898 }
3899 let format = detect_in_memory_format(file_name, bytes)?;
3900 let mut sink = BytePageSink::new(options, limits, &mut on_page);
3901 let warnings = match format {
3902 SourceFormat::Pdf => crate::pdf::convert_bytes(bytes, options, &mut sink)?,
3903 SourceFormat::Docx => crate::ooxml::docx::convert_bytes(bytes, options, &mut sink)?,
3904 SourceFormat::Xlsx => crate::ooxml::xlsx::convert_bytes(bytes, options, &mut sink)?,
3905 SourceFormat::Pptx => crate::ooxml::pptx::convert_bytes(bytes, options, &mut sink)?,
3906 SourceFormat::FlatOpc => {
3907 crate::document::flatopc::convert_bytes(bytes, options, &mut sink)?
3908 }
3909 _ => {
3910 return Err(Error::Unsupported(format!(
3911 "in-memory preview does not support {}",
3912 format
3913 )));
3914 }
3915 };
3916 let (page_count, largest_page_ir_bytes, emitted_warnings) = sink.finish()?;
3917 let mut warnings = warnings;
3918 warnings.extend(emitted_warnings);
3919 warnings.sort();
3920 warnings.dedup();
3921 Ok(ByteConversionReport {
3922 converter: "document-svg",
3923 version: env!("CARGO_PKG_VERSION"),
3924 source: file_name.to_owned(),
3925 source_format: format,
3926 elapsed_ms: started.elapsed().as_millis(),
3927 input_bytes: bytes.len() as u64,
3928 page_count,
3929 largest_page_ir_bytes,
3930 warnings,
3931 })
3932}
3933
3934fn detect_in_memory_format(file_name: &str, bytes: &[u8]) -> Result<SourceFormat> {
3935 if bytes.starts_with(b"%PDF-") {
3936 return Ok(SourceFormat::Pdf);
3937 }
3938 let extension = Path::new(file_name)
3939 .extension()
3940 .and_then(|value| value.to_str())
3941 .unwrap_or_default()
3942 .to_ascii_lowercase();
3943 match extension.as_str() {
3944 "pdf" => Ok(SourceFormat::Pdf),
3945 "docx" | "docm" | "dotx" | "dotm" => Ok(SourceFormat::Docx),
3946 "xlsx" | "xlsm" | "xltx" | "xltm" | "xlam" => Ok(SourceFormat::Xlsx),
3947 "pptx" | "pptm" | "potx" | "potm" | "ppsx" | "ppam" | "sldx" | "sldm" => {
3948 Ok(SourceFormat::Pptx)
3949 }
3950 "flatopc" | "fopc" => Ok(SourceFormat::FlatOpc),
3951 _ => {
3952 if crate::document::flatopc::looks_like_prefix(bytes) {
3953 return Ok(SourceFormat::FlatOpc);
3954 }
3955 let package = crate::ooxml::ZipPackage::from_reader(Cursor::new(bytes), 1024 * 1024)?;
3956 let mut package = package;
3957 if package.contains("word/document.xml") {
3958 Ok(SourceFormat::Docx)
3959 } else if package.contains("xl/workbook.xml") {
3960 Ok(SourceFormat::Xlsx)
3961 } else if package.contains("ppt/presentation.xml") {
3962 Ok(SourceFormat::Pptx)
3963 } else {
3964 Err(Error::Unsupported(format!(
3965 "cannot determine a supported in-memory format for {file_name:?}"
3966 )))
3967 }
3968 }
3969 }
3970}
3971
3972struct BytePageSink<'a, F> {
3973 svg_options: SvgOptions,
3974 max_pages: usize,
3975 max_page_svg_bytes: usize,
3976 max_total_svg_bytes: usize,
3977 total_svg_bytes: usize,
3978 max_page_text_spans: usize,
3979 max_total_text_spans: usize,
3980 max_total_text_bytes: usize,
3981 total_text_spans: usize,
3982 total_text_bytes: usize,
3983 pages_emitted: usize,
3984 largest_page_ir_bytes: usize,
3985 warnings: Vec<String>,
3986 on_page: &'a mut F,
3987}
3988
3989impl<'a, F> BytePageSink<'a, F> {
3990 fn new(options: &ConvertOptions, limits: ByteConvertLimits, on_page: &'a mut F) -> Self {
3991 Self {
3992 svg_options: SvgOptions {
3993 include_metadata: options.include_metadata,
3994 precision: options.precision,
3995 },
3996 max_pages: options.max_pages,
3997 max_page_svg_bytes: limits.max_page_svg_bytes,
3998 max_total_svg_bytes: limits.max_total_svg_bytes,
3999 total_svg_bytes: 0,
4000 max_page_text_spans: limits.max_page_text_spans,
4001 max_total_text_spans: limits.max_total_text_spans,
4002 max_total_text_bytes: limits.max_total_text_bytes,
4003 total_text_spans: 0,
4004 total_text_bytes: 0,
4005 pages_emitted: 0,
4006 largest_page_ir_bytes: 0,
4007 warnings: Vec::new(),
4008 on_page,
4009 }
4010 }
4011
4012 fn finish(self) -> Result<(usize, usize, Vec<String>)> {
4013 if self.pages_emitted == 0 {
4014 return Err(Error::InvalidInput(
4015 "input contains no renderable pages".into(),
4016 ));
4017 }
4018 Ok((
4019 self.pages_emitted,
4020 self.largest_page_ir_bytes,
4021 self.warnings,
4022 ))
4023 }
4024}
4025
4026impl<F> PageConsumer for BytePageSink<'_, F>
4027where
4028 F: FnMut(SvgPage) -> Result<()>,
4029{
4030 fn consume(&mut self, page: Page) -> Result<()> {
4031 if self.pages_emitted >= self.max_pages {
4032 return Err(Error::LimitExceeded(format!(
4033 "page count exceeds {}",
4034 self.max_pages
4035 )));
4036 }
4037 let estimated_ir_bytes = estimate_page_bytes(&page);
4038 let remaining_spans = self
4039 .max_total_text_spans
4040 .saturating_sub(self.total_text_spans)
4041 .min(self.max_page_text_spans);
4042 let remaining_text_bytes = self
4043 .max_total_text_bytes
4044 .saturating_sub(self.total_text_bytes);
4045 let (text_spans, text_was_truncated) =
4046 extract_text_spans(&page, remaining_spans, remaining_text_bytes);
4047 let text_bytes = text_spans.iter().map(|span| span.text.len()).sum::<usize>();
4048 self.total_text_spans += text_spans.len();
4049 self.total_text_bytes += text_bytes;
4050 self.largest_page_ir_bytes = self.largest_page_ir_bytes.max(estimated_ir_bytes);
4051 let mut page_warnings = page.warnings.clone();
4052 if text_was_truncated {
4053 page_warnings.push(
4054 "browser text selection/search layer reached its safety limit; later text may not be searchable or selectable".into(),
4055 );
4056 }
4057 self.warnings.extend(page_warnings.iter().cloned());
4058 let mut writer = LimitedBuffer::new(self.max_page_svg_bytes);
4059 let write_result = write_page(&page, &mut writer, self.svg_options);
4060 if writer.exceeded {
4061 return Err(Error::LimitExceeded(format!(
4062 "SVG page {} exceeds the {} byte preview limit",
4063 page.number, self.max_page_svg_bytes
4064 )));
4065 }
4066 write_result?;
4067 let svg_bytes = writer.bytes;
4068 let new_total = self
4069 .total_svg_bytes
4070 .checked_add(svg_bytes.len())
4071 .ok_or_else(|| Error::LimitExceeded("total SVG output size overflowed".into()))?;
4072 if new_total > self.max_total_svg_bytes {
4073 return Err(Error::LimitExceeded(format!(
4074 "SVG output exceeds the {} byte total preview limit",
4075 self.max_total_svg_bytes
4076 )));
4077 }
4078 let svg = String::from_utf8(svg_bytes).map_err(|error| {
4079 Error::InvalidInput(format!("SVG writer produced invalid UTF-8: {error}"))
4080 })?;
4081 self.total_svg_bytes = new_total;
4082 self.pages_emitted += 1;
4083 (self.on_page)(SvgPage {
4084 number: page.number,
4085 svg,
4086 width_points: page.width,
4087 height_points: page.height,
4088 node_count: page.nodes.len(),
4089 warning_count: page_warnings.len(),
4090 warnings: page_warnings,
4091 estimated_ir_bytes,
4092 text_spans,
4093 })
4094 }
4095}
4096
4097fn extract_text_spans(
4098 page: &Page,
4099 max_spans: usize,
4100 max_text_bytes: usize,
4101) -> (Vec<TextSpan>, bool) {
4102 fn visit(
4103 nodes: &[Node],
4104 parent_transform: Matrix,
4105 output: &mut Vec<TextSpan>,
4106 text_bytes: &mut usize,
4107 max_spans: usize,
4108 max_text_bytes: usize,
4109 truncated: &mut bool,
4110 ) {
4111 for node in nodes {
4112 if *truncated {
4113 return;
4114 }
4115 match node {
4116 Node::Text {
4117 x,
4118 y,
4119 runs,
4120 anchor,
4121 transform,
4122 ..
4123 } => {
4124 let text = runs.iter().map(|run| run.text.as_str()).collect::<String>();
4125 if text.is_empty() {
4126 continue;
4127 }
4128 let Some(next_text_bytes) = text_bytes.checked_add(text.len()) else {
4129 *truncated = true;
4130 return;
4131 };
4132 if output.len() >= max_spans || next_text_bytes > max_text_bytes {
4133 *truncated = true;
4134 return;
4135 }
4136 *text_bytes = next_text_bytes;
4137 let width = runs.iter().map(estimated_run_width).sum::<f64>();
4138 let font_size = runs
4139 .iter()
4140 .map(|run| run.font_size)
4141 .filter(|size| size.is_finite() && *size > 0.0)
4142 .fold(0.0_f64, f64::max)
4143 .max(1.0);
4144 let left = match anchor {
4145 TextAnchor::Start => *x,
4146 TextAnchor::Middle => *x - width / 2.0,
4147 TextAnchor::End => *x - width,
4148 };
4149 output.push(TextSpan {
4150 text,
4151 x: left,
4152 y: *y - font_size * 0.85,
4153 width: width.max(font_size * 0.25),
4154 height: font_size * 1.35,
4155 font_size,
4156 transform: compose(parent_transform, *transform),
4157 });
4158 }
4159 Node::Group {
4160 nodes, transform, ..
4161 } => visit(
4162 nodes,
4163 compose(parent_transform, *transform),
4164 output,
4165 text_bytes,
4166 max_spans,
4167 max_text_bytes,
4168 truncated,
4169 ),
4170 Node::Path { .. } | Node::Image { .. } => {}
4171 }
4172 }
4173 }
4174
4175 fn estimated_run_width(run: &crate::ir::TextRun) -> f64 {
4176 if let Some(width) = run
4177 .target_advance
4178 .filter(|width| width.is_finite() && *width > 0.0)
4179 {
4180 return width;
4181 }
4182 if run.glyph_x_offsets.len() > 1 {
4183 let first = run.glyph_x_offsets[0];
4184 let last = *run.glyph_x_offsets.last().unwrap_or(&first);
4185 let trailing = run.font_size * 0.55;
4186 return (last - first).abs() + trailing;
4187 }
4188 run.text
4189 .chars()
4190 .map(|character| {
4191 let code = u32::from(character);
4192 if matches!(code, 0x2e80..=0x9fff | 0xac00..=0xd7af | 0xf900..=0xfaff | 0x1f000..=0x1faff) {
4193 run.font_size
4194 } else if character.is_whitespace() {
4195 run.font_size * 0.3
4196 } else {
4197 run.font_size * 0.55
4198 }
4199 })
4200 .sum()
4201 }
4202
4203 let mut spans = Vec::new();
4204 let mut text_bytes = 0;
4205 let mut truncated = false;
4206 visit(
4207 &page.nodes,
4208 IDENTITY,
4209 &mut spans,
4210 &mut text_bytes,
4211 max_spans,
4212 max_text_bytes,
4213 &mut truncated,
4214 );
4215 (spans, truncated)
4216}
4217
4218struct LimitedBuffer {
4219 bytes: Vec<u8>,
4220 limit: usize,
4221 exceeded: bool,
4222}
4223
4224impl LimitedBuffer {
4225 fn new(limit: usize) -> Self {
4226 Self {
4227 bytes: Vec::with_capacity(limit.min(64 * 1024)),
4228 limit,
4229 exceeded: false,
4230 }
4231 }
4232}
4233
4234impl Write for LimitedBuffer {
4235 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
4236 if self
4237 .bytes
4238 .len()
4239 .checked_add(bytes.len())
4240 .is_none_or(|size| size > self.limit)
4241 {
4242 self.exceeded = true;
4243 return Err(std::io::Error::other("SVG output limit exceeded"));
4244 }
4245 self.bytes.extend_from_slice(bytes);
4246 Ok(bytes.len())
4247 }
4248
4249 fn flush(&mut self) -> std::io::Result<()> {
4250 Ok(())
4251 }
4252}
4253
4254pub(crate) trait PageConsumer {
4255 fn consume(&mut self, page: Page) -> Result<()>;
4256}
4257
4258pub(crate) struct PageSink<'a> {
4259 output_directory: &'a Path,
4260 svg_options: SvgOptions,
4261 reports: Vec<PageReport>,
4262 max_pages: usize,
4263}
4264
4265impl<'a> PageSink<'a> {
4266 fn new(output_directory: &'a Path, options: &ConvertOptions) -> Self {
4267 let reports = Vec::with_capacity(options.max_pages.min(1024));
4268 Self {
4269 output_directory,
4270 svg_options: SvgOptions {
4271 include_metadata: options.include_metadata,
4272 precision: options.precision,
4273 },
4274 reports,
4275 max_pages: options.max_pages,
4276 }
4277 }
4278
4279 fn finish(self) -> Result<Vec<PageReport>> {
4280 if self.reports.is_empty() {
4281 return Err(Error::InvalidInput(
4282 "input contains no renderable pages".into(),
4283 ));
4284 }
4285 Ok(self.reports)
4286 }
4287}
4288
4289impl PageConsumer for PageSink<'_> {
4290 fn consume(&mut self, page: Page) -> Result<()> {
4291 if self.reports.len() >= self.max_pages {
4292 return Err(Error::LimitExceeded(format!(
4293 "page count exceeds {}",
4294 self.max_pages
4295 )));
4296 }
4297 let file_name = format!("page-{:04}.svg", page.number);
4298 let final_path = self.output_directory.join(&file_name);
4299 let mut temporary = tempfile::NamedTempFile::new_in(self.output_directory)?;
4300 {
4301 let mut writer = BufWriter::with_capacity(64 * 1024, temporary.as_file_mut());
4302 write_page(&page, &mut writer, self.svg_options)?;
4303 writer.flush()?;
4304 }
4305 temporary
4306 .persist_noclobber(&final_path)
4307 .map_err(|error| error.error)?;
4308 let estimated_ir_bytes = estimate_page_bytes(&page);
4309 self.reports.push(PageReport {
4310 number: page.number,
4311 svg: file_name,
4312 width_points: page.width,
4313 height_points: page.height,
4314 node_count: page.nodes.len(),
4315 warning_count: page.warnings.len(),
4316 warnings: page.warnings,
4317 estimated_ir_bytes,
4318 });
4319 Ok(())
4320 }
4321}
4322
4323fn estimate_page_bytes(page: &Page) -> usize {
4324 let mut counter = ByteCounter::default();
4327 serde_json::to_writer(&mut counter, page).map_or(0, |()| counter.0)
4328}
4329
4330#[derive(Default)]
4331struct ByteCounter(usize);
4332
4333impl Write for ByteCounter {
4334 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
4335 self.0 = self
4336 .0
4337 .checked_add(bytes.len())
4338 .ok_or_else(|| std::io::Error::other("serialized page size overflow"))?;
4339 Ok(bytes.len())
4340 }
4341
4342 fn flush(&mut self) -> std::io::Result<()> {
4343 Ok(())
4344 }
4345}
4346
4347#[cfg(test)]
4348mod tests {
4349 use super::*;
4350
4351 #[test]
4352 fn format_sniffing_reads_only_a_small_prefix_of_large_json() {
4353 let directory = tempfile::tempdir().unwrap();
4354 let input = directory.path().join("large.json");
4355 let mut file = std::fs::File::create(&input).unwrap();
4356 file.write_all(br#"{"type":"bar","labels":[],"series":[]}"#)
4357 .unwrap();
4358 file.set_len(512 * 1024 * 1024).unwrap();
4359
4360 assert_eq!(
4361 read_format_prefix(&input).unwrap().len(),
4362 FORMAT_SNIFF_BYTES as usize
4363 );
4364 assert_eq!(SourceFormat::detect(&input).unwrap(), SourceFormat::Chart);
4365 }
4366
4367 #[test]
4368 fn limited_file_reader_stops_at_the_configured_budget() {
4369 let directory = tempfile::tempdir().unwrap();
4370 let input = directory.path().join("growing.txt");
4371 fs::write(&input, b"12345").unwrap();
4372
4373 assert!(matches!(
4374 read_limited_file(&input, 4, "test input"),
4375 Err(Error::LimitExceeded(message)) if message.contains("test input")
4376 ));
4377 assert_eq!(
4378 read_limited_file(&input, 5, "test input").unwrap(),
4379 b"12345"
4380 );
4381 }
4382
4383 #[test]
4384 fn page_size_counter_matches_serialized_bytes_without_retaining_json() {
4385 let mut page = Page::new(1, 612.0, 792.0, "test");
4386 page.title = "日本語 / 中文 / quotes: \" \\ \n".into();
4387 page.description = "large path or image data".repeat(100_000);
4388 page.warnings.push("control: \t\r".into());
4389 assert_eq!(
4390 estimate_page_bytes(&page),
4391 serde_json::to_vec(&page).unwrap().len()
4392 );
4393 }
4394}