Skip to main content

cloudiful_docling_convert/document/
input_kind.rs

1use std::path::Path;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{PdfConvertError, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InputKind {
11    Pdf,
12    Docx,
13    Pptx,
14    Html,
15    Asciidoc,
16    Markdown,
17    Csv,
18    Xlsx,
19    Odt,
20    Ods,
21    Odp,
22    Epub,
23    Email,
24    Image,
25    XmlUspto,
26    XmlJats,
27    XmlXbrl,
28    XmlDoclang,
29    MetsGbs,
30    JsonDocling,
31    Latex,
32    Text,
33}
34
35impl InputKind {
36    pub fn from_path(path: &Path) -> Option<Self> {
37        let file_name = path.file_name().and_then(|name| name.to_str())?;
38        Self::from_filename_and_media_type(file_name, None)
39    }
40
41    pub fn from_filename_and_media_type(filename: &str, media_type: Option<&str>) -> Option<Self> {
42        let ext = normalized_extension(filename);
43        let media_type = normalized_media_type(media_type);
44
45        match (ext.as_deref(), media_type.as_deref()) {
46            (Some("pdf"), _) | (_, Some("application/pdf")) => Some(Self::Pdf),
47            (Some("docx"), _)
48            | (
49                _,
50                Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
51            ) => Some(Self::Docx),
52            (Some("pptx"), _)
53            | (
54                _,
55                Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
56            ) => Some(Self::Pptx),
57            (Some("html"), _)
58            | (Some("htm"), _)
59            | (Some("xhtml"), _)
60            | (_, Some("text/html"))
61            | (_, Some("application/xhtml+xml")) => Some(Self::Html),
62            (Some("adoc"), _)
63            | (Some("asciidoc"), _)
64            | (Some("asc"), _)
65            | (_, Some("text/asciidoc"))
66            | (_, Some("text/x-asciidoc")) => Some(Self::Asciidoc),
67            (Some("md"), _)
68            | (Some("markdown"), _)
69            | (_, Some("text/markdown"))
70            | (_, Some("text/x-markdown")) => Some(Self::Markdown),
71            (Some("csv"), _) | (_, Some("text/csv")) | (_, Some("application/csv")) => {
72                Some(Self::Csv)
73            }
74            (Some("xlsx"), _)
75            | (_, Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) => {
76                Some(Self::Xlsx)
77            }
78            (Some("odt"), _) | (_, Some("application/vnd.oasis.opendocument.text")) => {
79                Some(Self::Odt)
80            }
81            (Some("ods"), _) | (_, Some("application/vnd.oasis.opendocument.spreadsheet")) => {
82                Some(Self::Ods)
83            }
84            (Some("odp"), _) | (_, Some("application/vnd.oasis.opendocument.presentation")) => {
85                Some(Self::Odp)
86            }
87            (Some("epub"), _) | (_, Some("application/epub+zip")) => Some(Self::Epub),
88            (Some("eml"), _)
89            | (Some("msg"), _)
90            | (_, Some("message/rfc822"))
91            | (_, Some("application/vnd.ms-outlook")) => Some(Self::Email),
92            (Some("png"), _)
93            | (Some("jpg"), _)
94            | (Some("jpeg"), _)
95            | (Some("gif"), _)
96            | (Some("bmp"), _)
97            | (Some("tif"), _)
98            | (Some("tiff"), _)
99            | (Some("webp"), _)
100            | (Some("svg"), _)
101            | (_, Some("image/png"))
102            | (_, Some("image/jpeg"))
103            | (_, Some("image/gif"))
104            | (_, Some("image/bmp"))
105            | (_, Some("image/tiff"))
106            | (_, Some("image/webp"))
107            | (_, Some("image/svg+xml")) => Some(Self::Image),
108            (Some("tex"), _)
109            | (_, Some("application/x-tex"))
110            | (_, Some("application/x-latex"))
111            | (_, Some("text/x-tex"))
112            | (_, Some("text/x-latex")) => Some(Self::Latex),
113            (Some("txt"), _) | (_, Some("text/plain")) => Some(Self::Text),
114            _ => None,
115        }
116    }
117
118    pub fn requires_explicit_override(filename: &str, media_type: Option<&str>) -> bool {
119        let ext = normalized_extension(filename);
120        let media_type = normalized_media_type(media_type);
121
122        matches!(ext.as_deref(), Some("xml") | Some("json"))
123            || matches!(
124                media_type.as_deref(),
125                Some("application/xml")
126                    | Some("text/xml")
127                    | Some("application/json")
128                    | Some("text/json")
129            )
130    }
131
132    pub fn media_type(self) -> &'static str {
133        match self {
134            Self::Pdf => "application/pdf",
135            Self::Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
136            Self::Pptx => {
137                "application/vnd.openxmlformats-officedocument.presentationml.presentation"
138            }
139            Self::Html => "text/html",
140            Self::Asciidoc => "text/asciidoc",
141            Self::Markdown => "text/markdown",
142            Self::Csv => "text/csv",
143            Self::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
144            Self::Odt => "application/vnd.oasis.opendocument.text",
145            Self::Ods => "application/vnd.oasis.opendocument.spreadsheet",
146            Self::Odp => "application/vnd.oasis.opendocument.presentation",
147            Self::Epub => "application/epub+zip",
148            Self::Email => "message/rfc822",
149            Self::Image => "image/png",
150            Self::XmlUspto | Self::XmlJats | Self::XmlXbrl | Self::XmlDoclang | Self::MetsGbs => {
151                "application/xml"
152            }
153            Self::JsonDocling => "application/json",
154            Self::Latex => "application/x-tex",
155            Self::Text => "text/plain",
156        }
157    }
158
159    pub fn canonical_media_type(self, filename: &str, media_type: Option<&str>) -> &'static str {
160        let ext = normalized_extension(filename);
161        let media_type = normalized_media_type(media_type);
162
163        match self {
164            Self::Html => match (ext.as_deref(), media_type.as_deref()) {
165                (Some("xhtml"), _) | (_, Some("application/xhtml+xml")) => "application/xhtml+xml",
166                _ => "text/html",
167            },
168            Self::Asciidoc => "text/asciidoc",
169            Self::Email => match (ext.as_deref(), media_type.as_deref()) {
170                (Some("msg"), _) | (_, Some("application/vnd.ms-outlook")) => {
171                    "application/vnd.ms-outlook"
172                }
173                _ => "message/rfc822",
174            },
175            Self::Image => match (ext.as_deref(), media_type.as_deref()) {
176                (Some("jpg"), _) | (Some("jpeg"), _) | (_, Some("image/jpeg")) => "image/jpeg",
177                (Some("gif"), _) | (_, Some("image/gif")) => "image/gif",
178                (Some("bmp"), _) | (_, Some("image/bmp")) => "image/bmp",
179                (Some("tif"), _) | (Some("tiff"), _) | (_, Some("image/tiff")) => "image/tiff",
180                (Some("webp"), _) | (_, Some("image/webp")) => "image/webp",
181                (Some("svg"), _) | (_, Some("image/svg+xml")) => "image/svg+xml",
182                _ => "image/png",
183            },
184            _ => self.media_type(),
185        }
186    }
187
188    pub fn default_extension(self) -> &'static str {
189        match self {
190            Self::Pdf => "pdf",
191            Self::Docx => "docx",
192            Self::Pptx => "pptx",
193            Self::Html => "html",
194            Self::Asciidoc => "adoc",
195            Self::Markdown => "md",
196            Self::Csv => "csv",
197            Self::Xlsx => "xlsx",
198            Self::Odt => "odt",
199            Self::Ods => "ods",
200            Self::Odp => "odp",
201            Self::Epub => "epub",
202            Self::Email => "eml",
203            Self::Image => "png",
204            Self::XmlUspto | Self::XmlJats | Self::XmlXbrl | Self::XmlDoclang | Self::MetsGbs => {
205                "xml"
206            }
207            Self::JsonDocling => "json",
208            Self::Latex => "tex",
209            Self::Text => "txt",
210        }
211    }
212
213    pub fn default_extension_for_media_type(self, media_type: Option<&str>) -> &'static str {
214        let media_type = normalized_media_type(media_type);
215
216        match self {
217            Self::Html => match media_type.as_deref() {
218                Some("application/xhtml+xml") => "xhtml",
219                _ => "html",
220            },
221            Self::Email => match media_type.as_deref() {
222                Some("application/vnd.ms-outlook") => "msg",
223                _ => "eml",
224            },
225            Self::Image => match media_type.as_deref() {
226                Some("image/jpeg") => "jpg",
227                Some("image/gif") => "gif",
228                Some("image/bmp") => "bmp",
229                Some("image/tiff") => "tiff",
230                Some("image/webp") => "webp",
231                Some("image/svg+xml") => "svg",
232                _ => "png",
233            },
234            _ => self.default_extension(),
235        }
236    }
237
238    pub fn reading_label(self) -> &'static str {
239        match self {
240            Self::Pdf => "Reading PDF...",
241            Self::Docx => "Reading DOCX...",
242            Self::Pptx => "Reading PPTX...",
243            Self::Html => "Reading HTML...",
244            Self::Asciidoc => "Reading AsciiDoc...",
245            Self::Markdown => "Reading Markdown...",
246            Self::Csv => "Reading CSV...",
247            Self::Xlsx => "Reading XLSX...",
248            Self::Odt => "Reading ODT...",
249            Self::Ods => "Reading ODS...",
250            Self::Odp => "Reading ODP...",
251            Self::Epub => "Reading EPUB...",
252            Self::Email => "Reading email...",
253            Self::Image => "Reading image...",
254            Self::XmlUspto => "Reading XML USPTO...",
255            Self::XmlJats => "Reading XML JATS...",
256            Self::XmlXbrl => "Reading XML XBRL...",
257            Self::XmlDoclang => "Reading XML Docling...",
258            Self::MetsGbs => "Reading METS GBS...",
259            Self::JsonDocling => "Reading JSON Docling...",
260            Self::Latex => "Reading LaTeX...",
261            Self::Text => "Reading text file...",
262        }
263    }
264
265    pub fn from_formats_value(self) -> &'static str {
266        match self {
267            Self::Pdf => "pdf",
268            Self::Docx => "docx",
269            Self::Pptx => "pptx",
270            Self::Html => "html",
271            Self::Asciidoc => "asciidoc",
272            Self::Markdown => "md",
273            Self::Csv => "csv",
274            Self::Xlsx => "xlsx",
275            Self::Odt => "odt",
276            Self::Ods => "ods",
277            Self::Odp => "odp",
278            Self::Epub => "epub",
279            Self::Email => "email",
280            Self::Image => "image",
281            Self::XmlUspto => "xml_uspto",
282            Self::XmlJats => "xml_jats",
283            Self::XmlXbrl => "xml_xbrl",
284            Self::XmlDoclang => "xml_doclang",
285            Self::MetsGbs => "mets_gbs",
286            Self::JsonDocling => "json_docling",
287            Self::Latex => "latex",
288            Self::Text => "text",
289        }
290    }
291
292    pub fn uses_generic_convert_options(self) -> bool {
293        !matches!(self, Self::Pdf | Self::Text)
294    }
295
296    pub fn supports_vlm(self) -> bool {
297        matches!(self, Self::Pdf | Self::Docx | Self::Markdown | Self::Image)
298    }
299}
300
301impl std::fmt::Display for InputKind {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.write_str(self.from_formats_value())
304    }
305}
306
307impl FromStr for InputKind {
308    type Err = PdfConvertError;
309
310    fn from_str(value: &str) -> Result<Self> {
311        match value.trim().to_ascii_lowercase().as_str() {
312            "pdf" => Ok(Self::Pdf),
313            "docx" => Ok(Self::Docx),
314            "pptx" => Ok(Self::Pptx),
315            "html" => Ok(Self::Html),
316            "asciidoc" | "adoc" => Ok(Self::Asciidoc),
317            "markdown" | "md" => Ok(Self::Markdown),
318            "csv" => Ok(Self::Csv),
319            "xlsx" => Ok(Self::Xlsx),
320            "odt" => Ok(Self::Odt),
321            "ods" => Ok(Self::Ods),
322            "odp" => Ok(Self::Odp),
323            "epub" => Ok(Self::Epub),
324            "email" | "eml" | "msg" => Ok(Self::Email),
325            "image" => Ok(Self::Image),
326            "xml_uspto" => Ok(Self::XmlUspto),
327            "xml_jats" => Ok(Self::XmlJats),
328            "xml_xbrl" => Ok(Self::XmlXbrl),
329            "xml_doclang" => Ok(Self::XmlDoclang),
330            "mets_gbs" => Ok(Self::MetsGbs),
331            "json_docling" => Ok(Self::JsonDocling),
332            "latex" | "tex" => Ok(Self::Latex),
333            "text" | "txt" => Ok(Self::Text),
334            other => Err(PdfConvertError::validation_error(
335                "input_format",
336                format!("unsupported input format: {}", other),
337            )),
338        }
339    }
340}
341
342fn normalized_extension(filename: &str) -> Option<String> {
343    Path::new(filename)
344        .extension()
345        .and_then(|ext| ext.to_str())
346        .map(|ext| ext.to_ascii_lowercase())
347}
348
349fn normalized_media_type(media_type: Option<&str>) -> Option<String> {
350    media_type.map(|value| {
351        value
352            .split(';')
353            .next()
354            .unwrap_or(value)
355            .trim()
356            .to_ascii_lowercase()
357    })
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn detects_first_wave_and_latex_input_kinds() {
366        assert_eq!(
367            InputKind::from_path(Path::new("a.pdf")),
368            Some(InputKind::Pdf)
369        );
370        assert_eq!(
371            InputKind::from_path(Path::new("a.docx")),
372            Some(InputKind::Docx)
373        );
374        assert_eq!(
375            InputKind::from_path(Path::new("a.pptx")),
376            Some(InputKind::Pptx)
377        );
378        assert_eq!(
379            InputKind::from_path(Path::new("a.html")),
380            Some(InputKind::Html)
381        );
382        assert_eq!(
383            InputKind::from_path(Path::new("a.adoc")),
384            Some(InputKind::Asciidoc)
385        );
386        assert_eq!(
387            InputKind::from_path(Path::new("a.md")),
388            Some(InputKind::Markdown)
389        );
390        assert_eq!(
391            InputKind::from_path(Path::new("a.csv")),
392            Some(InputKind::Csv)
393        );
394        assert_eq!(
395            InputKind::from_path(Path::new("a.xlsx")),
396            Some(InputKind::Xlsx)
397        );
398        assert_eq!(
399            InputKind::from_path(Path::new("a.odt")),
400            Some(InputKind::Odt)
401        );
402        assert_eq!(
403            InputKind::from_path(Path::new("a.ods")),
404            Some(InputKind::Ods)
405        );
406        assert_eq!(
407            InputKind::from_path(Path::new("a.odp")),
408            Some(InputKind::Odp)
409        );
410        assert_eq!(
411            InputKind::from_path(Path::new("a.epub")),
412            Some(InputKind::Epub)
413        );
414        assert_eq!(
415            InputKind::from_path(Path::new("a.eml")),
416            Some(InputKind::Email)
417        );
418        assert_eq!(
419            InputKind::from_path(Path::new("a.png")),
420            Some(InputKind::Image)
421        );
422        assert_eq!(
423            InputKind::from_path(Path::new("a.tex")),
424            Some(InputKind::Latex)
425        );
426        assert_eq!(
427            InputKind::from_path(Path::new("a.txt")),
428            Some(InputKind::Text)
429        );
430    }
431
432    #[test]
433    fn generic_xml_and_json_do_not_auto_map() {
434        assert_eq!(InputKind::from_path(Path::new("a.xml")), None);
435        assert_eq!(InputKind::from_path(Path::new("a.json")), None);
436        assert_eq!(
437            InputKind::from_filename_and_media_type("downloaded", Some("application/xml")),
438            None
439        );
440        assert_eq!(
441            InputKind::from_filename_and_media_type("downloaded", Some("application/json")),
442            None
443        );
444    }
445
446    #[test]
447    fn detects_supported_input_kinds_from_mime_type() {
448        assert_eq!(
449            InputKind::from_filename_and_media_type("upload", Some("application/pdf")),
450            Some(InputKind::Pdf)
451        );
452        assert_eq!(
453            InputKind::from_filename_and_media_type(
454                "upload",
455                Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
456            ),
457            Some(InputKind::Docx)
458        );
459        assert_eq!(
460            InputKind::from_filename_and_media_type(
461                "upload",
462                Some("application/vnd.openxmlformats-officedocument.presentationml.presentation")
463            ),
464            Some(InputKind::Pptx)
465        );
466        assert_eq!(
467            InputKind::from_filename_and_media_type("upload", Some("application/xhtml+xml")),
468            Some(InputKind::Html)
469        );
470        assert_eq!(
471            InputKind::from_filename_and_media_type("upload", Some("text/asciidoc")),
472            Some(InputKind::Asciidoc)
473        );
474        assert_eq!(
475            InputKind::from_filename_and_media_type("upload", Some("text/markdown")),
476            Some(InputKind::Markdown)
477        );
478        assert_eq!(
479            InputKind::from_filename_and_media_type("upload", Some("text/csv")),
480            Some(InputKind::Csv)
481        );
482        assert_eq!(
483            InputKind::from_filename_and_media_type(
484                "upload",
485                Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
486            ),
487            Some(InputKind::Xlsx)
488        );
489        assert_eq!(
490            InputKind::from_filename_and_media_type(
491                "upload",
492                Some("application/vnd.oasis.opendocument.text")
493            ),
494            Some(InputKind::Odt)
495        );
496        assert_eq!(
497            InputKind::from_filename_and_media_type(
498                "upload",
499                Some("application/vnd.oasis.opendocument.spreadsheet")
500            ),
501            Some(InputKind::Ods)
502        );
503        assert_eq!(
504            InputKind::from_filename_and_media_type(
505                "upload",
506                Some("application/vnd.oasis.opendocument.presentation")
507            ),
508            Some(InputKind::Odp)
509        );
510        assert_eq!(
511            InputKind::from_filename_and_media_type("upload", Some("application/epub+zip")),
512            Some(InputKind::Epub)
513        );
514        assert_eq!(
515            InputKind::from_filename_and_media_type("upload", Some("message/rfc822")),
516            Some(InputKind::Email)
517        );
518        assert_eq!(
519            InputKind::from_filename_and_media_type("upload", Some("image/webp")),
520            Some(InputKind::Image)
521        );
522        assert_eq!(
523            InputKind::from_filename_and_media_type("upload", Some("application/x-tex")),
524            Some(InputKind::Latex)
525        );
526        assert_eq!(
527            InputKind::from_filename_and_media_type("upload", Some("text/plain")),
528            Some(InputKind::Text)
529        );
530    }
531
532    #[test]
533    fn reports_override_only_sources() {
534        assert!(InputKind::requires_explicit_override("paper.xml", None));
535        assert!(InputKind::requires_explicit_override(
536            "paper",
537            Some("application/xml")
538        ));
539        assert!(InputKind::requires_explicit_override("paper.json", None));
540        assert!(InputKind::requires_explicit_override(
541            "paper",
542            Some("application/json")
543        ));
544        assert!(!InputKind::requires_explicit_override("paper.tex", None));
545    }
546
547    #[test]
548    fn derives_second_wave_defaults() {
549        assert_eq!(InputKind::XmlJats.default_extension(), "xml");
550        assert_eq!(InputKind::JsonDocling.default_extension(), "json");
551        assert_eq!(InputKind::Latex.default_extension(), "tex");
552        assert_eq!(InputKind::XmlUspto.from_formats_value(), "xml_uspto");
553        assert_eq!(InputKind::JsonDocling.from_formats_value(), "json_docling");
554        assert_eq!(InputKind::Latex.from_formats_value(), "latex");
555    }
556
557    #[test]
558    fn parses_input_format_strings() {
559        assert_eq!("xml_jats".parse::<InputKind>().unwrap(), InputKind::XmlJats);
560        assert_eq!(
561            "json_docling".parse::<InputKind>().unwrap(),
562            InputKind::JsonDocling
563        );
564        assert_eq!("latex".parse::<InputKind>().unwrap(), InputKind::Latex);
565        assert!("xml".parse::<InputKind>().is_err());
566    }
567
568    #[test]
569    fn preserves_specific_media_types() {
570        assert_eq!(
571            InputKind::Image.canonical_media_type("cover.jpg", None),
572            "image/jpeg"
573        );
574        assert_eq!(
575            InputKind::Email.canonical_media_type("message.msg", None),
576            "application/vnd.ms-outlook"
577        );
578        assert_eq!(
579            InputKind::Html.canonical_media_type("page.xhtml", None),
580            "application/xhtml+xml"
581        );
582    }
583}