micropdf 0.17.0

A pure Rust PDF library - A pure Rust PDF library with fz_/pdf_ API compatibility
//! FFI bindings for pdf::font::PdfFont
//!
//! Provides C-compatible exports for the native PDF font module.
//! Uses `mp_font_*` prefix to distinguish from MuPDF pdf_font.
//!
//! # 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::font::PdfFont;
use std::sync::LazyLock;

use super::{Handle, HandleStore};

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

/// Create a PdfFont from a PDF font dictionary.
///
/// # Arguments
/// * `dict_ptr` - Handle to a PDF dictionary object (from pdf_object module)
///   containing font entries (Type, Subtype, BaseFont, Encoding, etc.)
///
/// # Returns
/// Handle to PdfFont, or 0 on failure.
///
/// # Safety
/// dict_ptr must be a valid handle to a PDF dict object.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_font_from_dict(dict_ptr: PdfObjHandle) -> 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 font = match PdfFont::from_pdf_dict(&dict) {
        Ok(f) => f,
        Err(_) => return 0,
    };
    PDF_FONTS.insert(font)
}

/// Get the glyph width for a character code.
///
/// # Returns
/// Width in PDF units (1/1000 of em), or 0.0 on error.
///
/// # Safety
/// font must be a valid handle from mp_pdf_font_from_dict.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_font_glyph_width(font: super::Handle, char_code: u32) -> f64 {
    if font == 0 {
        return 0.0;
    }
    PDF_FONTS
        .get(font)
        .and_then(|arc| arc.lock().ok().map(|f| f.glyph_width(char_code)))
        .unwrap_or(0.0)
}

/// Decode a byte sequence to UTF-8 using the font's encoding.
///
/// # Arguments
/// * `font` - Handle from mp_pdf_font_from_dict
/// * `bytes` - Input byte sequence (char codes)
/// * `len` - Length of bytes
/// * `out_buf` - Output buffer for UTF-8
/// * `out_len` - Capacity of out_buf
///
/// # Returns
/// Number of bytes written to out_buf on success, negative on error.
///
/// # Safety
/// bytes must point to readable memory of at least len bytes.
/// out_buf must point to writable memory of at least out_len bytes.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_font_decode_text(
    font: super::Handle,
    bytes: *const u8,
    len: usize,
    out_buf: *mut u8,
    out_len: usize,
) -> i32 {
    if font == 0 || bytes.is_null() || out_buf.is_null() || out_len == 0 {
        return -1;
    }
    let input = unsafe { std::slice::from_raw_parts(bytes, len) };
    let encoding = match PDF_FONTS.get(font) {
        Some(arc) => match arc.lock() {
            Ok(f) => f.encoding().clone(),
            Err(_) => return -1,
        },
        None => return -1,
    };
    let mut s = String::new();
    for &b in input {
        if let Some(c) = encoding.decode_char(b) {
            s.push(c);
        }
    }
    let utf8 = s.into_bytes();
    let written = utf8.len().min(out_len);
    if written > 0 {
        unsafe { std::ptr::copy_nonoverlapping(utf8.as_ptr(), out_buf, written) };
    }
    written as i32
}

/// Free a PdfFont and release resources.
///
/// # Safety
/// font must be a valid handle from mp_pdf_font_from_dict.
#[unsafe(no_mangle)]
pub extern "C" fn mp_pdf_font_free(font: super::Handle) {
    if font == 0 {
        return;
    }
    PDF_FONTS.remove(font);
}

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

    fn make_font_dict() -> PdfObjHandle {
        let dict = pdf_new_dict(0, 0, 4);
        pdf_dict_puts(0, dict, c"Type".as_ptr(), pdf_new_name(0, c"Font".as_ptr()));
        pdf_dict_puts(
            0,
            dict,
            c"Subtype".as_ptr(),
            pdf_new_name(0, c"Type1".as_ptr()),
        );
        pdf_dict_puts(
            0,
            dict,
            c"BaseFont".as_ptr(),
            pdf_new_name(0, c"Helvetica".as_ptr()),
        );
        dict
    }

    #[test]
    fn test_mp_pdf_font_from_dict_and_free() {
        let dict = make_font_dict();
        let font = mp_pdf_font_from_dict(dict);
        assert_ne!(font, 0);
        mp_pdf_font_free(font);
    }

    #[test]
    fn test_mp_pdf_font_glyph_width() {
        let dict = make_font_dict();
        let font = mp_pdf_font_from_dict(dict);
        assert_ne!(font, 0);
        let w = mp_pdf_font_glyph_width(font, 65);
        assert!(w > 0.0);
        mp_pdf_font_free(font);
    }

    #[test]
    fn test_mp_pdf_font_decode_text() {
        let dict = make_font_dict();
        let font = mp_pdf_font_from_dict(dict);
        assert_ne!(font, 0);
        let input = b"Hello";
        let mut out = [0u8; 32];
        let n = mp_pdf_font_decode_text(font, input.as_ptr(), 5, out.as_mut_ptr(), 32);
        assert!(n > 0);
        assert_eq!(&out[..n as usize], b"Hello");
        mp_pdf_font_free(font);
    }

    #[test]
    fn test_mp_pdf_font_null_checks() {
        assert_eq!(mp_pdf_font_from_dict(0), 0);
        assert_eq!(mp_pdf_font_glyph_width(0, 65), 0.0);
    }
}