1use std::sync::OnceLock;
8use std::time::Duration;
9
10use serde::Deserialize;
11use spdf_types::{SpdfError, SpdfResult};
12use tokio::runtime::Runtime;
13
14use crate::engine::{OcrEngine, OcrOptions, OcrResult};
15
16fn rt() -> SpdfResult<&'static Runtime> {
17 static RT: OnceLock<Runtime> = OnceLock::new();
18 if let Some(r) = RT.get() {
19 return Ok(r);
20 }
21 let r = tokio::runtime::Builder::new_current_thread()
22 .enable_all()
23 .build()
24 .map_err(|e| SpdfError::Ocr(format!("tokio runtime: {e}")))?;
25 Ok(RT.get_or_init(|| r))
27}
28
29#[derive(Debug, Clone)]
31pub struct HttpOcrEngine {
32 url: String,
33 client: reqwest::Client,
34}
35
36impl HttpOcrEngine {
37 pub fn new(url: impl Into<String>) -> Self {
38 let client = reqwest::Client::builder()
39 .timeout(Duration::from_secs(120))
40 .build()
41 .expect("reqwest client builds");
42 Self {
43 url: url.into(),
44 client,
45 }
46 }
47}
48
49#[derive(Debug, Deserialize)]
50struct Response {
51 results: Vec<OcrResult>,
52}
53
54impl OcrEngine for HttpOcrEngine {
55 fn name(&self) -> &'static str {
56 "http"
57 }
58
59 fn recognize(&self, image: &[u8], options: &OcrOptions) -> SpdfResult<Vec<OcrResult>> {
60 let url = self.url.clone();
61 let client = self.client.clone();
62 let language = options
63 .languages
64 .first()
65 .cloned()
66 .unwrap_or_else(|| "en".into());
67 let image = image.to_vec();
68
69 rt()?.block_on(async move {
70 let form = reqwest::multipart::Form::new()
71 .part(
72 "file",
73 reqwest::multipart::Part::bytes(image)
74 .file_name("page.png")
75 .mime_str("image/png")
76 .map_err(|e| SpdfError::Ocr(format!("mime: {e}")))?,
77 )
78 .text("language", language);
79
80 let resp = client
81 .post(&url)
82 .multipart(form)
83 .send()
84 .await
85 .map_err(|e| SpdfError::Ocr(format!("http send: {e}")))?;
86
87 if !resp.status().is_success() {
88 return Err(SpdfError::Ocr(format!(
89 "OCR server returned HTTP {}",
90 resp.status()
91 )));
92 }
93 let body: Response = resp
94 .json()
95 .await
96 .map_err(|e| SpdfError::Ocr(format!("http decode: {e}")))?;
97 Ok(body.results)
98 })
99 }
100}