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::api::DoclingResult;
8use crate::document::{
9    ConvertedDocument, ConvertedDocumentMetadata, InputDocument, InputKind, OutputFormat,
10};
11use crate::error::{PdfConvertError, Result};
12use crate::models::{ChunkDocumentResponse, ExportDocumentResponse};
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(|error| {
34                PdfConvertError::io_error(
35                    format!("creating output directory: {}", parent.display()),
36                    error,
37                )
38            })?;
39        }
40
41        let content = select_output_content(document, output_format)?;
42        fs::write(output_path, content).await.map_err(|error| {
43            PdfConvertError::io_error(
44                format!("writing output file: {}", output_path.display()),
45                error,
46            )
47        })?;
48        Ok(())
49    }
50
51    pub(super) fn build_document(
52        input: &InputDocument,
53        input_kind: InputKind,
54        result: DoclingResult,
55    ) -> ConvertedDocument {
56        let mut document = empty_document(input, input_kind);
57        match result {
58            DoclingResult::Convert(response) => {
59                document.filename = response.document.filename.clone();
60                apply_export_document(&mut document, response.document);
61                document.errors = response
62                    .errors
63                    .iter()
64                    .filter_map(|error| error.error_message.clone())
65                    .collect();
66            }
67            DoclingResult::Chunk(response) => {
68                apply_chunk_response(&mut document, response);
69            }
70            DoclingResult::Json(value) => {
71                apply_json_result(&mut document, value);
72            }
73            DoclingResult::Zip(bytes) => {
74                document.archive = Some(bytes.to_vec());
75            }
76            DoclingResult::Failure(failure) => {
77                document.errors.push(failure.failure.message);
78            }
79        }
80        document
81    }
82}
83
84fn empty_document(input: &InputDocument, input_kind: InputKind) -> ConvertedDocument {
85    ConvertedDocument {
86        filename: input.filename.clone(),
87        markdown: None,
88        text: None,
89        json: None,
90        html: None,
91        doctags: None,
92        doclang: None,
93        chunks: Vec::new(),
94        chunk_response: None,
95        archive: None,
96        metadata: ConvertedDocumentMetadata {
97            input_kind,
98            media_type: input.media_type.clone(),
99        },
100        errors: Vec::new(),
101    }
102}
103
104fn apply_export_document(document: &mut ConvertedDocument, response: ExportDocumentResponse) {
105    document.markdown = response.md_content.clone();
106    document.text = response.text_content.clone();
107    document.json = response.json_content.clone();
108    document.html = response.html_content.clone();
109    document.doctags = response.doctags_content.clone();
110    document.doclang = response.doclang_content.clone();
111}
112
113fn apply_chunk_response(document: &mut ConvertedDocument, response: ChunkDocumentResponse) {
114    document.filename = response
115        .chunks
116        .first()
117        .map(|chunk| chunk.filename.clone())
118        .or_else(|| {
119            response
120                .documents
121                .first()
122                .map(|item| item.document.filename.clone())
123        })
124        .unwrap_or_else(|| document.filename.clone());
125    document.chunks = response.chunks.clone();
126    document.errors = response
127        .documents
128        .iter()
129        .flat_map(|item| item.errors.iter())
130        .filter_map(|error| error.error_message.clone())
131        .collect();
132    if let Some(item) = response.documents.first().cloned() {
133        apply_export_document(document, item.document);
134    }
135    document.chunk_response = Some(response);
136}
137
138fn apply_json_result(document: &mut ConvertedDocument, value: Value) {
139    let source = value.get("document").unwrap_or(&value);
140    if let Some(object) = source.as_object() {
141        document.markdown = object
142            .get("md_content")
143            .and_then(Value::as_str)
144            .map(ToString::to_string);
145        document.text = object
146            .get("text_content")
147            .and_then(Value::as_str)
148            .map(ToString::to_string);
149        document.html = object
150            .get("html_content")
151            .and_then(Value::as_str)
152            .map(ToString::to_string);
153        document.doctags = object
154            .get("doctags_content")
155            .and_then(Value::as_str)
156            .map(ToString::to_string);
157        document.doclang = object
158            .get("doclang_content")
159            .and_then(Value::as_str)
160            .map(ToString::to_string);
161        document.json = object.get("json_content").cloned();
162    }
163    document.errors = collect_json_errors(&value);
164}
165
166fn collect_json_errors(value: &Value) -> Vec<String> {
167    let mut errors = Vec::new();
168    if let Some(message) = value.get("error_message").and_then(Value::as_str) {
169        errors.push(message.to_string());
170    }
171    if let Some(items) = value.get("errors").and_then(Value::as_array) {
172        errors.extend(items.iter().filter_map(|item| {
173            item.get("error_message")
174                .or_else(|| item.get("message"))
175                .and_then(Value::as_str)
176                .map(ToString::to_string)
177        }));
178    }
179    errors
180}
181
182fn select_output_content(
183    document: &ConvertedDocument,
184    output_format: OutputFormat,
185) -> Result<Vec<u8>> {
186    if output_format.is_archive() {
187        return document.archive.clone().ok_or_else(|| {
188            PdfConvertError::operation_error(
189                "writing archive",
190                "Docling did not return an application/zip response",
191            )
192        });
193    }
194
195    if output_format.is_chunk_output() {
196        return document
197            .chunk_response
198            .as_ref()
199            .map(serde_json::to_vec_pretty)
200            .transpose()
201            .map_err(PdfConvertError::from)?
202            .ok_or_else(|| {
203                PdfConvertError::operation_error(
204                    "writing chunks",
205                    "Docling did not return a chunk response",
206                )
207            });
208    }
209
210    match output_format {
211        OutputFormat::Md => text_content(document.markdown.as_deref(), "markdown"),
212        OutputFormat::Text => text_content(document.text.as_deref(), "text"),
213        OutputFormat::Html => text_content(document.html.as_deref(), "html"),
214        OutputFormat::Doctags => text_content(document.doctags.as_deref(), "doctags"),
215        OutputFormat::Doclang => text_content(document.doclang.as_deref(), "doclang"),
216        OutputFormat::Json => document
217            .json
218            .as_ref()
219            .map(serde_json::to_vec_pretty)
220            .transpose()
221            .map_err(PdfConvertError::from)?
222            .ok_or_else(|| {
223                PdfConvertError::operation_error("writing json", "JSON output is empty")
224            }),
225        OutputFormat::Yaml
226        | OutputFormat::HtmlSplitPage
227        | OutputFormat::Vtt
228        | OutputFormat::Dclx
229        | OutputFormat::Chunks => unreachable!("handled above"),
230    }
231}
232
233fn text_content(value: Option<&str>, name: &str) -> Result<Vec<u8>> {
234    value.map(|value| value.as_bytes().to_vec()).ok_or_else(|| {
235        PdfConvertError::operation_error(format!("writing {name}"), "output is empty")
236    })
237}