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        };
53        self.docling_client
54            .submit_file_async(&request.input, &remote_request)
55            .await
56    }
57
58    pub async fn convert(&self, request: ConvertRequest) -> Result<ConvertedDocument> {
59        self.convert_with_progress(request, |_, _| async {}).await
60    }
61
62    pub async fn convert_async(&self, request: ConvertRequest) -> Result<ConvertedDocument> {
63        self.convert_async_with_progress(request, |_, _| async {})
64            .await
65    }
66
67    pub async fn convert_with_progress<F, Fut>(
68        &self,
69        request: ConvertRequest,
70        mut on_progress: F,
71    ) -> Result<ConvertedDocument>
72    where
73        F: FnMut(usize, usize) -> Fut + Send,
74        Fut: Future<Output = ()> + Send,
75    {
76        self.convert_internal(request, false, &mut on_progress)
77            .await
78    }
79
80    pub async fn convert_async_with_progress<F, Fut>(
81        &self,
82        request: ConvertRequest,
83        mut on_progress: F,
84    ) -> Result<ConvertedDocument>
85    where
86        F: FnMut(usize, usize) -> Fut + Send,
87        Fut: Future<Output = ()> + Send,
88    {
89        self.convert_internal(request, true, &mut on_progress).await
90    }
91
92    pub async fn convert_async_with_docling_progress<F, Fut>(
93        &self,
94        request: ConvertRequest,
95        mut on_status: F,
96    ) -> Result<ConvertedDocument>
97    where
98        F: FnMut(TaskStatusResponse) -> Fut + Send,
99        Fut: Future<Output = ()> + Send,
100    {
101        let input_kind = request.validate()?;
102        match (&request.options, input_kind) {
103            (ConvertOptions::Text(options), InputKind::Text) => {
104                self.convert_text(&request.input, options, &request.output_formats)
105            }
106            (ConvertOptions::Generic(options), _) if input_kind.uses_generic_convert_options() => {
107                self.convert_remote_with_docling_progress(
108                    &request.input,
109                    options,
110                    &request.output_formats,
111                    &mut on_status,
112                )
113                .await
114            }
115            (ConvertOptions::Pdf(options), InputKind::Pdf) => {
116                self.convert_remote_with_docling_progress(
117                    &request.input,
118                    options,
119                    &request.output_formats,
120                    &mut on_status,
121                )
122                .await
123            }
124            _ => Err(PdfConvertError::validation_error(
125                "request",
126                "input kind and convert options do not match",
127            )),
128        }
129    }
130
131    async fn convert_internal<F, Fut>(
132        &self,
133        request: ConvertRequest,
134        asynchronous: bool,
135        on_progress: &mut F,
136    ) -> Result<ConvertedDocument>
137    where
138        F: FnMut(usize, usize) -> Fut + Send,
139        Fut: Future<Output = ()> + Send,
140    {
141        let input_kind = request.validate()?;
142        match (&request.options, input_kind) {
143            (ConvertOptions::Text(options), InputKind::Text) => {
144                let document =
145                    self.convert_text(&request.input, options, &request.output_formats)?;
146                on_progress(1, 1).await;
147                Ok(document)
148            }
149            (ConvertOptions::Generic(options), _) if input_kind.uses_generic_convert_options() => {
150                self.convert_remote(
151                    &request.input,
152                    options,
153                    &request.output_formats,
154                    asynchronous,
155                    on_progress,
156                )
157                .await
158            }
159            (ConvertOptions::Pdf(options), InputKind::Pdf) => {
160                self.convert_remote(
161                    &request.input,
162                    options,
163                    &request.output_formats,
164                    asynchronous,
165                    on_progress,
166                )
167                .await
168            }
169            _ => Err(PdfConvertError::validation_error(
170                "request",
171                "input kind and convert options do not match",
172            )),
173        }
174    }
175
176    pub async fn convert_to_file(&self, request: FileConvertRequest) -> Result<ConvertedFile> {
177        self.convert_to_file_with_progress(request, |_, _| async {})
178            .await
179    }
180
181    pub async fn convert_to_file_with_progress<F, Fut>(
182        &self,
183        request: FileConvertRequest,
184        on_progress: F,
185    ) -> Result<ConvertedFile>
186    where
187        F: FnMut(usize, usize) -> Fut + Send,
188        Fut: Future<Output = ()> + Send,
189    {
190        self.convert_to_file_internal(request, false, on_progress)
191            .await
192    }
193
194    pub async fn convert_to_file_async(
195        &self,
196        request: FileConvertRequest,
197    ) -> Result<ConvertedFile> {
198        self.convert_to_file_async_with_progress(request, |_, _| async {})
199            .await
200    }
201
202    pub async fn convert_to_file_async_with_progress<F, Fut>(
203        &self,
204        request: FileConvertRequest,
205        on_progress: F,
206    ) -> Result<ConvertedFile>
207    where
208        F: FnMut(usize, usize) -> Fut + Send,
209        Fut: Future<Output = ()> + Send,
210    {
211        self.convert_to_file_internal(request, true, on_progress)
212            .await
213    }
214
215    pub async fn convert_to_file_async_with_docling_progress<F, Fut>(
216        &self,
217        request: FileConvertRequest,
218        on_status: F,
219    ) -> Result<ConvertedFile>
220    where
221        F: FnMut(TaskStatusResponse) -> Fut + Send,
222        Fut: Future<Output = ()> + Send,
223    {
224        let selected_output = request.selected_output;
225        let output_dir = request.output_dir.clone();
226        let overwrite = request.overwrite;
227        let document = self
228            .convert_async_with_docling_progress(request.request, on_status)
229            .await?;
230        let output_path =
231            Self::calculate_output_path(&output_dir, &document.filename, selected_output);
232
233        if !overwrite && output_path.exists() {
234            return Err(PdfConvertError::operation_error(
235                "writing output",
236                format!(
237                    "output already exists and overwrite is disabled: {}",
238                    output_path.display()
239                ),
240            ));
241        }
242
243        Self::write_output_file(&output_path, &document, selected_output).await?;
244        Ok(ConvertedFile {
245            document,
246            output_paths: vec![output_path],
247        })
248    }
249
250    async fn convert_to_file_internal<F, Fut>(
251        &self,
252        request: FileConvertRequest,
253        asynchronous: bool,
254        on_progress: F,
255    ) -> Result<ConvertedFile>
256    where
257        F: FnMut(usize, usize) -> Fut + Send,
258        Fut: Future<Output = ()> + Send,
259    {
260        let selected_output = request.selected_output;
261        let output_dir = request.output_dir.clone();
262        let overwrite = request.overwrite;
263        let document = if asynchronous {
264            self.convert_async_with_progress(request.request, on_progress)
265                .await?
266        } else {
267            self.convert_with_progress(request.request, on_progress)
268                .await?
269        };
270        let output_path =
271            Self::calculate_output_path(&output_dir, &document.filename, selected_output);
272
273        if !overwrite && output_path.exists() {
274            return Err(PdfConvertError::operation_error(
275                "writing output",
276                format!(
277                    "output already exists and overwrite is disabled: {}",
278                    output_path.display()
279                ),
280            ));
281        }
282
283        Self::write_output_file(&output_path, &document, selected_output).await?;
284        Ok(ConvertedFile {
285            document,
286            output_paths: vec![output_path],
287        })
288    }
289}