1use core::ffi::{c_char, c_void};
2use core::ops::{BitOr, BitOrAssign};
3use core::ptr;
4use std::path::Path;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::VisionKitError;
9use crate::ffi;
10use crate::image_analysis::ImageAnalysis;
11use crate::private::{error_from_status, json_cstring, parse_json_ptr, path_to_cstring};
12
13type AnalyzePathFn = unsafe extern "C" fn(
14 token: *mut c_void,
15 path: *const c_char,
16 orientation_raw: u32,
17 configuration_json: *const c_char,
18 out_analysis_token: *mut *mut c_void,
19 out_error_message: *mut *mut c_char,
20) -> i32;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ImageAnalysisTypes(u64);
26
27impl ImageAnalysisTypes {
28 pub const NONE: Self = Self(0);
30 pub const TEXT: Self = Self(1);
32 pub const MACHINE_READABLE_CODE: Self = Self(2);
34 pub const VISUAL_LOOK_UP: Self = Self(4);
36 pub const ALL: Self =
38 Self(Self::TEXT.0 | Self::MACHINE_READABLE_CODE.0 | Self::VISUAL_LOOK_UP.0);
39
40 #[must_use]
41 pub const fn new(raw: u64) -> Self {
43 Self(raw)
44 }
45
46 #[must_use]
47 pub const fn bits(self) -> u64 {
49 self.0
50 }
51
52 #[must_use]
53 pub const fn contains(self, other: Self) -> bool {
55 (self.0 & other.0) == other.0
56 }
57}
58
59impl BitOr for ImageAnalysisTypes {
60 type Output = Self;
61
62 fn bitor(self, rhs: Self) -> Self::Output {
63 Self(self.0 | rhs.0)
64 }
65}
66
67impl BitOrAssign for ImageAnalysisTypes {
68 fn bitor_assign(&mut self, rhs: Self) {
69 self.0 |= rhs.0;
70 }
71}
72
73impl Default for ImageAnalysisTypes {
74 fn default() -> Self {
75 Self::NONE
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
80pub enum ImageOrientation {
82 #[default]
83 Up,
85 UpMirrored,
87 Down,
89 DownMirrored,
91 LeftMirrored,
93 Right,
95 RightMirrored,
97 Left,
99}
100
101impl ImageOrientation {
102 #[must_use]
103 pub const fn raw_value(self) -> u32 {
105 match self {
106 Self::Up => 1,
107 Self::UpMirrored => 2,
108 Self::Down => 3,
109 Self::DownMirrored => 4,
110 Self::LeftMirrored => 5,
111 Self::Right => 6,
112 Self::RightMirrored => 7,
113 Self::Left => 8,
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct ImageAnalyzerConfiguration {
122 analysis_types: ImageAnalysisTypes,
123 locales: Vec<String>,
124}
125
126impl ImageAnalyzerConfiguration {
127 #[must_use]
128 pub fn new(analysis_types: ImageAnalysisTypes) -> Self {
130 Self {
131 analysis_types,
132 locales: Vec::new(),
133 }
134 }
135
136 #[must_use]
137 pub fn analysis_types(&self) -> ImageAnalysisTypes {
139 self.analysis_types
140 }
141
142 #[must_use]
143 pub fn locales(&self) -> &[String] {
145 &self.locales
146 }
147
148 #[must_use]
149 pub fn with_locales<I, S>(mut self, locales: I) -> Self
151 where
152 I: IntoIterator<Item = S>,
153 S: Into<String>,
154 {
155 self.locales = locales.into_iter().map(Into::into).collect();
156 self
157 }
158}
159
160pub struct ImageAnalyzer {
162 token: *mut c_void,
163}
164
165impl Drop for ImageAnalyzer {
166 fn drop(&mut self) {
167 if !self.token.is_null() {
168 unsafe { ffi::image_analyzer::vk_image_analyzer_release(self.token) };
169 self.token = ptr::null_mut();
170 }
171 }
172}
173
174impl ImageAnalyzer {
175 pub fn new() -> Result<Self, VisionKitError> {
177 let token = unsafe { ffi::image_analyzer::vk_image_analyzer_new() };
178 if token.is_null() {
179 return Err(VisionKitError::UnavailableOnThisMacOS(
180 "ImageAnalyzer requires macOS 13+".to_owned(),
181 ));
182 }
183 Ok(Self { token })
184 }
185
186 #[must_use]
187 pub fn is_supported() -> bool {
189 unsafe { ffi::image_analyzer::vk_image_analyzer_is_supported() != 0 }
190 }
191
192 pub fn supported_text_recognition_languages() -> Result<Vec<String>, VisionKitError> {
194 let mut languages_json: *mut c_char = ptr::null_mut();
195 let mut err_msg: *mut c_char = ptr::null_mut();
196 let status = unsafe {
197 ffi::image_analyzer::vk_image_analyzer_supported_text_recognition_languages_json(
198 &mut languages_json,
199 &mut err_msg,
200 )
201 };
202 if status == ffi::status::OK {
203 let mut languages: Vec<String> =
204 unsafe { parse_json_ptr(languages_json, "supported text recognition languages") }?;
205 languages.sort();
206 Ok(languages)
207 } else {
208 Err(unsafe { error_from_status(status, err_msg) })
209 }
210 }
211
212 pub fn analyze_image_at_path<P: AsRef<Path>>(
214 &self,
215 path: P,
216 orientation: ImageOrientation,
217 configuration: &ImageAnalyzerConfiguration,
218 ) -> Result<ImageAnalysis, VisionKitError> {
219 self.analyze_with_loader(
220 path,
221 orientation,
222 configuration,
223 ffi::image_analyzer::vk_image_analyzer_analyze_image_at_path,
224 "file URL image analysis",
225 )
226 }
227
228 pub fn analyze_ns_image_at_path<P: AsRef<Path>>(
230 &self,
231 path: P,
232 orientation: ImageOrientation,
233 configuration: &ImageAnalyzerConfiguration,
234 ) -> Result<ImageAnalysis, VisionKitError> {
235 self.analyze_with_loader(
236 path,
237 orientation,
238 configuration,
239 ffi::image_analyzer::vk_image_analyzer_analyze_ns_image_at_path,
240 "NSImage image analysis",
241 )
242 }
243
244 pub fn analyze_cg_image_at_path<P: AsRef<Path>>(
246 &self,
247 path: P,
248 orientation: ImageOrientation,
249 configuration: &ImageAnalyzerConfiguration,
250 ) -> Result<ImageAnalysis, VisionKitError> {
251 self.analyze_with_loader(
252 path,
253 orientation,
254 configuration,
255 ffi::image_analyzer::vk_image_analyzer_analyze_cg_image_at_path,
256 "CGImage image analysis",
257 )
258 }
259
260 pub fn analyze_ci_image_at_path<P: AsRef<Path>>(
262 &self,
263 path: P,
264 orientation: ImageOrientation,
265 configuration: &ImageAnalyzerConfiguration,
266 ) -> Result<ImageAnalysis, VisionKitError> {
267 self.analyze_with_loader(
268 path,
269 orientation,
270 configuration,
271 ffi::image_analyzer::vk_image_analyzer_analyze_ci_image_at_path,
272 "CIImage image analysis",
273 )
274 }
275
276 pub fn analyze_pixel_buffer_at_path<P: AsRef<Path>>(
278 &self,
279 path: P,
280 orientation: ImageOrientation,
281 configuration: &ImageAnalyzerConfiguration,
282 ) -> Result<ImageAnalysis, VisionKitError> {
283 self.analyze_with_loader(
284 path,
285 orientation,
286 configuration,
287 ffi::image_analyzer::vk_image_analyzer_analyze_pixel_buffer_at_path,
288 "CVPixelBuffer image analysis",
289 )
290 }
291
292 fn analyze_with_loader<P: AsRef<Path>>(
293 &self,
294 path: P,
295 orientation: ImageOrientation,
296 configuration: &ImageAnalyzerConfiguration,
297 analyze_fn: AnalyzePathFn,
298 context: &str,
299 ) -> Result<ImageAnalysis, VisionKitError> {
300 let path = path_to_cstring(path.as_ref())?;
301 let configuration_json = json_cstring(configuration)?;
302 let mut analysis_token: *mut c_void = ptr::null_mut();
303 let mut err_msg: *mut c_char = ptr::null_mut();
304 let status = unsafe {
305 analyze_fn(
306 self.token,
307 path.as_ptr(),
308 orientation.raw_value(),
309 configuration_json.as_ptr(),
310 &mut analysis_token,
311 &mut err_msg,
312 )
313 };
314 if status == ffi::status::OK {
315 if analysis_token.is_null() {
316 return Err(VisionKitError::Unknown(format!(
317 "Swift bridge returned an empty image analysis token for {context}"
318 )));
319 }
320 Ok(ImageAnalysis::from_token(analysis_token))
321 } else {
322 Err(unsafe { error_from_status(status, err_msg) })
323 }
324 }
325}