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}