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);
25
26impl ImageAnalysisTypes {
27 pub const NONE: Self = Self(0);
28 pub const TEXT: Self = Self(1);
29 pub const MACHINE_READABLE_CODE: Self = Self(2);
30 pub const VISUAL_LOOK_UP: Self = Self(4);
31 pub const ALL: Self =
32 Self(Self::TEXT.0 | Self::MACHINE_READABLE_CODE.0 | Self::VISUAL_LOOK_UP.0);
33
34 #[must_use]
35 pub const fn new(raw: u64) -> Self {
36 Self(raw)
37 }
38
39 #[must_use]
40 pub const fn bits(self) -> u64 {
41 self.0
42 }
43
44 #[must_use]
45 pub const fn contains(self, other: Self) -> bool {
46 (self.0 & other.0) == other.0
47 }
48}
49
50impl BitOr for ImageAnalysisTypes {
51 type Output = Self;
52
53 fn bitor(self, rhs: Self) -> Self::Output {
54 Self(self.0 | rhs.0)
55 }
56}
57
58impl BitOrAssign for ImageAnalysisTypes {
59 fn bitor_assign(&mut self, rhs: Self) {
60 self.0 |= rhs.0;
61 }
62}
63
64impl Default for ImageAnalysisTypes {
65 fn default() -> Self {
66 Self::NONE
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
71pub enum ImageOrientation {
72 #[default]
73 Up,
74 UpMirrored,
75 Down,
76 DownMirrored,
77 LeftMirrored,
78 Right,
79 RightMirrored,
80 Left,
81}
82
83impl ImageOrientation {
84 #[must_use]
85 pub const fn raw_value(self) -> u32 {
86 match self {
87 Self::Up => 1,
88 Self::UpMirrored => 2,
89 Self::Down => 3,
90 Self::DownMirrored => 4,
91 Self::LeftMirrored => 5,
92 Self::Right => 6,
93 Self::RightMirrored => 7,
94 Self::Left => 8,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct ImageAnalyzerConfiguration {
102 analysis_types: ImageAnalysisTypes,
103 locales: Vec<String>,
104}
105
106impl ImageAnalyzerConfiguration {
107 #[must_use]
108 pub fn new(analysis_types: ImageAnalysisTypes) -> Self {
109 Self {
110 analysis_types,
111 locales: Vec::new(),
112 }
113 }
114
115 #[must_use]
116 pub fn analysis_types(&self) -> ImageAnalysisTypes {
117 self.analysis_types
118 }
119
120 #[must_use]
121 pub fn locales(&self) -> &[String] {
122 &self.locales
123 }
124
125 #[must_use]
126 pub fn with_locales<I, S>(mut self, locales: I) -> Self
127 where
128 I: IntoIterator<Item = S>,
129 S: Into<String>,
130 {
131 self.locales = locales.into_iter().map(Into::into).collect();
132 self
133 }
134}
135
136pub struct ImageAnalyzer {
137 token: *mut c_void,
138}
139
140impl Drop for ImageAnalyzer {
141 fn drop(&mut self) {
142 if !self.token.is_null() {
143 unsafe { ffi::image_analyzer::vk_image_analyzer_release(self.token) };
144 self.token = ptr::null_mut();
145 }
146 }
147}
148
149impl ImageAnalyzer {
150 pub fn new() -> Result<Self, VisionKitError> {
151 let token = unsafe { ffi::image_analyzer::vk_image_analyzer_new() };
152 if token.is_null() {
153 return Err(VisionKitError::UnavailableOnThisMacOS(
154 "ImageAnalyzer requires macOS 13+".to_owned(),
155 ));
156 }
157 Ok(Self { token })
158 }
159
160 #[must_use]
161 pub fn is_supported() -> bool {
162 unsafe { ffi::image_analyzer::vk_image_analyzer_is_supported() != 0 }
163 }
164
165 pub fn supported_text_recognition_languages() -> Result<Vec<String>, VisionKitError> {
166 let mut languages_json: *mut c_char = ptr::null_mut();
167 let mut err_msg: *mut c_char = ptr::null_mut();
168 let status = unsafe {
169 ffi::image_analyzer::vk_image_analyzer_supported_text_recognition_languages_json(
170 &mut languages_json,
171 &mut err_msg,
172 )
173 };
174 if status == ffi::status::OK {
175 let mut languages: Vec<String> =
176 unsafe { parse_json_ptr(languages_json, "supported text recognition languages") }?;
177 languages.sort();
178 Ok(languages)
179 } else {
180 Err(unsafe { error_from_status(status, err_msg) })
181 }
182 }
183
184 pub fn analyze_image_at_path<P: AsRef<Path>>(
185 &self,
186 path: P,
187 orientation: ImageOrientation,
188 configuration: &ImageAnalyzerConfiguration,
189 ) -> Result<ImageAnalysis, VisionKitError> {
190 self.analyze_with_loader(
191 path,
192 orientation,
193 configuration,
194 ffi::image_analyzer::vk_image_analyzer_analyze_image_at_path,
195 "file URL image analysis",
196 )
197 }
198
199 pub fn analyze_ns_image_at_path<P: AsRef<Path>>(
200 &self,
201 path: P,
202 orientation: ImageOrientation,
203 configuration: &ImageAnalyzerConfiguration,
204 ) -> Result<ImageAnalysis, VisionKitError> {
205 self.analyze_with_loader(
206 path,
207 orientation,
208 configuration,
209 ffi::image_analyzer::vk_image_analyzer_analyze_ns_image_at_path,
210 "NSImage image analysis",
211 )
212 }
213
214 pub fn analyze_cg_image_at_path<P: AsRef<Path>>(
215 &self,
216 path: P,
217 orientation: ImageOrientation,
218 configuration: &ImageAnalyzerConfiguration,
219 ) -> Result<ImageAnalysis, VisionKitError> {
220 self.analyze_with_loader(
221 path,
222 orientation,
223 configuration,
224 ffi::image_analyzer::vk_image_analyzer_analyze_cg_image_at_path,
225 "CGImage image analysis",
226 )
227 }
228
229 pub fn analyze_ci_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_ci_image_at_path,
240 "CIImage image analysis",
241 )
242 }
243
244 pub fn analyze_pixel_buffer_at_path<P: AsRef<Path>>(
245 &self,
246 path: P,
247 orientation: ImageOrientation,
248 configuration: &ImageAnalyzerConfiguration,
249 ) -> Result<ImageAnalysis, VisionKitError> {
250 self.analyze_with_loader(
251 path,
252 orientation,
253 configuration,
254 ffi::image_analyzer::vk_image_analyzer_analyze_pixel_buffer_at_path,
255 "CVPixelBuffer image analysis",
256 )
257 }
258
259 fn analyze_with_loader<P: AsRef<Path>>(
260 &self,
261 path: P,
262 orientation: ImageOrientation,
263 configuration: &ImageAnalyzerConfiguration,
264 analyze_fn: AnalyzePathFn,
265 context: &str,
266 ) -> Result<ImageAnalysis, VisionKitError> {
267 let path = path_to_cstring(path.as_ref())?;
268 let configuration_json = json_cstring(configuration)?;
269 let mut analysis_token: *mut c_void = ptr::null_mut();
270 let mut err_msg: *mut c_char = ptr::null_mut();
271 let status = unsafe {
272 analyze_fn(
273 self.token,
274 path.as_ptr(),
275 orientation.raw_value(),
276 configuration_json.as_ptr(),
277 &mut analysis_token,
278 &mut err_msg,
279 )
280 };
281 if status == ffi::status::OK {
282 if analysis_token.is_null() {
283 return Err(VisionKitError::Unknown(format!(
284 "Swift bridge returned an empty image analysis token for {context}"
285 )));
286 }
287 Ok(ImageAnalysis::from_token(analysis_token))
288 } else {
289 Err(unsafe { error_from_status(status, err_msg) })
290 }
291 }
292}