Skip to main content

apple_vision/
async_api.rs

1//! Async Vision API — Future-based wrappers for `VNImageRequestHandler` and friends.
2//!
3//! Enable with `features = ["async"]`. Each wrapper dispatches the synchronous
4//! Vision request on a background queue (via `DispatchQueue.global`) and returns
5//! a `std::future::Future` that resolves when the request completes.
6//!
7//! ## Tier-2 note
8//!
9//! Multi-fire delegates, KVO, and continuous observation streams (e.g.
10//! `VNVideoProcessor` frame-by-frame callbacks, optical-flow streaming) are
11//! **not** included here — they follow a Stream pattern and belong in a
12//! future Tier-2 module.
13//!
14//! ## Example
15//!
16//! ```rust,no_run
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! use apple_vision::async_api::AsyncRecognizeText;
19//! use apple_vision::recognize_text::RecognitionLevel;
20//!
21//! let texts = AsyncRecognizeText::new(RecognitionLevel::Accurate, true)
22//!     .recognize_in_path("/path/to/image.png")
23//!     .await?;
24//! for text in &texts {
25//!     println!("{}", text.text);
26//! }
27//! # Ok(())
28//! # }
29//! ```
30
31use std::{
32    ffi::{c_void, CString},
33    future::Future,
34    panic::AssertUnwindSafe,
35    path::{Path, PathBuf},
36    pin::Pin,
37    task::{Context, Poll},
38};
39
40use doom_fish_utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
41use doom_fish_utils::panic_safe::log_callback_panic;
42
43use crate::{error::VisionError, ffi};
44
45#[cfg(feature = "coreml")]
46use crate::classify::Classification;
47#[cfg(feature = "coreml")]
48use crate::coreml::{CoreMLFeatureValueObservation, CoreMLRequest};
49#[cfg(feature = "detect_barcodes")]
50use crate::detect_barcodes::DetectedBarcode;
51#[cfg(feature = "detect_faces")]
52use crate::detect_faces::DetectedFace;
53use crate::human_body_pose_3d::HumanBodyPose3DObservation;
54#[cfg(feature = "recognize_text")]
55use crate::recognize_text::{RecognitionLevel, RecognizedText};
56#[cfg(feature = "segmentation")]
57use crate::segmentation::{SegmentationMask, SegmentationQuality};
58use crate::trajectories::Trajectory;
59
60enum FutureState<T> {
61    Ready(Option<Result<T, VisionError>>),
62    Pending(AsyncCompletionFuture<T>),
63}
64
65impl<T> FutureState<T> {
66    const fn ready_err(error: VisionError) -> Self {
67        Self::Ready(Some(Err(error)))
68    }
69
70    const fn pending(future: AsyncCompletionFuture<T>) -> Self {
71        Self::Pending(future)
72    }
73}
74
75impl<T: Unpin> Future for FutureState<T> {
76    type Output = Result<T, VisionError>;
77
78    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
79        match self.as_mut().get_mut() {
80            Self::Ready(result) => Poll::Ready(
81                result
82                    .take()
83                    .expect("async Vision future polled after completion"),
84            ),
85            Self::Pending(future) => Pin::new(future)
86                .poll(cx)
87                .map(|result| result.map_err(VisionError::RequestFailed)),
88        }
89    }
90}
91
92fn path_to_cstring(path: impl AsRef<Path>) -> Result<CString, VisionError> {
93    let path_str = path
94        .as_ref()
95        .to_str()
96        .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
97    CString::new(path_str)
98        .map_err(|error| VisionError::InvalidArgument(format!("path NUL byte: {error}")))
99}
100
101struct WorkerFuture<T> {
102    inner: AsyncCompletionFuture<Result<T, VisionError>>,
103}
104
105impl<T> std::fmt::Debug for WorkerFuture<T> {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("WorkerFuture").finish_non_exhaustive()
108    }
109}
110
111impl<T> Future for WorkerFuture<T> {
112    type Output = Result<T, VisionError>;
113
114    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
115        Pin::new(&mut self.inner).poll(cx).map(|result| {
116            result.unwrap_or_else(|message| {
117                Err(VisionError::Unknown {
118                    code: ffi::status::UNKNOWN,
119                    message,
120                })
121            })
122        })
123    }
124}
125
126fn run_sync_on_worker<T, F>(work: F) -> WorkerFuture<T>
127where
128    T: Send + 'static,
129    F: FnOnce() -> Result<T, VisionError> + Send + 'static,
130{
131    let (future, ctx) = AsyncCompletion::<Result<T, VisionError>>::create();
132    let ctx = ctx as usize;
133    std::thread::spawn(move || unsafe {
134        AsyncCompletion::complete_ok(ctx as *mut c_void, work());
135    });
136    WorkerFuture { inner: future }
137}
138
139#[cfg(feature = "coreml")]
140pub struct CoreMLClassifyFuture {
141    inner: WorkerFuture<Vec<Classification>>,
142}
143
144#[cfg(feature = "coreml")]
145impl std::fmt::Debug for CoreMLClassifyFuture {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("CoreMLClassifyFuture")
148            .finish_non_exhaustive()
149    }
150}
151
152#[cfg(feature = "coreml")]
153impl Future for CoreMLClassifyFuture {
154    type Output = Result<Vec<Classification>, VisionError>;
155
156    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
157        Pin::new(&mut self.inner).poll(cx)
158    }
159}
160
161#[cfg(feature = "coreml")]
162pub struct CoreMLFeatureValueFuture {
163    inner: WorkerFuture<Option<CoreMLFeatureValueObservation>>,
164}
165
166#[cfg(feature = "coreml")]
167impl std::fmt::Debug for CoreMLFeatureValueFuture {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.debug_struct("CoreMLFeatureValueFuture")
170            .finish_non_exhaustive()
171    }
172}
173
174#[cfg(feature = "coreml")]
175impl Future for CoreMLFeatureValueFuture {
176    type Output = Result<Option<CoreMLFeatureValueObservation>, VisionError>;
177
178    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
179        Pin::new(&mut self.inner).poll(cx)
180    }
181}
182
183#[cfg(feature = "coreml")]
184#[derive(Debug, Clone)]
185pub struct AsyncCoreMLRequest {
186    request: CoreMLRequest,
187}
188
189#[cfg(feature = "coreml")]
190impl AsyncCoreMLRequest {
191    #[must_use]
192    pub const fn new(request: CoreMLRequest) -> Self {
193        Self { request }
194    }
195
196    #[must_use]
197    pub fn classify_in_path(&self, path: impl AsRef<Path>) -> CoreMLClassifyFuture {
198        let request = self.request.clone();
199        let path = path.as_ref().to_path_buf();
200        CoreMLClassifyFuture {
201            inner: run_sync_on_worker(move || request.classify(path.as_path())),
202        }
203    }
204
205    #[must_use]
206    pub fn feature_value_in_path(&self, path: impl AsRef<Path>) -> CoreMLFeatureValueFuture {
207        let request = self.request.clone();
208        let path = path.as_ref().to_path_buf();
209        CoreMLFeatureValueFuture {
210            inner: run_sync_on_worker(move || request.feature_value(path.as_path())),
211        }
212    }
213}
214
215pub struct DetectHumanBodyPose3DFuture {
216    inner: WorkerFuture<Vec<HumanBodyPose3DObservation>>,
217}
218
219impl std::fmt::Debug for DetectHumanBodyPose3DFuture {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        f.debug_struct("DetectHumanBodyPose3DFuture")
222            .finish_non_exhaustive()
223    }
224}
225
226impl Future for DetectHumanBodyPose3DFuture {
227    type Output = Result<Vec<HumanBodyPose3DObservation>, VisionError>;
228
229    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
230        Pin::new(&mut self.inner).poll(cx)
231    }
232}
233
234#[derive(Debug, Clone, Copy, Default)]
235pub struct AsyncDetectHumanBodyPose3D;
236
237impl AsyncDetectHumanBodyPose3D {
238    #[must_use]
239    pub const fn new() -> Self {
240        Self
241    }
242
243    #[must_use]
244    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectHumanBodyPose3DFuture {
245        let path = path.as_ref().to_path_buf();
246        DetectHumanBodyPose3DFuture {
247            inner: run_sync_on_worker(move || {
248                crate::human_body_pose_3d::detect_human_body_pose_3d_observations(path.as_path())
249            }),
250        }
251    }
252}
253
254pub struct DetectTrajectoriesFuture {
255    inner: WorkerFuture<Vec<Trajectory>>,
256}
257
258impl std::fmt::Debug for DetectTrajectoriesFuture {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        f.debug_struct("DetectTrajectoriesFuture")
261            .finish_non_exhaustive()
262    }
263}
264
265impl Future for DetectTrajectoriesFuture {
266    type Output = Result<Vec<Trajectory>, VisionError>;
267
268    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
269        Pin::new(&mut self.inner).poll(cx)
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct AsyncDetectTrajectories {
275    trajectory_length: usize,
276}
277
278impl AsyncDetectTrajectories {
279    #[must_use]
280    pub const fn new(trajectory_length: usize) -> Self {
281        Self { trajectory_length }
282    }
283
284    #[must_use]
285    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectTrajectoriesFuture {
286        let path: PathBuf = path.as_ref().to_path_buf();
287        let trajectory_length = self.trajectory_length;
288        DetectTrajectoriesFuture {
289            inner: run_sync_on_worker(move || {
290                crate::trajectories::detect_trajectories(path.as_path(), trajectory_length)
291            }),
292        }
293    }
294}
295
296// ============================================================================
297// Text Recognition Future
298// ============================================================================
299
300/// Parse the raw text-recognition result coming from the Swift bridge.
301///
302/// Returns `Ok(results)` on success or `Err(message)` on any Swift-reported error.
303/// Frees the Swift-owned `result` allocation before returning.
304///
305/// # Safety
306///
307/// `result` must be either null or a valid pointer to an `AsyncArrayResultRaw` struct
308/// produced by the Swift bridge whose `array` field, when non-null, points to
309/// `count` valid `RecognizedTextRaw` elements.  `error` must be either null or a
310/// valid null-terminated C string owned by the bridge.
311#[cfg(feature = "recognize_text")]
312unsafe fn parse_text_result(
313    result: *const c_void,
314    error: *const i8,
315) -> Result<Vec<RecognizedText>, String> {
316    if !error.is_null() {
317        // SAFETY: caller guarantees `error` is a valid C string when non-null.
318        return Err(unsafe { error_from_cstr(error) });
319    }
320    if result.is_null() {
321        return Err("text recognition returned null".into());
322    }
323
324    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
325    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
326    let texts = if raw.array.is_null() || raw.count == 0 {
327        Vec::new()
328    } else {
329        let typed = raw.array.cast::<ffi::RecognizedTextRaw>();
330        let mut out = Vec::with_capacity(raw.count);
331        for index in 0..raw.count {
332            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
333            let entry = unsafe { &*typed.add(index) };
334            let text = if entry.text.is_null() {
335                String::new()
336            } else {
337                // SAFETY: `entry.text` is a valid C string when non-null.
338                unsafe { std::ffi::CStr::from_ptr(entry.text) }
339                    .to_string_lossy()
340                    .into_owned()
341            };
342            out.push(RecognizedText {
343                text,
344                confidence: entry.confidence,
345                bounding_box: crate::recognize_text::BoundingBox {
346                    x: entry.bbox_x,
347                    y: entry.bbox_y,
348                    width: entry.bbox_w,
349                    height: entry.bbox_h,
350                },
351            });
352        }
353        // SAFETY: `raw.array` and `raw.count` are the pair produced by the Swift bridge;
354        // this is the unique call site that frees them.
355        unsafe { ffi::vn_recognized_text_free(raw.array, raw.count) };
356        out
357    };
358
359    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge;
360    // freeing here is safe because this is the unique call site for this allocation.
361    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
362    Ok(texts)
363}
364
365/// `extern "C"` callback invoked by the Swift bridge when text recognition completes.
366///
367/// # Safety contract
368///
369/// Called from a Swift `DispatchQueue`; all pointer arguments follow the
370/// Swift-bridge protocol documented on [`parse_text_result`].  The body is
371/// wrapped in `catch_unwind` so that an unexpected Rust panic does not unwind
372/// through the Swift/C ABI (which is undefined behaviour).  On panic the
373/// future is completed with an error rather than left permanently pending.
374#[cfg(feature = "recognize_text")]
375extern "C" fn text_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
376    // SAFETY: `result` and `error` are valid for the duration of this call per
377    // the Swift bridge contract. `AssertUnwindSafe` is correct here because the
378    // raw pointers are not accessed after unwinding.
379    let outcome =
380        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_text_result(result, error) }));
381    match outcome {
382        Ok(Ok(texts)) => {
383            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from
384            // `AsyncCompletion::create()`; it is valid and unconsumed at this point.
385            unsafe { AsyncCompletion::complete_ok(ctx, texts) };
386        }
387        Ok(Err(msg)) => {
388            // SAFETY: same as above.
389            unsafe { AsyncCompletion::<Vec<RecognizedText>>::complete_err(ctx, msg) };
390        }
391        Err(payload) => {
392            log_callback_panic("text_result_cb", payload.as_ref());
393            // SAFETY: same as above.
394            unsafe {
395                AsyncCompletion::<Vec<RecognizedText>>::complete_err(
396                    ctx,
397                    "panic in Vision text_result_cb".into(),
398                );
399            };
400        }
401    }
402}
403
404/// Future resolving to a `Vec<RecognizedText>`.
405#[cfg(feature = "recognize_text")]
406pub struct RecognizeTextFuture {
407    inner: FutureState<Vec<RecognizedText>>,
408}
409
410#[cfg(feature = "recognize_text")]
411impl std::fmt::Debug for RecognizeTextFuture {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        f.debug_struct("RecognizeTextFuture")
414            .finish_non_exhaustive()
415    }
416}
417
418#[cfg(feature = "recognize_text")]
419impl Future for RecognizeTextFuture {
420    type Output = Result<Vec<RecognizedText>, VisionError>;
421
422    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
423        Pin::new(&mut self.inner).poll(cx)
424    }
425}
426
427/// Async wrapper for `VNRecognizeTextRequest`.
428///
429/// Runs text recognition on a background `DispatchQueue` and returns a
430/// [`RecognizeTextFuture`] that resolves when the request completes.
431#[cfg(feature = "recognize_text")]
432#[derive(Debug, Clone)]
433pub struct AsyncRecognizeText {
434    recognition_level: RecognitionLevel,
435    uses_language_correction: bool,
436}
437
438#[cfg(feature = "recognize_text")]
439impl Default for AsyncRecognizeText {
440    fn default() -> Self {
441        Self::new(RecognitionLevel::Accurate, true)
442    }
443}
444
445#[cfg(feature = "recognize_text")]
446impl AsyncRecognizeText {
447    #[must_use]
448    pub const fn new(recognition_level: RecognitionLevel, uses_language_correction: bool) -> Self {
449        Self {
450            recognition_level,
451            uses_language_correction,
452        }
453    }
454
455    /// Recognize text in the image at `path` asynchronously.
456    ///
457    /// # Errors
458    ///
459    /// Returns [`VisionError::RequestFailed`] if Vision fails, or
460    /// [`VisionError::InvalidArgument`] if the path cannot be encoded.
461    pub fn recognize_in_path(&self, path: impl AsRef<Path>) -> RecognizeTextFuture {
462        match path_to_cstring(path) {
463            Err(error) => RecognizeTextFuture {
464                inner: FutureState::ready_err(error),
465            },
466            Ok(path_c) => {
467                let (future, ctx) = AsyncCompletion::create();
468                // SAFETY: `path_c` is a valid null-terminated C string for the duration of
469                // this call. `text_result_cb` satisfies the callback contract: single-fire,
470                // completes the context exactly once. `ctx` is the `Arc` context from
471                // `AsyncCompletion::create()` cast to `*mut c_void`.
472                unsafe {
473                    ffi::vn_recognize_text_in_path_async(
474                        path_c.as_ptr(),
475                        self.recognition_level.as_raw(),
476                        self.uses_language_correction,
477                        text_result_cb,
478                        ctx,
479                    );
480                };
481                RecognizeTextFuture {
482                    inner: FutureState::pending(future),
483                }
484            }
485        }
486    }
487}
488
489// ============================================================================
490// Face Detection Future
491// ============================================================================
492
493/// Parse the raw face-detection result from the Swift bridge.
494///
495/// # Safety
496///
497/// `result` must be either null or a valid `AsyncArrayResultRaw` pointer whose
498/// `array` field, when non-null, points to `count` valid `DetectedFaceRaw`
499/// elements.  `error` must be either null or a valid null-terminated C string.
500#[cfg(feature = "detect_faces")]
501unsafe fn parse_face_result(
502    result: *const c_void,
503    error: *const i8,
504) -> Result<Vec<DetectedFace>, String> {
505    if !error.is_null() {
506        // SAFETY: caller guarantees `error` is a valid C string when non-null.
507        return Err(unsafe { error_from_cstr(error) });
508    }
509    if result.is_null() {
510        return Err("face detection returned null".into());
511    }
512
513    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
514    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
515    let faces = if raw.array.is_null() || raw.count == 0 {
516        Vec::new()
517    } else {
518        let typed = raw.array.cast::<ffi::DetectedFaceRaw>();
519        let mut out = Vec::with_capacity(raw.count);
520        let nan_to_none = |value: f32| if value.is_nan() { None } else { Some(value) };
521        for index in 0..raw.count {
522            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
523            let entry = unsafe { &*typed.add(index) };
524            out.push(DetectedFace {
525                bounding_box: crate::recognize_text::BoundingBox {
526                    x: entry.bbox_x,
527                    y: entry.bbox_y,
528                    width: entry.bbox_w,
529                    height: entry.bbox_h,
530                },
531                confidence: entry.confidence,
532                roll: nan_to_none(entry.roll),
533                yaw: nan_to_none(entry.yaw),
534                pitch: nan_to_none(entry.pitch),
535            });
536        }
537        // SAFETY: `raw.array` and `raw.count` are the pair produced by the Swift bridge;
538        // this is the unique call site that frees them.
539        unsafe { ffi::vn_detected_faces_free(raw.array, raw.count) };
540        out
541    };
542
543    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
544    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
545    Ok(faces)
546}
547
548/// `extern "C"` callback invoked by the Swift bridge when face detection completes.
549///
550/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
551#[cfg(feature = "detect_faces")]
552extern "C" fn face_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
553    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
554    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
555    let outcome =
556        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_face_result(result, error) }));
557    match outcome {
558        Ok(Ok(faces)) => {
559            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
560            unsafe { AsyncCompletion::complete_ok(ctx, faces) };
561        }
562        Ok(Err(msg)) => {
563            // SAFETY: same as above.
564            unsafe { AsyncCompletion::<Vec<DetectedFace>>::complete_err(ctx, msg) };
565        }
566        Err(payload) => {
567            log_callback_panic("face_result_cb", payload.as_ref());
568            // SAFETY: same as above.
569            unsafe {
570                AsyncCompletion::<Vec<DetectedFace>>::complete_err(
571                    ctx,
572                    "panic in Vision face_result_cb".into(),
573                );
574            };
575        }
576    }
577}
578
579/// Future resolving to a `Vec<DetectedFace>`.
580#[cfg(feature = "detect_faces")]
581pub struct DetectFacesFuture {
582    inner: FutureState<Vec<DetectedFace>>,
583}
584
585#[cfg(feature = "detect_faces")]
586impl std::fmt::Debug for DetectFacesFuture {
587    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588        f.debug_struct("DetectFacesFuture").finish_non_exhaustive()
589    }
590}
591
592#[cfg(feature = "detect_faces")]
593impl Future for DetectFacesFuture {
594    type Output = Result<Vec<DetectedFace>, VisionError>;
595
596    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
597        Pin::new(&mut self.inner).poll(cx)
598    }
599}
600
601/// Async wrapper for `VNDetectFaceRectanglesRequest`.
602#[cfg(feature = "detect_faces")]
603#[derive(Debug, Clone, Copy, Default)]
604pub struct AsyncDetectFaces;
605
606#[cfg(feature = "detect_faces")]
607impl AsyncDetectFaces {
608    #[must_use]
609    pub const fn new() -> Self {
610        Self
611    }
612
613    /// Detect faces in the image at `path` asynchronously.
614    ///
615    /// # Errors
616    ///
617    /// Returns [`VisionError::RequestFailed`] if Vision fails.
618    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectFacesFuture {
619        match path_to_cstring(path) {
620            Err(error) => DetectFacesFuture {
621                inner: FutureState::ready_err(error),
622            },
623            Ok(path_c) => {
624                let (future, ctx) = AsyncCompletion::create();
625                // SAFETY: `path_c` is a valid C string. `face_result_cb` satisfies the
626                // single-fire callback contract and completes `ctx` exactly once.
627                unsafe {
628                    ffi::vn_detect_faces_in_path_async(path_c.as_ptr(), face_result_cb, ctx);
629                };
630                DetectFacesFuture {
631                    inner: FutureState::pending(future),
632                }
633            }
634        }
635    }
636}
637
638// ============================================================================
639// Barcode Detection Future
640// ============================================================================
641
642/// Parse the raw barcode-detection result from the Swift bridge.
643///
644/// # Safety
645///
646/// `result` must be either null or a valid `AsyncArrayResultRaw` pointer whose
647/// `array` field, when non-null, points to `count` valid `DetectedBarcodeRaw`
648/// elements.  `error` must be either null or a valid null-terminated C string.
649#[cfg(feature = "detect_barcodes")]
650unsafe fn parse_barcode_result(
651    result: *const c_void,
652    error: *const i8,
653) -> Result<Vec<DetectedBarcode>, String> {
654    if !error.is_null() {
655        // SAFETY: caller guarantees `error` is a valid C string when non-null.
656        return Err(unsafe { error_from_cstr(error) });
657    }
658    if result.is_null() {
659        return Err("barcode detection returned null".into());
660    }
661
662    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
663    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
664    let barcodes = if raw.array.is_null() || raw.count == 0 {
665        Vec::new()
666    } else {
667        let typed = raw.array.cast::<ffi::DetectedBarcodeRaw>();
668        let mut out = Vec::with_capacity(raw.count);
669        for index in 0..raw.count {
670            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
671            let entry = unsafe { &*typed.add(index) };
672            let payload = if entry.payload.is_null() {
673                String::new()
674            } else {
675                // SAFETY: `entry.payload` is a valid C string when non-null.
676                unsafe { std::ffi::CStr::from_ptr(entry.payload) }
677                    .to_string_lossy()
678                    .into_owned()
679            };
680            let symbology = if entry.symbology.is_null() {
681                String::new()
682            } else {
683                // SAFETY: `entry.symbology` is a valid C string when non-null.
684                unsafe { std::ffi::CStr::from_ptr(entry.symbology) }
685                    .to_string_lossy()
686                    .into_owned()
687            };
688            out.push(DetectedBarcode {
689                payload,
690                symbology,
691                confidence: entry.confidence,
692                bounding_box: crate::recognize_text::BoundingBox {
693                    x: entry.bbox_x,
694                    y: entry.bbox_y,
695                    width: entry.bbox_w,
696                    height: entry.bbox_h,
697                },
698            });
699        }
700        // SAFETY: `raw.array` and `raw.count` are the Swift-bridge pair; unique free site.
701        unsafe { ffi::vn_detected_barcodes_free(raw.array, raw.count) };
702        out
703    };
704
705    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
706    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
707    Ok(barcodes)
708}
709
710/// `extern "C"` callback invoked by the Swift bridge when barcode detection completes.
711///
712/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
713#[cfg(feature = "detect_barcodes")]
714extern "C" fn barcode_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
715    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
716    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
717    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
718        parse_barcode_result(result, error)
719    }));
720    match outcome {
721        Ok(Ok(barcodes)) => {
722            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
723            unsafe { AsyncCompletion::complete_ok(ctx, barcodes) };
724        }
725        Ok(Err(msg)) => {
726            // SAFETY: same as above.
727            unsafe { AsyncCompletion::<Vec<DetectedBarcode>>::complete_err(ctx, msg) };
728        }
729        Err(payload) => {
730            log_callback_panic("barcode_result_cb", payload.as_ref());
731            // SAFETY: same as above.
732            unsafe {
733                AsyncCompletion::<Vec<DetectedBarcode>>::complete_err(
734                    ctx,
735                    "panic in Vision barcode_result_cb".into(),
736                );
737            };
738        }
739    }
740}
741
742/// Future resolving to a `Vec<DetectedBarcode>`.
743#[cfg(feature = "detect_barcodes")]
744pub struct DetectBarcodesFuture {
745    inner: FutureState<Vec<DetectedBarcode>>,
746}
747
748#[cfg(feature = "detect_barcodes")]
749impl std::fmt::Debug for DetectBarcodesFuture {
750    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751        f.debug_struct("DetectBarcodesFuture")
752            .finish_non_exhaustive()
753    }
754}
755
756#[cfg(feature = "detect_barcodes")]
757impl Future for DetectBarcodesFuture {
758    type Output = Result<Vec<DetectedBarcode>, VisionError>;
759
760    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
761        Pin::new(&mut self.inner).poll(cx)
762    }
763}
764
765/// Async wrapper for `VNDetectBarcodesRequest`.
766#[cfg(feature = "detect_barcodes")]
767#[derive(Debug, Clone, Copy, Default)]
768pub struct AsyncDetectBarcodes;
769
770#[cfg(feature = "detect_barcodes")]
771impl AsyncDetectBarcodes {
772    #[must_use]
773    pub const fn new() -> Self {
774        Self
775    }
776
777    /// Detect barcodes in the image at `path` asynchronously.
778    ///
779    /// # Errors
780    ///
781    /// Returns [`VisionError::RequestFailed`] if Vision fails.
782    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectBarcodesFuture {
783        match path_to_cstring(path) {
784            Err(error) => DetectBarcodesFuture {
785                inner: FutureState::ready_err(error),
786            },
787            Ok(path_c) => {
788                let (future, ctx) = AsyncCompletion::create();
789                // SAFETY: `path_c` is a valid C string. `barcode_result_cb` satisfies the
790                // single-fire callback contract and completes `ctx` exactly once.
791                unsafe {
792                    ffi::vn_detect_barcodes_in_path_async(path_c.as_ptr(), barcode_result_cb, ctx);
793                };
794                DetectBarcodesFuture {
795                    inner: FutureState::pending(future),
796                }
797            }
798        }
799    }
800}
801
802// ============================================================================
803// Person Segmentation Future
804// ============================================================================
805
806/// Parse the raw person-segmentation result from the Swift bridge.
807///
808/// # Safety
809///
810/// `result` must be either null or a valid `AsyncSegResultRaw` pointer whose
811/// `bytes` field, when non-null, points to at least `height * bytes_per_row` bytes.
812/// `error` must be either null or a valid null-terminated C string.
813#[cfg(feature = "segmentation")]
814unsafe fn parse_seg_result(
815    result: *const c_void,
816    error: *const i8,
817) -> Result<SegmentationMask, String> {
818    if !error.is_null() {
819        // SAFETY: caller guarantees `error` is a valid C string when non-null.
820        return Err(unsafe { error_from_cstr(error) });
821    }
822    if result.is_null() {
823        return Err("segmentation returned null".into());
824    }
825
826    // SAFETY: caller guarantees `result` is a valid `AsyncSegResultRaw` pointer.
827    let raw = unsafe { &*(result.cast::<ffi::AsyncSegResultRaw>()) };
828    if raw.bytes.is_null() {
829        // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
830        unsafe { ffi::vn_async_seg_result_free(result.cast_mut()) };
831        return Err("segmentation bytes were null".into());
832    }
833
834    let len = raw.height.saturating_mul(raw.bytes_per_row);
835    // SAFETY: `raw.bytes` is valid for `len` bytes as guaranteed by the Swift bridge.
836    let bytes = unsafe { core::slice::from_raw_parts(raw.bytes, len) }.to_vec();
837    let mask = SegmentationMask {
838        width: raw.width,
839        height: raw.height,
840        bytes_per_row: raw.bytes_per_row,
841        bytes,
842    };
843
844    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge;
845    // unique free site.
846    unsafe { ffi::vn_async_seg_result_free(result.cast_mut()) };
847    Ok(mask)
848}
849
850/// `extern "C"` callback invoked by the Swift bridge when person segmentation completes.
851///
852/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
853#[cfg(feature = "segmentation")]
854extern "C" fn seg_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
855    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
856    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
857    let outcome =
858        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_seg_result(result, error) }));
859    match outcome {
860        Ok(Ok(mask)) => {
861            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
862            unsafe { AsyncCompletion::complete_ok(ctx, mask) };
863        }
864        Ok(Err(msg)) => {
865            // SAFETY: same as above.
866            unsafe { AsyncCompletion::<SegmentationMask>::complete_err(ctx, msg) };
867        }
868        Err(payload) => {
869            log_callback_panic("seg_result_cb", payload.as_ref());
870            // SAFETY: same as above.
871            unsafe {
872                AsyncCompletion::<SegmentationMask>::complete_err(
873                    ctx,
874                    "panic in Vision seg_result_cb".into(),
875                );
876            };
877        }
878    }
879}
880
881/// Future resolving to a `SegmentationMask`.
882#[cfg(feature = "segmentation")]
883pub struct PersonSegmentationFuture {
884    inner: FutureState<SegmentationMask>,
885}
886
887#[cfg(feature = "segmentation")]
888impl std::fmt::Debug for PersonSegmentationFuture {
889    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890        f.debug_struct("PersonSegmentationFuture")
891            .finish_non_exhaustive()
892    }
893}
894
895#[cfg(feature = "segmentation")]
896impl Future for PersonSegmentationFuture {
897    type Output = Result<SegmentationMask, VisionError>;
898
899    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
900        Pin::new(&mut self.inner).poll(cx)
901    }
902}
903
904/// Async wrapper for `VNGeneratePersonSegmentationRequest`.
905#[cfg(feature = "segmentation")]
906#[derive(Debug, Clone, Copy)]
907pub struct AsyncPersonSegmentation {
908    quality: SegmentationQuality,
909}
910
911#[cfg(feature = "segmentation")]
912impl Default for AsyncPersonSegmentation {
913    fn default() -> Self {
914        Self::new(SegmentationQuality::Balanced)
915    }
916}
917
918#[cfg(feature = "segmentation")]
919impl AsyncPersonSegmentation {
920    #[must_use]
921    pub const fn new(quality: SegmentationQuality) -> Self {
922        Self { quality }
923    }
924
925    /// Generate a person segmentation mask for the image at `path` asynchronously.
926    ///
927    /// # Errors
928    ///
929    /// Returns [`VisionError::RequestFailed`] if Vision fails.
930    pub fn generate_in_path(&self, path: impl AsRef<Path>) -> PersonSegmentationFuture {
931        match path_to_cstring(path) {
932            Err(error) => PersonSegmentationFuture {
933                inner: FutureState::ready_err(error),
934            },
935            Ok(path_c) => {
936                let (future, ctx) = AsyncCompletion::create();
937                // SAFETY: `path_c` is a valid C string. `seg_result_cb` satisfies the
938                // single-fire callback contract and completes `ctx` exactly once.
939                // `self.quality as i32` is always a valid quality-level enum value.
940                unsafe {
941                    ffi::vn_generate_person_segmentation_async(
942                        path_c.as_ptr(),
943                        self.quality as i32,
944                        seg_result_cb,
945                        ctx,
946                    );
947                };
948                PersonSegmentationFuture {
949                    inner: FutureState::pending(future),
950                }
951            }
952        }
953    }
954}