1use crate::{Base14Font, Font, extended_glyph_name, normalize::nfc_text, shape, winansi_byte};
2
3#[must_use]
33pub fn text_width(font: Font, size: f32, text: &str) -> f32 {
34 let text = nfc_text(text);
35 let text = text.as_ref();
36 match font {
37 Font::Base14(f) => {
38 let mut units: f32 = 0.0;
39 for ch in text.chars() {
40 units += base14_glyph_units(f, ch);
41 }
42 units * size / 1000.0
43 }
44 Font::Embedded(id) => {
45 let ef = id.data();
46 let glyphs = shape(ef, text);
47 let upem = f32::from(ef.units_per_em);
48 glyphs
49 .iter()
50 .map(|g| advance_units_to_pt(g.advance_units, size, upem))
51 .sum()
52 }
53 }
54}
55
56#[must_use]
71pub fn advance_units_to_pt(advance_units: i32, size_pt: f32, upem: f32) -> f32 {
72 let magnitude = u16::try_from(advance_units.unsigned_abs()).unwrap_or(u16::MAX);
73 let advance = f32::from(magnitude);
74 if advance_units.is_negative() {
75 -advance * size_pt / upem
76 } else {
77 advance * size_pt / upem
78 }
79}
80
81#[must_use]
93pub fn glyph_width(font: Font, size: f32, ch: char) -> f32 {
94 let mut buf = [0u8; 4];
95 let s = ch.encode_utf8(&mut buf);
96 text_width(font, size, s)
97}
98
99#[must_use]
109pub fn ascent(font: Font, size: f32) -> f32 {
110 match font {
111 Font::Base14(f) => f.metrics().ascender * size / 1000.0,
112 Font::Embedded(id) => {
113 let ef = id.data();
114 f32::from(ef.ascender) * size / f32::from(ef.units_per_em)
115 }
116 }
117}
118
119#[must_use]
131pub fn descent(font: Font, size: f32) -> f32 {
132 match font {
133 Font::Base14(f) => -f.metrics().descender * size / 1000.0,
134 Font::Embedded(id) => {
135 let ef = id.data();
136 -f32::from(ef.descender) * size / f32::from(ef.units_per_em)
137 }
138 }
139}
140
141fn base14_glyph_units(face: Base14Font, ch: char) -> f32 {
150 if matches!(face, Base14Font::Symbol | Base14Font::ZapfDingbats) {
151 return 0.0;
155 }
156 if let Some(byte) = winansi_byte(ch) {
157 return face.winansi_width(byte).unwrap_or(0.0);
158 }
159 if let Some(name) = extended_glyph_name(ch)
160 && let Some(w) = face.glyph_width_by_name(name)
161 {
162 return w;
163 }
164 face.winansi_width(b'?').unwrap_or(0.0)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::EmbeddedFontId;
173
174 const HELV: Font = Font::Base14(Base14Font::Helvetica);
175 const HELV_BOLD: Font = Font::Base14(Base14Font::HelveticaBold);
176 const HELV_OBLIQUE: Font = Font::Base14(Base14Font::HelveticaOblique);
177 const COURIER: Font = Font::Base14(Base14Font::Courier);
178
179 #[test]
180 fn helvetica_space_width_is_278_thou_em() {
181 let w = text_width(HELV, 1000.0, " ");
182 assert!((w - 278.0).abs() < 1e-6);
183 }
184
185 #[test]
186 fn helvetica_apostrophe_matches_afm() {
187 let w = text_width(HELV, 1000.0, "'");
188 assert!((w - 191.0).abs() < 1e-6, "got {w}");
189 }
190
191 #[test]
192 fn courier_is_monospace() {
193 let a = text_width(COURIER, 12.0, "a");
194 let m = text_width(COURIER, 12.0, "M");
195 assert!((a - m).abs() < f32::EPSILON);
196 }
197
198 #[test]
199 fn bold_is_wider_than_regular_for_caps() {
200 let r = text_width(HELV, 100.0, "B");
201 let b = text_width(HELV_BOLD, 100.0, "B");
202 assert!(b > r);
203 }
204
205 #[test]
206 fn helvetica_capital_a_matches_adobe_core14_afm() {
207 let w = text_width(HELV, 1000.0, "A");
208 assert!((w - 667.0).abs() < 1e-3, "got {w}");
209 let wo = text_width(HELV_OBLIQUE, 1000.0, "A");
210 assert!((wo - 667.0).abs() < 1e-3, "got {wo}");
211 let wb = text_width(HELV_BOLD, 1000.0, "A");
212 assert!((wb - 722.0).abs() < 1e-3, "got {wb}");
213 }
214
215 #[test]
216 fn helvetica_eacute_matches_adobe_core14_afm() {
217 let lower = text_width(HELV, 1000.0, "é");
218 assert!((lower - 556.0).abs() < 1e-3, "got {lower}");
219 let upper = text_width(HELV, 1000.0, "É");
220 assert!((upper - 667.0).abs() < 1e-3, "got {upper}");
221 }
222
223 #[test]
224 fn base14_non_winansi_falls_back_to_question_mark_silently() {
225 let q = text_width(HELV, 1000.0, "?");
230 let cyrillic = text_width(HELV, 1000.0, "П");
231 assert!((q - cyrillic).abs() < 1e-3, "q={q} cyr={cyrillic}");
232 }
233
234 #[test]
235 fn helvetica_lslash_resolves_through_extended_glyph_name_lookup() {
236 let w = text_width(HELV, 1000.0, "ł");
237 assert!((w - 222.0).abs() < 1e-3, "got {w}");
238 let lodz = text_width(HELV, 1000.0, "Łódź");
239 assert!(
240 (lodz - (556.0 + 556.0 + 556.0 + 500.0)).abs() < 1e-3,
241 "got {lodz}"
242 );
243 }
244
245 #[test]
246 fn embedded_text_width_is_nonzero_for_cyrillic() {
247 let font = Font::Embedded(EmbeddedFontId::Regular);
250 let w = text_width(font, 12.0, "Привет");
251 assert!(w > 0.0);
252 }
253
254 #[test]
255 fn embedded_text_width_normalizes_decomposed_romanian() {
256 let font = Font::Embedded(EmbeddedFontId::Regular);
257 let decomposed = text_width(font, 12.0, "S\u{0326}");
258 let precomposed = text_width(font, 12.0, "\u{0218}");
259
260 assert!((decomposed - precomposed).abs() < f32::EPSILON);
261 }
262
263 #[test]
264 fn advance_units_to_pt_preserves_negative_sign() {
265 let actual = advance_units_to_pt(-1000, 12.0, 1000.0);
266 assert!((actual + 12.0).abs() < f32::EPSILON, "got {actual}");
267 }
268}