1use pdfboss_core::{Dict, Name, Object};
6
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum Standard14 {
12 Helvetica,
14 HelveticaBold,
16 HelveticaOblique,
18 HelveticaBoldOblique,
20 TimesRoman,
22 TimesBold,
24 TimesItalic,
26 TimesBoldItalic,
28 Courier,
30 CourierBold,
32 CourierOblique,
34 CourierBoldOblique,
36 Symbol,
38 ZapfDingbats,
40}
41
42impl Standard14 {
43 pub const ALL: [Standard14; 14] = [
45 Standard14::Helvetica,
46 Standard14::HelveticaBold,
47 Standard14::HelveticaOblique,
48 Standard14::HelveticaBoldOblique,
49 Standard14::TimesRoman,
50 Standard14::TimesBold,
51 Standard14::TimesItalic,
52 Standard14::TimesBoldItalic,
53 Standard14::Courier,
54 Standard14::CourierBold,
55 Standard14::CourierOblique,
56 Standard14::CourierBoldOblique,
57 Standard14::Symbol,
58 Standard14::ZapfDingbats,
59 ];
60
61 pub fn base_font(self) -> &'static str {
63 match self {
64 Standard14::Helvetica => "Helvetica",
65 Standard14::HelveticaBold => "Helvetica-Bold",
66 Standard14::HelveticaOblique => "Helvetica-Oblique",
67 Standard14::HelveticaBoldOblique => "Helvetica-BoldOblique",
68 Standard14::TimesRoman => "Times-Roman",
69 Standard14::TimesBold => "Times-Bold",
70 Standard14::TimesItalic => "Times-Italic",
71 Standard14::TimesBoldItalic => "Times-BoldItalic",
72 Standard14::Courier => "Courier",
73 Standard14::CourierBold => "Courier-Bold",
74 Standard14::CourierOblique => "Courier-Oblique",
75 Standard14::CourierBoldOblique => "Courier-BoldOblique",
76 Standard14::Symbol => "Symbol",
77 Standard14::ZapfDingbats => "ZapfDingbats",
78 }
79 }
80
81 pub fn from_base_font(name: &str) -> Option<Standard14> {
85 Standard14::ALL
86 .into_iter()
87 .find(|font| font.base_font() == name)
88 }
89
90 fn is_win_ansi(self) -> bool {
93 !matches!(self, Standard14::Symbol | Standard14::ZapfDingbats)
94 }
95
96 fn encode_char(self, ch: char) -> Result<u8> {
101 if !self.is_win_ansi() {
102 return Err(Error::Unencodable {
103 ch,
104 font: self.base_font(),
105 });
106 }
107 (0u8..=255)
108 .find(|&code| pdfboss_encoding::win_ansi(code) == Some(ch))
109 .ok_or(Error::Unencodable {
110 ch,
111 font: self.base_font(),
112 })
113 }
114
115 pub fn encode(self, text: &str) -> Result<Vec<u8>> {
122 text.chars().map(|ch| self.encode_char(ch)).collect()
123 }
124
125 pub fn width(self, ch: char) -> Option<f32> {
128 let code = self.encode_char(ch).ok()?;
129 let glyph = pdfboss_encoding::win_ansi_glyph_name(code)?;
130 pdfboss_encoding::standard_14_width(self.base_font(), glyph)
131 }
132
133 pub fn text_width(self, text: &str, size: f32) -> Result<f32> {
139 let mut sum = 0.0f32;
140 for ch in text.chars() {
141 let code = self.encode_char(ch)?;
142 let glyph = pdfboss_encoding::win_ansi_glyph_name(code).ok_or_else(|| {
143 Error::Other(format!("code {code:#04x} has no WinAnsi glyph name"))
144 })?;
145 let width =
146 pdfboss_encoding::standard_14_width(self.base_font(), glyph).ok_or_else(|| {
147 Error::Other(format!(
148 "{} has no metric for glyph {glyph:?}",
149 self.base_font()
150 ))
151 })?;
152 sum += width;
153 }
154 Ok(sum * size / 1000.0)
155 }
156
157 pub(crate) fn font_dict(self) -> Dict {
161 let mut dict = Dict::new();
162 dict.insert(Name("Type".into()), Object::Name(Name("Font".into())));
163 dict.insert(Name("Subtype".into()), Object::Name(Name("Type1".into())));
164 dict.insert(
165 Name("BaseFont".into()),
166 Object::Name(Name(self.base_font().into())),
167 );
168 if self.is_win_ansi() {
169 dict.insert(
170 Name("Encoding".into()),
171 Object::Name(Name("WinAnsiEncoding".into())),
172 );
173 }
174 dict
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use pdfboss_encoding::standard_14_width;
181
182 use super::*;
183 use crate::error::Error;
184
185 #[test]
186 fn base_font_round_trips_all_fourteen() {
187 for font in Standard14::ALL {
188 assert_eq!(Standard14::from_base_font(font.base_font()), Some(font));
189 }
190 assert_eq!(Standard14::Helvetica.base_font(), "Helvetica");
191 assert_eq!(
192 Standard14::HelveticaBoldOblique.base_font(),
193 "Helvetica-BoldOblique"
194 );
195 assert_eq!(Standard14::TimesRoman.base_font(), "Times-Roman");
196 assert_eq!(Standard14::TimesBoldItalic.base_font(), "Times-BoldItalic");
197 assert_eq!(Standard14::CourierOblique.base_font(), "Courier-Oblique");
198 assert_eq!(Standard14::Symbol.base_font(), "Symbol");
199 assert_eq!(Standard14::ZapfDingbats.base_font(), "ZapfDingbats");
200 assert_eq!(Standard14::from_base_font("helvetica"), None);
201 assert_eq!(Standard14::from_base_font("Arial"), None);
202 assert_eq!(Standard14::from_base_font(""), None);
203 }
204
205 #[test]
206 fn encode_ascii_and_win_ansi_specials() {
207 assert_eq!(Standard14::Helvetica.encode("Hi").unwrap(), b"Hi");
208 assert_eq!(Standard14::TimesRoman.encode("\u{E9}").unwrap(), [0xE9]);
209 assert_eq!(Standard14::Courier.encode("\u{20AC}").unwrap(), [0x80]);
210 assert_eq!(Standard14::Helvetica.encode("\u{201C}").unwrap(), [0x93]);
211 }
212
213 #[test]
214 fn encode_rejects_unencodable_chars() {
215 match Standard14::HelveticaBold.encode("\u{2318}").unwrap_err() {
216 Error::Unencodable { ch, font } => {
217 assert_eq!(ch, '\u{2318}');
218 assert_eq!(font, "Helvetica-Bold");
219 }
220 other => panic!("expected Unencodable, got {other:?}"),
221 }
222 }
223
224 #[test]
225 fn symbol_faces_reject_every_char_for_now() {
226 for font in [Standard14::Symbol, Standard14::ZapfDingbats] {
227 assert!(matches!(
228 font.encode("a"),
229 Err(Error::Unencodable { ch: 'a', .. })
230 ));
231 assert_eq!(font.width('a'), None);
232 }
233 assert_eq!(Standard14::Symbol.encode("").unwrap(), Vec::<u8>::new());
234 }
235
236 #[test]
237 fn width_matches_direct_afm_lookups() {
238 let font = Standard14::Helvetica;
239 for (ch, glyph) in [('H', "H"), ('e', "e"), ('l', "l"), ('o', "o")] {
240 assert_eq!(font.width(ch), standard_14_width("Helvetica", glyph));
241 assert!(font.width(ch).is_some());
242 }
243 assert_eq!(font.width('\u{2318}'), None); assert_eq!(font.width('\u{20AC}'), None); }
246
247 #[test]
248 fn text_width_scales_by_size() {
249 let font = Standard14::TimesBold;
250 let sum: f32 = "Hello".chars().map(|ch| font.width(ch).unwrap()).sum();
251 assert_eq!(font.text_width("Hello", 12.0).unwrap(), sum * 12.0 / 1000.0);
252 assert_eq!(
253 font.text_width("Hello", 1000.0).unwrap(),
254 font.text_width("Hello", 500.0).unwrap() * 2.0
255 );
256 assert_eq!(font.text_width("", 12.0).unwrap(), 0.0);
257 assert!(matches!(
258 font.text_width("a\u{2318}", 12.0),
259 Err(Error::Unencodable { ch: '\u{2318}', .. })
260 ));
261 assert!(matches!(
262 font.text_width("\u{20AC}", 12.0),
263 Err(Error::Other(msg)) if msg.contains("Euro")
264 ));
265 }
266
267 #[test]
268 fn font_dict_win_ansi_and_symbol() {
269 let dict = Standard14::HelveticaOblique.font_dict();
270 assert_eq!(dict.len(), 4);
271 assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Font"));
272 assert_eq!(
273 dict.get_name("Subtype").map(|n| n.0.as_str()),
274 Some("Type1")
275 );
276 assert_eq!(
277 dict.get_name("BaseFont").map(|n| n.0.as_str()),
278 Some("Helvetica-Oblique")
279 );
280 assert_eq!(
281 dict.get_name("Encoding").map(|n| n.0.as_str()),
282 Some("WinAnsiEncoding")
283 );
284
285 let dict = Standard14::Symbol.font_dict();
286 assert_eq!(dict.len(), 3);
287 assert_eq!(
288 dict.get_name("BaseFont").map(|n| n.0.as_str()),
289 Some("Symbol")
290 );
291 assert!(dict.get("Encoding").is_none());
292 }
293}