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