Skip to main content

visionkit/
image_analyzer.rs

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)]
24/// Wraps VisionKit image analysis type flags.
25pub struct ImageAnalysisTypes(u64);
26
27impl ImageAnalysisTypes {
28    /// Matches the empty VisionKit `ImageAnalysisTypes` flag set.
29    pub const NONE: Self = Self(0);
30    /// Matches the VisionKit `TEXT` flag.
31    pub const TEXT: Self = Self(1);
32    /// Matches the VisionKit `MACHINE_READABLE_CODE` flag.
33    pub const MACHINE_READABLE_CODE: Self = Self(2);
34    /// Matches the VisionKit `VISUAL_LOOK_UP` flag.
35    pub const VISUAL_LOOK_UP: Self = Self(4);
36    /// Matches the VisionKit `ImageAnalysisTypes` flag set containing every supported option.
37    pub const ALL: Self =
38        Self(Self::TEXT.0 | Self::MACHINE_READABLE_CODE.0 | Self::VISUAL_LOOK_UP.0);
39
40    #[must_use]
41    /// Creates the VisionKit `ImageAnalysisTypes` wrapper.
42    pub const fn new(raw: u64) -> Self {
43        Self(raw)
44    }
45
46    #[must_use]
47    /// Returns the raw VisionKit `ImageAnalysisTypes` value.
48    pub const fn bits(self) -> u64 {
49        self.0
50    }
51
52    #[must_use]
53    /// Returns whether this VisionKit `ImageAnalysisTypes` value contains `other`.
54    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)]
80/// Represents a VisionKit image orientation.
81pub enum ImageOrientation {
82    #[default]
83    /// Matches the VisionKit `Up` orientation case.
84    Up,
85    /// Matches the VisionKit `UpMirrored` orientation case.
86    UpMirrored,
87    /// Matches the VisionKit `Down` orientation case.
88    Down,
89    /// Matches the VisionKit `DownMirrored` orientation case.
90    DownMirrored,
91    /// Matches the VisionKit `LeftMirrored` orientation case.
92    LeftMirrored,
93    /// Matches the VisionKit `Right` orientation case.
94    Right,
95    /// Matches the VisionKit `RightMirrored` orientation case.
96    RightMirrored,
97    /// Matches the VisionKit `Left` orientation case.
98    Left,
99}
100
101impl ImageOrientation {
102    #[must_use]
103    /// Returns the raw VisionKit `ImageOrientation` value.
104    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")]
120/// Represents a VisionKit image analyzer configuration.
121pub struct ImageAnalyzerConfiguration {
122    analysis_types: ImageAnalysisTypes,
123    locales: Vec<String>,
124}
125
126impl ImageAnalyzerConfiguration {
127    #[must_use]
128    /// Creates the VisionKit `ImageAnalyzerConfiguration` wrapper.
129    pub fn new(analysis_types: ImageAnalysisTypes) -> Self {
130        Self {
131            analysis_types,
132            locales: Vec::new(),
133        }
134    }
135
136    #[must_use]
137    /// Returns the configured VisionKit analysis types.
138    pub fn analysis_types(&self) -> ImageAnalysisTypes {
139        self.analysis_types
140    }
141
142    #[must_use]
143    /// Returns the configured VisionKit locales.
144    pub fn locales(&self) -> &[String] {
145        &self.locales
146    }
147
148    #[must_use]
149    /// Returns this VisionKit analyzer configuration with the provided locales.
150    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
160/// Wraps the VisionKit ImageAnalyzer counterpart.
161pub 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    /// Creates the VisionKit `ImageAnalyzer` wrapper.
176    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    /// Returns whether the VisionKit `ImageAnalyzer` counterpart is supported on this system.
188    pub fn is_supported() -> bool {
189        unsafe { ffi::image_analyzer::vk_image_analyzer_is_supported() != 0 }
190    }
191
192    /// Returns the text-recognition languages supported by VisionKit on this system.
193    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    /// Runs the matching VisionKit analyze image at path operation.
213    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    /// Runs the matching VisionKit analyze ns image at path operation.
229    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    /// Runs the matching VisionKit analyze cg image at path operation.
245    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    /// Runs the matching VisionKit analyze ci image at path operation.
261    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    /// Runs the matching VisionKit analyze pixel buffer at path operation.
277    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}