pdfkit-rs 0.2.4

Safe Rust bindings for Apple's PDFKit framework — documents, pages, selections, outlines, annotations, destinations, actions, and view state on macOS
Documentation
use std::ffi::{CStr, CString};
use std::fmt;
use std::os::raw::{c_char, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;

use crate::action_remote_goto::PdfActionRemoteGoTo;
use crate::error::Result;
use crate::ffi;
use crate::handle::ObjectHandle;
use crate::view::PdfView;

/// Mirrors the `PDFViewDelegate` callback surface.
pub trait PdfViewDelegate: 'static {
    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn handle_link_click(&mut self, _view: PdfView, _url: &str) -> bool {
        false
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn will_change_scale_factor(&mut self, _view: PdfView, scale_factor: f64) -> f64 {
        scale_factor.clamp(0.1, 10.0)
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn print_job_title(&mut self, _view: PdfView) -> Option<String> {
        None
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn perform_print(&mut self, _view: PdfView) -> bool {
        false
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn perform_find(&mut self, _view: PdfView) -> bool {
        false
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn perform_go_to_page(&mut self, _view: PdfView) -> bool {
        false
    }

    /// Mirrors the corresponding `PDFViewDelegate` callback.
    fn open_pdf_for_remote_goto_action(
        &mut self,
        _view: PdfView,
        _action: PdfActionRemoteGoTo,
    ) -> bool {
        false
    }
}

struct DelegateState {
    delegate: Box<dyn PdfViewDelegate>,
}

/// Wraps `PDFViewDelegateHandle`.
pub struct PdfViewDelegateHandle {
    handle: ObjectHandle,
    _state: Box<DelegateState>,
}

impl PdfViewDelegateHandle {
    /// Registers a Rust implementation of `PDFViewDelegate`.
    pub fn new(delegate: impl PdfViewDelegate) -> Result<Self> {
        let mut state = Box::new(DelegateState {
            delegate: Box::new(delegate),
        });
        let context = ptr::addr_of_mut!(*state).cast::<c_void>();
        let mut out_delegate = ptr::null_mut();
        let mut out_error = ptr::null_mut();
        let status = unsafe {
            ffi::pdf_view_delegate_new(
                context,
                Some(pdf_view_delegate_link_click_trampoline),
                Some(pdf_view_delegate_scale_factor_trampoline),
                Some(pdf_view_delegate_print_job_title_trampoline),
                Some(pdf_view_delegate_perform_print_trampoline),
                Some(pdf_view_delegate_perform_find_trampoline),
                Some(pdf_view_delegate_perform_go_to_page_trampoline),
                Some(pdf_view_delegate_remote_goto_trampoline),
                &mut out_delegate,
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)?;
        Ok(Self {
            handle: crate::util::required_handle(out_delegate, "PDFViewDelegate")?,
            _state: state,
        })
    }

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

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

fn duplicate_string(value: Option<String>) -> *mut c_char {
    value
        .and_then(|value| CString::new(value).ok())
        .map_or(ptr::null_mut(), |value| unsafe {
            libc::strdup(value.as_ptr())
        })
}

/// # Safety
/// The caller must ensure that `context` is either null or a valid pointer to `DelegateState`
/// that was obtained from `Box::into_raw()` and has not yet been freed.
unsafe fn delegate_state(context: *mut c_void) -> Option<&'static mut DelegateState> {
    context.cast::<DelegateState>().as_mut()
}

/// Helper to convert a retained PDFView pointer to a PdfView.
///
/// # Safety
/// `handle` must be either null or a valid, retained pointer to a PDFView object from Swift.
unsafe fn retained_view(handle: *mut c_void) -> Option<PdfView> {
    // SAFETY: caller guarantees valid handle or null
    unsafe { ObjectHandle::from_retained_ptr(handle) }.map(PdfView::from_handle)
}

/// Helper to convert a retained PDFActionRemoteGoTo pointer to a PdfActionRemoteGoTo.
///
/// # Safety
/// `handle` must be either null or a valid, retained pointer to a PDFActionRemoteGoTo object from Swift.
unsafe fn retained_remote_goto_action(handle: *mut c_void) -> Option<PdfActionRemoteGoTo> {
    // SAFETY: caller guarantees valid handle or null
    unsafe { ObjectHandle::from_retained_ptr(handle) }.map(PdfActionRemoteGoTo::from_handle)
}

/// # Safety
/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
/// `context` pointer that points to `DelegateState`, and `view_handle` must be a retained
/// PDFView pointer from Swift (or null). The caller must pass a valid C string for `url`
/// (or null). Panics are caught to prevent unwinding across the FFI boundary.
unsafe extern "C" fn pdf_view_delegate_link_click_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
    url: *const c_char,
) -> i32 {
    catch_unwind(AssertUnwindSafe(|| {
        // SAFETY: caller is responsible for providing valid context and view_handle pointers
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return 0;
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return 0;
        };
        let Some(url) = (!url.is_null()).then(|| unsafe {
            // SAFETY: checked for null; Swift guarantees valid C string
            CStr::from_ptr(url).to_string_lossy().into_owned()
        }) else {
            return 0;
        };
        i32::from(state.delegate.handle_link_click(view, &url))
    }))
    .unwrap_or(0)
}

/// # Safety
/// This is an extern "C" callback invoked by Swift. The caller must pass a valid, non-null
/// `context` pointer that points to `DelegateState`, and `view_handle` must be a retained
/// PDFView pointer from Swift (or null). Panics are caught to prevent unwinding across the
/// FFI boundary.
unsafe extern "C" fn pdf_view_delegate_scale_factor_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
    scale_factor: f64,
) -> f64 {
    catch_unwind(AssertUnwindSafe(|| {
        // SAFETY: caller is responsible for providing valid context and view_handle pointers
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return scale_factor.clamp(0.1, 10.0);
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return scale_factor.clamp(0.1, 10.0);
        };
        state.delegate.will_change_scale_factor(view, scale_factor)
    }))
    .unwrap_or_else(|_| scale_factor.clamp(0.1, 10.0))
}

unsafe extern "C" fn pdf_view_delegate_print_job_title_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
) -> *mut c_char {
    catch_unwind(AssertUnwindSafe(|| {
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return ptr::null_mut();
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return ptr::null_mut();
        };
        duplicate_string(state.delegate.print_job_title(view))
    }))
    .unwrap_or(ptr::null_mut())
}

unsafe extern "C" fn pdf_view_delegate_perform_print_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
) -> i32 {
    catch_unwind(AssertUnwindSafe(|| {
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return 0;
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return 0;
        };
        i32::from(state.delegate.perform_print(view))
    }))
    .unwrap_or(0)
}

unsafe extern "C" fn pdf_view_delegate_perform_find_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
) -> i32 {
    catch_unwind(AssertUnwindSafe(|| {
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return 0;
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return 0;
        };
        i32::from(state.delegate.perform_find(view))
    }))
    .unwrap_or(0)
}

unsafe extern "C" fn pdf_view_delegate_perform_go_to_page_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
) -> i32 {
    catch_unwind(AssertUnwindSafe(|| {
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return 0;
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return 0;
        };
        i32::from(state.delegate.perform_go_to_page(view))
    }))
    .unwrap_or(0)
}

unsafe extern "C" fn pdf_view_delegate_remote_goto_trampoline(
    context: *mut c_void,
    view_handle: *mut c_void,
    action_handle: *mut c_void,
) -> i32 {
    catch_unwind(AssertUnwindSafe(|| {
        let Some(state) = (unsafe { delegate_state(context) }) else {
            return 0;
        };
        let Some(view) = (unsafe { retained_view(view_handle) }) else {
            return 0;
        };
        let Some(action) = (unsafe { retained_remote_goto_action(action_handle) }) else {
            return 0;
        };
        i32::from(state.delegate.open_pdf_for_remote_goto_action(view, action))
    }))
    .unwrap_or(0)
}