Skip to main content

ttf_rs/
modifier.rs

1//! Font modification API for updating font metadata and metrics.
2//!
3//! This module provides a builder-style API for modifying various aspects
4//! of TrueType fonts, including metadata (names, copyright), metrics
5//! (ascent, descent, advance widths), and embedding permissions.
6//!
7//! # Examples
8//!
9//! ```no_run
10//! use ttf_rs::Font;
11//!
12//! let font = Font::load("input.ttf")?;
13//! let mut modifier = font.modify();
14//!
15//! modifier.set_font_name("My Custom Font")?;
16//! modifier.set_version(2, 0)?;
17//! modifier.set_font_metrics(2048, 1638, -410, 204)?;
18//!
19//! let modified_font = modifier.commit()?;
20//! modified_font.save("output.ttf")?;
21//! # Ok::<(), ttf_rs::TtfError>(())
22//! ```
23
24use crate::error::Result;
25use crate::font::Font;
26use crate::stream::FontWriter;
27use crate::tables::name::NameTable;
28use crate::tables::head::HeadTable;
29use crate::tables::hhea::HheaTable;
30use crate::tables::os2::Os2Table;
31use crate::tables::hmtx::HmtxTable;
32use crate::tables::TtfTableWrite;
33use std::collections::HashMap;
34
35/// Font modifier for updating font properties.
36///
37/// Provides a builder-style API for modifying font metadata, metrics,
38/// and other properties. Changes are tracked internally and applied
39/// when `commit()` is called.
40///
41/// # Note
42///
43/// Full round-trip serialization with proper offset recalculation is
44/// currently in development. Some modifications may not persist correctly
45/// when saving and reloading the font.
46pub struct FontModifier {
47    font: Font,
48    modified_tables: HashMap<[u8; 4], Vec<u8>>,
49}
50
51impl FontModifier {
52    pub fn new(font: Font) -> Self {
53        Self {
54            font,
55            modified_tables: HashMap::new(),
56        }
57    }
58
59    /// Set font family name (name ID 1).
60    ///
61    /// Updates the font family name in the name table.
62    ///
63    /// # Arguments
64    ///
65    /// * `name` - The new font family name
66    ///
67    /// # Examples
68    ///
69    /// ```no_run
70    /// # use ttf_rs::Font;
71    /// let font = Font::load("font.ttf")?;
72    /// let mut modifier = font.modify();
73    /// modifier.set_font_name("My Font Family")?;
74    /// # Ok::<(), ttf_rs::TtfError>(())
75    /// ```
76    pub fn set_font_name(&mut self, name: &str) -> Result<&mut Self> {
77        let mut name_table = self.font.name_table()?;
78
79        let platform_id = 3u16;
80        let encoding_id = 1u16;
81        let language_id = 0x0409u16;
82        let name_id = 1u16;
83
84        name_table.set_name(name, platform_id, encoding_id, language_id, name_id);
85        self.serialize_name_table(name_table)?;
86        Ok(self)
87    }
88
89    /// Set full font name (name ID 4).
90    ///
91    /// Updates the full font name (family + style) in the name table.
92    ///
93    /// # Arguments
94    ///
95    /// * `name` - The new full font name
96    pub fn set_full_font_name(&mut self, name: &str) -> Result<&mut Self> {
97        let mut name_table = self.font.name_table()?;
98
99        let platform_id = 3u16;
100        let encoding_id = 1u16;
101        let language_id = 0x0409u16;
102        let name_id = 4u16;
103
104        name_table.set_name(name, platform_id, encoding_id, language_id, name_id);
105        self.serialize_name_table(name_table)?;
106        Ok(self)
107    }
108
109    /// Set version string (name ID 5).
110    ///
111    /// Updates the version string in the name table and font revision in head table.
112    ///
113    /// # Arguments
114    ///
115    /// * `major` - Major version number
116    /// * `minor` - Minor version number
117    ///
118    /// # Examples
119    ///
120    /// ```no_run
121    /// # use ttf_rs::Font;
122    /// let font = Font::load("font.ttf")?;
123    /// let mut modifier = font.modify();
124    /// modifier.set_version(2, 0)?;
125    /// # Ok::<(), ttf_rs::TtfError>(())
126    /// ```
127    pub fn set_version(&mut self, major: u16, minor: u16) -> Result<&mut Self> {
128        let version_string = format!("Version {}.{}", major, minor);
129
130        let mut name_table = self.font.name_table()?;
131
132        let platform_id = 3u16;
133        let encoding_id = 1u16;
134        let language_id = 0x0409u16;
135        let name_id = 5u16;
136
137        name_table.set_name(&version_string, platform_id, encoding_id, language_id, name_id);
138        self.serialize_name_table(name_table)?;
139
140        // Also update the font revision in head table
141        let mut head_table = self.font.head_table()?;
142        head_table.font_revision = (major as f32) + (minor as f32) / 100.0;
143        self.serialize_head_table(head_table)?;
144
145        Ok(self)
146    }
147
148    /// Set copyright notice (name ID 0).
149    ///
150    /// Updates the copyright notice in the name table.
151    ///
152    /// # Arguments
153    ///
154    /// * `copyright` - The copyright notice text
155    pub fn set_copyright(&mut self, copyright: &str) -> Result<&mut Self> {
156        let mut name_table = self.font.name_table()?;
157
158        let platform_id = 3u16;
159        let encoding_id = 1u16;
160        let language_id = 0x0409u16;
161        let name_id = 0u16;
162
163        name_table.set_name(copyright, platform_id, encoding_id, language_id, name_id);
164        self.serialize_name_table(name_table)?;
165        Ok(self)
166    }
167
168    /// Set trademark notice (name ID 7).
169    ///
170    /// Updates the trademark notice in the name table.
171    ///
172    /// # Arguments
173    ///
174    /// * `trademark` - The trademark notice text
175    pub fn set_trademark(&mut self, trademark: &str) -> Result<&mut Self> {
176        let mut name_table = self.font.name_table()?;
177
178        let platform_id = 3u16;
179        let encoding_id = 1u16;
180        let language_id = 0x0409u16;
181        let name_id = 7u16;
182
183        name_table.set_name(trademark, platform_id, encoding_id, language_id, name_id);
184        self.serialize_name_table(name_table)?;
185        Ok(self)
186    }
187
188    /// Update font version and revision in head table.
189    ///
190    /// Sets the font revision number in the head table.
191    ///
192    /// # Arguments
193    ///
194    /// * `major` - Major version number
195    /// * `minor` - Minor version number (0-99)
196    pub fn set_font_revision(&mut self, major: u16, minor: u16) -> Result<&mut Self> {
197        let mut head_table = self.font.head_table()?;
198        head_table.font_revision = (major as f32) + (minor as f32) / 100.0;
199        self.serialize_head_table(head_table)?;
200        Ok(self)
201    }
202
203    /// Modify embedding permissions in OS/2 table
204    pub fn set_embedding_type(&mut self, embedding_type: u16) -> Result<&mut Self> {
205        let mut os2_table = self.font.os2_table()?;
206        os2_table.fs_type = embedding_type;
207        self.serialize_os2_table(os2_table)?;
208        Ok(self)
209    }
210
211    /// Set font name in multiple languages
212    pub fn set_localized_font_name(&mut self, name: &str, language_id: u16) -> Result<&mut Self> {
213        let mut name_table = self.font.name_table()?;
214
215        let platform_id = 3u16;
216        let encoding_id = 1u16;
217        let name_id = 1u16;
218
219        name_table.set_name(name, platform_id, encoding_id, language_id, name_id);
220        self.serialize_name_table(name_table)?;
221        Ok(self)
222    }
223
224    /// Update font metrics in head and hhea tables
225    pub fn set_font_metrics(&mut self, units_per_em: u16, ascender: i16, descender: i16, line_gap: i16) -> Result<&mut Self> {
226        // Update head table
227        let mut head_table = self.font.head_table()?;
228        head_table.units_per_em = units_per_em;
229        self.serialize_head_table(head_table)?;
230
231        // Update hhea table (field names are ascent/descent, not ascender/descender)
232        let mut hhea_table = self.font.hhea_table()?;
233        hhea_table.ascent = ascender;
234        hhea_table.descent = descender;
235        hhea_table.line_gap = line_gap;
236        self.serialize_hhea_table(hhea_table)?;
237
238        Ok(self)
239    }
240
241    /// Modify glyph advance widths
242    pub fn set_glyph_advance(&mut self, glyph_index: usize, advance_width: u16) -> Result<&mut Self> {
243        let mut hmtx_table = self.font.hmtx_table()?;
244        let hhea_table = self.font.hhea_table()?;
245
246        if glyph_index < hmtx_table.h_metrics.len() {
247            if glyph_index < hhea_table.number_of_h_metrics as usize {
248                hmtx_table.h_metrics[glyph_index].advance_width = advance_width;
249            }
250            self.serialize_hmtx_table(hmtx_table)?;
251        }
252
253        Ok(self)
254    }
255
256    /// Serialize modified name table
257    fn serialize_name_table(&mut self, table: NameTable) -> Result<()> {
258        let mut writer = FontWriter::new();
259        table.write(&mut writer)?;
260        self.modified_tables.insert(*b"name", writer.into_inner());
261        Ok(())
262    }
263
264    /// Serialize modified head table
265    fn serialize_head_table(&mut self, table: HeadTable) -> Result<()> {
266        let mut writer = FontWriter::new();
267        table.write(&mut writer)?;
268        self.modified_tables.insert(*b"head", writer.into_inner());
269        Ok(())
270    }
271
272    /// Serialize modified hhea table
273    fn serialize_hhea_table(&mut self, table: HheaTable) -> Result<()> {
274        let mut writer = FontWriter::new();
275        table.write(&mut writer)?;
276        self.modified_tables.insert(*b"hhea", writer.into_inner());
277        Ok(())
278    }
279
280    /// Serialize modified OS/2 table
281    fn serialize_os2_table(&mut self, table: Os2Table) -> Result<()> {
282        let mut writer = FontWriter::new();
283        table.write(&mut writer)?;
284        self.modified_tables.insert(*b"OS/2", writer.into_inner());
285        Ok(())
286    }
287
288    /// Serialize modified hmtx table
289    fn serialize_hmtx_table(&mut self, table: HmtxTable) -> Result<()> {
290        let mut writer = FontWriter::new();
291        table.write(&mut writer)?;
292        self.modified_tables.insert(*b"hmtx", writer.into_inner());
293        Ok(())
294    }
295
296    /// Commit all modifications and return the modified font
297    pub fn commit(mut self) -> Result<Font> {
298        // Apply all modified tables to the font
299        for (tag, data) in &self.modified_tables {
300            // Find the table record and update it
301            if let Some(record) = self.font.table_records.iter_mut().find(|r| r.table_tag == *tag) {
302                record.length = data.len() as u32;
303                // Offset will be recalculated during save
304            }
305        }
306
307        // Create new data with modified tables
308        let mut new_data = self.font.data.clone();
309
310        // Update the data with modified tables (simplified approach)
311        for (tag, data) in &self.modified_tables {
312            if let Some(record) = self.font.get_table_record(tag) {
313                let offset = record.offset as usize;
314                // Make sure we have enough space
315                if offset + data.len() <= new_data.len() {
316                    new_data[offset..offset + data.len()].copy_from_slice(data);
317                }
318            }
319        }
320
321        self.font.data = new_data;
322        Ok(self.font)
323    }
324}
325
326impl Font {
327    pub fn modify(self) -> FontModifier {
328        FontModifier::new(self)
329    }
330}