micropdf 0.16.0

A pure Rust PDF library - A pure Rust PDF library with fz_/pdf_ API compatibility
//! C FFI for cpdf document I/O.

use super::{clear_error, set_error};
use crate::cpdf::document::{CpdfDocument, merge, parse_page_range, split_by_range};
use crate::ffi::{Handle, HandleStore};
use std::ffi::{CStr, CString, c_char, c_int};
use std::sync::LazyLock;

/// Global document store for cpdf handles.
pub static CPDF_DOCS: LazyLock<HandleStore<CpdfDocument>> = LazyLock::new(HandleStore::new);

/// Global range store for cpdf range handles.
pub static CPDF_RANGES: LazyLock<HandleStore<Vec<usize>>> = LazyLock::new(HandleStore::new);

macro_rules! get_doc {
    ($handle:expr) => {
        match CPDF_DOCS.get($handle) {
            Some(arc) => arc,
            None => {
                set_error(1, &format!("invalid document handle: {}", $handle));
                return 0;
            }
        }
    };
    ($handle:expr, $ret:expr) => {
        match CPDF_DOCS.get($handle) {
            Some(arc) => arc,
            None => {
                set_error(1, &format!("invalid document handle: {}", $handle));
                return $ret;
            }
        }
    };
}

/// Load a PDF from a file. Returns handle or 0 on error.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_fromFile(path: *const c_char, _password: *const c_char) -> Handle {
    clear_error();
    if path.is_null() {
        set_error(1, "cpdf_fromFile: null path");
        return 0;
    }
    let path = unsafe { CStr::from_ptr(path) }.to_string_lossy();
    match CpdfDocument::from_file(&path) {
        Ok(doc) => CPDF_DOCS.insert(doc),
        Err(e) => {
            set_error(1, &e.to_string());
            0
        }
    }
}

/// Load a PDF from memory. Returns handle or 0 on error.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_fromMemory(data: *const u8, len: c_int) -> Handle {
    clear_error();
    if data.is_null() || len <= 0 {
        set_error(1, "cpdf_fromMemory: null/empty data");
        return 0;
    }
    let bytes = unsafe { std::slice::from_raw_parts(data, len as usize) }.to_vec();
    match CpdfDocument::from_bytes(bytes) {
        Ok(doc) => CPDF_DOCS.insert(doc),
        Err(e) => {
            set_error(1, &e.to_string());
            0
        }
    }
}

/// Write a PDF to a file. Returns 0 on success, 1 on error.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_toFile(
    handle: Handle,
    path: *const c_char,
    _linearize: c_int,
    _make_id: c_int,
) -> c_int {
    clear_error();
    if path.is_null() {
        set_error(1, "cpdf_toFile: null path");
        return 1;
    }
    let path = unsafe { CStr::from_ptr(path) }.to_string_lossy();
    let arc = get_doc!(handle, 1);
    match arc.lock().unwrap().to_file(&path) {
        Ok(()) => 0,
        Err(e) => {
            set_error(1, &e.to_string());
            1
        }
    }
}

/// Write a PDF to a caller-owned heap buffer. `len_out` receives the byte count.
/// Free the returned pointer with `cpdf_free(ptr, len)` where `len` is the value
/// written to `len_out`. Returns null on error.
///
/// # Safety
/// `len_out` must be a valid non-null pointer.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_toMemory(
    handle: Handle,
    _linearize: c_int,
    _make_id: c_int,
    len_out: *mut c_int,
) -> *mut u8 {
    clear_error();
    if len_out.is_null() {
        set_error(1, "cpdf_toMemory: null len_out");
        return std::ptr::null_mut();
    }
    let arc = get_doc!(handle, std::ptr::null_mut());
    let bytes = arc.lock().unwrap().to_bytes();
    let byte_len = bytes.len();
    if byte_len > i32::MAX as usize {
        set_error(1, "cpdf_toMemory: PDF too large for c_int length (>2GB)");
        return std::ptr::null_mut();
    }
    unsafe { *len_out = byte_len as c_int };
    let mut boxed = bytes.into_boxed_slice();
    let ptr = boxed.as_mut_ptr();
    std::mem::forget(boxed);
    ptr
}

/// Free a buffer returned by `cpdf_toMemory`.
///
/// # Safety
/// `ptr` must have been returned by `cpdf_toMemory` and `len` must match.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_free(ptr: *mut u8, len: c_int) {
    if !ptr.is_null() && len > 0 {
        drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len as usize)) });
    }
}

/// Return the page count of a document. Returns -1 on error.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_pages(handle: Handle) -> c_int {
    clear_error();
    let arc = get_doc!(handle, -1);
    arc.lock().unwrap().page_count() as c_int
}

/// Create a blank document. `width` and `height` in points. Returns handle or 0.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_blankDocument(width: f64, height: f64, num_pages: c_int) -> Handle {
    clear_error();
    if num_pages <= 0 {
        set_error(1, "cpdf_blankDocument: num_pages must be > 0");
        return 0;
    }
    match CpdfDocument::blank(width, height, num_pages as usize) {
        Ok(doc) => CPDF_DOCS.insert(doc),
        Err(e) => {
            set_error(1, &e.to_string());
            0
        }
    }
}

/// Merge an array of handles into one document. Returns new handle or 0.
///
/// # Safety
/// `handles` must point to `num` valid Handle values.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_mergeSimple(handles: *const Handle, num: c_int) -> Handle {
    clear_error();
    if handles.is_null() || num <= 0 {
        set_error(1, "cpdf_mergeSimple: invalid args");
        return 0;
    }
    let hs = unsafe { std::slice::from_raw_parts(handles, num as usize) };
    let mut docs = Vec::with_capacity(num as usize);
    for &h in hs {
        match CPDF_DOCS.get(h) {
            Some(arc) => docs.push(arc.lock().unwrap().clone()),
            None => {
                set_error(1, &format!("invalid handle {h}"));
                return 0;
            }
        }
    }
    match merge(docs) {
        Ok(doc) => CPDF_DOCS.insert(doc),
        Err(e) => {
            set_error(1, &e.to_string());
            0
        }
    }
}

/// Release a document handle and free its memory.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_deletePdf(handle: Handle) {
    CPDF_DOCS.remove(handle);
}

/// Return the PDF version string (e.g. "1.7"). Caller frees with `cpdf_freeString`.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_version(handle: Handle) -> *mut c_char {
    clear_error();
    let arc = get_doc!(handle, std::ptr::null_mut());
    let v = arc.lock().unwrap().version();
    CString::new(v)
        .map(|cs| cs.into_raw())
        .unwrap_or(std::ptr::null_mut())
}

/// Free a string returned by cpdf (e.g. `cpdf_version`).
///
/// # Safety
/// `ptr` must have been returned by a cpdf function that documents caller-owned strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_freeString(ptr: *mut c_char) {
    if !ptr.is_null() {
        drop(unsafe { CString::from_raw(ptr) });
    }
}

// ── Page ranges ────────────────────────────────────────────────────────────

/// Create a range handle from a start and end page (inclusive, 1-indexed).
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_range(start: c_int, end: c_int) -> Handle {
    clear_error();
    if start <= 0 || end <= 0 {
        set_error(1, "cpdf_range: page numbers must be >= 1");
        return 0;
    }
    let pages: Vec<usize> = if start <= end {
        (start as usize..=end as usize).collect()
    } else {
        (end as usize..=start as usize).rev().collect()
    };
    CPDF_RANGES.insert(pages)
}

/// Create a range covering all pages in the document.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_all(doc_handle: Handle) -> Handle {
    clear_error();
    let arc = get_doc!(doc_handle);
    let n = arc.lock().unwrap().page_count();
    if n == 0 {
        set_error(
            1,
            "cpdf_all: document has no pages or page count could not be determined",
        );
        return 0;
    }
    CPDF_RANGES.insert((1..=n).collect())
}

/// Create a range of odd-numbered pages.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_odd(doc_handle: Handle) -> Handle {
    clear_error();
    let arc = get_doc!(doc_handle);
    let n = arc.lock().unwrap().page_count();
    if n == 0 {
        set_error(
            1,
            "cpdf_odd: document has no pages or page count could not be determined",
        );
        return 0;
    }
    CPDF_RANGES.insert((1..=n).filter(|p| p % 2 == 1).collect())
}

/// Create a range of even-numbered pages.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_even(doc_handle: Handle) -> Handle {
    clear_error();
    let arc = get_doc!(doc_handle);
    let n = arc.lock().unwrap().page_count();
    if n == 0 {
        set_error(
            1,
            "cpdf_even: document has no pages or page count could not be determined",
        );
        return 0;
    }
    CPDF_RANGES.insert((1..=n).filter(|p| p % 2 == 0).collect())
}

/// Parse a cpdf range specification string (e.g. "1-3,5,odd") for the given document.
///
/// # Safety
/// `spec` must be a valid null-terminated C string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_parsePagespec(doc_handle: Handle, spec: *const c_char) -> Handle {
    clear_error();
    if spec.is_null() {
        set_error(1, "cpdf_parsePagespec: null spec");
        return 0;
    }
    let spec_str = unsafe { CStr::from_ptr(spec) }.to_string_lossy();
    let arc = get_doc!(doc_handle);
    let n = arc.lock().unwrap().page_count();
    if n == 0 {
        set_error(
            1,
            &format!(
                "cpdf_parsePagespec: document has no pages or page count could not be \
                 determined (spec=\"{spec_str}\")"
            ),
        );
        return 0;
    }
    match parse_page_range(&spec_str, n) {
        Ok(pages) => CPDF_RANGES.insert(pages),
        Err(e) => {
            set_error(1, &e.to_string());
            0
        }
    }
}

/// Free a range handle.
#[unsafe(no_mangle)]
pub extern "C" fn cpdf_deleteRange(handle: Handle) {
    CPDF_RANGES.remove(handle);
}

/// Split a document by range handles. Returns null-terminated array of handles.
///
/// `range_handles` is an array of `num_ranges` range handles produced by
/// `cpdf_range` / `cpdf_parsePagespec`. Each resulting document handle must
/// be freed individually with `cpdf_deletePdf`. The returned array must be
/// freed with `cpdf_freeHandleArray`.
///
/// Returns null on error.
///
/// # Safety
/// `range_handles` must point to `num_ranges` valid range Handle values.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_splitByRange(
    doc_handle: Handle,
    range_handles: *const Handle,
    num_ranges: c_int,
    out_count: *mut c_int,
) -> *mut Handle {
    clear_error();
    if range_handles.is_null() || num_ranges <= 0 || out_count.is_null() {
        set_error(1, "cpdf_splitByRange: invalid args");
        return std::ptr::null_mut();
    }

    let rhs = unsafe { std::slice::from_raw_parts(range_handles, num_ranges as usize) };
    let doc_arc = get_doc!(doc_handle, std::ptr::null_mut());
    let doc = doc_arc.lock().unwrap().clone();

    let mut ranges: Vec<Vec<usize>> = Vec::with_capacity(num_ranges as usize);
    for &rh in rhs {
        match CPDF_RANGES.get(rh) {
            Some(arc) => ranges.push(arc.lock().unwrap().clone()),
            None => {
                set_error(1, &format!("invalid range handle {rh}"));
                return std::ptr::null_mut();
            }
        }
    }

    match split_by_range(&doc, &ranges) {
        Ok(parts) => {
            unsafe { *out_count = parts.len() as c_int };
            let handles: Vec<Handle> = parts.into_iter().map(|d| CPDF_DOCS.insert(d)).collect();
            let mut boxed = handles.into_boxed_slice();
            let ptr = boxed.as_mut_ptr();
            std::mem::forget(boxed);
            ptr
        }
        Err(e) => {
            set_error(1, &e.to_string());
            std::ptr::null_mut()
        }
    }
}

/// Free an array of handles returned by `cpdf_splitByRange`.
///
/// # Safety
/// `ptr` must have been returned by `cpdf_splitByRange` and `count` must match.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cpdf_freeHandleArray(ptr: *mut Handle, count: c_int) {
    if !ptr.is_null() && count > 0 {
        drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, count as usize)) });
    }
}