cloudiful_docling_convert/processor/service/
mod.rs1use crate::api::DoclingClient;
2use crate::document::{
3 ConvertOptions, ConvertRequest, ConvertedDocument, ConvertedFile, FileConvertRequest, InputKind,
4};
5use crate::error::{PdfConvertError, Result};
6use crate::models::TaskStatusResponse;
7use std::future::Future;
8
9mod output;
10mod remote;
11mod text;
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Clone)]
17pub struct DocumentConverter {
18 pub(crate) docling_client: DoclingClient,
19}
20
21impl DocumentConverter {
22 pub fn new(docling_client: DoclingClient) -> Self {
23 Self { docling_client }
24 }
25
26 pub async fn submit_async(&self, request: &ConvertRequest) -> Result<String> {
30 let input_kind = request.validate()?;
31 let options = match &request.options {
32 ConvertOptions::Pdf(options) | ConvertOptions::Generic(options) => options,
33 ConvertOptions::Text(_) => {
34 return Err(PdfConvertError::validation_error(
35 "request",
36 "text inputs are converted locally and cannot be submitted to Docling",
37 ));
38 }
39 };
40 if matches!(input_kind, InputKind::Text) {
41 return Err(PdfConvertError::validation_error(
42 "request",
43 "text inputs are converted locally and cannot be submitted to Docling",
44 ));
45 }
46 let remote_request = crate::api::DoclingConvertRequest {
47 output_formats: request.output_formats.clone(),
48 page_range: None,
49 chunker: options.chunker,
50 chunking: options.chunking.clone(),
51 pipeline: options.pipeline,
52 picture_description_preset: options.picture_description_preset.clone(),
53 };
54 self.docling_client
55 .submit_file_async(&request.input, &remote_request)
56 .await
57 }
58
59 pub async fn convert(&self, request: ConvertRequest) -> Result<ConvertedDocument> {
60 self.convert_with_progress(request, |_, _| async {}).await
61 }
62
63 pub async fn convert_async(&self, request: ConvertRequest) -> Result<ConvertedDocument> {
64 self.convert_async_with_progress(request, |_, _| async {})
65 .await
66 }
67
68 pub async fn convert_with_progress<F, Fut>(
69 &self,
70 request: ConvertRequest,
71 mut on_progress: F,
72 ) -> Result<ConvertedDocument>
73 where
74 F: FnMut(usize, usize) -> Fut + Send,
75 Fut: Future<Output = ()> + Send,
76 {
77 self.convert_internal(request, false, &mut on_progress)
78 .await
79 }
80
81 pub async fn convert_async_with_progress<F, Fut>(
82 &self,
83 request: ConvertRequest,
84 mut on_progress: F,
85 ) -> Result<ConvertedDocument>
86 where
87 F: FnMut(usize, usize) -> Fut + Send,
88 Fut: Future<Output = ()> + Send,
89 {
90 self.convert_internal(request, true, &mut on_progress).await
91 }
92
93 pub async fn convert_async_with_docling_progress<F, Fut>(
94 &self,
95 request: ConvertRequest,
96 mut on_status: F,
97 ) -> Result<ConvertedDocument>
98 where
99 F: FnMut(TaskStatusResponse) -> Fut + Send,
100 Fut: Future<Output = ()> + Send,
101 {
102 let input_kind = request.validate()?;
103 match (&request.options, input_kind) {
104 (ConvertOptions::Text(options), InputKind::Text) => {
105 self.convert_text(&request.input, options, &request.output_formats)
106 }
107 (ConvertOptions::Generic(options), _) if input_kind.uses_generic_convert_options() => {
108 self.convert_remote_with_docling_progress(
109 &request.input,
110 options,
111 &request.output_formats,
112 &mut on_status,
113 )
114 .await
115 }
116 (ConvertOptions::Pdf(options), InputKind::Pdf) => {
117 self.convert_remote_with_docling_progress(
118 &request.input,
119 options,
120 &request.output_formats,
121 &mut on_status,
122 )
123 .await
124 }
125 _ => Err(PdfConvertError::validation_error(
126 "request",
127 "input kind and convert options do not match",
128 )),
129 }
130 }
131
132 async fn convert_internal<F, Fut>(
133 &self,
134 request: ConvertRequest,
135 asynchronous: bool,
136 on_progress: &mut F,
137 ) -> Result<ConvertedDocument>
138 where
139 F: FnMut(usize, usize) -> Fut + Send,
140 Fut: Future<Output = ()> + Send,
141 {
142 let input_kind = request.validate()?;
143 match (&request.options, input_kind) {
144 (ConvertOptions::Text(options), InputKind::Text) => {
145 let document =
146 self.convert_text(&request.input, options, &request.output_formats)?;
147 on_progress(1, 1).await;
148 Ok(document)
149 }
150 (ConvertOptions::Generic(options), _) if input_kind.uses_generic_convert_options() => {
151 self.convert_remote(
152 &request.input,
153 options,
154 &request.output_formats,
155 asynchronous,
156 on_progress,
157 )
158 .await
159 }
160 (ConvertOptions::Pdf(options), InputKind::Pdf) => {
161 self.convert_remote(
162 &request.input,
163 options,
164 &request.output_formats,
165 asynchronous,
166 on_progress,
167 )
168 .await
169 }
170 _ => Err(PdfConvertError::validation_error(
171 "request",
172 "input kind and convert options do not match",
173 )),
174 }
175 }
176
177 pub async fn convert_to_file(&self, request: FileConvertRequest) -> Result<ConvertedFile> {
178 self.convert_to_file_with_progress(request, |_, _| async {})
179 .await
180 }
181
182 pub async fn convert_to_file_with_progress<F, Fut>(
183 &self,
184 request: FileConvertRequest,
185 on_progress: F,
186 ) -> Result<ConvertedFile>
187 where
188 F: FnMut(usize, usize) -> Fut + Send,
189 Fut: Future<Output = ()> + Send,
190 {
191 self.convert_to_file_internal(request, false, on_progress)
192 .await
193 }
194
195 pub async fn convert_to_file_async(
196 &self,
197 request: FileConvertRequest,
198 ) -> Result<ConvertedFile> {
199 self.convert_to_file_async_with_progress(request, |_, _| async {})
200 .await
201 }
202
203 pub async fn convert_to_file_async_with_progress<F, Fut>(
204 &self,
205 request: FileConvertRequest,
206 on_progress: F,
207 ) -> Result<ConvertedFile>
208 where
209 F: FnMut(usize, usize) -> Fut + Send,
210 Fut: Future<Output = ()> + Send,
211 {
212 self.convert_to_file_internal(request, true, on_progress)
213 .await
214 }
215
216 pub async fn convert_to_file_async_with_docling_progress<F, Fut>(
217 &self,
218 request: FileConvertRequest,
219 on_status: F,
220 ) -> Result<ConvertedFile>
221 where
222 F: FnMut(TaskStatusResponse) -> Fut + Send,
223 Fut: Future<Output = ()> + Send,
224 {
225 let selected_output = request.selected_output;
226 let output_dir = request.output_dir.clone();
227 let overwrite = request.overwrite;
228 let document = self
229 .convert_async_with_docling_progress(request.request, on_status)
230 .await?;
231 let output_path =
232 Self::calculate_output_path(&output_dir, &document.filename, selected_output);
233
234 if !overwrite && output_path.exists() {
235 return Err(PdfConvertError::operation_error(
236 "writing output",
237 format!(
238 "output already exists and overwrite is disabled: {}",
239 output_path.display()
240 ),
241 ));
242 }
243
244 Self::write_output_file(&output_path, &document, selected_output).await?;
245 Ok(ConvertedFile {
246 document,
247 output_paths: vec![output_path],
248 })
249 }
250
251 async fn convert_to_file_internal<F, Fut>(
252 &self,
253 request: FileConvertRequest,
254 asynchronous: bool,
255 on_progress: F,
256 ) -> Result<ConvertedFile>
257 where
258 F: FnMut(usize, usize) -> Fut + Send,
259 Fut: Future<Output = ()> + Send,
260 {
261 let selected_output = request.selected_output;
262 let output_dir = request.output_dir.clone();
263 let overwrite = request.overwrite;
264 let document = if asynchronous {
265 self.convert_async_with_progress(request.request, on_progress)
266 .await?
267 } else {
268 self.convert_with_progress(request.request, on_progress)
269 .await?
270 };
271 let output_path =
272 Self::calculate_output_path(&output_dir, &document.filename, selected_output);
273
274 if !overwrite && output_path.exists() {
275 return Err(PdfConvertError::operation_error(
276 "writing output",
277 format!(
278 "output already exists and overwrite is disabled: {}",
279 output_path.display()
280 ),
281 ));
282 }
283
284 Self::write_output_file(&output_path, &document, selected_output).await?;
285 Ok(ConvertedFile {
286 document,
287 output_paths: vec![output_path],
288 })
289 }
290}