Skip to main content

oxidize_pdf/fonts/
embedder.rs

1//! Font embedding functionality for PDF generation
2
3use super::Font;
4use crate::objects::{Dictionary, Object, ObjectId};
5use crate::text::fonts::embedding::CjkFontType;
6use crate::Result;
7
8/// Font embedding options
9#[derive(Debug, Clone)]
10pub struct EmbeddingOptions {
11    /// Whether to subset the font (only include used glyphs)
12    pub subset: bool,
13    /// Whether to compress the font data
14    pub compress: bool,
15    /// Font encoding to use
16    pub encoding: FontEncoding,
17}
18
19impl Default for EmbeddingOptions {
20    fn default() -> Self {
21        EmbeddingOptions {
22            subset: true,
23            compress: true,
24            encoding: FontEncoding::WinAnsiEncoding,
25        }
26    }
27}
28
29/// Font encoding options
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum FontEncoding {
32    /// Windows ANSI encoding (CP1252)
33    WinAnsiEncoding,
34    /// Mac Roman encoding
35    MacRomanEncoding,
36    /// Standard PDF encoding
37    StandardEncoding,
38    /// Identity encoding for CID fonts
39    IdentityH,
40}
41
42impl FontEncoding {
43    /// Get the encoding name for PDF
44    pub fn name(&self) -> &'static str {
45        match self {
46            FontEncoding::WinAnsiEncoding => "WinAnsiEncoding",
47            FontEncoding::MacRomanEncoding => "MacRomanEncoding",
48            FontEncoding::StandardEncoding => "StandardEncoding",
49            FontEncoding::IdentityH => "Identity-H",
50        }
51    }
52}
53
54/// Font embedder for creating PDF font objects
55pub struct FontEmbedder<'a> {
56    font: &'a Font,
57    options: EmbeddingOptions,
58    used_chars: Vec<char>,
59}
60
61impl<'a> FontEmbedder<'a> {
62    /// Create a new font embedder
63    pub fn new(font: &'a Font, options: EmbeddingOptions) -> Self {
64        FontEmbedder {
65            font,
66            options,
67            used_chars: Vec::new(),
68        }
69    }
70
71    /// Add characters that will be used with this font
72    pub fn add_used_chars(&mut self, text: &str) {
73        for ch in text.chars() {
74            if !self.used_chars.contains(&ch) {
75                self.used_chars.push(ch);
76            }
77        }
78    }
79
80    /// Create the font dictionary for embedding
81    pub fn create_font_dict(
82        &self,
83        descriptor_id: ObjectId,
84        to_unicode_id: Option<ObjectId>,
85    ) -> Dictionary {
86        let mut dict = Dictionary::new();
87
88        // Type and Subtype
89        dict.set("Type", Object::Name("Font".into()));
90
91        // Determine font type based on encoding
92        if self.options.encoding == FontEncoding::IdentityH {
93            // Type 0 (composite) font for Unicode support
94            self.create_type0_font_dict(&mut dict, descriptor_id, to_unicode_id);
95        } else {
96            // Type 1 or TrueType font
97            self.create_simple_font_dict(&mut dict, descriptor_id, to_unicode_id);
98        }
99
100        dict
101    }
102
103    /// Create a Type 0 (composite) font dictionary
104    fn create_type0_font_dict(
105        &self,
106        dict: &mut Dictionary,
107        descriptor_id: ObjectId,
108        to_unicode_id: Option<ObjectId>,
109    ) {
110        dict.set("Subtype", Object::Name("Type0".into()));
111        dict.set("BaseFont", Object::Name(self.font.postscript_name().into()));
112        dict.set(
113            "Encoding",
114            Object::Name(self.options.encoding.name().into()),
115        );
116
117        // DescendantFonts array with CIDFont
118        let cid_font_dict = self.create_cid_font_dict(descriptor_id);
119        dict.set(
120            "DescendantFonts",
121            Object::Array(vec![Object::Dictionary(cid_font_dict)]),
122        );
123
124        if let Some(to_unicode) = to_unicode_id {
125            dict.set("ToUnicode", Object::Reference(to_unicode));
126        }
127    }
128
129    /// Create a CIDFont dictionary
130    fn create_cid_font_dict(&self, descriptor_id: ObjectId) -> Dictionary {
131        let mut dict = Dictionary::new();
132
133        dict.set("Type", Object::Name("Font".into()));
134
135        let font_name = self.font.postscript_name();
136        // ISO 32000-1 §9.7.4: CIDFontType0 for CFF/OpenType, CIDFontType2 for TrueType.
137        let cid_font_subtype = match self.font.format {
138            super::FontFormat::OpenType => "CIDFontType0",
139            super::FontFormat::TrueType => "CIDFontType2",
140        };
141
142        dict.set("Subtype", Object::Name(cid_font_subtype.into()));
143        dict.set("BaseFont", Object::Name(font_name.into()));
144
145        // CIDSystemInfo - Use appropriate values for CJK fonts
146        let mut cid_system_info = Dictionary::new();
147        let font_name = self.font.postscript_name();
148        let (registry, ordering, supplement) =
149            if let Some(cjk_type) = CjkFontType::detect_from_name(font_name) {
150                cjk_type.cid_system_info()
151            } else {
152                ("Adobe", "Identity", 0)
153            };
154
155        cid_system_info.set("Registry", Object::String(registry.into()));
156        cid_system_info.set("Ordering", Object::String(ordering.into()));
157        cid_system_info.set("Supplement", Object::Integer(supplement as i64));
158        dict.set("CIDSystemInfo", Object::Dictionary(cid_system_info));
159
160        dict.set("FontDescriptor", Object::Reference(descriptor_id));
161
162        // Default width
163        dict.set("DW", Object::Integer(1000));
164
165        // Width array with actual glyph widths
166        let widths_array = self.create_cid_widths_array();
167        dict.set("W", Object::Array(widths_array));
168
169        dict
170    }
171
172    /// Create a simple font dictionary (Type1/TrueType)
173    fn create_simple_font_dict(
174        &self,
175        dict: &mut Dictionary,
176        descriptor_id: ObjectId,
177        to_unicode_id: Option<ObjectId>,
178    ) {
179        dict.set("Subtype", Object::Name("TrueType".into()));
180        dict.set("BaseFont", Object::Name(self.font.postscript_name().into()));
181        dict.set(
182            "Encoding",
183            Object::Name(self.options.encoding.name().into()),
184        );
185
186        dict.set("FontDescriptor", Object::Reference(descriptor_id));
187
188        // FirstChar and LastChar
189        let (first_char, last_char) = self.get_char_range();
190        dict.set("FirstChar", Object::Integer(first_char as i64));
191        dict.set("LastChar", Object::Integer(last_char as i64));
192
193        // Widths array
194        let widths = self.create_widths_array(first_char, last_char);
195        dict.set("Widths", Object::Array(widths));
196
197        if let Some(to_unicode) = to_unicode_id {
198            dict.set("ToUnicode", Object::Reference(to_unicode));
199        }
200    }
201
202    /// Get the range of characters used
203    fn get_char_range(&self) -> (u8, u8) {
204        if self.used_chars.is_empty() {
205            return (32, 126); // Default ASCII range
206        }
207
208        let mut min = 255;
209        let mut max = 0;
210
211        for &ch in &self.used_chars {
212            if ch as u32 <= 255 {
213                let byte = ch as u8;
214                if byte < min {
215                    min = byte;
216                }
217                if byte > max {
218                    max = byte;
219                }
220            }
221        }
222
223        (min, max)
224    }
225
226    /// Create widths array for the font
227    fn create_widths_array(&self, first_char: u8, last_char: u8) -> Vec<Object> {
228        let mut widths = Vec::new();
229
230        for ch in first_char..=last_char {
231            if let Some(width) = self.font.glyph_mapping.get_char_width(char::from(ch)) {
232                // Convert from font units to PDF units (1/1000)
233                let pdf_width = (width as f64 * 1000.0) / self.font.metrics.units_per_em as f64;
234                widths.push(Object::Integer(pdf_width as i64));
235            } else {
236                // Default width for missing glyphs
237                widths.push(Object::Integer(600));
238            }
239        }
240
241        widths
242    }
243
244    /// Create CID widths array for CID fonts
245    fn create_cid_widths_array(&self) -> Vec<Object> {
246        let mut width_array = Vec::new();
247
248        // Create a map of character widths
249        let mut char_widths = std::collections::HashMap::new();
250
251        // For each used character, get its width
252        for &ch in &self.used_chars {
253            if let Some(width) = self.font.glyph_mapping.get_char_width(ch) {
254                // Convert from font units to PDF units (1/1000)
255                let pdf_width = (width as f64 * 1000.0) / self.font.metrics.units_per_em as f64;
256                char_widths.insert(ch as u32, pdf_width as i64);
257            }
258        }
259
260        // Group consecutive characters with same width for efficiency
261        let mut sorted_chars: Vec<_> = char_widths.iter().collect();
262        sorted_chars.sort_by_key(|(code, _)| *code);
263
264        let mut current_range_start = None;
265        let mut current_range_end = None; // Track the end of the current range
266        let mut current_width = None;
267
268        for (&code, &width) in sorted_chars {
269            match (current_range_start, current_range_end) {
270                (None, _) => {
271                    // Start a new range
272                    current_range_start = Some(code);
273                    current_range_end = Some(code);
274                    current_width = Some(width);
275                }
276                (Some(_start), Some(end)) => {
277                    // Check if we can continue the current range
278                    // (consecutive code AND same width)
279                    if current_width == Some(width) && code == end + 1 {
280                        // Continue the range
281                        current_range_end = Some(code);
282                    } else {
283                        // End current range and add to array
284                        if let (Some(start_code), Some(w)) = (current_range_start, current_width) {
285                            width_array.push(Object::Integer(start_code as i64));
286                            width_array.push(Object::Array(vec![Object::Integer(w)]));
287                        }
288
289                        // Start new range
290                        current_range_start = Some(code);
291                        current_range_end = Some(code);
292                        current_width = Some(width);
293                    }
294                }
295                (Some(_), None) => {
296                    // This should never happen (invariant: if start is Some, end must be Some)
297                    unreachable!("Range start without range end (this is a bug)")
298                }
299            }
300        }
301
302        // Don't forget the last range
303        if let (Some(start_code), Some(w)) = (current_range_start, current_width) {
304            width_array.push(Object::Integer(start_code as i64));
305            width_array.push(Object::Array(vec![Object::Integer(w)]));
306        }
307
308        width_array
309    }
310
311    /// Create ToUnicode CMap for text extraction
312    pub fn create_to_unicode_cmap(&self) -> Vec<u8> {
313        let mut cmap = String::new();
314
315        // CMap header
316        cmap.push_str("/CIDInit /ProcSet findresource begin\n");
317        cmap.push_str("12 dict begin\n");
318        cmap.push_str("begincmap\n");
319        cmap.push_str("/CIDSystemInfo\n");
320        cmap.push_str("<< /Registry (Adobe)\n");
321        cmap.push_str("   /Ordering (UCS)\n");
322        cmap.push_str("   /Supplement 0\n");
323        cmap.push_str(">> def\n");
324        cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
325        cmap.push_str("/CMapType 2 def\n");
326        cmap.push_str("1 begincodespacerange\n");
327        cmap.push_str("<0000> <FFFF>\n");
328        cmap.push_str("endcodespacerange\n");
329
330        // Character mappings
331        let mut mappings = Vec::new();
332        for &ch in &self.used_chars {
333            if let Some(glyph) = self.font.glyph_mapping.char_to_glyph(ch) {
334                mappings.push((glyph, ch));
335            }
336        }
337
338        if !mappings.is_empty() {
339            cmap.push_str(&format!("{} beginbfchar\n", mappings.len()));
340            for (glyph, ch) in mappings {
341                cmap.push_str(&format!("<{:04X}> <{:04X}>\n", glyph, ch as u32));
342            }
343            cmap.push_str("endbfchar\n");
344        }
345
346        // CMap footer
347        cmap.push_str("endcmap\n");
348        cmap.push_str("CMapName currentdict /CMap defineresource pop\n");
349        cmap.push_str("end\n");
350        cmap.push_str("end\n");
351
352        cmap.into_bytes()
353    }
354
355    /// Get the font data for embedding
356    pub fn get_font_data(&self) -> Result<Vec<u8>> {
357        if self.options.subset {
358            // Basic subsetting: currently returns full font
359            // Full TrueType subsetting requires complex table manipulation:
360            // - Reordering glyphs in glyf/loca tables
361            // - Updating glyph indices in cmap table
362            // - Recalculating table checksums
363            // - Updating cross-references between tables
364            //
365            // For now, return the full font data but track used characters
366            // for proper width array generation
367            if !self.used_chars.is_empty() {
368                // Font will be optimized with proper width arrays
369                Ok(self.font.data.clone())
370            } else {
371                // No characters used - return minimal font
372                Ok(self.font.data.clone())
373            }
374        } else {
375            Ok(self.font.data.clone())
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::fonts::{Font, FontDescriptor, FontFormat, FontMetrics, GlyphMapping};
384
385    fn create_test_font() -> Font {
386        let mut glyph_mapping = GlyphMapping::default();
387        for ch in 32..127 {
388            glyph_mapping.add_mapping(char::from(ch), ch as u16);
389            glyph_mapping.set_glyph_width(ch as u16, 600);
390        }
391
392        Font {
393            name: "TestFont".to_string(),
394            data: vec![0; 1000],
395            format: FontFormat::TrueType,
396            metrics: FontMetrics {
397                units_per_em: 1000,
398                ascent: 800,
399                descent: -200,
400                line_gap: 200,
401                cap_height: 700,
402                x_height: 500,
403            },
404            descriptor: FontDescriptor::new("TestFont"),
405            glyph_mapping,
406        }
407    }
408
409    #[test]
410    fn test_font_embedder_creation() {
411        let font = create_test_font();
412        let options = EmbeddingOptions::default();
413        let embedder = FontEmbedder::new(&font, options);
414
415        assert_eq!(embedder.used_chars.len(), 0);
416    }
417
418    #[test]
419    fn test_add_used_chars() {
420        let font = create_test_font();
421        let options = EmbeddingOptions::default();
422        let mut embedder = FontEmbedder::new(&font, options);
423
424        embedder.add_used_chars("Hello");
425        assert_eq!(embedder.used_chars.len(), 4); // H, e, l, o (l appears twice but is deduplicated)
426
427        embedder.add_used_chars("World");
428        assert_eq!(embedder.used_chars.len(), 7); // H,e,l,o,W,r,d (o and l overlap between Hello and World)
429    }
430
431    #[test]
432    fn test_char_range() {
433        let font = create_test_font();
434        let options = EmbeddingOptions::default();
435        let mut embedder = FontEmbedder::new(&font, options);
436
437        embedder.add_used_chars("AZ");
438        let (first, last) = embedder.get_char_range();
439        assert_eq!(first, b'A');
440        assert_eq!(last, b'Z');
441    }
442
443    #[test]
444    fn test_font_encoding_names() {
445        assert_eq!(FontEncoding::WinAnsiEncoding.name(), "WinAnsiEncoding");
446        assert_eq!(FontEncoding::MacRomanEncoding.name(), "MacRomanEncoding");
447        assert_eq!(FontEncoding::StandardEncoding.name(), "StandardEncoding");
448        assert_eq!(FontEncoding::IdentityH.name(), "Identity-H");
449    }
450
451    #[test]
452    fn test_create_simple_font_dict() {
453        let font = create_test_font();
454        let options = EmbeddingOptions {
455            subset: false,
456            compress: false,
457            encoding: FontEncoding::WinAnsiEncoding,
458        };
459        let mut embedder = FontEmbedder::new(&font, options);
460        embedder.add_used_chars("ABC");
461
462        let font_dict = embedder.create_font_dict(ObjectId::new(10, 0), Some(ObjectId::new(11, 0)));
463
464        assert_eq!(font_dict.get("Type").unwrap(), &Object::Name("Font".into()));
465        assert_eq!(
466            font_dict.get("Subtype").unwrap(),
467            &Object::Name("TrueType".into())
468        );
469        assert!(font_dict.get("FirstChar").is_some());
470        assert!(font_dict.get("LastChar").is_some());
471        assert!(font_dict.get("Widths").is_some());
472    }
473
474    #[test]
475    fn test_create_type0_font_dict() {
476        let font = create_test_font();
477        let options = EmbeddingOptions {
478            subset: false,
479            compress: false,
480            encoding: FontEncoding::IdentityH,
481        };
482        let embedder = FontEmbedder::new(&font, options);
483
484        let font_dict = embedder.create_font_dict(ObjectId::new(10, 0), Some(ObjectId::new(11, 0)));
485
486        assert_eq!(font_dict.get("Type").unwrap(), &Object::Name("Font".into()));
487        assert_eq!(
488            font_dict.get("Subtype").unwrap(),
489            &Object::Name("Type0".into())
490        );
491        assert_eq!(
492            font_dict.get("Encoding").unwrap(),
493            &Object::Name("Identity-H".into())
494        );
495        assert!(font_dict.get("DescendantFonts").is_some());
496    }
497
498    #[test]
499    fn test_create_widths_array() {
500        let font = create_test_font();
501        let options = EmbeddingOptions::default();
502        let embedder = FontEmbedder::new(&font, options);
503
504        let widths = embedder.create_widths_array(65, 67); // A, B, C
505        assert_eq!(widths.len(), 3);
506        for width in &widths {
507            if let Object::Integer(w) = width {
508                assert_eq!(*w, 600); // All test glyphs have width 600
509            } else {
510                panic!("Expected Integer object");
511            }
512        }
513    }
514
515    #[test]
516    fn test_create_to_unicode_cmap() {
517        let font = create_test_font();
518        let options = EmbeddingOptions::default();
519        let mut embedder = FontEmbedder::new(&font, options);
520        embedder.add_used_chars("Hello");
521
522        let cmap = embedder.create_to_unicode_cmap();
523        let cmap_str = String::from_utf8(cmap).unwrap();
524
525        assert!(cmap_str.contains("begincmap"));
526        assert!(cmap_str.contains("endcmap"));
527        assert!(cmap_str.contains("beginbfchar"));
528        assert!(cmap_str.contains("endbfchar"));
529    }
530
531    #[test]
532    fn test_get_font_data() {
533        let font = create_test_font();
534        let options = EmbeddingOptions {
535            subset: false,
536            compress: false,
537            encoding: FontEncoding::WinAnsiEncoding,
538        };
539        let embedder = FontEmbedder::new(&font, options);
540
541        let font_data = embedder.get_font_data().unwrap();
542        assert_eq!(font_data.len(), 1000);
543    }
544
545    #[test]
546    fn test_embedding_options_default() {
547        let options = EmbeddingOptions::default();
548        assert!(options.subset);
549        assert!(options.compress);
550        assert_eq!(options.encoding, FontEncoding::WinAnsiEncoding);
551    }
552
553    #[test]
554    fn test_char_range_empty() {
555        let font = create_test_font();
556        let options = EmbeddingOptions::default();
557        let embedder = FontEmbedder::new(&font, options);
558
559        let (first, last) = embedder.get_char_range();
560        assert_eq!(first, 32); // Default ASCII range
561        assert_eq!(last, 126);
562    }
563
564    #[test]
565    fn test_char_range_with_unicode() {
566        let font = create_test_font();
567        let options = EmbeddingOptions::default();
568        let mut embedder = FontEmbedder::new(&font, options);
569
570        // Add characters including non-ASCII
571        embedder.add_used_chars("A€B"); // Euro sign is > 255
572        let (first, last) = embedder.get_char_range();
573
574        // Should only consider ASCII characters
575        assert_eq!(first, b'A');
576        assert_eq!(last, b'B');
577    }
578
579    #[test]
580    fn test_cid_font_dict_creation() {
581        let font = create_test_font();
582        let options = EmbeddingOptions {
583            subset: false,
584            compress: false,
585            encoding: FontEncoding::IdentityH,
586        };
587        let embedder = FontEmbedder::new(&font, options);
588
589        let cid_dict = embedder.create_cid_font_dict(ObjectId::new(10, 0));
590
591        assert_eq!(cid_dict.get("Type").unwrap(), &Object::Name("Font".into()));
592        assert_eq!(
593            cid_dict.get("Subtype").unwrap(),
594            &Object::Name("CIDFontType2".into())
595        );
596        assert!(cid_dict.get("CIDSystemInfo").is_some());
597        assert_eq!(cid_dict.get("DW").unwrap(), &Object::Integer(1000));
598
599        // Check CIDSystemInfo
600        if let Object::Dictionary(sys_info) = cid_dict.get("CIDSystemInfo").unwrap() {
601            assert_eq!(
602                sys_info.get("Registry").unwrap(),
603                &Object::String("Adobe".into())
604            );
605            assert_eq!(
606                sys_info.get("Ordering").unwrap(),
607                &Object::String("Identity".into())
608            );
609            assert_eq!(sys_info.get("Supplement").unwrap(), &Object::Integer(0));
610        } else {
611            panic!("Expected Dictionary for CIDSystemInfo");
612        }
613    }
614
615    #[test]
616    fn test_font_encoding_equality() {
617        assert_eq!(FontEncoding::WinAnsiEncoding, FontEncoding::WinAnsiEncoding);
618        assert_ne!(
619            FontEncoding::WinAnsiEncoding,
620            FontEncoding::MacRomanEncoding
621        );
622        assert_ne!(FontEncoding::StandardEncoding, FontEncoding::IdentityH);
623    }
624
625    #[test]
626    fn test_add_duplicate_chars() {
627        let font = create_test_font();
628        let options = EmbeddingOptions::default();
629        let mut embedder = FontEmbedder::new(&font, options);
630
631        embedder.add_used_chars("AAA");
632        assert_eq!(embedder.used_chars.len(), 1); // Only one 'A' should be stored
633
634        embedder.add_used_chars("ABBA");
635        assert_eq!(embedder.used_chars.len(), 2); // 'A' and 'B'
636    }
637
638    #[test]
639    fn test_widths_array_missing_glyphs() {
640        let mut font = create_test_font();
641        // Clear all glyph mappings to test missing glyph handling
642        font.glyph_mapping = GlyphMapping::default();
643
644        let options = EmbeddingOptions::default();
645        let embedder = FontEmbedder::new(&font, options);
646
647        let widths = embedder.create_widths_array(65, 67); // A, B, C
648        assert_eq!(widths.len(), 3);
649
650        // Should use default width of 600 for missing glyphs
651        for width in &widths {
652            if let Object::Integer(w) = width {
653                assert_eq!(*w, 600);
654            }
655        }
656    }
657}