Skip to main content

pdfium_render/
font_provider.rs

1//! Memory-based font provider for Pdfium.
2//!
3//! This module provides an in-memory font provider implementation that allows
4//! Pdfium to access pre-loaded font data without requiring system font installation.
5//!
6//! The provider implements the FPDF_SYSFONTINFO interface callbacks, enabling
7//! efficient font management through Rust's memory management and Arc for zero-copy
8//! sharing of font data.
9
10use crate::bindgen::*;
11use std::collections::HashMap;
12use std::ffi::CStr;
13use std::os::raw::*;
14use std::ptr;
15use std::sync::Arc;
16
17/// A font descriptor containing pre-loaded font data.
18///
19/// Use `Arc<[u8]>` for zero-copy sharing when caching fonts in memory.
20#[derive(Clone, Debug)]
21pub struct FontDescriptor {
22    /// Font family name (e.g., "Arial", "Roboto")
23    pub family: String,
24
25    /// Font weight (400 = normal, 700 = bold)
26    pub weight: i32,
27
28    /// Whether the font is italic
29    pub is_italic: bool,
30
31    /// Character set for the font.
32    /// Common values:
33    /// - 0 = ANSI charset (Western)
34    /// - 128 = Shift-JIS charset (Japanese)
35    /// - 134 = GB2312 charset (Simplified Chinese)
36    /// - 136 = Hangeul charset (Korean)
37    pub charset: i32,
38
39    /// Raw font file bytes (TrueType or OpenType).
40    /// Use Arc for zero-copy sharing across multiple font provider instances.
41    pub data: Arc<[u8]>,
42}
43
44/// Font key for HashMap lookups with case-insensitive family name matching.
45#[derive(Hash, Eq, PartialEq, Clone, Debug)]
46struct FontKey {
47    family: String,
48    weight: i32,
49    is_italic: bool,
50    charset: i32,
51}
52
53/// Font handle returned to Pdfium by the font provider callbacks.
54struct FontHandle {
55    key: FontKey,
56}
57
58/// Memory-based font provider for Pdfium.
59///
60/// This struct manages a collection of pre-loaded fonts and provides callbacks
61/// for Pdfium's FPDF_SYSFONTINFO interface.
62pub(crate) struct MemoryFontProvider {
63    sys_font_info: FPDF_SYSFONTINFO,
64    fonts: HashMap<FontKey, Arc<[u8]>>,
65}
66
67impl MemoryFontProvider {
68    /// Create a new memory font provider from a list of font descriptors.
69    pub(crate) fn new(descriptors: Vec<FontDescriptor>) -> Self {
70        let mut fonts = HashMap::new();
71        for descriptor in descriptors {
72            let key = FontKey {
73                family: descriptor.family.to_lowercase(),
74                weight: descriptor.weight,
75                is_italic: descriptor.is_italic,
76                charset: descriptor.charset,
77            };
78            fonts.insert(key, descriptor.data);
79        }
80
81        let sys_font_info = FPDF_SYSFONTINFO {
82            version: 2,
83            Release: Some(release_callback),
84            EnumFonts: None,
85            MapFont: Some(map_font_callback),
86            GetFont: Some(get_font_callback),
87            GetFontData: Some(get_font_data_callback),
88            GetFaceName: None,
89            GetFontCharset: None,
90            DeleteFont: Some(delete_font_callback),
91        };
92
93        MemoryFontProvider { sys_font_info, fonts }
94    }
95
96    /// Get a mutable pointer to the FPDF_SYSFONTINFO structure for Pdfium.
97    pub(crate) fn as_mut_ptr(&mut self) -> *mut FPDF_SYSFONTINFO {
98        &mut self.sys_font_info as *mut FPDF_SYSFONTINFO
99    }
100
101    /// Reconstruct a mutable reference to the MemoryFontProvider from pThis pointer.
102    ///
103    /// # Safety
104    ///
105    /// This function assumes that pThis is a valid pointer to a MemoryFontProvider
106    /// instance that was stored via Box::leak().
107    unsafe fn from_pthis<'a>(pthis: *mut FPDF_SYSFONTINFO) -> &'a mut Self {
108        unsafe { &mut *(pthis as *mut MemoryFontProvider) }
109    }
110}
111
112/// Release callback - no-op since we leak via Box::leak.
113///
114/// Called when Pdfium no longer needs the font info interface.
115/// We don't actually free the memory here because we've leaked it via Box::leak
116/// to ensure it lives for the duration of the Pdfium library.
117unsafe extern "C" fn release_callback(_pthis: *mut FPDF_SYSFONTINFO) {}
118
119/// MapFont callback - match font with 3-tier fallback strategy.
120///
121/// Called by Pdfium to map a font request to an available font handle.
122/// Uses a 3-tier fallback strategy:
123/// 1. Exact match (family, weight, italic, charset)
124/// 2. Fallback to weight=400 (normal weight) if exact match fails
125/// 3. Fallback to any font in the family if weight-specific match fails
126unsafe extern "C" fn map_font_callback(
127    pthis: *mut FPDF_SYSFONTINFO,
128    weight: c_int,
129    bitalic: FPDF_BOOL,
130    charset: c_int,
131    _pitch_family: c_int,
132    face: *const c_char,
133    bexact: *mut FPDF_BOOL,
134) -> *mut c_void {
135    unsafe {
136        if pthis.is_null() || face.is_null() {
137            return ptr::null_mut();
138        }
139
140        let face_name = match CStr::from_ptr(face).to_str() {
141            Ok(s) => s,
142            Err(_) => return ptr::null_mut(),
143        };
144
145        let provider = MemoryFontProvider::from_pthis(pthis);
146        let face_lower = face_name.to_lowercase();
147        let is_italic = bitalic != 0;
148
149        let exact_key = FontKey {
150            family: face_lower.clone(),
151            weight,
152            is_italic,
153            charset,
154        };
155
156        if provider.fonts.contains_key(&exact_key) {
157            if !bexact.is_null() {
158                *bexact = 1;
159            }
160            let handle = Box::new(FontHandle { key: exact_key });
161            return Box::into_raw(handle) as *mut c_void;
162        }
163
164        let normal_weight_key = FontKey {
165            family: face_lower.clone(),
166            weight: 400,
167            is_italic,
168            charset,
169        };
170
171        if provider.fonts.contains_key(&normal_weight_key) {
172            if !bexact.is_null() {
173                *bexact = 0;
174            }
175            let handle = Box::new(FontHandle { key: normal_weight_key });
176            return Box::into_raw(handle) as *mut c_void;
177        }
178
179        for key in provider.fonts.keys() {
180            if key.family == face_lower && key.is_italic == is_italic {
181                if !bexact.is_null() {
182                    *bexact = 0;
183                }
184                let handle = Box::new(FontHandle { key: key.clone() });
185                return Box::into_raw(handle) as *mut c_void;
186            }
187        }
188
189        ptr::null_mut()
190    }
191}
192
193/// GetFont callback - get first font matching family name.
194///
195/// Called by Pdfium to retrieve a font handle by family name.
196unsafe extern "C" fn get_font_callback(pthis: *mut FPDF_SYSFONTINFO, face: *const c_char) -> *mut c_void {
197    unsafe {
198        if pthis.is_null() || face.is_null() {
199            return ptr::null_mut();
200        }
201
202        let face_name = match CStr::from_ptr(face).to_str() {
203            Ok(s) => s,
204            Err(_) => return ptr::null_mut(),
205        };
206
207        let provider = MemoryFontProvider::from_pthis(pthis);
208        let face_lower = face_name.to_lowercase();
209
210        for key in provider.fonts.keys() {
211            if key.family == face_lower {
212                let handle = Box::new(FontHandle { key: key.clone() });
213                return Box::into_raw(handle) as *mut c_void;
214            }
215        }
216
217        ptr::null_mut()
218    }
219}
220
221/// GetFontData callback - return font bytes.
222///
223/// Called by Pdfium to retrieve font data. Supports table=0 for full font file only.
224/// Returns 0 for specific table requests as we only support full file access.
225unsafe extern "C" fn get_font_data_callback(
226    pthis: *mut FPDF_SYSFONTINFO,
227    hfont: *mut c_void,
228    table: c_uint,
229    buffer: *mut c_uchar,
230    buf_size: c_ulong,
231) -> c_ulong {
232    unsafe {
233        if pthis.is_null() || hfont.is_null() {
234            return 0;
235        }
236
237        if table != 0 {
238            return 0;
239        }
240
241        let provider = MemoryFontProvider::from_pthis(pthis);
242        let handle = &*(hfont as *const FontHandle);
243
244        let font_data = match provider.fonts.get(&handle.key) {
245            Some(data) => data,
246            None => return 0,
247        };
248
249        let font_size = font_data.len() as c_ulong;
250
251        if buffer.is_null() {
252            return font_size;
253        }
254
255        let copy_size = std::cmp::min(buf_size, font_size) as usize;
256        if copy_size > 0 {
257            ptr::copy_nonoverlapping(font_data.as_ptr(), buffer, copy_size);
258        }
259
260        copy_size as c_ulong
261    }
262}
263
264/// DeleteFont callback - drop the font handle.
265///
266/// Called by Pdfium when it no longer needs a font handle.
267unsafe extern "C" fn delete_font_callback(_pthis: *mut FPDF_SYSFONTINFO, hfont: *mut c_void) {
268    unsafe {
269        if hfont.is_null() {
270            return;
271        }
272
273        let _handle = Box::from_raw(hfont as *mut FontHandle);
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_font_key_case_insensitive() {
283        let key1 = FontKey {
284            family: "Arial".to_lowercase(),
285            weight: 400,
286            is_italic: false,
287            charset: 0,
288        };
289
290        let key2 = FontKey {
291            family: "arial".to_lowercase(),
292            weight: 400,
293            is_italic: false,
294            charset: 0,
295        };
296
297        assert_eq!(key1, key2);
298    }
299
300    #[test]
301    fn test_font_descriptor_creation() {
302        let data: Arc<[u8]> = Arc::from(vec![0u8, 1u8, 2u8].into_boxed_slice());
303        let descriptor = FontDescriptor {
304            family: "TestFont".to_string(),
305            weight: 400,
306            is_italic: false,
307            charset: 0,
308            data,
309        };
310
311        assert_eq!(descriptor.family, "TestFont");
312        assert_eq!(descriptor.weight, 400);
313        assert!(!descriptor.is_italic);
314        assert_eq!(descriptor.charset, 0);
315    }
316
317    #[test]
318    fn test_memory_font_provider_empty() {
319        let provider = MemoryFontProvider::new(vec![]);
320        assert_eq!(provider.fonts.len(), 0);
321    }
322
323    #[test]
324    fn test_memory_font_provider_with_fonts() {
325        let data: Arc<[u8]> = Arc::from(vec![0u8; 100].into_boxed_slice());
326        let descriptors = vec![
327            FontDescriptor {
328                family: "Arial".to_string(),
329                weight: 400,
330                is_italic: false,
331                charset: 0,
332                data: data.clone(),
333            },
334            FontDescriptor {
335                family: "Arial".to_string(),
336                weight: 700,
337                is_italic: false,
338                charset: 0,
339                data: data.clone(),
340            },
341        ];
342
343        let provider = MemoryFontProvider::new(descriptors);
344        assert_eq!(provider.fonts.len(), 2);
345    }
346
347    #[test]
348    fn test_memory_font_provider_sys_font_info() {
349        let provider = MemoryFontProvider::new(vec![]);
350        assert_eq!(provider.sys_font_info.version, 2);
351        assert!(provider.sys_font_info.Release.is_some());
352        assert!(provider.sys_font_info.MapFont.is_some());
353        assert!(provider.sys_font_info.GetFont.is_some());
354        assert!(provider.sys_font_info.GetFontData.is_some());
355        assert!(provider.sys_font_info.DeleteFont.is_some());
356        assert!(provider.sys_font_info.EnumFonts.is_none());
357    }
358}