Skip to main content

apple_vision/aesthetics/
mod.rs

1//! Aesthetics scoring (`VNCalculateImageAestheticsScoresRequest`)
2//! and face capture quality (`VNDetectFaceCaptureQualityRequest`).
3
4use core::ffi::c_char;
5use core::ptr;
6use std::ffi::CString;
7use std::path::Path;
8
9use crate::error::{from_swift, VisionError};
10use crate::ffi;
11use crate::recognize_text::BoundingBox;
12
13/// One image's aesthetics score. Higher = more visually appealing.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct AestheticsScores {
16    /// Apple's overall aesthetics score in roughly `-1.0..=1.0`.
17    pub overall_score: f32,
18    /// True if Apple's model thinks the image is "utility" (e.g.
19    /// screenshots, scanned documents) rather than expressive content.
20    pub is_utility: bool,
21}
22
23/// Calculate Apple's image aesthetics scores (macOS 15+).
24///
25/// # Errors
26///
27/// Returns [`VisionError::ImageLoadFailed`] / [`VisionError::RequestFailed`].
28pub fn calculate_aesthetics_scores_in_path(
29    path: impl AsRef<Path>,
30) -> Result<Option<AestheticsScores>, VisionError> {
31    let path_str = path
32        .as_ref()
33        .to_str()
34        .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
35    let path_c = CString::new(path_str)
36        .map_err(|e| VisionError::InvalidArgument(format!("path NUL byte: {e}")))?;
37
38    let mut raw = ffi::AestheticsScoresRaw {
39        overall_score: 0.0,
40        is_utility: false,
41    };
42    let mut has_value = false;
43    let mut err_msg: *mut c_char = ptr::null_mut();
44    // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
45    let status = unsafe {
46        ffi::vn_calculate_aesthetics_scores_in_path(
47            path_c.as_ptr(),
48            &mut raw,
49            &mut has_value,
50            &mut err_msg,
51        )
52    };
53    if status != ffi::status::OK {
54        // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
55        return Err(unsafe { from_swift(status, err_msg) });
56    }
57    Ok(if has_value {
58        Some(AestheticsScores {
59            overall_score: raw.overall_score,
60            is_utility: raw.is_utility,
61        })
62    } else {
63        None
64    })
65}
66
67/// One face plus Apple's portrait-quality score (`0.0..=1.0`,
68/// higher = better candidate for "best moment" selection).
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct FaceCaptureQuality {
71    pub bounding_box: BoundingBox,
72    pub confidence: f32,
73    /// `Some(0.0..=1.0)` if Apple produced a score; `None` if the
74    /// face was detected but quality couldn't be evaluated.
75    pub capture_quality: Option<f32>,
76}
77
78/// Detect faces and grade each one's capture quality.
79///
80/// # Errors
81///
82/// Returns [`VisionError::ImageLoadFailed`] / [`VisionError::RequestFailed`].
83pub fn detect_face_capture_quality_in_path(
84    path: impl AsRef<Path>,
85) -> Result<Vec<FaceCaptureQuality>, VisionError> {
86    let path_str = path
87        .as_ref()
88        .to_str()
89        .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
90    let path_c = CString::new(path_str)
91        .map_err(|e| VisionError::InvalidArgument(format!("path NUL byte: {e}")))?;
92
93    let mut out_array: *mut core::ffi::c_void = ptr::null_mut();
94    let mut out_count: usize = 0;
95    let mut err_msg: *mut c_char = ptr::null_mut();
96
97    // SAFETY: all pointer arguments are valid stack locations or null-initialised out-params; strings are valid C strings for the duration of the call.
98    let status = unsafe {
99        ffi::vn_detect_face_capture_quality_in_path(
100            path_c.as_ptr(),
101            &mut out_array,
102            &mut out_count,
103            &mut err_msg,
104        )
105    };
106    if status != ffi::status::OK {
107        // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
108        return Err(unsafe { from_swift(status, err_msg) });
109    }
110    if out_array.is_null() || out_count == 0 {
111        return Ok(Vec::new());
112    }
113    let typed = out_array.cast::<ffi::FaceQualityRaw>();
114    let mut v = Vec::with_capacity(out_count);
115    for i in 0..out_count {
116        // SAFETY: the pointer is valid for the reported element count; the index is in bounds.
117        let r = unsafe { &*typed.add(i) };
118        v.push(FaceCaptureQuality {
119            bounding_box: BoundingBox {
120                x: r.bbox_x,
121                y: r.bbox_y,
122                width: r.bbox_w,
123                height: r.bbox_h,
124            },
125            confidence: r.confidence,
126            capture_quality: if r.has_quality {
127                Some(r.capture_quality)
128            } else {
129                None
130            },
131        });
132    }
133    // SAFETY: the pointer/count pair was allocated by the bridge and is freed exactly once here.
134    unsafe { ffi::vn_face_quality_observations_free(out_array, out_count) };
135    Ok(v)
136}