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}