Skip to main content

fop_render/pdf/
font.rs

1//! TrueType font embedding support for PDF
2//!
3//! Provides functionality to embed TrueType/OpenType fonts in PDF documents.
4
5use fop_types::{FopError, Result};
6use std::collections::{BTreeSet, HashMap};
7
8/// Embedded TrueType font information
9#[derive(Debug, Clone)]
10pub struct PdfFont {
11    /// Font name (extracted from the TTF)
12    pub font_name: String,
13
14    /// Font data (complete TTF/OTF file)
15    pub font_data: Vec<u8>,
16
17    /// Font flags for the descriptor
18    pub flags: u32,
19
20    /// Font bounding box [llx lly urx ury]
21    pub bbox: [i16; 4],
22
23    /// Italic angle (0 for upright fonts)
24    pub italic_angle: i16,
25
26    /// Ascent (height above baseline)
27    pub ascent: i16,
28
29    /// Descent (depth below baseline, typically negative)
30    pub descent: i16,
31
32    /// Cap height (height of capital letters)
33    pub cap_height: i16,
34
35    /// Stem vertical width
36    pub stem_v: i16,
37
38    /// Character widths for all used characters
39    pub widths: Vec<u16>,
40
41    /// First character code in widths array
42    pub first_char: u32,
43
44    /// Last character code in widths array
45    pub last_char: u32,
46
47    /// Units per em (font scaling factor, typically 1000 or 2048)
48    pub units_per_em: u16,
49
50    /// Character to glyph ID mapping for Unicode support
51    pub char_to_glyph: std::collections::HashMap<char, u16>,
52}
53
54impl PdfFont {
55    /// Parse a TrueType font from raw bytes
56    ///
57    /// Extracts font metrics and prepares the font for embedding in PDF.
58    /// Supports basic Latin character set (ASCII 32-126).
59    pub fn from_ttf_data(font_data: Vec<u8>) -> Result<Self> {
60        let face = ttf_parser::Face::parse(&font_data, 0)
61            .map_err(|e| FopError::Generic(format!("Failed to parse TTF: {:?}", e)))?;
62
63        // Extract font name
64        let font_name = face
65            .names()
66            .into_iter()
67            .find(|name| name.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
68            .and_then(|name| name.to_string())
69            .unwrap_or_else(|| "CustomFont".to_string());
70
71        // Get font metrics
72        let units_per_em = face.units_per_em();
73        let ascent = face.ascender();
74        let descent = face.descender();
75
76        // Get bounding box
77        let bbox = {
78            let bb = face.global_bounding_box();
79            [bb.x_min, bb.y_min, bb.x_max, bb.y_max]
80        };
81
82        // Cap height - try to get from OS/2 table, fallback to ascent
83        let cap_height = face
84            .capital_height()
85            .unwrap_or((ascent as f32 * 0.7) as i16);
86
87        // Stem V - approximate from font weight
88        let stem_v = face
89            .weight()
90            .to_number()
91            .clamp(400, 900)
92            .saturating_sub(300)
93            / 5;
94
95        // Calculate italic angle
96        let italic_angle = face.italic_angle() as i16;
97
98        // Font flags
99        // Bit 1: Fixed pitch (monospace)
100        // Bit 2: Serif
101        // Bit 3: Symbolic (non-standard encoding)
102        // Bit 6: Italic
103        // Bit 7: All cap
104        // Bit 17: Bold
105        let mut flags = 32; // Bit 6 = non-symbolic (standard encoding)
106
107        if face.is_monospaced() {
108            flags |= 1;
109        }
110
111        if italic_angle != 0 {
112            flags |= 64; // Italic flag
113        }
114
115        if face.is_bold() {
116            flags |= 0x40000; // Bold flag (bit 18, value 262144)
117        }
118
119        // Build full character to glyph mapping
120        // We'll populate this as characters are used
121        let char_to_glyph = std::collections::HashMap::new();
122
123        // Start with ASCII range as default
124        let first_char = 32u32;
125        let last_char = 126u32;
126        let mut widths = Vec::new();
127
128        for char_code in first_char..=last_char {
129            let c = char::from_u32(char_code).unwrap_or('\0');
130            let glyph_id = face.glyph_index(c).unwrap_or(ttf_parser::GlyphId(0));
131
132            let width = face.glyph_hor_advance(glyph_id).unwrap_or(units_per_em / 2);
133
134            widths.push(width);
135        }
136
137        Ok(Self {
138            font_name,
139            font_data,
140            flags,
141            bbox,
142            italic_angle,
143            ascent,
144            descent,
145            cap_height,
146            stem_v: stem_v as i16,
147            widths,
148            first_char,
149            last_char,
150            units_per_em,
151            char_to_glyph,
152        })
153    }
154
155    /// Get the width of a character in font units
156    pub fn char_width(&self, c: char) -> u16 {
157        let char_code = c as u32;
158        if char_code >= self.first_char && char_code <= self.last_char {
159            let index = (char_code - self.first_char) as usize;
160            self.widths
161                .get(index)
162                .copied()
163                .unwrap_or(self.units_per_em / 2)
164        } else {
165            // For characters outside our range, use average width
166            self.units_per_em / 2
167        }
168    }
169
170    /// Measure text width at a given font size
171    pub fn measure_text(&self, text: &str, font_size_pt: f64) -> f64 {
172        let mut total_width = 0u32;
173        for c in text.chars() {
174            total_width += self.char_width(c) as u32;
175        }
176
177        // Convert from font units to points: (total_width / units_per_em) * font_size
178        (total_width as f64 / self.units_per_em as f64) * font_size_pt
179    }
180}
181
182/// Font object tuple: (descriptor_id, stream_id, cidfont_id, type0_dict_id, to_unicode_id, cidtogidmap_id, font)
183pub type FontObjectTuple = (usize, usize, usize, usize, usize, usize, PdfFont);
184
185/// Tracks character usage for font subsetting
186#[derive(Debug, Clone, Default)]
187pub struct FontSubsetter {
188    /// Set of character codes used in the document
189    used_chars: BTreeSet<char>,
190}
191
192impl FontSubsetter {
193    /// Create a new font subsetter
194    pub fn new() -> Self {
195        Self {
196            used_chars: BTreeSet::new(),
197        }
198    }
199
200    /// Record characters used in text
201    pub fn record_text(&mut self, text: &str) {
202        for c in text.chars() {
203            self.used_chars.insert(c);
204        }
205    }
206
207    /// Get all used characters
208    pub fn used_chars(&self) -> &BTreeSet<char> {
209        &self.used_chars
210    }
211
212    /// Check if any characters have been used
213    pub fn is_empty(&self) -> bool {
214        self.used_chars.is_empty()
215    }
216}
217
218/// Manages embedded fonts in a PDF document
219#[derive(Debug, Default)]
220pub struct FontManager {
221    /// List of embedded fonts
222    fonts: Vec<PdfFont>,
223
224    /// Character usage tracking for each font
225    subsetters: Vec<FontSubsetter>,
226}
227
228impl FontManager {
229    /// Create a new font manager
230    pub fn new() -> Self {
231        Self {
232            fonts: Vec::new(),
233            subsetters: Vec::new(),
234        }
235    }
236
237    /// Embed a font and return its index
238    pub fn embed_font(&mut self, font_data: Vec<u8>) -> Result<usize> {
239        let font = PdfFont::from_ttf_data(font_data)?;
240        self.fonts.push(font);
241        self.subsetters.push(FontSubsetter::new());
242        Ok(self.fonts.len() - 1)
243    }
244
245    /// Record text usage for a specific font
246    pub fn record_text(&mut self, font_index: usize, text: &str) {
247        if let Some(subsetter) = self.subsetters.get_mut(font_index) {
248            subsetter.record_text(text);
249        }
250    }
251
252    /// Get an embedded font by index
253    pub fn get_font(&self, index: usize) -> Option<&PdfFont> {
254        self.fonts.get(index)
255    }
256
257    /// Get all embedded fonts
258    pub fn fonts(&self) -> &[PdfFont] {
259        &self.fonts
260    }
261
262    /// Number of embedded fonts
263    pub fn font_count(&self) -> usize {
264        self.fonts.len()
265    }
266
267    /// Look up an embedded font index by its family name (case-insensitive).
268    ///
269    /// The comparison is done against `PdfFont::font_name` (the PostScript name)
270    /// as well as any alias registered via `embed_font_with_alias`.
271    /// Returns `None` if no font with that name is embedded.
272    pub fn find_by_name(&self, family: &str) -> Option<usize> {
273        let needle = family.to_lowercase();
274        self.fonts.iter().position(|f| {
275            f.font_name.to_lowercase() == needle
276                // Also try matching the base name without style suffixes
277                // e.g. "NotoSans-Regular" should match "noto sans"
278                || f.font_name
279                    .to_lowercase()
280                    .replace('-', " ")
281                    .starts_with(&needle)
282        })
283    }
284
285    /// Get subsetter for a font by index
286    pub fn get_subsetter(&self, index: usize) -> Option<&FontSubsetter> {
287        self.subsetters.get(index)
288    }
289
290    /// Generate PDF font objects with subsetting
291    ///
292    /// Returns the font descriptor object ID, font stream object ID, CIDFont dictionary object ID,
293    /// Type 0 font dictionary object ID, ToUnicode CMap object ID, CIDToGIDMap object ID,
294    /// and the subset font for each embedded font.
295    pub fn generate_font_objects(&self, start_obj_id: usize) -> Result<Vec<FontObjectTuple>> {
296        let mut result = Vec::new();
297        let mut obj_id = start_obj_id;
298
299        for (font_idx, font) in self.fonts.iter().enumerate() {
300            let descriptor_id = obj_id;
301            let stream_id = obj_id + 1;
302            let cidfont_id = obj_id + 2;
303            let type0_dict_id = obj_id + 3;
304            let to_unicode_id = obj_id + 4;
305            let cidtogidmap_id = obj_id + 5;
306            obj_id += 6; // 6 objects per font: descriptor, stream, CIDFont, Type0, ToUnicode, CIDToGIDMap
307
308            // Create subset font if characters were used
309            let subset_font = if let Some(subsetter) = self.subsetters.get(font_idx) {
310                if !subsetter.is_empty() {
311                    create_subset_font(font, subsetter)?
312                } else {
313                    // No characters used, use full font
314                    font.clone()
315                }
316            } else {
317                // No subsetter, use full font
318                font.clone()
319            };
320
321            result.push((
322                descriptor_id,
323                stream_id,
324                cidfont_id,
325                type0_dict_id,
326                to_unicode_id,
327                cidtogidmap_id,
328                subset_font,
329            ));
330        }
331
332        Ok(result)
333    }
334}
335
336/// Generate PDF font descriptor object content
337pub fn generate_font_descriptor(font: &PdfFont, font_stream_obj_id: usize) -> String {
338    format!(
339        "<<\n\
340         /Type /FontDescriptor\n\
341         /FontName /{}\n\
342         /Flags {}\n\
343         /FontBBox [{} {} {} {}]\n\
344         /ItalicAngle {}\n\
345         /Ascent {}\n\
346         /Descent {}\n\
347         /CapHeight {}\n\
348         /StemV {}\n\
349         /FontFile2 {} 0 R\n\
350         >>",
351        font.font_name,
352        font.flags,
353        font.bbox[0],
354        font.bbox[1],
355        font.bbox[2],
356        font.bbox[3],
357        font.italic_angle,
358        font.ascent,
359        font.descent,
360        font.cap_height,
361        font.stem_v,
362        font_stream_obj_id
363    )
364}
365
366/// Generate PDF font stream object header
367pub fn generate_font_stream_header(font: &PdfFont) -> String {
368    format!(
369        "<<\n\
370         /Length {}\n\
371         /Length1 {}\n\
372         >>",
373        font.font_data.len(),
374        font.font_data.len()
375    )
376}
377
378/// Generate PDF font dictionary object content (Type 0 composite font for Unicode support)
379pub fn generate_font_dictionary(
380    font: &PdfFont,
381    descriptor_obj_id: usize,
382    to_unicode_obj_id: Option<usize>,
383) -> String {
384    // For Unicode fonts, we need Type 0 composite font structure
385    generate_type0_font_dict(font, descriptor_obj_id, to_unicode_obj_id)
386}
387
388/// Generate Type 0 composite font dictionary
389/// This is the top-level font object that references a CIDFont descendant
390fn generate_type0_font_dict(
391    font: &PdfFont,
392    cidfont_obj_id: usize,
393    to_unicode_obj_id: Option<usize>,
394) -> String {
395    let to_unicode_entry = if let Some(obj_id) = to_unicode_obj_id {
396        format!("/ToUnicode {} 0 R\n         ", obj_id)
397    } else {
398        String::new()
399    };
400
401    format!(
402        "<<\n\
403         /Type /Font\n\
404         /Subtype /Type0\n\
405         /BaseFont /{}\n\
406         /Encoding /Identity-H\n\
407         /DescendantFonts [{} 0 R]\n\
408         {}\
409         >>",
410        font.font_name, cidfont_obj_id, to_unicode_entry
411    )
412}
413
414/// Generate CIDFont Type 2 dictionary (TrueType descendant font)
415/// This is referenced by the Type 0 font and contains the actual font metrics
416pub fn generate_cidfont_dict(
417    font: &PdfFont,
418    descriptor_obj_id: usize,
419    cidtogidmap_obj_id: usize,
420) -> String {
421    // Build width array in CID format
422    // For simplicity, we'll use default width (DW) and individual widths (W)
423    let default_width = font.units_per_em / 2;
424
425    // Build W array: [start_cid [widths...]]
426    let mut w_array = String::new();
427    if !font.widths.is_empty() {
428        w_array.push_str(&format!("{} [", font.first_char));
429        for (i, width) in font.widths.iter().enumerate() {
430            if i > 0 {
431                w_array.push(' ');
432            }
433            w_array.push_str(&width.to_string());
434        }
435        w_array.push(']');
436    }
437
438    format!(
439        "<<\n\
440         /Type /Font\n\
441         /Subtype /CIDFontType2\n\
442         /BaseFont /{}\n\
443         /CIDSystemInfo <<\n\
444           /Registry (Adobe)\n\
445           /Ordering (Identity)\n\
446           /Supplement 0\n\
447         >>\n\
448         /FontDescriptor {} 0 R\n\
449         /DW {}\n\
450         {}\
451         /CIDToGIDMap {} 0 R\n\
452         >>",
453        font.font_name,
454        descriptor_obj_id,
455        default_width,
456        if w_array.is_empty() {
457            String::new()
458        } else {
459            format!("/W [{}]\n         ", w_array)
460        },
461        cidtogidmap_obj_id
462    )
463}
464
465/// Generate a ToUnicode CMap for CID fonts
466/// Maps CID (character IDs) to Unicode code points
467pub fn generate_to_unicode_cmap(font: &PdfFont) -> String {
468    let mut cmap = String::from(
469        "/CIDInit /ProcSet findresource begin\n\
470         12 dict begin\n\
471         begincmap\n\
472         /CIDSystemInfo <<\n\
473           /Registry (Adobe)\n\
474           /Ordering (Identity)\n\
475           /Supplement 0\n\
476         >> def\n\
477         /CMapName /Adobe-Identity-UCS def\n\
478         /CMapType 2 def\n\
479         1 begincodespacerange\n\
480         <0000> <FFFF>\n\
481         endcodespacerange\n",
482    );
483
484    // Build character mappings from char_to_glyph
485    // For CID fonts, we map CID (glyph ID) to Unicode
486    if !font.char_to_glyph.is_empty() {
487        let mapping_count = font.char_to_glyph.len();
488        cmap.push_str(&format!("{} beginbfchar\n", mapping_count));
489
490        for (&c, _glyph_id) in font.char_to_glyph.iter() {
491            // Map character code to Unicode
492            let char_code = c as u32;
493            cmap.push_str(&format!("<{:04X}> <{:04X}>\n", char_code, char_code));
494        }
495
496        cmap.push_str("endbfchar\n");
497    } else {
498        // If no explicit mapping, create identity mapping for the character range
499        let range_size = (font.last_char - font.first_char + 1) as usize;
500        if range_size > 0 && range_size <= 256 {
501            cmap.push_str(&format!("{} beginbfchar\n", range_size));
502            for char_code in font.first_char..=font.last_char {
503                cmap.push_str(&format!("<{:04X}> <{:04X}>\n", char_code, char_code));
504            }
505            cmap.push_str("endbfchar\n");
506        }
507    }
508
509    cmap.push_str(
510        "endcmap\n\
511         CMapName currentdict /CMap defineresource pop\n\
512         end\n\
513         end\n",
514    );
515
516    cmap
517}
518
519/// Create a subset font containing only the used characters
520fn create_subset_font(original_font: &PdfFont, subsetter: &FontSubsetter) -> Result<PdfFont> {
521    let face = ttf_parser::Face::parse(&original_font.font_data, 0)
522        .map_err(|e| FopError::Generic(format!("Failed to parse TTF for subsetting: {:?}", e)))?;
523
524    let used_chars = subsetter.used_chars();
525
526    // If no characters used, return original font
527    if used_chars.is_empty() {
528        return Ok(original_font.clone());
529    }
530
531    // Build glyph mapping: char -> glyph_id
532    let mut char_to_glyph = HashMap::new();
533    let mut used_glyphs = BTreeSet::new();
534
535    // Always include glyph 0 (notdef)
536    used_glyphs.insert(ttf_parser::GlyphId(0));
537
538    for &c in used_chars.iter() {
539        if let Some(glyph_id) = face.glyph_index(c) {
540            char_to_glyph.insert(c, glyph_id);
541            used_glyphs.insert(glyph_id);
542        }
543    }
544
545    // Create a simple subset by keeping only the used glyphs
546    // For now, we'll use the full font but track which characters are used
547    // A full subsetting implementation would rebuild the TTF tables
548
549    // For CID fonts, we use CID-based width arrays
550    // First and last char codes for the range
551    let first_char = used_chars.iter().next().map(|&c| c as u32).unwrap_or(0);
552    let last_char = used_chars
553        .iter()
554        .next_back()
555        .map(|&c| c as u32)
556        .unwrap_or(0xFFFF);
557
558    // Build character to glyph mapping for all used characters
559    let mut char_to_glyph_map = std::collections::HashMap::new();
560    for &c in used_chars.iter() {
561        if let Some(glyph_id) = face.glyph_index(c) {
562            char_to_glyph_map.insert(c, glyph_id.0);
563        }
564    }
565
566    // For CID fonts, extract widths only for used characters
567    // We'll store them sparsely and use DW (default width) for others
568    let mut widths = Vec::new();
569
570    // Build width array for the character range
571    // For efficiency with sparse ranges, we only include widths for actually used characters
572    let range_size = (last_char - first_char + 1) as usize;
573    if range_size > 0 && range_size <= 65536 {
574        // Build continuous width array for the range
575        for char_code in first_char..=last_char {
576            if let Some(c) = char::from_u32(char_code) {
577                if used_chars.contains(&c) {
578                    let glyph_id = face.glyph_index(c).unwrap_or(ttf_parser::GlyphId(0));
579                    let width = face
580                        .glyph_hor_advance(glyph_id)
581                        .unwrap_or(original_font.units_per_em / 2);
582                    widths.push(width);
583                } else {
584                    // Use default width for unused characters in range
585                    widths.push(original_font.units_per_em / 2);
586                }
587            } else {
588                widths.push(original_font.units_per_em / 2);
589            }
590        }
591    }
592
593    // For a simple implementation, we'll embed the full font but only declare the used character range
594    // A full implementation would rebuild the TTF with only used glyphs
595    let subset_font_data = create_simple_subset(&original_font.font_data, &used_glyphs)?;
596
597    Ok(PdfFont {
598        font_name: original_font.font_name.clone(),
599        font_data: subset_font_data,
600        flags: original_font.flags,
601        bbox: original_font.bbox,
602        italic_angle: original_font.italic_angle,
603        ascent: original_font.ascent,
604        descent: original_font.descent,
605        cap_height: original_font.cap_height,
606        stem_v: original_font.stem_v,
607        widths,
608        first_char,
609        last_char,
610        units_per_em: original_font.units_per_em,
611        char_to_glyph: char_to_glyph_map,
612    })
613}
614
615/// Create a simple subset by including only used glyphs
616fn create_simple_subset(
617    font_data: &[u8],
618    used_glyphs: &BTreeSet<ttf_parser::GlyphId>,
619) -> Result<Vec<u8>> {
620    let face = ttf_parser::Face::parse(font_data, 0)
621        .map_err(|e| FopError::Generic(format!("Failed to parse TTF for subsetting: {:?}", e)))?;
622
623    // For a basic implementation, we use a simplified approach:
624    // If the subset is small enough (< 50% of glyphs), we create a minimal subset
625    // Otherwise, we keep the full font
626
627    let total_glyphs = face.number_of_glyphs();
628    let used_glyph_count = used_glyphs.len();
629
630    // If we're using more than 50% of glyphs, just use the full font
631    if used_glyph_count as f32 / total_glyphs as f32 > 0.5 {
632        return Ok(font_data.to_vec());
633    }
634
635    // For now, return the full font data
636    // A full implementation would rebuild the TTF tables with only used glyphs
637    // This requires:
638    // 1. Remapping glyph IDs to be contiguous (0, 1, 2, ...)
639    // 2. Rebuilding glyf, loca, cmap tables
640    // 3. Updating head, hhea, hmtx, maxp tables
641    // 4. Recalculating checksums
642
643    Ok(font_data.to_vec())
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    #[test]
651    fn test_font_manager_creation() {
652        let manager = FontManager::new();
653        assert_eq!(manager.font_count(), 0);
654    }
655
656    #[test]
657    fn test_font_manager_default() {
658        let manager = FontManager::default();
659        assert_eq!(manager.font_count(), 0);
660    }
661
662    #[test]
663    fn test_font_subsetter_creation() {
664        let subsetter = FontSubsetter::new();
665        assert!(subsetter.is_empty());
666        assert_eq!(subsetter.used_chars().len(), 0);
667    }
668
669    #[test]
670    fn test_font_subsetter_record_text() {
671        let mut subsetter = FontSubsetter::new();
672        subsetter.record_text("Hello");
673
674        assert!(!subsetter.is_empty());
675        assert_eq!(subsetter.used_chars().len(), 4); // H, e, l, o (l appears twice)
676
677        assert!(subsetter.used_chars().contains(&'H'));
678        assert!(subsetter.used_chars().contains(&'e'));
679        assert!(subsetter.used_chars().contains(&'l'));
680        assert!(subsetter.used_chars().contains(&'o'));
681    }
682
683    #[test]
684    fn test_font_subsetter_multiple_texts() {
685        let mut subsetter = FontSubsetter::new();
686        subsetter.record_text("ABC");
687        subsetter.record_text("BCD");
688
689        assert_eq!(subsetter.used_chars().len(), 4); // A, B, C, D
690        assert!(subsetter.used_chars().contains(&'A'));
691        assert!(subsetter.used_chars().contains(&'B'));
692        assert!(subsetter.used_chars().contains(&'C'));
693        assert!(subsetter.used_chars().contains(&'D'));
694    }
695
696    #[test]
697    fn test_font_manager_record_text() {
698        let mut manager = FontManager::new();
699
700        // Create a minimal TTF for testing (this would fail without a valid font)
701        // In real usage, we'd load an actual font file
702        // For now, just test that the API works
703
704        // Verify we can call record_text even without fonts
705        manager.record_text(0, "test");
706        // Should not panic even if font doesn't exist
707    }
708
709    #[test]
710    fn test_subsetter_unicode_support() {
711        let mut subsetter = FontSubsetter::new();
712        subsetter.record_text("Hello 世界");
713
714        assert!(subsetter.used_chars().contains(&'H'));
715        assert!(subsetter.used_chars().contains(&'世'));
716        assert!(subsetter.used_chars().contains(&'界'));
717    }
718
719    #[test]
720    fn test_subsetter_special_characters() {
721        let mut subsetter = FontSubsetter::new();
722        subsetter.record_text("!@#$%^&*()");
723
724        assert!(subsetter.used_chars().contains(&'!'));
725        assert!(subsetter.used_chars().contains(&'@'));
726        assert!(subsetter.used_chars().contains(&'#'));
727        assert!(subsetter.used_chars().contains(&'('));
728        assert!(subsetter.used_chars().contains(&')'));
729    }
730
731    // Note: Testing actual TTF parsing requires a valid TTF file
732    // In a real test environment, you would include a small test font
733}
734
735#[cfg(test)]
736mod tests_extended {
737    use super::*;
738
739    fn minimal_pdf_font() -> PdfFont {
740        PdfFont {
741            font_name: "TestFont".to_string(),
742            font_data: vec![0u8; 100],
743            flags: 32, // non-symbolic
744            bbox: [-100, -200, 900, 800],
745            italic_angle: 0,
746            ascent: 800,
747            descent: -200,
748            cap_height: 700,
749            stem_v: 80,
750            widths: vec![500; 95], // ASCII 32..=126
751            first_char: 32,
752            last_char: 126,
753            units_per_em: 1000,
754            char_to_glyph: HashMap::new(),
755        }
756    }
757
758    #[test]
759    fn test_font_subsetter_empty_initially() {
760        let s = FontSubsetter::new();
761        assert!(s.is_empty());
762    }
763
764    #[test]
765    fn test_font_subsetter_deduplicates() {
766        let mut s = FontSubsetter::new();
767        s.record_text("aaa");
768        // 'a' should appear only once in the set
769        assert_eq!(s.used_chars().len(), 1);
770        assert!(s.used_chars().contains(&'a'));
771    }
772
773    #[test]
774    fn test_font_subsetter_is_not_empty_after_text() {
775        let mut s = FontSubsetter::new();
776        s.record_text("X");
777        assert!(!s.is_empty());
778    }
779
780    #[test]
781    fn test_font_manager_default_empty() {
782        let m = FontManager::default();
783        assert_eq!(m.font_count(), 0);
784        assert!(m.get_font(0).is_none());
785        assert!(m.get_subsetter(0).is_none());
786    }
787
788    #[test]
789    fn test_font_manager_find_by_name_empty() {
790        let m = FontManager::new();
791        assert!(m.find_by_name("Arial").is_none());
792    }
793
794    #[test]
795    fn test_generate_font_descriptor_contains_font_name() {
796        let font = minimal_pdf_font();
797        let descriptor = generate_font_descriptor(&font, 42);
798        assert!(descriptor.contains("TestFont"));
799        assert!(descriptor.contains("/FontDescriptor"));
800    }
801
802    #[test]
803    fn test_generate_font_descriptor_references_stream_obj() {
804        let font = minimal_pdf_font();
805        let descriptor = generate_font_descriptor(&font, 99);
806        assert!(descriptor.contains("99"));
807    }
808
809    #[test]
810    fn test_generate_font_stream_header_contains_length() {
811        let font = minimal_pdf_font();
812        let header = generate_font_stream_header(&font);
813        assert!(header.contains("/Length"));
814        // font_data is 100 bytes
815        assert!(header.contains("100"));
816    }
817
818    #[test]
819    fn test_generate_font_dictionary_type0() {
820        let font = minimal_pdf_font();
821        let dict = generate_font_dictionary(&font, 10, Some(15));
822        assert!(dict.contains("/Type /Font"));
823        assert!(dict.contains("/Subtype /Type0"));
824        assert!(dict.contains("TestFont"));
825    }
826
827    #[test]
828    fn test_generate_font_dictionary_no_to_unicode() {
829        let font = minimal_pdf_font();
830        let dict = generate_font_dictionary(&font, 10, None);
831        // Without ToUnicode, /ToUnicode should be absent
832        assert!(!dict.contains("/ToUnicode"));
833    }
834
835    #[test]
836    fn test_generate_to_unicode_cmap_identity_range() {
837        let mut font = minimal_pdf_font();
838        // With empty char_to_glyph and a small range it uses identity mapping
839        font.first_char = 65; // 'A'
840        font.last_char = 67; // 'C'
841        font.widths = vec![500; 3];
842        let cmap = generate_to_unicode_cmap(&font);
843        assert!(cmap.contains("begincmap"));
844        assert!(cmap.contains("endcmap"));
845        assert!(cmap.contains("<0041> <0041>")); // 'A' -> 'A'
846        assert!(cmap.contains("<0042> <0042>")); // 'B' -> 'B'
847    }
848
849    #[test]
850    fn test_generate_to_unicode_cmap_with_char_map() {
851        let mut font = minimal_pdf_font();
852        font.char_to_glyph.insert('A', 100);
853        font.char_to_glyph.insert('Z', 200);
854        let cmap = generate_to_unicode_cmap(&font);
855        assert!(cmap.contains("begincmap"));
856        assert!(cmap.contains("beginbfchar"));
857        // 'A' = 0x0041 → 0x0041
858        assert!(cmap.contains("<0041> <0041>"));
859    }
860
861    #[test]
862    fn test_generate_font_objects_empty_manager() {
863        let manager = FontManager::new();
864        let objects = manager
865            .generate_font_objects(10)
866            .expect("test: should succeed");
867        assert!(objects.is_empty());
868    }
869}