cloudiful_docling_convert/processor/service/
output.rs1use 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}