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::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}