Skip to main content

rpfm_lib/files/font/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! CUF (Creative Assembly Unicode Font) file format support.
12//!
13//! This module handles `.cuf` font files used by Total War games to render text.
14//! CUF files contain glyph data, metrics, and optional kerning information for
15//! bitmap-based font rendering.
16//!
17//! # File Format
18//!
19//! CUF files use a custom binary format with the signature `CUF0`. The structure includes:
20//! - Font properties (line height, baseline, spacing, etc.)
21//! - Glyph mapping table (character code to glyph index)
22//! - Glyph dimensions (allocated size, actual size)
23//! - Glyph bitmap data (8-bit grayscale)
24//! - Optional kerning data (pair-wise spacing adjustments)
25//!
26//! # Testing Status
27//!
28//! Currently, only Empire: Total War fonts have been thoroughly tested. Other games
29//! may use slight variations of the format.
30//!
31//! # Credits
32//!
33//! Most of the reverse-engineering work was done by the Europa Barbarorum Team for
34//! their CUF tool. Their comments and insights have been ported here for reference.
35//!
36//! # Glyph Storage
37//!
38//! Glyphs are stored as 8-bit grayscale bitmaps. The format uses a sparse representation
39//! where only used character codes (non-0xFFFF values) have associated glyph data.
40//!
41//! # Kerning
42//!
43//! Kerning support is optional and only present in some font files. When present,
44//! kerning data provides spacing adjustments for specific character pairs to improve
45//! visual appearance.
46//!
47//! # Usage
48//!
49//! ```ignore
50//! use rpfm_lib::files::font::Font;
51//! use rpfm_lib::files::Decodeable;
52//!
53//! // Decode a font file
54//! let font = Font::decode(&mut data, &None)?;
55//!
56//! // Access font properties
57//! println!("Line height: {}", font.properties().line_height());
58//! println!("Supports kerning: {}", font.supports_kerning());
59//!
60//! // Access glyphs
61//! for (char_code, glyph) in font.glyphs() {
62//!     println!("Character {}: {}x{}", char_code, glyph.width(), glyph.height());
63//! }
64//! ```
65
66use getset::*;
67use serde_derive::{Serialize, Deserialize};
68
69use std::collections::BTreeMap;
70use std::io::Write;
71
72use crate::binary::{ReadBytes, WriteBytes};
73use crate::error::{Result, RLibError};
74use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable};
75use crate::utils::check_size_mismatch;
76
77/// File extension for CUF font files.
78pub const EXTENSION: &str = ".cuf";
79
80#[cfg(test)] mod font_test;
81
82/// CUF file format signature (`CUF0`).
83const SIGNATURE: &[u8; 4] = b"CUF0";
84
85//---------------------------------------------------------------------------//
86//                              Enum & Structs
87//---------------------------------------------------------------------------//
88
89/// Represents a CUF font file.
90///
91/// Contains font properties, glyph data, and optional kerning information.
92/// Glyphs are stored in a sparse map indexed by character code (0-65535).
93#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
94#[getset(get = "pub", get_mut = "pub", set = "pub")]
95pub struct Font {
96    /// Font rendering properties and metrics.
97    properties: CUFProperties,
98
99    /// Map of character codes to glyph data.
100    ///
101    /// Only contains entries for characters actually defined in the font.
102    /// Character codes map to Unicode-like values.
103    glyphs: BTreeMap<u16, Glyph>,
104
105    /// Whether this font file includes kerning data.
106    supports_kerning: bool,
107
108    /// Character codes below this value do not have kerning data.
109    kerning_skip: u16,
110
111    /// Kerning adjustment blocks (one per character code >= kerning_skip).
112    kerning_blocks: Vec<Vec<u8>>,
113}
114
115/// Font properties controlling text layout and rendering.
116///
117/// Many of these properties are indices or references whose exact purpose is still
118/// being researched. Comments from the Europa Barbarorum Team's research have been
119/// preserved for reference.
120#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
121#[getset(get = "pub", get_mut = "pub", set = "pub")]
122pub struct CUFProperties {
123    /// Unknown purpose (first CUF property).
124    first_prop: u16,
125
126    /// Unknown purpose. Second CUF property.
127    second_prop: u16,
128
129    /// Index of the value which appears to have something to do with line height. Underscore line? Base line?
130    line_height: u16,
131
132    /// Unknown purpose. Fourth CUF property.
133    fourth_prop: u16,
134
135    /// Unknown purpose. Fifth CUF property.
136    fifth_prop: u16,
137
138    /// Index of the value which appears to correspond to a ‘baseline’ of sorts in the CUF file format.
139    baseline: u16,
140
141    /// Index of the value which determines y-offset w.r.t. the bounding box of a string of text in this font.
142    layout_y_offset: u16,
143
144    /// Used to specify how wide a space is for justification and text wrapping calculations.
145    space_justify: u16,
146
147    /// Index of the value which determines x-offset w.r.t. the bounding box of a string of text in this font.
148    layout_x_offset: u16,
149
150    /// Index of the value which determines a maximum width for glyphs.
151    /// Glyphs which are wider than the maximum specified for this property will appear cut-off.
152    ///
153    /// There appears to be no effect on the position of a glyph
154    /// after a glyph of which the advance is larger than the value specified for this setting.
155    ///
156    /// Note that individual glyphs contain sufficient information to calculate a much more optimal bounding box than by simply using
157    /// multiples of the value corresponding to this index.
158    h_size: u16,
159
160    /// Index of the value which determines a maximum height for glyphs.
161    /// The corresponding value probably should include leading.
162    /// Glyphs which are taller than the maximum specified for this property will appear cut-off.
163    ///
164    /// Too small values for this property may result in crashes or unspecified errors on exit in M2TW.
165    ///
166    /// Note that individual glyphs contain sufficient information to calculate a much more optimal bounding box than by simply using
167    /// multiples of the value corresponding to this index.
168    v_size: u16,
169}
170
171/// Represents a single glyph (character) in a font.
172///
173/// Contains the character's bitmap data and rendering metrics. Glyphs store both
174/// allocated dimensions (for layout) and actual bitmap dimensions (for rendering).
175///
176/// # Bitmap Data
177///
178/// The `data` field contains 8-bit grayscale pixel data in row-major order:
179/// - Each byte represents one pixel's intensity/alpha (0-255)
180/// - Total size is `width × height` bytes
181/// - Empty glyphs (spaces, etc.) may have zero-sized data
182///
183/// # Dimensions
184///
185/// Two sets of dimensions are stored:
186/// - **Allocated** (`alloc_width`, `alloc_height`): Space reserved for layout
187/// - **Actual** (`width`, `height`): Size of the bitmap data
188///
189/// Allocated height can be negative for characters with descenders (e.g., 'g', 'y').
190#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
191#[getset(get = "pub", get_mut = "pub", set = "pub")]
192pub struct Glyph {
193    /// Glyph code/index in the font.
194    ///
195    /// This is the internal glyph identifier used by the font file format.
196    code: u16,
197
198    /// Unicode character code this glyph represents.
199    ///
200    /// Maps to the Unicode character value (0-65535 range for BMP).
201    /// This is the character that will be displayed when this glyph is rendered.
202    character: u16,
203
204    /// Allocated height in the font layout (can be negative).
205    ///
206    /// This is the vertical space reserved for the glyph in text layout.
207    /// Negative values indicate descenders (parts of characters below the baseline).
208    /// For example, lowercase 'g' or 'y' typically have negative allocated heights.
209    alloc_height: i8,
210
211    /// Allocated width in the font layout.
212    ///
213    /// This is the horizontal advance width - how far to move the cursor after
214    /// rendering this glyph. May differ from the actual bitmap width.
215    alloc_width: u8,
216
217    /// Actual bitmap width in pixels.
218    ///
219    /// The width of the glyph's pixel data. The `data` field contains
220    /// `width × height` bytes of bitmap information.
221    width: u8,
222
223    /// Actual bitmap height in pixels.
224    ///
225    /// The height of the glyph's pixel data. The `data` field contains
226    /// `width × height` bytes of bitmap information.
227    height: u8,
228
229    /// Kerning adjustment value.
230    ///
231    /// Used for pair-wise spacing adjustments between specific character combinations.
232    /// The exact interpretation depends on the kerning data in the font.
233    kerning: u32,
234
235    /// 8-bit grayscale bitmap data.
236    ///
237    /// Contains the glyph's pixel data in row-major order:
238    /// - Size: `width × height` bytes
239    /// - Format: One byte per pixel (0 = transparent, 255 = opaque)
240    /// - Empty for characters with no visual representation (e.g., spaces)
241    ///
242    /// # Example Layout
243    ///
244    /// For a 3×2 glyph, data is stored as:
245    /// ```text
246    /// [row0_col0, row0_col1, row0_col2, row1_col0, row1_col1, row1_col2]
247    /// ```
248    data: Vec<u8>,
249}
250
251//---------------------------------------------------------------------------//
252//                          Implementation of Font
253//---------------------------------------------------------------------------//
254
255impl Decodeable for Font {
256
257    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
258        let signature_bytes = data.read_slice(4, false)?;
259        if signature_bytes.as_slice() != SIGNATURE {
260            return Err(RLibError::DecodingFontUnsupportedSignature(signature_bytes));
261        }
262
263        let mut font = Self::default();
264
265        // Get the properties of the font.
266        font.properties.first_prop = data.read_u16()?;
267        font.properties.second_prop = data.read_u16()?;
268        font.properties.line_height = data.read_u16()?;
269        font.properties.fourth_prop = data.read_u16()?;
270        font.properties.fifth_prop = data.read_u16()?;
271        font.properties.baseline = data.read_u16()?;
272        font.properties.layout_y_offset = data.read_u16()?;
273        font.properties.space_justify = data.read_u16()?;
274        font.properties.layout_x_offset = data.read_u16()?;
275        font.properties.h_size = data.read_u16()?;
276        font.properties.v_size = data.read_u16()?;
277
278        // These are used glyph count, and the size of the data section. Unused by the decoder.
279        let _glyph_count = data.read_u16()?;
280        let _glyph_data_size = data.read_u32()?;
281
282        // Get the glyphs/characters table. This is a list of u16 from 0 to u16 max value.
283        //
284        // 0xFFFF values are unused.
285        for index in 0..=u16::MAX {
286            let code = data.read_u16()?;
287            if code == 0xFFFF {
288                continue;
289            }
290
291            let mut glyph = Glyph::default();
292
293            glyph.code = code;
294            glyph.character = index;
295
296            font.glyphs.insert(index, glyph);
297        }
298
299        // Get the glyph dimensions data.
300        for index in 0..=u16::MAX {
301            if let Some(glyph) = font.glyphs_mut().get_mut(&index) {
302                glyph.alloc_height = data.read_i8()?;
303                glyph.alloc_width = data.read_u8()?;
304                glyph.width = data.read_u8()?;
305                glyph.height = data.read_u8()?;
306            }
307        }
308
309        // Get the glyph data offset for the next section. As they're consecutive and have a fixed size, we really don't use
310        // these offsets, but this code is left here for format documentation.
311        //
312        // This list only contains the glyphs that are used, because fuck consistency.
313        for _ in 0..font.glyphs().len() {
314            let _offset = data.read_u32()?;
315        }
316
317        for glyph in font.glyphs_mut().values_mut() {
318            let size = glyph.height as usize * glyph.width as usize;
319            if size != 0 {
320                glyph.data = data.read_slice(size, false)?;
321            }
322        }
323
324        // Get the glyph kerning info. This seems to be only from certain files onward, so a fail here has to be considered as
325        // not an error.
326        if let Ok(kerning_size) = data.read_u16() {
327            font.supports_kerning = true;
328
329            // Codes lower than the skip one do not have kerning data.
330            font.kerning_skip = data.read_u16()?;
331
332            for _ in 0..kerning_size {
333                let block = data.read_slice(kerning_size as usize, false)?;
334                font.kerning_blocks.push(block);
335            }
336        }
337
338        // If we are not in the last byte, it means we didn't parse the entire file, which means this file is corrupt.
339        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
340
341        Ok(font)
342    }
343}
344
345impl Encodeable for Font {
346
347    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
348        buffer.write_all(SIGNATURE)?;
349
350        buffer.write_u16(*self.properties().first_prop())?;
351        buffer.write_u16(*self.properties().second_prop())?;
352        buffer.write_u16(*self.properties().line_height())?;
353        buffer.write_u16(*self.properties().fourth_prop())?;
354        buffer.write_u16(*self.properties().fifth_prop())?;
355        buffer.write_u16(*self.properties().baseline())?;
356        buffer.write_u16(*self.properties().layout_y_offset())?;
357        buffer.write_u16(*self.properties().space_justify())?;
358        buffer.write_u16(*self.properties().layout_x_offset())?;
359        buffer.write_u16(*self.properties().h_size())?;
360        buffer.write_u16(*self.properties().v_size())?;
361
362        buffer.write_u16(self.glyphs().len() as u16)?;
363
364        let mut glyphs = vec![];
365        let mut dimensions = vec![];
366        let mut offsets = vec![];
367        let mut data = vec![];
368
369        for index in 0..=u16::MAX {
370            match self.glyphs().get(&index) {
371                Some(glyph) => {
372                    glyphs.write_u16(glyph.code)?;
373
374                    dimensions.write_i8(glyph.alloc_height)?;
375                    dimensions.write_u8(glyph.alloc_width)?;
376                    dimensions.write_u8(glyph.width)?;
377                    dimensions.write_u8(glyph.height)?;
378
379                    if glyph.data().is_empty() &&
380                        glyph.alloc_height == 0 &&
381                        glyph.alloc_width == 0 &&
382                        glyph.width == 0 &&
383                        glyph.height == 0 {
384                        offsets.write_u32(0)?;
385                    } else {
386                        offsets.write_u32(data.len() as u32)?;
387
388                        data.write_all(&glyph.data)?;
389                    }
390
391                },
392                None => {
393                    glyphs.write_u16(0xFFFF)?;
394                },
395            }
396        }
397        buffer.write_u32(data.len() as u32)?;
398
399        buffer.write_all(&glyphs)?;
400        buffer.write_all(&dimensions)?;
401        buffer.write_all(&offsets)?;
402        buffer.write_all(&data)?;
403
404        if self.supports_kerning {
405            buffer.write_u16(self.kerning_blocks.len() as u16)?;
406            buffer.write_u16(self.kerning_skip)?;
407
408            for block in self.kerning_blocks() {
409                buffer.write_all(block)?;
410            }
411        }
412
413        Ok(())
414    }
415}