micropdf 0.15.15

A pure Rust PDF library - A pure Rust PDF library with fz_/pdf_ API compatibility
//! FFI bindings for pdf::image::PdfImage
//!
//! Provides C-compatible exports for the native PDF image module.
//! Uses `mp_image_*` prefix to distinguish from MuPDF fz_image.
//!
//! # Safety
//!
//! All functions are `unsafe extern "C"` and require the caller to ensure:
//! - Pointers are valid and non-null where documented
//! - Buffers have sufficient capacity for output
//! - Handles are not used after free

use super::pdf_native_conversion;
use crate::ffi::pdf_object::types::PdfObjHandle;
use crate::pdf::image::PdfImage;
use std::sync::LazyLock;

use super::{Handle, HandleStore};

/// Global store for PdfImage instances
pub static PDF_IMAGES: LazyLock<HandleStore<PdfImage>> = LazyLock::new(HandleStore::default);

/// Create a PdfImage from a PDF image dictionary and raw stream data.
///
/// # Arguments
/// * `dict_ptr` - Handle to PDF image XObject dictionary (Width, Height, etc.)
/// * `data` - Decoded image stream data (after Filter decompression)
/// * `data_len` - Length of data in bytes
///
/// # Returns
/// Handle to PdfImage, or 0 on failure.
///
/// # Safety
/// dict_ptr must be a valid handle to a PDF dict. data must point to readable
/// memory of at least data_len bytes (or be null if data_len is 0).
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_image_from_dict(
    dict_ptr: PdfObjHandle,
    data: *const u8,
    data_len: usize,
) -> super::Handle {
    if dict_ptr == 0 {
        return 0;
    }
    let dict = match pdf_native_conversion::pdf_obj_to_dict(dict_ptr) {
        Some(d) => d,
        None => return 0,
    };
    let data_vec = if data.is_null() || data_len == 0 {
        Vec::new()
    } else {
        unsafe { std::slice::from_raw_parts(data, data_len) }.to_vec()
    };
    let image = match PdfImage::from_pdf_dict(&dict, data_vec) {
        Ok(img) => img,
        Err(_) => return 0,
    };
    PDF_IMAGES.insert(image)
}

/// Get the image width in pixels.
///
/// # Safety
/// img must be a valid handle from mp_pdf_image_from_dict.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_image_width(img: super::Handle) -> u32 {
    if img == 0 {
        return 0;
    }
    PDF_IMAGES
        .get(img)
        .and_then(|arc| arc.lock().ok().map(|i| i.width))
        .unwrap_or(0)
}

/// Get the image height in pixels.
///
/// # Safety
/// img must be a valid handle from mp_pdf_image_from_dict.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_image_height(img: super::Handle) -> u32 {
    if img == 0 {
        return 0;
    }
    PDF_IMAGES
        .get(img)
        .and_then(|arc| arc.lock().ok().map(|i| i.height))
        .unwrap_or(0)
}

/// Convert image to RGBA format.
///
/// # Arguments
/// * `img` - Handle from mp_pdf_image_from_dict
/// * `out_buf` - Output buffer for RGBA bytes (4 bytes per pixel)
/// * `out_len` - Capacity of out_buf
///
/// # Returns
/// Number of bytes written (width*height*4) on success, negative on error.
///
/// # Safety
/// out_buf must point to writable memory of at least out_len bytes.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_image_to_rgba(
    img: super::Handle,
    out_buf: *mut u8,
    out_len: usize,
) -> i32 {
    if img == 0 || out_buf.is_null() {
        return -1;
    }
    let rgba = match PDF_IMAGES.get(img) {
        Some(arc) => match arc.lock() {
            Ok(i) => match i.to_rgba() {
                Ok(r) => r,
                Err(_) => return -2,
            },
            Err(_) => return -1,
        },
        None => return -1,
    };
    let written = rgba.len().min(out_len);
    if written > 0 {
        unsafe { std::ptr::copy_nonoverlapping(rgba.as_ptr(), out_buf, written) };
    }
    written as i32
}

/// Free a PdfImage and release resources.
///
/// # Safety
/// img must be a valid handle from mp_pdf_image_from_dict.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_image_free(img: super::Handle) {
    if img == 0 {
        return;
    }
    PDF_IMAGES.remove(img);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ffi::pdf_object::{pdf_dict_puts, pdf_new_dict, pdf_new_int, pdf_new_name};
    use std::ffi::CStr;

    fn make_image_dict() -> PdfObjHandle {
        let dict = pdf_new_dict(0, 0, 8);
        pdf_dict_puts(0, dict, c"Width".as_ptr(), pdf_new_int(0, 10));
        pdf_dict_puts(0, dict, c"Height".as_ptr(), pdf_new_int(0, 10));
        pdf_dict_puts(0, dict, c"BitsPerComponent".as_ptr(), pdf_new_int(0, 8));
        pdf_dict_puts(
            0,
            dict,
            c"ColorSpace".as_ptr(),
            pdf_new_name(0, c"DeviceRGB".as_ptr()),
        );
        dict
    }

    #[test]
    fn test_mp_pdf_image_from_dict_and_free() {
        let dict = make_image_dict();
        let data = vec![0u8; 10 * 10 * 3];
        let img = mp_pdf_image_from_dict(dict, data.as_ptr(), data.len());
        assert_ne!(img, 0);
        mp_pdf_image_free(img);
    }

    #[test]
    fn test_mp_pdf_image_width_height() {
        let dict = make_image_dict();
        let data = vec![0u8; 300];
        let img = mp_pdf_image_from_dict(dict, data.as_ptr(), data.len());
        assert_ne!(img, 0);
        assert_eq!(mp_pdf_image_width(img), 10);
        assert_eq!(mp_pdf_image_height(img), 10);
        mp_pdf_image_free(img);
    }

    #[test]
    fn test_mp_pdf_image_to_rgba() {
        let dict = make_image_dict();
        let data = vec![255u8; 300];
        let img = mp_pdf_image_from_dict(dict, data.as_ptr(), data.len());
        assert_ne!(img, 0);
        let mut rgba = vec![0u8; 400];
        let n = mp_pdf_image_to_rgba(img, rgba.as_mut_ptr(), 400);
        assert_eq!(n, 400);
        assert_eq!(rgba[0..4], [255, 255, 255, 255]);
        mp_pdf_image_free(img);
    }

    #[test]
    fn test_mp_pdf_image_null_checks() {
        assert_eq!(mp_pdf_image_from_dict(0, std::ptr::null(), 0), 0);
        assert_eq!(mp_pdf_image_width(0), 0);
        assert_eq!(mp_pdf_image_height(0), 0);
    }
}