Skip to main content

document_svg/
convert.rs

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` is also Graphviz; identify an OLE Word template by
964                // its WordDocument stream before falling back to Graphviz.
965                "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` is used by many unrelated binary/text formats; only
1256                // claim it after the conservative Tecplot signature check.
1257                "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        // Content-based sniffing fallback when extension is missing, unknown, or generic
1573        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        // EMF (Enhanced Metafile): 0x00000001 at 0..4 and " EMF" (0x464D4520) at 40..44
2270        if buf.len() >= 44 && buf[0..4] == [0x01, 0x00, 0x00, 0x00] && &buf[40..44] == b" EMF" {
2271            return Ok(Self::Emf);
2272        }
2273        // WMF (Aldus Placeable Metafile header key 0x9AC6CDD7 or standard WMF header)
2274        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        // Text heuristics
2284        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    /// Maximum expanded PDF stream or ZIP entry/image bytes.
3223    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    /// Render embedded PDF fonts as exact glyph outlines instead of editable
3230    /// substitute-font text. This improves fidelity but requires the caller to
3231    /// verify the source font's outline/embedding rights.
3232    pub outline_embedded_pdf_text: bool,
3233    /// draw.io shape libraries to draw `shape=mxgraph.<library>.<name>` with.
3234    ///
3235    /// Each entry is a stencil XML file or a directory of them, as shipped in
3236    /// draw.io's own `stencils` folder. Without one, a shape from a library is
3237    /// drawn as a labelled placeholder, because the outline lives in the
3238    /// library rather than in the diagram. A shape the diagram carries inline,
3239    /// as `shape=stencil(...)`, is always drawn.
3240    pub stencil_paths: Vec<std::path::PathBuf>,
3241    /// Keep a copy of a draw.io page's own source in the SVG it produces, in
3242    /// the `content` attribute draw.io itself uses.
3243    ///
3244    /// The SVG stays a picture for every renderer, and both draw.io and
3245    /// [`crate::svg_to_document`] can restore the editable diagram from it.
3246    /// Off by default: it roughly doubles the output and puts the source
3247    /// document inside a file that is usually shared as an image.
3248    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/// Output bounds for [`convert_bytes`]. These limits cover generated SVG and
3801/// selectable/searchable text geometry; input, expanded ZIP parts, XML events,
3802/// and page count are bounded by [`ConvertOptions`].
3803#[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/// One self-contained SVG page produced by [`convert_bytes`].
3825#[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/// Approximate selectable/searchable text geometry associated with a page.
3839/// Coordinates and transforms use SVG user units (points).
3840#[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/// Document-level conversion details returned after all pages are emitted.
3852#[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
3865/// Convert a supported document directly from memory and emit completed pages
3866/// as soon as they are rendered. This API is designed for browser/WASM and
3867/// server preview callers that should not need temporary files.
3868pub 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    // Keep the existing serialized-size metric without allocating a second
4325    // copy of image data and paths for every page.
4326    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}