apple-vision 0.16.4

Safe Rust bindings for Apple's Vision framework — OCR, object detection, face landmarks on macOS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
//! Async Vision API — Future-based wrappers for `VNImageRequestHandler` and friends.
//!
//! Enable with `features = ["async"]`. Each wrapper dispatches the synchronous
//! Vision request on a background queue (via `DispatchQueue.global`) and returns
//! a `std::future::Future` that resolves when the request completes.
//!
//! ## Tier-2 note
//!
//! Multi-fire delegates, KVO, and continuous observation streams (e.g.
//! `VNVideoProcessor` frame-by-frame callbacks, optical-flow streaming) are
//! **not** included here — they follow a Stream pattern and belong in a
//! future Tier-2 module.
//!
//! ## Example
//!
//! ```rust,no_run
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! use apple_vision::async_api::AsyncRecognizeText;
//! use apple_vision::recognize_text::RecognitionLevel;
//!
//! let texts = AsyncRecognizeText::new(RecognitionLevel::Accurate, true)
//!     .recognize_in_path("/path/to/image.png")
//!     .await?;
//! for text in &texts {
//!     println!("{}", text.text);
//! }
//! # Ok(())
//! # }
//! ```

use std::{
    ffi::{c_void, CString},
    future::Future,
    panic::AssertUnwindSafe,
    path::Path,
    pin::Pin,
    task::{Context, Poll},
};

use doom_fish_utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
use doom_fish_utils::panic_safe::log_callback_panic;

use crate::{error::VisionError, ffi};

#[cfg(feature = "detect_barcodes")]
use crate::detect_barcodes::DetectedBarcode;
#[cfg(feature = "detect_faces")]
use crate::detect_faces::DetectedFace;
#[cfg(feature = "recognize_text")]
use crate::recognize_text::{RecognitionLevel, RecognizedText};
#[cfg(feature = "segmentation")]
use crate::segmentation::{SegmentationMask, SegmentationQuality};

enum FutureState<T> {
    Ready(Option<Result<T, VisionError>>),
    Pending(AsyncCompletionFuture<T>),
}

impl<T> FutureState<T> {
    const fn ready_err(error: VisionError) -> Self {
        Self::Ready(Some(Err(error)))
    }

    const fn pending(future: AsyncCompletionFuture<T>) -> Self {
        Self::Pending(future)
    }
}

impl<T: Unpin> Future for FutureState<T> {
    type Output = Result<T, VisionError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self.as_mut().get_mut() {
            Self::Ready(result) => Poll::Ready(
                result
                    .take()
                    .expect("async Vision future polled after completion"),
            ),
            Self::Pending(future) => Pin::new(future)
                .poll(cx)
                .map(|result| result.map_err(VisionError::RequestFailed)),
        }
    }
}

fn path_to_cstring(path: impl AsRef<Path>) -> Result<CString, VisionError> {
    let path_str = path
        .as_ref()
        .to_str()
        .ok_or_else(|| VisionError::InvalidArgument("non-UTF-8 path".into()))?;
    CString::new(path_str)
        .map_err(|error| VisionError::InvalidArgument(format!("path NUL byte: {error}")))
}

// ============================================================================
// Text Recognition Future
// ============================================================================

/// Parse the raw text-recognition result coming from the Swift bridge.
///
/// Returns `Ok(results)` on success or `Err(message)` on any Swift-reported error.
/// Frees the Swift-owned `result` allocation before returning.
///
/// # Safety
///
/// `result` must be either null or a valid pointer to an `AsyncArrayResultRaw` struct
/// produced by the Swift bridge whose `array` field, when non-null, points to
/// `count` valid `RecognizedTextRaw` elements.  `error` must be either null or a
/// valid null-terminated C string owned by the bridge.
#[cfg(feature = "recognize_text")]
unsafe fn parse_text_result(
    result: *const c_void,
    error: *const i8,
) -> Result<Vec<RecognizedText>, String> {
    if !error.is_null() {
        // SAFETY: caller guarantees `error` is a valid C string when non-null.
        return Err(unsafe { error_from_cstr(error) });
    }
    if result.is_null() {
        return Err("text recognition returned null".into());
    }

    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
    let texts = if raw.array.is_null() || raw.count == 0 {
        Vec::new()
    } else {
        let typed = raw.array.cast::<ffi::RecognizedTextRaw>();
        let mut out = Vec::with_capacity(raw.count);
        for index in 0..raw.count {
            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
            let entry = unsafe { &*typed.add(index) };
            let text = if entry.text.is_null() {
                String::new()
            } else {
                // SAFETY: `entry.text` is a valid C string when non-null.
                unsafe { std::ffi::CStr::from_ptr(entry.text) }
                    .to_string_lossy()
                    .into_owned()
            };
            out.push(RecognizedText {
                text,
                confidence: entry.confidence,
                bounding_box: crate::recognize_text::BoundingBox {
                    x: entry.bbox_x,
                    y: entry.bbox_y,
                    width: entry.bbox_w,
                    height: entry.bbox_h,
                },
            });
        }
        // SAFETY: `raw.array` and `raw.count` are the pair produced by the Swift bridge;
        // this is the unique call site that frees them.
        unsafe { ffi::vn_recognized_text_free(raw.array, raw.count) };
        out
    };

    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge;
    // freeing here is safe because this is the unique call site for this allocation.
    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
    Ok(texts)
}

/// `extern "C"` callback invoked by the Swift bridge when text recognition completes.
///
/// # Safety contract
///
/// Called from a Swift `DispatchQueue`; all pointer arguments follow the
/// Swift-bridge protocol documented on [`parse_text_result`].  The body is
/// wrapped in `catch_unwind` so that an unexpected Rust panic does not unwind
/// through the Swift/C ABI (which is undefined behaviour).  On panic the
/// future is completed with an error rather than left permanently pending.
#[cfg(feature = "recognize_text")]
extern "C" fn text_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    // SAFETY: `result` and `error` are valid for the duration of this call per
    // the Swift bridge contract. `AssertUnwindSafe` is correct here because the
    // raw pointers are not accessed after unwinding.
    let outcome =
        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_text_result(result, error) }));
    match outcome {
        Ok(Ok(texts)) => {
            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from
            // `AsyncCompletion::create()`; it is valid and unconsumed at this point.
            unsafe { AsyncCompletion::complete_ok(ctx, texts) };
        }
        Ok(Err(msg)) => {
            // SAFETY: same as above.
            unsafe { AsyncCompletion::<Vec<RecognizedText>>::complete_err(ctx, msg) };
        }
        Err(payload) => {
            log_callback_panic("text_result_cb", payload.as_ref());
            // SAFETY: same as above.
            unsafe {
                AsyncCompletion::<Vec<RecognizedText>>::complete_err(
                    ctx,
                    "panic in Vision text_result_cb".into(),
                );
            };
        }
    }
}

/// Future resolving to a `Vec<RecognizedText>`.
#[cfg(feature = "recognize_text")]
pub struct RecognizeTextFuture {
    inner: FutureState<Vec<RecognizedText>>,
}

#[cfg(feature = "recognize_text")]
impl std::fmt::Debug for RecognizeTextFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RecognizeTextFuture")
            .finish_non_exhaustive()
    }
}

#[cfg(feature = "recognize_text")]
impl Future for RecognizeTextFuture {
    type Output = Result<Vec<RecognizedText>, VisionError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx)
    }
}

/// Async wrapper for `VNRecognizeTextRequest`.
///
/// Runs text recognition on a background `DispatchQueue` and returns a
/// [`RecognizeTextFuture`] that resolves when the request completes.
#[cfg(feature = "recognize_text")]
#[derive(Debug, Clone)]
pub struct AsyncRecognizeText {
    recognition_level: RecognitionLevel,
    uses_language_correction: bool,
}

#[cfg(feature = "recognize_text")]
impl Default for AsyncRecognizeText {
    fn default() -> Self {
        Self::new(RecognitionLevel::Accurate, true)
    }
}

#[cfg(feature = "recognize_text")]
impl AsyncRecognizeText {
    #[must_use]
    pub const fn new(recognition_level: RecognitionLevel, uses_language_correction: bool) -> Self {
        Self {
            recognition_level,
            uses_language_correction,
        }
    }

    /// Recognize text in the image at `path` asynchronously.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError::RequestFailed`] if Vision fails, or
    /// [`VisionError::InvalidArgument`] if the path cannot be encoded.
    pub fn recognize_in_path(&self, path: impl AsRef<Path>) -> RecognizeTextFuture {
        match path_to_cstring(path) {
            Err(error) => RecognizeTextFuture {
                inner: FutureState::ready_err(error),
            },
            Ok(path_c) => {
                let (future, ctx) = AsyncCompletion::create();
                // SAFETY: `path_c` is a valid null-terminated C string for the duration of
                // this call. `text_result_cb` satisfies the callback contract: single-fire,
                // completes the context exactly once. `ctx` is the `Arc` context from
                // `AsyncCompletion::create()` cast to `*mut c_void`.
                unsafe {
                    ffi::vn_recognize_text_in_path_async(
                        path_c.as_ptr(),
                        self.recognition_level.as_raw(),
                        self.uses_language_correction,
                        text_result_cb,
                        ctx,
                    );
                };
                RecognizeTextFuture {
                    inner: FutureState::pending(future),
                }
            }
        }
    }
}

// ============================================================================
// Face Detection Future
// ============================================================================

/// Parse the raw face-detection result from the Swift bridge.
///
/// # Safety
///
/// `result` must be either null or a valid `AsyncArrayResultRaw` pointer whose
/// `array` field, when non-null, points to `count` valid `DetectedFaceRaw`
/// elements.  `error` must be either null or a valid null-terminated C string.
#[cfg(feature = "detect_faces")]
unsafe fn parse_face_result(
    result: *const c_void,
    error: *const i8,
) -> Result<Vec<DetectedFace>, String> {
    if !error.is_null() {
        // SAFETY: caller guarantees `error` is a valid C string when non-null.
        return Err(unsafe { error_from_cstr(error) });
    }
    if result.is_null() {
        return Err("face detection returned null".into());
    }

    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
    let faces = if raw.array.is_null() || raw.count == 0 {
        Vec::new()
    } else {
        let typed = raw.array.cast::<ffi::DetectedFaceRaw>();
        let mut out = Vec::with_capacity(raw.count);
        let nan_to_none = |value: f32| if value.is_nan() { None } else { Some(value) };
        for index in 0..raw.count {
            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
            let entry = unsafe { &*typed.add(index) };
            out.push(DetectedFace {
                bounding_box: crate::recognize_text::BoundingBox {
                    x: entry.bbox_x,
                    y: entry.bbox_y,
                    width: entry.bbox_w,
                    height: entry.bbox_h,
                },
                confidence: entry.confidence,
                roll: nan_to_none(entry.roll),
                yaw: nan_to_none(entry.yaw),
                pitch: nan_to_none(entry.pitch),
            });
        }
        // SAFETY: `raw.array` and `raw.count` are the pair produced by the Swift bridge;
        // this is the unique call site that frees them.
        unsafe { ffi::vn_detected_faces_free(raw.array, raw.count) };
        out
    };

    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
    Ok(faces)
}

/// `extern "C"` callback invoked by the Swift bridge when face detection completes.
///
/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
#[cfg(feature = "detect_faces")]
extern "C" fn face_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
    let outcome =
        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_face_result(result, error) }));
    match outcome {
        Ok(Ok(faces)) => {
            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
            unsafe { AsyncCompletion::complete_ok(ctx, faces) };
        }
        Ok(Err(msg)) => {
            // SAFETY: same as above.
            unsafe { AsyncCompletion::<Vec<DetectedFace>>::complete_err(ctx, msg) };
        }
        Err(payload) => {
            log_callback_panic("face_result_cb", payload.as_ref());
            // SAFETY: same as above.
            unsafe {
                AsyncCompletion::<Vec<DetectedFace>>::complete_err(
                    ctx,
                    "panic in Vision face_result_cb".into(),
                );
            };
        }
    }
}

/// Future resolving to a `Vec<DetectedFace>`.
#[cfg(feature = "detect_faces")]
pub struct DetectFacesFuture {
    inner: FutureState<Vec<DetectedFace>>,
}

#[cfg(feature = "detect_faces")]
impl std::fmt::Debug for DetectFacesFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DetectFacesFuture").finish_non_exhaustive()
    }
}

#[cfg(feature = "detect_faces")]
impl Future for DetectFacesFuture {
    type Output = Result<Vec<DetectedFace>, VisionError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx)
    }
}

/// Async wrapper for `VNDetectFaceRectanglesRequest`.
#[cfg(feature = "detect_faces")]
#[derive(Debug, Clone, Copy, Default)]
pub struct AsyncDetectFaces;

#[cfg(feature = "detect_faces")]
impl AsyncDetectFaces {
    #[must_use]
    pub const fn new() -> Self {
        Self
    }

    /// Detect faces in the image at `path` asynchronously.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError::RequestFailed`] if Vision fails.
    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectFacesFuture {
        match path_to_cstring(path) {
            Err(error) => DetectFacesFuture {
                inner: FutureState::ready_err(error),
            },
            Ok(path_c) => {
                let (future, ctx) = AsyncCompletion::create();
                // SAFETY: `path_c` is a valid C string. `face_result_cb` satisfies the
                // single-fire callback contract and completes `ctx` exactly once.
                unsafe {
                    ffi::vn_detect_faces_in_path_async(path_c.as_ptr(), face_result_cb, ctx);
                };
                DetectFacesFuture {
                    inner: FutureState::pending(future),
                }
            }
        }
    }
}

// ============================================================================
// Barcode Detection Future
// ============================================================================

/// Parse the raw barcode-detection result from the Swift bridge.
///
/// # Safety
///
/// `result` must be either null or a valid `AsyncArrayResultRaw` pointer whose
/// `array` field, when non-null, points to `count` valid `DetectedBarcodeRaw`
/// elements.  `error` must be either null or a valid null-terminated C string.
#[cfg(feature = "detect_barcodes")]
unsafe fn parse_barcode_result(
    result: *const c_void,
    error: *const i8,
) -> Result<Vec<DetectedBarcode>, String> {
    if !error.is_null() {
        // SAFETY: caller guarantees `error` is a valid C string when non-null.
        return Err(unsafe { error_from_cstr(error) });
    }
    if result.is_null() {
        return Err("barcode detection returned null".into());
    }

    // SAFETY: caller guarantees `result` is a valid `AsyncArrayResultRaw` pointer.
    let raw = unsafe { &*(result.cast::<ffi::AsyncArrayResultRaw>()) };
    let barcodes = if raw.array.is_null() || raw.count == 0 {
        Vec::new()
    } else {
        let typed = raw.array.cast::<ffi::DetectedBarcodeRaw>();
        let mut out = Vec::with_capacity(raw.count);
        for index in 0..raw.count {
            // SAFETY: `typed` is valid for `raw.count` elements; `index` is in bounds.
            let entry = unsafe { &*typed.add(index) };
            let payload = if entry.payload.is_null() {
                String::new()
            } else {
                // SAFETY: `entry.payload` is a valid C string when non-null.
                unsafe { std::ffi::CStr::from_ptr(entry.payload) }
                    .to_string_lossy()
                    .into_owned()
            };
            let symbology = if entry.symbology.is_null() {
                String::new()
            } else {
                // SAFETY: `entry.symbology` is a valid C string when non-null.
                unsafe { std::ffi::CStr::from_ptr(entry.symbology) }
                    .to_string_lossy()
                    .into_owned()
            };
            out.push(DetectedBarcode {
                payload,
                symbology,
                confidence: entry.confidence,
                bounding_box: crate::recognize_text::BoundingBox {
                    x: entry.bbox_x,
                    y: entry.bbox_y,
                    width: entry.bbox_w,
                    height: entry.bbox_h,
                },
            });
        }
        // SAFETY: `raw.array` and `raw.count` are the Swift-bridge pair; unique free site.
        unsafe { ffi::vn_detected_barcodes_free(raw.array, raw.count) };
        out
    };

    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
    unsafe { ffi::vn_async_array_result_free(result.cast_mut()) };
    Ok(barcodes)
}

/// `extern "C"` callback invoked by the Swift bridge when barcode detection completes.
///
/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
#[cfg(feature = "detect_barcodes")]
extern "C" fn barcode_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
    let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
        parse_barcode_result(result, error)
    }));
    match outcome {
        Ok(Ok(barcodes)) => {
            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
            unsafe { AsyncCompletion::complete_ok(ctx, barcodes) };
        }
        Ok(Err(msg)) => {
            // SAFETY: same as above.
            unsafe { AsyncCompletion::<Vec<DetectedBarcode>>::complete_err(ctx, msg) };
        }
        Err(payload) => {
            log_callback_panic("barcode_result_cb", payload.as_ref());
            // SAFETY: same as above.
            unsafe {
                AsyncCompletion::<Vec<DetectedBarcode>>::complete_err(
                    ctx,
                    "panic in Vision barcode_result_cb".into(),
                );
            };
        }
    }
}

/// Future resolving to a `Vec<DetectedBarcode>`.
#[cfg(feature = "detect_barcodes")]
pub struct DetectBarcodesFuture {
    inner: FutureState<Vec<DetectedBarcode>>,
}

#[cfg(feature = "detect_barcodes")]
impl std::fmt::Debug for DetectBarcodesFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DetectBarcodesFuture")
            .finish_non_exhaustive()
    }
}

#[cfg(feature = "detect_barcodes")]
impl Future for DetectBarcodesFuture {
    type Output = Result<Vec<DetectedBarcode>, VisionError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx)
    }
}

/// Async wrapper for `VNDetectBarcodesRequest`.
#[cfg(feature = "detect_barcodes")]
#[derive(Debug, Clone, Copy, Default)]
pub struct AsyncDetectBarcodes;

#[cfg(feature = "detect_barcodes")]
impl AsyncDetectBarcodes {
    #[must_use]
    pub const fn new() -> Self {
        Self
    }

    /// Detect barcodes in the image at `path` asynchronously.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError::RequestFailed`] if Vision fails.
    pub fn detect_in_path(&self, path: impl AsRef<Path>) -> DetectBarcodesFuture {
        match path_to_cstring(path) {
            Err(error) => DetectBarcodesFuture {
                inner: FutureState::ready_err(error),
            },
            Ok(path_c) => {
                let (future, ctx) = AsyncCompletion::create();
                // SAFETY: `path_c` is a valid C string. `barcode_result_cb` satisfies the
                // single-fire callback contract and completes `ctx` exactly once.
                unsafe {
                    ffi::vn_detect_barcodes_in_path_async(path_c.as_ptr(), barcode_result_cb, ctx);
                };
                DetectBarcodesFuture {
                    inner: FutureState::pending(future),
                }
            }
        }
    }
}

// ============================================================================
// Person Segmentation Future
// ============================================================================

/// Parse the raw person-segmentation result from the Swift bridge.
///
/// # Safety
///
/// `result` must be either null or a valid `AsyncSegResultRaw` pointer whose
/// `bytes` field, when non-null, points to at least `height * bytes_per_row` bytes.
/// `error` must be either null or a valid null-terminated C string.
#[cfg(feature = "segmentation")]
unsafe fn parse_seg_result(
    result: *const c_void,
    error: *const i8,
) -> Result<SegmentationMask, String> {
    if !error.is_null() {
        // SAFETY: caller guarantees `error` is a valid C string when non-null.
        return Err(unsafe { error_from_cstr(error) });
    }
    if result.is_null() {
        return Err("segmentation returned null".into());
    }

    // SAFETY: caller guarantees `result` is a valid `AsyncSegResultRaw` pointer.
    let raw = unsafe { &*(result.cast::<ffi::AsyncSegResultRaw>()) };
    if raw.bytes.is_null() {
        // SAFETY: `result` is the non-null allocation produced by the Swift async bridge.
        unsafe { ffi::vn_async_seg_result_free(result.cast_mut()) };
        return Err("segmentation bytes were null".into());
    }

    let len = raw.height.saturating_mul(raw.bytes_per_row);
    // SAFETY: `raw.bytes` is valid for `len` bytes as guaranteed by the Swift bridge.
    let bytes = unsafe { core::slice::from_raw_parts(raw.bytes, len) }.to_vec();
    let mask = SegmentationMask {
        width: raw.width,
        height: raw.height,
        bytes_per_row: raw.bytes_per_row,
        bytes,
    };

    // SAFETY: `result` is the non-null allocation produced by the Swift async bridge;
    // unique free site.
    unsafe { ffi::vn_async_seg_result_free(result.cast_mut()) };
    Ok(mask)
}

/// `extern "C"` callback invoked by the Swift bridge when person segmentation completes.
///
/// Wrapped in `catch_unwind`; on panic the future is resolved with an error.
#[cfg(feature = "segmentation")]
extern "C" fn seg_result_cb(result: *const c_void, error: *const i8, ctx: *mut c_void) {
    // SAFETY: `result` and `error` are valid for the duration of this call per the bridge
    // contract. `AssertUnwindSafe` is correct: raw pointers are not accessed after unwinding.
    let outcome =
        std::panic::catch_unwind(AssertUnwindSafe(|| unsafe { parse_seg_result(result, error) }));
    match outcome {
        Ok(Ok(mask)) => {
            // SAFETY: `ctx` is the `Arc<AsyncCompletionInner<_>>` context from `AsyncCompletion::create()`.
            unsafe { AsyncCompletion::complete_ok(ctx, mask) };
        }
        Ok(Err(msg)) => {
            // SAFETY: same as above.
            unsafe { AsyncCompletion::<SegmentationMask>::complete_err(ctx, msg) };
        }
        Err(payload) => {
            log_callback_panic("seg_result_cb", payload.as_ref());
            // SAFETY: same as above.
            unsafe {
                AsyncCompletion::<SegmentationMask>::complete_err(
                    ctx,
                    "panic in Vision seg_result_cb".into(),
                );
            };
        }
    }
}

/// Future resolving to a `SegmentationMask`.
#[cfg(feature = "segmentation")]
pub struct PersonSegmentationFuture {
    inner: FutureState<SegmentationMask>,
}

#[cfg(feature = "segmentation")]
impl std::fmt::Debug for PersonSegmentationFuture {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PersonSegmentationFuture")
            .finish_non_exhaustive()
    }
}

#[cfg(feature = "segmentation")]
impl Future for PersonSegmentationFuture {
    type Output = Result<SegmentationMask, VisionError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.inner).poll(cx)
    }
}

/// Async wrapper for `VNGeneratePersonSegmentationRequest`.
#[cfg(feature = "segmentation")]
#[derive(Debug, Clone, Copy)]
pub struct AsyncPersonSegmentation {
    quality: SegmentationQuality,
}

#[cfg(feature = "segmentation")]
impl Default for AsyncPersonSegmentation {
    fn default() -> Self {
        Self::new(SegmentationQuality::Balanced)
    }
}

#[cfg(feature = "segmentation")]
impl AsyncPersonSegmentation {
    #[must_use]
    pub const fn new(quality: SegmentationQuality) -> Self {
        Self { quality }
    }

    /// Generate a person segmentation mask for the image at `path` asynchronously.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError::RequestFailed`] if Vision fails.
    pub fn generate_in_path(&self, path: impl AsRef<Path>) -> PersonSegmentationFuture {
        match path_to_cstring(path) {
            Err(error) => PersonSegmentationFuture {
                inner: FutureState::ready_err(error),
            },
            Ok(path_c) => {
                let (future, ctx) = AsyncCompletion::create();
                // SAFETY: `path_c` is a valid C string. `seg_result_cb` satisfies the
                // single-fire callback contract and completes `ctx` exactly once.
                // `self.quality as i32` is always a valid quality-level enum value.
                unsafe {
                    ffi::vn_generate_person_segmentation_async(
                        path_c.as_ptr(),
                        self.quality as i32,
                        seg_result_cb,
                        ctx,
                    );
                };
                PersonSegmentationFuture {
                    inner: FutureState::pending(future),
                }
            }
        }
    }
}