Skip to main content

rust_pdf_box/font/
truetype.rs

1use crate::cos::document::COSDocument;
2use crate::cos::object::{COSArray, COSDictionary, COSName, COSStream, ObjectId};
3use crate::error::{PdfError, PdfResult};
4use ttf_parser::Face;
5
6/// Embedded TrueType font.
7#[derive(Debug, Clone)]
8pub struct PDTrueTypeFont {
9    pub font_name: String,
10    pub units_per_em: u16,
11    pub ascent: i16,
12    pub descent: i16,
13    pub font_id: ObjectId,
14}
15
16impl PDTrueTypeFont {
17    /// Loads and embeds a TrueType font from font file bytes into the given COSDocument.
18    pub fn load(cos_doc: &mut COSDocument, font_bytes: &[u8]) -> PdfResult<Self> {
19        let face = Face::parse(font_bytes, 0)
20            .map_err(|e| PdfError::Font(format!("Failed parsing TrueType font: {e}")))?;
21
22        let font_name = face
23            .names()
24            .into_iter()
25            .find(|name| name.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
26            .and_then(|name| name.to_string())
27            .unwrap_or_else(|| "CustomFont".to_string());
28
29        let units_per_em = face.units_per_em();
30        let ascent = face.ascender();
31        let descent = face.descender();
32
33        // 1. Font file stream (/FontFile2)
34        let mut font_file_dict = COSDictionary::new();
35        font_file_dict.set("Length1", font_bytes.len() as i64);
36        let mut font_stream = COSStream::new(font_file_dict, font_bytes.to_vec());
37        let _ = font_stream.encode_flate();
38        let font_file_id = cos_doc.add_object(font_stream);
39
40        // 2. FontDescriptor
41        let mut desc = COSDictionary::new();
42        desc.set("Type", COSName::new("FontDescriptor"));
43        desc.set("FontName", COSName::new(&font_name));
44        desc.set("Flags", 32); // Non-symbolic
45        let mut bbox = COSArray::new();
46        bbox.push(face.global_bounding_box().x_min as i64);
47        bbox.push(face.global_bounding_box().y_min as i64);
48        bbox.push(face.global_bounding_box().x_max as i64);
49        bbox.push(face.global_bounding_box().y_max as i64);
50        desc.set("FontBBox", bbox);
51        desc.set("ItalicAngle", face.italic_angle() as f64);
52        desc.set("Ascent", ascent as i64);
53        desc.set("Descent", descent as i64);
54        desc.set("CapHeight", face.capital_height().unwrap_or(ascent) as i64);
55        desc.set("StemV", 80);
56        desc.set("FontFile2", font_file_id);
57        let desc_id = cos_doc.add_object(desc);
58
59        // 3. TrueType Font Dictionary
60        let mut font_dict = COSDictionary::new();
61        font_dict.set("Type", COSName::new("Font"));
62        font_dict.set("Subtype", COSName::new("TrueType"));
63        font_dict.set("BaseFont", COSName::new(&font_name));
64        font_dict.set("FontDescriptor", desc_id);
65        font_dict.set("Encoding", COSName::new("WinAnsiEncoding"));
66        font_dict.set("FirstChar", 32);
67        font_dict.set("LastChar", 255);
68
69        // Widths array for ASCII 32..=255
70        let mut widths = COSArray::new();
71        let scale = 1000.0 / (units_per_em as f64);
72        for byte_val in 32u8..=255u8 {
73            let ch = byte_val as char;
74            let width = if let Some(glyph_id) = face.glyph_index(ch) {
75                let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0);
76                (advance as f64 * scale).round() as i64
77            } else {
78                500
79            };
80            widths.push(width);
81        }
82        font_dict.set("Widths", widths);
83
84        let font_id = cos_doc.add_object(font_dict);
85
86        Ok(Self {
87            font_name,
88            units_per_em,
89            ascent,
90            descent,
91            font_id,
92        })
93    }
94}