photokit 0.4.0

Safe Rust bindings for Apple's Photos framework — photo library access on macOS
Documentation
use core::ffi::{c_char, c_void};
use std::ffi::CStr;
use std::ops::Deref;
use std::ptr::{self, NonNull};

use doom_fish_utils::panic_safe::catch_user_panic;
use serde::{Deserialize, Serialize};

use crate::content_editing_input::PHContentEditingInput;
use crate::content_editing_output::PHContentEditingOutput;
use crate::error::PhotoKitError;
use crate::ffi;
use crate::live_photo::PHLivePhotoResult;
use crate::private::parse_json_ptr;

type FrameProcessorCallback =
    dyn FnMut(PHLivePhotoFrame) -> PHLivePhotoFrameProcessingDecision + Send;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
/// Wraps `PHLivePhotoFrameType`.
pub struct PHLivePhotoFrameType(
    /// Raw value for `PHLivePhotoFrameType`.
    pub i64,
);

impl PHLivePhotoFrameType {
    /// Constant on `PHLivePhotoFrameType`.
    pub const PHOTO: Self = Self(0);
    /// Constant on `PHLivePhotoFrameType`.
    pub const VIDEO: Self = Self(1);
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Serialized frame payload delivered to a `PHLivePhotoEditingContext` frame processor.
pub struct PHLivePhotoFrame {
    /// Corresponds to `PHLivePhotoFrame.frameType`.
    pub frame_type: PHLivePhotoFrameType,
    /// Corresponds to `PHLivePhotoFrame.timeSeconds`.
    pub time_seconds: f64,
    /// Corresponds to `PHLivePhotoFrame.renderScale`.
    pub render_scale: f64,
    /// Corresponds to `PHLivePhotoFrame.imageWidth`.
    pub image_width: f64,
    /// Corresponds to `PHLivePhotoFrame.imageHeight`.
    pub image_height: f64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Decision returned from a `PHLivePhotoEditingContext` frame processor.
pub enum PHLivePhotoFrameProcessingDecision {
    /// Case of `PHLivePhotoFrameProcessingDecision`.
    KeepOriginal,
    /// Case of `PHLivePhotoFrameProcessingDecision`.
    SkipFrame,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Serialized snapshot of `PHLivePhotoEditingContext` properties.
pub struct PHLivePhotoEditingContextInfo {
    /// Corresponds to `PHLivePhotoEditingContextInfo.fullSizeImageWidth`.
    pub full_size_image_width: f64,
    /// Corresponds to `PHLivePhotoEditingContextInfo.fullSizeImageHeight`.
    pub full_size_image_height: f64,
    /// Corresponds to `PHLivePhotoEditingContextInfo.durationSeconds`.
    pub duration_seconds: f64,
    /// Corresponds to `PHLivePhotoEditingContextInfo.photoTimeSeconds`.
    pub photo_time_seconds: f64,
    /// Corresponds to `PHLivePhotoEditingContextInfo.audioVolume`.
    pub audio_volume: f32,
    /// Corresponds to `PHLivePhotoEditingContextInfo.orientation`.
    pub orientation: i32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Serialized result from `PHLivePhotoEditingContext.saveLivePhoto`.
pub struct PHLivePhotoEditingSaveResult {
    /// Corresponds to `PHLivePhotoEditingSaveResult.success`.
    pub success: bool,
}

/// Wraps `PHLivePhotoEditingContext`.
pub struct PHLivePhotoEditingContext {
    raw: NonNull<c_void>,
    info: PHLivePhotoEditingContextInfo,
    frame_processor_user_info: Option<NonNull<c_void>>,
}

impl PHLivePhotoEditingContext {
    /// Creates a helper value for the related Photos framework API.
    pub fn new(input: &PHContentEditingInput) -> Result<Self, PhotoKitError> {
        let mut error = ptr::null_mut();
        let raw = unsafe { ffi::ph_live_photo_editing_context_new(input.raw.as_ptr(), &mut error) };
        let raw = NonNull::new(raw).ok_or_else(|| unsafe {
            PhotoKitError::from_error_ptr(error, "create live photo editing context failed")
        })?;
        let mut context = Self {
            raw,
            info: PHLivePhotoEditingContextInfo {
                full_size_image_width: 0.0,
                full_size_image_height: 0.0,
                duration_seconds: 0.0,
                photo_time_seconds: 0.0,
                audio_volume: 1.0,
                orientation: 0,
            },
            frame_processor_user_info: None,
        };
        context.refresh_info()?;
        Ok(context)
    }

    /// Returns the cached Photos framework snapshot for `PHLivePhotoEditingContext`.
    pub fn snapshot(&self) -> &PHLivePhotoEditingContextInfo {
        &self.info
    }

    /// Updates the wrapped Photos framework value on `PHLivePhotoEditingContext`.
    pub fn set_audio_volume(&mut self, audio_volume: f32) -> Result<(), PhotoKitError> {
        let mut error = ptr::null_mut();
        let status = unsafe {
            ffi::ph_live_photo_editing_context_set_audio_volume(
                self.raw.as_ptr(),
                audio_volume,
                &mut error,
            )
        };
        if status == ffi::status::OK && error.is_null() {
            self.refresh_info()
        } else {
            Err(unsafe { PhotoKitError::from_error_ptr(error, "set audio volume failed") })
        }
    }

    /// Updates the wrapped Photos framework value on `PHLivePhotoEditingContext`.
    pub fn set_frame_processor<F>(&mut self, callback: F) -> Result<(), PhotoKitError>
    where
        F: FnMut(PHLivePhotoFrame) -> PHLivePhotoFrameProcessingDecision + Send + 'static,
    {
        self.clear_frame_processor();
        let callback: Box<FrameProcessorCallback> = Box::new(callback);
        // SAFETY: `Box::into_raw` never returns null, so `NonNull::new_unchecked` is safe.
        let user_info =
            unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(callback)).cast::<c_void>()) };
        let mut error = ptr::null_mut();
        // SAFETY: `self.raw` is a valid editing context pointer; the trampoline
        // is a valid `extern "C"` fn; `user_info` is a live allocation managed
        // by `self.frame_processor_user_info`.
        let status = unsafe {
            ffi::ph_live_photo_editing_context_set_frame_processor(
                self.raw.as_ptr(),
                live_photo_frame_processor_trampoline,
                user_info.as_ptr(),
                &mut error,
            )
        };
        if status == ffi::status::OK && error.is_null() {
            self.frame_processor_user_info = Some(user_info);
            Ok(())
        } else {
            // SAFETY: Setting the processor failed; `user_info` was never passed
            // to the Swift bridge, so this is the only `from_raw` call on it.
            unsafe {
                drop(Box::from_raw(
                    user_info.as_ptr().cast::<Box<FrameProcessorCallback>>(),
                ));
            }
            // SAFETY: `error` is a non-null pointer set by the Swift bridge on failure.
            Err(unsafe { PhotoKitError::from_error_ptr(error, "set frame processor failed") })
        }
    }

    /// Clears Photos framework state on `PHLivePhotoEditingContext`.
    pub fn clear_frame_processor(&mut self) {
        // SAFETY: `self.raw` is a valid editing context pointer.
        unsafe { ffi::ph_live_photo_editing_context_clear_frame_processor(self.raw.as_ptr()) };
        if let Some(user_info) = self.frame_processor_user_info.take() {
            // SAFETY: `user_info` was created from `Box<Box<FrameProcessorCallback>>`
            // via `Box::into_raw`.  After `clear_frame_processor` above the Swift
            // bridge will no longer invoke the trampoline, so this is the only
            // `from_raw` call on this pointer.
            unsafe {
                drop(Box::from_raw(
                    user_info.as_ptr().cast::<Box<FrameProcessorCallback>>(),
                ));
            }
        }
    }

    /// Wraps a Photos framework operation on `PHLivePhotoEditingContext`.
    pub fn prepare_live_photo_for_playback(
        &self,
        target_width: f64,
        target_height: f64,
        timeout_ms: u64,
    ) -> Result<PHLivePhotoResult, PhotoKitError> {
        let mut error = ptr::null_mut();
        let payload = unsafe {
            ffi::ph_live_photo_editing_context_prepare_live_photo_json(
                self.raw.as_ptr(),
                target_width,
                target_height,
                timeout_ms,
                &mut error,
            )
        };
        if payload.is_null() {
            Err(unsafe {
                PhotoKitError::from_error_ptr(error, "prepare live photo for playback failed")
            })
        } else {
            unsafe { parse_json_ptr(payload, "PHLivePhotoResult") }
        }
    }

    /// Wraps a Photos framework operation on `PHLivePhotoEditingContext`.
    pub fn save_live_photo_to_output(
        &self,
        output: &PHContentEditingOutput,
        timeout_ms: u64,
    ) -> Result<PHLivePhotoEditingSaveResult, PhotoKitError> {
        let mut error = ptr::null_mut();
        let payload = unsafe {
            ffi::ph_live_photo_editing_context_save_json(
                self.raw.as_ptr(),
                output.as_raw(),
                timeout_ms,
                &mut error,
            )
        };
        if payload.is_null() {
            Err(unsafe { PhotoKitError::from_error_ptr(error, "save live photo failed") })
        } else {
            unsafe { parse_json_ptr(payload, "PHLivePhotoEditingSaveResult") }
        }
    }

    /// Cancels the Photos framework operation represented by `PHLivePhotoEditingContext`.
    pub fn cancel(&self) {
        unsafe { ffi::ph_live_photo_editing_context_cancel(self.raw.as_ptr()) };
    }

    pub(crate) fn as_raw(&self) -> *mut c_void {
        self.raw.as_ptr()
    }

    fn refresh_info(&mut self) -> Result<(), PhotoKitError> {
        let mut error = ptr::null_mut();
        let payload =
            unsafe { ffi::ph_live_photo_editing_context_json(self.raw.as_ptr(), &mut error) };
        if payload.is_null() {
            Err(unsafe {
                PhotoKitError::from_error_ptr(error, "live photo context snapshot failed")
            })
        } else {
            self.info = unsafe { parse_json_ptr(payload, "PHLivePhotoEditingContext") }?;
            Ok(())
        }
    }
}

impl Deref for PHLivePhotoEditingContext {
    type Target = PHLivePhotoEditingContextInfo;

    fn deref(&self) -> &Self::Target {
        &self.info
    }
}

impl core::fmt::Debug for PHLivePhotoEditingContext {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("PHLivePhotoEditingContext")
            .field("info", &self.info)
            .finish_non_exhaustive()
    }
}

impl Drop for PHLivePhotoEditingContext {
    fn drop(&mut self) {
        self.clear_frame_processor();
        unsafe { ffi::ph_live_photo_editing_context_release(self.raw.as_ptr()) };
    }
}

unsafe extern "C" fn live_photo_frame_processor_trampoline(
    frame_json: *const c_char,
    user_info: *mut c_void,
) -> i32 {
    if frame_json.is_null() || user_info.is_null() {
        return 0;
    }

    // SAFETY: `frame_json` is a valid NUL-terminated C string from the Swift bridge.
    let frame_json = CStr::from_ptr(frame_json).to_string_lossy();
    let Ok(frame) = serde_json::from_str::<PHLivePhotoFrame>(&frame_json) else {
        return 0;
    };
    // SAFETY: `user_info` is a `Box<Box<FrameProcessorCallback>>` kept alive by
    // the `PHLivePhotoEditingContext` that owns this registration.
    let callback = &mut **user_info.cast::<Box<FrameProcessorCallback>>();
    let mut decision = 0i32;
    catch_user_panic("live_photo_frame_processor_trampoline", || {
        decision = match callback(frame) {
            PHLivePhotoFrameProcessingDecision::KeepOriginal => 0,
            PHLivePhotoFrameProcessingDecision::SkipFrame => 1,
        };
    });
    decision
}