Skip to main content

cloudiful_docling_convert/processor/service/
mod.rs

1use 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    /// Submit a whole-document asynchronous conversion and return only the remote
27    /// task id. The caller owns polling and result fetching, which makes the
28    /// conversion resumable across restarts.
29    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}