Skip to main content

cloudiful_docling_convert/processor/service/
output.rs

1use std::path::{Path, PathBuf};
2
3use serde_json::Value;
4use tokio::fs;
5
6use super::DocumentConverter;
7use crate::document::{
8    ConvertedChunk, ConvertedDocument, ConvertedDocumentMetadata, InputDocument, InputKind,
9    OutputFormat,
10};
11use crate::error::{PdfConvertError, Result};
12use crate::models::{Bookmark, ChunkMetadata};
13
14impl DocumentConverter {
15    pub fn calculate_output_path(
16        output_dir: &Path,
17        filename: &str,
18        output_format: OutputFormat,
19    ) -> PathBuf {
20        let stem = Path::new(filename)
21            .file_stem()
22            .and_then(|value| value.to_str())
23            .unwrap_or("output");
24        output_dir.join(format!("{}.{}", stem, output_format.extension()))
25    }
26
27    pub(super) async fn write_output_file(
28        output_path: &Path,
29        document: &ConvertedDocument,
30        output_format: OutputFormat,
31    ) -> Result<()> {
32        if let Some(parent) = output_path.parent() {
33            fs::create_dir_all(parent).await.map_err(|e| {
34                PdfConvertError::io_error(
35                    format!("creating output directory: {}", parent.display()),
36                    e,
37                )
38            })?;
39        }
40
41        let content = Self::select_output_content(document, output_format)?;
42        fs::write(output_path, content).await.map_err(|e| {
43            PdfConvertError::io_error(format!("writing output file: {}", output_path.display()), e)
44        })?;
45
46        Ok(())
47    }
48
49    fn select_output_content(
50        document: &ConvertedDocument,
51        output_format: OutputFormat,
52    ) -> Result<Vec<u8>> {
53        match output_format {
54            OutputFormat::Md => document
55                .markdown
56                .as_ref()
57                .map(|value| value.as_bytes().to_vec())
58                .ok_or_else(|| {
59                    PdfConvertError::operation_error("writing markdown", "markdown output is empty")
60                }),
61            OutputFormat::Text => document
62                .text
63                .as_ref()
64                .map(|value| value.as_bytes().to_vec())
65                .ok_or_else(|| {
66                    PdfConvertError::operation_error("writing text", "text output is empty")
67                }),
68            OutputFormat::Json => document
69                .json
70                .as_ref()
71                .map(serde_json::to_vec_pretty)
72                .transpose()
73                .map_err(PdfConvertError::from)?
74                .ok_or_else(|| {
75                    PdfConvertError::operation_error("writing json", "json output is empty")
76                }),
77        }
78    }
79
80    pub(super) fn chunk_from_result(
81        metadata: Option<ChunkMetadata>,
82        raw_result: Value,
83    ) -> ConvertedChunk {
84        ConvertedChunk {
85            metadata,
86            markdown: extract_document_field(&raw_result, "md_content"),
87            text: extract_document_field(&raw_result, "text_content"),
88            json: raw_result
89                .get("document")
90                .and_then(|document| document.get("json_content"))
91                .cloned(),
92            raw_result,
93        }
94    }
95
96    pub(super) fn assemble_document(
97        input: &InputDocument,
98        input_kind: InputKind,
99        page_count: Option<u32>,
100        outlines: Vec<Bookmark>,
101        chunks: Vec<ConvertedChunk>,
102    ) -> ConvertedDocument {
103        let markdown = join_optional_chunks(chunks.iter().map(|chunk| chunk.markdown.as_deref()));
104        let text = join_optional_chunks(chunks.iter().map(|chunk| chunk.text.as_deref()));
105        let json_values: Vec<Value> = chunks
106            .iter()
107            .filter_map(|chunk| chunk.json.clone())
108            .collect();
109        let json = match json_values.len() {
110            0 => None,
111            1 => json_values.into_iter().next(),
112            _ => Some(Value::Array(json_values)),
113        };
114
115        ConvertedDocument {
116            filename: input.filename.clone(),
117            markdown,
118            text,
119            json,
120            chunks,
121            metadata: ConvertedDocumentMetadata {
122                input_kind,
123                media_type: input.media_type.clone(),
124                page_count,
125                outlines,
126            },
127            errors: Vec::new(),
128        }
129    }
130}
131
132fn extract_document_field(raw_result: &Value, field: &str) -> Option<String> {
133    raw_result
134        .get("document")
135        .and_then(|document| document.get(field))
136        .and_then(|value| value.as_str())
137        .map(ToString::to_string)
138}
139
140fn join_optional_chunks<'a>(values: impl Iterator<Item = Option<&'a str>>) -> Option<String> {
141    let mut collected = Vec::new();
142    for value in values.flatten() {
143        collected.push(value.trim_end_matches('\n').to_string());
144    }
145
146    if collected.is_empty() {
147        None
148    } else {
149        Some(collected.join("\n"))
150    }
151}