visionkit-rs 0.2.1

Safe Rust bindings for VisionKit.framework — image analysis, Live Text, and availability-aware area coverage on macOS
Documentation
use core::ffi::{c_char, c_void};
use core::ops::{BitOr, BitOrAssign};
use core::ptr;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::error::VisionKitError;
use crate::ffi;
use crate::image_analysis::ImageAnalysis;
use crate::private::{error_from_status, json_cstring, parse_json_ptr, path_to_cstring};

type AnalyzePathFn = unsafe extern "C" fn(
    token: *mut c_void,
    path: *const c_char,
    orientation_raw: u32,
    configuration_json: *const c_char,
    out_analysis_token: *mut *mut c_void,
    out_error_message: *mut *mut c_char,
) -> i32;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ImageAnalysisTypes(u64);

impl ImageAnalysisTypes {
    pub const NONE: Self = Self(0);
    pub const TEXT: Self = Self(1);
    pub const MACHINE_READABLE_CODE: Self = Self(2);
    pub const VISUAL_LOOK_UP: Self = Self(4);
    pub const ALL: Self =
        Self(Self::TEXT.0 | Self::MACHINE_READABLE_CODE.0 | Self::VISUAL_LOOK_UP.0);

    #[must_use]
    pub const fn new(raw: u64) -> Self {
        Self(raw)
    }

    #[must_use]
    pub const fn bits(self) -> u64 {
        self.0
    }

    #[must_use]
    pub const fn contains(self, other: Self) -> bool {
        (self.0 & other.0) == other.0
    }
}

impl BitOr for ImageAnalysisTypes {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self(self.0 | rhs.0)
    }
}

impl BitOrAssign for ImageAnalysisTypes {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

impl Default for ImageAnalysisTypes {
    fn default() -> Self {
        Self::NONE
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ImageOrientation {
    #[default]
    Up,
    UpMirrored,
    Down,
    DownMirrored,
    LeftMirrored,
    Right,
    RightMirrored,
    Left,
}

impl ImageOrientation {
    #[must_use]
    pub const fn raw_value(self) -> u32 {
        match self {
            Self::Up => 1,
            Self::UpMirrored => 2,
            Self::Down => 3,
            Self::DownMirrored => 4,
            Self::LeftMirrored => 5,
            Self::Right => 6,
            Self::RightMirrored => 7,
            Self::Left => 8,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageAnalyzerConfiguration {
    analysis_types: ImageAnalysisTypes,
    locales: Vec<String>,
}

impl ImageAnalyzerConfiguration {
    #[must_use]
    pub fn new(analysis_types: ImageAnalysisTypes) -> Self {
        Self {
            analysis_types,
            locales: Vec::new(),
        }
    }

    #[must_use]
    pub fn analysis_types(&self) -> ImageAnalysisTypes {
        self.analysis_types
    }

    #[must_use]
    pub fn locales(&self) -> &[String] {
        &self.locales
    }

    #[must_use]
    pub fn with_locales<I, S>(mut self, locales: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.locales = locales.into_iter().map(Into::into).collect();
        self
    }
}

pub struct ImageAnalyzer {
    token: *mut c_void,
}

impl Drop for ImageAnalyzer {
    fn drop(&mut self) {
        if !self.token.is_null() {
            unsafe { ffi::image_analyzer::vk_image_analyzer_release(self.token) };
            self.token = ptr::null_mut();
        }
    }
}

impl ImageAnalyzer {
    pub fn new() -> Result<Self, VisionKitError> {
        let token = unsafe { ffi::image_analyzer::vk_image_analyzer_new() };
        if token.is_null() {
            return Err(VisionKitError::UnavailableOnThisMacOS(
                "ImageAnalyzer requires macOS 13+".to_owned(),
            ));
        }
        Ok(Self { token })
    }

    #[must_use]
    pub fn is_supported() -> bool {
        unsafe { ffi::image_analyzer::vk_image_analyzer_is_supported() != 0 }
    }

    pub fn supported_text_recognition_languages() -> Result<Vec<String>, VisionKitError> {
        let mut languages_json: *mut c_char = ptr::null_mut();
        let mut err_msg: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::image_analyzer::vk_image_analyzer_supported_text_recognition_languages_json(
                &mut languages_json,
                &mut err_msg,
            )
        };
        if status == ffi::status::OK {
            let mut languages: Vec<String> =
                unsafe { parse_json_ptr(languages_json, "supported text recognition languages") }?;
            languages.sort();
            Ok(languages)
        } else {
            Err(unsafe { error_from_status(status, err_msg) })
        }
    }

    pub fn analyze_image_at_path<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
    ) -> Result<ImageAnalysis, VisionKitError> {
        self.analyze_with_loader(
            path,
            orientation,
            configuration,
            ffi::image_analyzer::vk_image_analyzer_analyze_image_at_path,
            "file URL image analysis",
        )
    }

    pub fn analyze_ns_image_at_path<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
    ) -> Result<ImageAnalysis, VisionKitError> {
        self.analyze_with_loader(
            path,
            orientation,
            configuration,
            ffi::image_analyzer::vk_image_analyzer_analyze_ns_image_at_path,
            "NSImage image analysis",
        )
    }

    pub fn analyze_cg_image_at_path<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
    ) -> Result<ImageAnalysis, VisionKitError> {
        self.analyze_with_loader(
            path,
            orientation,
            configuration,
            ffi::image_analyzer::vk_image_analyzer_analyze_cg_image_at_path,
            "CGImage image analysis",
        )
    }

    pub fn analyze_ci_image_at_path<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
    ) -> Result<ImageAnalysis, VisionKitError> {
        self.analyze_with_loader(
            path,
            orientation,
            configuration,
            ffi::image_analyzer::vk_image_analyzer_analyze_ci_image_at_path,
            "CIImage image analysis",
        )
    }

    pub fn analyze_pixel_buffer_at_path<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
    ) -> Result<ImageAnalysis, VisionKitError> {
        self.analyze_with_loader(
            path,
            orientation,
            configuration,
            ffi::image_analyzer::vk_image_analyzer_analyze_pixel_buffer_at_path,
            "CVPixelBuffer image analysis",
        )
    }

    fn analyze_with_loader<P: AsRef<Path>>(
        &self,
        path: P,
        orientation: ImageOrientation,
        configuration: &ImageAnalyzerConfiguration,
        analyze_fn: AnalyzePathFn,
        context: &str,
    ) -> Result<ImageAnalysis, VisionKitError> {
        let path = path_to_cstring(path.as_ref())?;
        let configuration_json = json_cstring(configuration)?;
        let mut analysis_token: *mut c_void = ptr::null_mut();
        let mut err_msg: *mut c_char = ptr::null_mut();
        let status = unsafe {
            analyze_fn(
                self.token,
                path.as_ptr(),
                orientation.raw_value(),
                configuration_json.as_ptr(),
                &mut analysis_token,
                &mut err_msg,
            )
        };
        if status == ffi::status::OK {
            if analysis_token.is_null() {
                return Err(VisionKitError::Unknown(format!(
                    "Swift bridge returned an empty image analysis token for {context}"
                )));
            }
            Ok(ImageAnalysis::from_token(analysis_token))
        } else {
            Err(unsafe { error_from_status(status, err_msg) })
        }
    }
}