Skip to main content

oxidize_pdf/fonts/
mod.rs

1//! Font loading and embedding functionality for custom fonts
2//!
3//! This module provides support for loading TrueType (TTF) and OpenType (OTF) fonts,
4//! embedding them in PDF documents, and using them for text rendering.
5
6pub mod cid_mapper;
7pub mod cmap_utils;
8pub mod embedder;
9pub mod font_cache;
10pub mod font_descriptor;
11pub mod font_metrics;
12pub mod loader;
13pub mod standard_14;
14pub mod ttf_parser;
15pub mod type0;
16pub mod type0_parsing;
17
18pub use cid_mapper::{analyze_unicode_ranges, CidMapping, UnicodeRanges};
19pub use embedder::{EmbeddingOptions, FontEmbedder, FontEncoding};
20pub use font_cache::FontCache;
21pub use font_descriptor::{FontDescriptor, FontFlags};
22pub use font_metrics::{FontMetrics, TextMeasurement};
23pub use loader::{FontData, FontFormat, FontLoader};
24pub use standard_14::Standard14Font;
25pub use ttf_parser::{GlyphMapping, TtfParser};
26pub use type0::{create_type0_from_font, needs_type0_font, Type0Font};
27pub use type0_parsing::{
28    detect_cidfont_subtype, detect_type0_font, extract_default_width, extract_descendant_fonts_ref,
29    extract_font_descriptor_ref, extract_font_file_ref, extract_tounicode_ref, extract_widths_ref,
30    resolve_type0_hierarchy, CIDFontSubtype, FontFileType, Type0FontInfo, MAX_FONT_STREAM_SIZE,
31};
32
33use crate::Result;
34
35/// Represents a loaded font ready for embedding
36#[derive(Debug, Clone)]
37pub struct Font {
38    /// Font name as it will appear in the PDF
39    pub name: String,
40    /// Raw font data
41    pub data: Vec<u8>,
42    /// Font format (TTF or OTF)
43    pub format: FontFormat,
44    /// Font metrics
45    pub metrics: FontMetrics,
46    /// Font descriptor
47    pub descriptor: FontDescriptor,
48    /// Character to glyph mapping
49    pub glyph_mapping: GlyphMapping,
50}
51
52impl Font {
53    /// Create a new font with default values
54    pub fn new(name: impl Into<String>) -> Self {
55        Font {
56            name: name.into(),
57            data: Vec::new(),
58            format: FontFormat::TrueType,
59            metrics: FontMetrics::default(),
60            descriptor: FontDescriptor::default(),
61            glyph_mapping: GlyphMapping::default(),
62        }
63    }
64
65    /// Load a font from file path
66    pub fn from_file(name: impl Into<String>, path: impl AsRef<std::path::Path>) -> Result<Self> {
67        let data = std::fs::read(path)?;
68        Self::from_bytes(name, data)
69    }
70
71    /// Load a font from byte data
72    pub fn from_bytes(name: impl Into<String>, data: Vec<u8>) -> Result<Self> {
73        let name = name.into();
74        let format = FontFormat::detect(&data)?;
75
76        let parser = TtfParser::new(&data)?;
77        let metrics = parser.extract_metrics()?;
78        let descriptor = parser.create_descriptor()?;
79        let glyph_mapping = parser.extract_glyph_mapping()?;
80
81        Ok(Font {
82            name,
83            data,
84            format,
85            metrics,
86            descriptor,
87            glyph_mapping,
88        })
89    }
90
91    /// Get the PostScript name of the font
92    pub fn postscript_name(&self) -> &str {
93        &self.descriptor.font_name
94    }
95
96    /// Check if the font contains a specific character
97    pub fn has_glyph(&self, ch: char) -> bool {
98        self.glyph_mapping.char_to_glyph(ch).is_some()
99    }
100
101    /// Characters in `text` the font has no glyph for, deduplicated and in
102    /// first-seen order. Control characters (newlines, tabs, …) are ignored
103    /// because they are never rendered.
104    ///
105    /// Such characters render as `.notdef` (an empty box) — this is the
106    /// correct PDF behaviour when the embedded font genuinely lacks the glyph
107    /// (issue #287). Use this to detect coverage gaps before rendering rather
108    /// than discovering empty boxes in the output.
109    pub fn missing_glyphs(&self, text: &str) -> Vec<char> {
110        // Coverage cannot be determined if the real cmap was not parsed (no
111        // cmap, or the ASCII fallback fired): report nothing rather than
112        // flagging every character as missing.
113        if !self.glyph_mapping.coverage_known() {
114            return Vec::new();
115        }
116        let mut seen = std::collections::HashSet::new();
117        let mut missing = Vec::new();
118        for ch in text.chars() {
119            if ch.is_control() {
120                continue;
121            }
122            // `seen` dedups across all characters so repeated present
123            // characters skip the per-occurrence `has_glyph` lookup.
124            if seen.insert(ch) && !self.has_glyph(ch) {
125                missing.push(ch);
126            }
127        }
128        missing
129    }
130
131    /// Measure text using this font at a specific size
132    pub fn measure_text(&self, text: &str, font_size: f32) -> TextMeasurement {
133        self.metrics
134            .measure_text(text, font_size, &self.glyph_mapping)
135    }
136
137    /// Get the recommended line height for this font at a specific size
138    pub fn line_height(&self, font_size: f32) -> f32 {
139        self.metrics.line_height(font_size)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_font_format_detection() {
149        // TTF magic bytes
150        let ttf_data = vec![0x00, 0x01, 0x00, 0x00];
151        assert!(matches!(
152            FontFormat::detect(&ttf_data),
153            Ok(FontFormat::TrueType)
154        ));
155
156        // OTF magic bytes
157        let otf_data = vec![0x4F, 0x54, 0x54, 0x4F];
158        assert!(matches!(
159            FontFormat::detect(&otf_data),
160            Ok(FontFormat::OpenType)
161        ));
162
163        // Invalid data
164        let invalid_data = vec![0xFF, 0xFF, 0xFF, 0xFF];
165        assert!(FontFormat::detect(&invalid_data).is_err());
166    }
167
168    // =============================================================================
169    // RIGOROUS TESTS FOR Font STRUCT
170    // =============================================================================
171
172    #[test]
173    fn test_font_new() {
174        let font = Font::new("TestFont");
175
176        assert_eq!(font.name, "TestFont");
177        assert!(font.data.is_empty(), "Data should be empty for new font");
178        assert!(
179            matches!(font.format, FontFormat::TrueType),
180            "Default format should be TrueType"
181        );
182    }
183
184    #[test]
185    fn test_font_new_with_string() {
186        let font = Font::new("Arial".to_string());
187
188        assert_eq!(font.name, "Arial");
189        assert!(font.data.is_empty());
190    }
191
192    #[test]
193    fn test_font_postscript_name() {
194        let mut font = Font::new("TestFont");
195        font.descriptor.font_name = "Helvetica-Bold".to_string();
196
197        assert_eq!(font.postscript_name(), "Helvetica-Bold");
198    }
199
200    #[test]
201    fn test_font_has_glyph_with_empty_mapping() {
202        let font = Font::new("TestFont");
203
204        // Default glyph_mapping has no glyphs
205        assert!(!font.has_glyph('A'), "Empty mapping should not have glyph");
206        assert!(!font.has_glyph('€'), "Empty mapping should not have glyph");
207    }
208
209    #[test]
210    fn test_font_measure_text_with_defaults() {
211        let font = Font::new("TestFont");
212
213        // With default metrics and empty glyph mapping
214        let measurement = font.measure_text("Hello", 12.0);
215
216        // Empty glyph mapping means chars have no width (600 units default)
217        // "Hello" = 5 chars * 600 units * 12.0 / 1000 = 36.0
218        assert_eq!(
219            measurement.width, 36.0,
220            "5 chars with default 600 units at 12pt should be 36.0"
221        );
222    }
223
224    #[test]
225    fn test_font_line_height_with_defaults() {
226        let font = Font::new("TestFont");
227
228        let line_height = font.line_height(12.0);
229
230        // Default metrics: (ascent + |descent| + line_gap) * font_size / units_per_em
231        // (750 + 250 + 200) * 12.0 / 1000 = 14.4
232        assert_eq!(
233            line_height, 14.4,
234            "Default metrics should produce 14.4 line height at 12pt"
235        );
236    }
237
238    #[test]
239    fn test_font_from_file_nonexistent() {
240        let result = Font::from_file("TestFont", "/nonexistent/path/font.ttf");
241
242        assert!(
243            result.is_err(),
244            "Loading nonexistent file should return error"
245        );
246    }
247
248    #[test]
249    fn test_font_from_bytes_invalid_format() {
250        // Invalid font data (not TTF or OTF)
251        let invalid_data = vec![0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x01, 0x02, 0x03];
252
253        let result = Font::from_bytes("InvalidFont", invalid_data);
254
255        assert!(
256            result.is_err(),
257            "Invalid font data should return error from FontFormat::detect"
258        );
259    }
260
261    #[test]
262    fn test_font_from_bytes_too_small() {
263        // Data too small to be valid font
264        let tiny_data = vec![0x00, 0x01];
265
266        let result = Font::from_bytes("TinyFont", tiny_data);
267
268        assert!(
269            result.is_err(),
270            "Too small data should return error during detection"
271        );
272    }
273
274    #[test]
275    fn test_font_name_conversion() {
276        // Test that name accepts both &str and String
277        let font1 = Font::new("StrName");
278        let font2 = Font::new("StringName".to_string());
279
280        assert_eq!(font1.name, "StrName");
281        assert_eq!(font2.name, "StringName");
282    }
283
284    #[test]
285    fn test_font_fields_are_accessible() {
286        let mut font = Font::new("TestFont");
287
288        // Verify all fields are accessible and mutable
289        font.name = "ModifiedName".to_string();
290        font.data = vec![1, 2, 3, 4];
291        font.format = FontFormat::OpenType;
292
293        assert_eq!(font.name, "ModifiedName");
294        assert_eq!(font.data, vec![1, 2, 3, 4]);
295        assert!(matches!(font.format, FontFormat::OpenType));
296    }
297}