tfm/pl/
mod.rs

1/*!
2The property list (.pl) file format.
3
4|                                     | from .pl source code              | to .pl source code    | from lower level         | to lower level
5|-------------------------------------|-----------------------------------|-----------------------|--------------------------|----
6| fully parsed .pl file ([`File`])    | [`File::from_pl_source_code`]     | [`File::display`]     | [`File::from_ast`]       | [`File::lower`]
7| abstract syntax tree ([`ast::Ast`]) | [`ast::Ast::from_pl_source_code`] | `ast::Ast::display` (TODO) | [`ast::Ast::from_cst`]   | [`ast::Ast::lower`]
8| concrete syntax tree ([`cst::Cst`]) | [`cst::Cst::from_pl_source_code`] | [`cst::Cst::display`] | N/A | N/A
9
10*/
11
12use std::collections::{BTreeMap, HashMap};
13
14use crate::{
15    ligkern,
16    pl::ast::{DesignSize, ParameterNumber},
17    Char, ExtensibleRecipe, FixWord, Header, NamedParameter, NextLargerProgram,
18    NextLargerProgramWarning,
19};
20
21pub mod ast;
22pub mod cst;
23mod error;
24pub use error::*;
25
26/// Maximum number of lig/kern instructions in a property list file.
27///
28/// This limit is defined without explanation in PLtoTF.2014.3.
29/// Here's an explanation for the specific value.
30///
31/// First, in a TFM file the sub-file sizes, including the number of lig/kern instructions `nl`,
32///     are 8-bit integers in the range `[0, i16::MAX]`.
33/// (I don't know why the range is restricted like this, maybe for portability.)
34/// Thus the maximum number of lig/kern instructions is less than or equal to `i16::MAX`.
35///
36/// Second, after a PL file is read some additional instructions may need to be prepended to support
37///     LABEL entries with an index larger than u8::MAX.
38/// This is the "embarrassing problem" described in PLtoTF.2014.138.
39/// In TFM files the index for the starting lig/kern instruction for a character is a u8.
40/// To support higher indices, a special lig/kern instruction is prepended to the list of instructions.
41/// This instruction specifies where to actually start.
42/// The payload for this instruction supports 16-bit integers.
43///
44/// There are 257 possible characters (the usual 256 plus the boundary character),
45///     and thus we may need to insert up to 257 additional instructions.
46/// After this we still need to be under the `i16::MAX limit`.
47/// So the limit is `i16::MAX - 257`.
48pub const MAX_LIG_KERN_INSTRUCTIONS: u16 = (i16::MAX as u16) - 257;
49
50/// Data about one character in a .pl file.
51#[derive(Clone, Default, PartialEq, Eq, Debug)]
52pub struct CharDimensions {
53    pub width: Option<FixWord>,
54    pub height: Option<FixWord>,
55    pub depth: Option<FixWord>,
56    pub italic_correction: Option<FixWord>,
57}
58
59/// Tag of a character in a .pl file.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum CharTag {
62    Ligature(u16),
63    List(Char),
64    Extension(ExtensibleRecipe),
65}
66
67impl CharTag {
68    pub fn ligature(&self) -> Option<u16> {
69        match self {
70            CharTag::Ligature(l) => Some(*l),
71            _ => None,
72        }
73    }
74    pub fn list(&self) -> Option<Char> {
75        match self {
76            CharTag::List(c) => Some(*c),
77            _ => None,
78        }
79    }
80    pub fn extension(&self) -> Option<ExtensibleRecipe> {
81        match self {
82            CharTag::Extension(u) => Some(u.clone()),
83            CharTag::Ligature(_) | CharTag::List(_) => None,
84        }
85    }
86}
87
88/// Complete contents of a property list (.pl) file.
89#[derive(PartialEq, Eq, Debug)]
90pub struct File {
91    pub header: Header,
92    pub design_units: FixWord,
93    pub char_dimens: BTreeMap<Char, CharDimensions>,
94    pub char_tags: BTreeMap<Char, CharTag>,
95    /// Tags that have been unset, but whose discriminant is still written to a .tfm file by PLtoTF.
96    pub unset_char_tags: BTreeMap<Char, u8>,
97    pub lig_kern_program: ligkern::lang::Program,
98    pub params: Vec<FixWord>,
99
100    /// Additional widths that appear in the plst file but not appear in the fully parsed file.
101    ///
102    /// This can happen due to the following plst listing:
103    /// ```txt
104    /// (CHARACTER C X (CHARWD D 8.0))
105    /// (CHARACTER C X (CHARWD D 9.0))
106    /// ```
107    /// In this case the width `8.0` is not in the fully parsed file because it is overwritten
108    ///     by `9.0`.
109    /// However pltotf still writes the `8.0` width to the .tfm file.
110    pub additional_widths: Vec<FixWord>,
111    /// Additional heights; similar to additional widths.
112    pub additional_heights: Vec<FixWord>,
113    /// Additional depths; similar to additional widths.
114    pub additional_depths: Vec<FixWord>,
115    /// Additional italic corrections; similar to additional widths.
116    pub additional_italics: Vec<FixWord>,
117}
118
119impl Default for File {
120    fn default() -> Self {
121        Self {
122            header: Header::pl_default(),
123            design_units: FixWord::ONE,
124            char_dimens: Default::default(),
125            char_tags: Default::default(),
126            unset_char_tags: Default::default(),
127            lig_kern_program: Default::default(),
128            params: Default::default(),
129            additional_widths: vec![],
130            additional_heights: vec![],
131            additional_depths: vec![],
132            additional_italics: vec![],
133        }
134    }
135}
136
137impl File {
138    /// Build a File from PL source code.
139    pub fn from_pl_source_code(source: &str) -> (File, Vec<ParseWarning>) {
140        let (ast, mut errors) = ast::Ast::from_pl_source_code(source);
141        let file = File::from_ast(ast, &mut errors);
142        (file, errors)
143    }
144
145    /// Return a map from characters to the lig/kern entrypoint for that character.
146    pub fn lig_kern_entrypoints(&self, include_orphans: bool) -> HashMap<Char, u16> {
147        self.char_tags
148            .iter()
149            .filter(|(c, _)| self.char_dimens.contains_key(c) || include_orphans)
150            .filter_map(|d| match d.1 {
151                CharTag::Ligature(l) => Some((*d.0, *l)),
152                _ => None,
153            })
154            .collect()
155    }
156
157    /// Clear all lig/kern data from the file.
158    pub fn clear_lig_kern_data(&mut self) {
159        // PLtoTF.2014.125
160        self.char_tags = self
161            .char_tags
162            .iter()
163            .filter_map(|(c, tag)| match tag {
164                CharTag::Ligature(_) => None,
165                _ => Some((*c, tag.clone())),
166            })
167            .collect();
168        self.lig_kern_program = Default::default();
169    }
170
171    /// Build a File from an AST.
172    pub fn from_ast(ast: ast::Ast, errors: &mut Vec<error::ParseWarning>) -> File {
173        let mut file: File = Default::default();
174        let mut lig_kern_precedes = false;
175
176        let mut next_larger_span = HashMap::<Char, std::ops::Range<usize>>::new();
177
178        for node in ast.0 {
179            match node {
180                ast::Root::Checksum(v) => {
181                    file.header.checksum = Some(v.data);
182                }
183                ast::Root::DesignSize(v) => {
184                    if let DesignSize::Valid(design_size) = v.data {
185                        file.header.design_size = design_size;
186                    }
187                }
188                ast::Root::DesignUnits(v) => {
189                    file.design_units = v.data;
190                }
191                ast::Root::CodingScheme(v) => {
192                    file.header.character_coding_scheme = Some(v.data);
193                }
194                ast::Root::Family(v) => {
195                    file.header.font_family = Some(v.data);
196                }
197                ast::Root::Face(v) => {
198                    file.header.face = Some(v.data);
199                }
200                ast::Root::SevenBitSafeFlag(v) => {
201                    file.header.seven_bit_safe = Some(v.data);
202                }
203                ast::Root::Header(v) => match v.left.0.checked_sub(18) {
204                    None => errors.push(ParseWarning {
205                        span: v.left_span.clone(),
206                        knuth_pltotf_offset: Some(v.left_span.end),
207                        kind: ParseWarningKind::HeaderIndexIsTooSmall,
208                    }),
209                    Some(i) => {
210                        let i = i as usize;
211                        if file.header.additional_data.len() <= i {
212                            file.header.additional_data.resize(i + 1, 0);
213                        }
214                        file.header.additional_data[i] = v.right;
215                    }
216                },
217                ast::Root::FontDimension(b) => {
218                    for node in b.children {
219                        let (number, value) = match node {
220                            ast::FontDimension::NamedParam(named_param, v) => {
221                                (named_param.number() as usize, v.data)
222                            }
223                            ast::FontDimension::IndexedParam(v) => (v.left.0 as usize, v.right),
224                            ast::FontDimension::Comment(_) => continue,
225                        };
226                        let index = number - 1;
227                        if file.params.len() < number {
228                            file.params.resize(number, Default::default());
229                        }
230                        file.params[index] = value;
231                    }
232                }
233                ast::Root::LigTable(b) => {
234                    for node in b.children {
235                        let mut insert_lig_kern_instruction = |instruction, span| {
236                            if file.lig_kern_program.instructions.len()
237                                < MAX_LIG_KERN_INSTRUCTIONS as usize
238                            {
239                                file.lig_kern_program.instructions.push(instruction);
240                            } else {
241                                // TODO: add a test for this case
242                                errors.push(error::ParseWarning {
243                                    span,
244                                    knuth_pltotf_offset: None,
245                                    kind: ParseWarningKind::LigTableIsTooBig,
246                                });
247                            }
248                        };
249                        match node {
250                            ast::LigTable::Label(v) => {
251                                let u: u16 = file.lig_kern_program.instructions.len().try_into().expect("lig_kern_instructions.len()<= MAX_LIG_KERN_INSTRUCTIONS which is a u16");
252                                match v.data {
253                                    ast::LigTableLabel::Char(c) => {
254                                        // TODO: error if the tag is already set
255                                        file.char_tags.insert(c, CharTag::Ligature(u));
256                                    }
257                                    ast::LigTableLabel::BoundaryChar => {
258                                        file.lig_kern_program.left_boundary_char_entrypoint =
259                                            Some(u);
260                                    }
261                                }
262                                lig_kern_precedes = false;
263                            }
264                            ast::LigTable::Lig(post_lig_operation, v) => {
265                                insert_lig_kern_instruction(
266                                    ligkern::lang::Instruction {
267                                        next_instruction: Some(0),
268                                        right_char: v.left,
269                                        operation: ligkern::lang::Operation::Ligature {
270                                            char_to_insert: v.right,
271                                            post_lig_operation,
272                                            post_lig_tag_invalid: false,
273                                        },
274                                        // TODO: should the span of the entire LIG node not just some of the data
275                                    },
276                                    v.left_span,
277                                );
278                                lig_kern_precedes = true;
279                            }
280                            ast::LigTable::Kern(v) => {
281                                insert_lig_kern_instruction(
282                                    ligkern::lang::Instruction {
283                                        next_instruction: Some(0),
284                                        right_char: v.left,
285                                        operation: ligkern::lang::Operation::Kern(v.right),
286                                        // TODO: should the span of the entire KRN node
287                                    },
288                                    v.left_span,
289                                );
290                                lig_kern_precedes = true;
291                            }
292                            ast::LigTable::Stop(_) => {
293                                if lig_kern_precedes {
294                                    file.lig_kern_program
295                                        .instructions
296                                        .last_mut()
297                                        .unwrap()
298                                        .next_instruction = None;
299                                } else {
300                                    // TODO: error
301                                }
302                                lig_kern_precedes = false;
303                            }
304                            ast::LigTable::Skip(v) => {
305                                if lig_kern_precedes {
306                                    file.lig_kern_program
307                                        .instructions
308                                        .last_mut()
309                                        .unwrap()
310                                        .next_instruction = Some(v.data.0);
311                                } else {
312                                    // TODO: error
313                                }
314                                lig_kern_precedes = false;
315                            }
316                            ast::LigTable::Comment(_) => {}
317                        }
318                    }
319                }
320                ast::Root::BoundaryChar(v) => {
321                    file.lig_kern_program.right_boundary_char = Some(v.data);
322                }
323                ast::Root::Character(b) => {
324                    let char_dimens = file.char_dimens.entry(b.data).or_default();
325                    for node in b.children {
326                        match node {
327                            ast::Character::Width(v) => {
328                                if let Some(additional_width) =
329                                    char_dimens.width.replace(v.data.unwrap_or(FixWord::ZERO))
330                                {
331                                    file.additional_widths.push(additional_width);
332                                }
333                            }
334                            ast::Character::Height(v) => {
335                                if let Some(additional_height) = char_dimens.height.replace(v.data)
336                                {
337                                    file.additional_heights.push(additional_height);
338                                }
339                            }
340                            ast::Character::Depth(v) => {
341                                if let Some(additional_depth) = char_dimens.depth.replace(v.data) {
342                                    file.additional_depths.push(additional_depth);
343                                }
344                            }
345                            ast::Character::ItalicCorrection(v) => {
346                                if let Some(additional_italic) =
347                                    char_dimens.italic_correction.replace(v.data)
348                                {
349                                    file.additional_italics.push(additional_italic);
350                                }
351                            }
352                            ast::Character::NextLarger(c) => {
353                                // TODO: warning if tag != CharTag::None
354                                file.char_tags.insert(b.data, CharTag::List(c.data));
355                                next_larger_span.insert(b.data, c.data_span);
356                            }
357                            ast::Character::ExtensibleCharacter(e) => {
358                                let mut recipe: ExtensibleRecipe = Default::default();
359                                for node in e.children {
360                                    match node {
361                                        ast::ExtensibleCharacter::Top(v) => {
362                                            recipe.top = Some(v.data)
363                                        }
364                                        ast::ExtensibleCharacter::Middle(v) => {
365                                            recipe.middle = Some(v.data)
366                                        }
367                                        ast::ExtensibleCharacter::Bottom(v) => {
368                                            recipe.bottom = Some(v.data)
369                                        }
370                                        ast::ExtensibleCharacter::Replicated(v) => {
371                                            recipe.rep = v.data
372                                        }
373                                        ast::ExtensibleCharacter::Comment(_) => {}
374                                    }
375                                }
376                                // TODO: warning if tag != CharTag::None
377                                file.char_tags.insert(b.data, CharTag::Extension(recipe));
378                            }
379                            ast::Character::Comment(_) => {}
380                        }
381                    }
382                }
383                ast::Root::Comment(_) => {}
384            }
385        }
386
387        // In pltotf the file is parsed in a single pass, whereas here we parse it in at least 2
388        // passes (CST, then AST). As a result some of the warnings will be out of order versus
389        // pltotf - e.g., all CST level warnings will come first, whereas in pltotf the warnings are
390        // interleaved depending on where they appear in the file. This is easy to fix.
391        errors.sort_by_key(|w| {
392            w.knuth_pltotf_offset
393                .expect("all warnings generated so far have an offset populated")
394        });
395
396        // PLtoTF.2014.116
397        if let Some(final_instruction) = file.lig_kern_program.instructions.last_mut() {
398            if final_instruction.next_instruction == Some(0) {
399                final_instruction.next_instruction = None;
400            }
401        }
402
403        // Validate and fix the lig kern program.
404        let lig_kern_seven_bit_safe = {
405            for err in crate::ligkern::CompiledProgram::compile(
406                &file.lig_kern_program,
407                file.header.design_size,
408                &[],
409                file.lig_kern_entrypoints(true), // todo include orphans?
410            )
411            .1
412            {
413                errors.push(ParseWarning {
414                    span: 0..0, // todo
415                    knuth_pltotf_offset: None,
416                    kind: ParseWarningKind::CycleInLigKernProgram(err),
417                });
418                file.clear_lig_kern_data();
419            }
420            file.lig_kern_program
421                .is_seven_bit_safe(file.lig_kern_entrypoints(false))
422        };
423
424        // Validate and fix next larger tags
425        let next_larger_seven_bit_safe = {
426            let (program, next_larger_warnings) = NextLargerProgram::new(
427                file.char_tags
428                    .iter()
429                    .filter_map(|(c, t)| t.list().map(|next_larger| (*c, next_larger))),
430                |c| file.char_dimens.contains_key(&c),
431                false,
432            );
433            for warning in next_larger_warnings {
434                match &warning {
435                    NextLargerProgramWarning::NonExistentCharacter {
436                        original: _,
437                        next_larger,
438                    } => {
439                        file.char_dimens.insert(
440                            *next_larger,
441                            CharDimensions {
442                                width: Some(FixWord::ZERO),
443                                ..Default::default()
444                            },
445                        );
446                    }
447                    NextLargerProgramWarning::InfiniteLoop { original, .. } => {
448                        let discriminant = file
449                            .char_tags
450                            .remove(original)
451                            .and_then(|t| t.list())
452                            .expect("this char has a NEXTLARGER SPEC");
453                        file.unset_char_tags.insert(*original, discriminant.0);
454                    }
455                }
456                let span = next_larger_span
457                    .get(&warning.bad_char())
458                    .cloned()
459                    .expect("every char with a next larger tag had a NEXTLARGER AST node");
460                errors.push(ParseWarning {
461                    span,
462                    knuth_pltotf_offset: None,
463                    kind: ParseWarningKind::CycleInNextLargerProgram(warning),
464                });
465            }
466            program.is_seven_bit_safe()
467        };
468
469        // PLtoTF.2014.112
470        let mut extensible_seven_bit_safe = true;
471        file.char_tags
472            .iter()
473            .filter_map(|(c, t)| t.extension().map(|t| (c, t)))
474            .for_each(|(c, e)| {
475                if c.is_seven_bit() && !e.is_seven_bit() {
476                    extensible_seven_bit_safe = false;
477                }
478                for c in e.chars() {
479                    if let std::collections::btree_map::Entry::Vacant(v) = file.char_dimens.entry(c)
480                    {
481                        v.insert(Default::default());
482                        // todo warning
483                    };
484                }
485            });
486
487        let seven_bit_safe =
488            lig_kern_seven_bit_safe && next_larger_seven_bit_safe && extensible_seven_bit_safe;
489        if file.header.seven_bit_safe == Some(true) && !seven_bit_safe {
490            errors.push(ParseWarning {
491                span: 0..0, //todo,
492                knuth_pltotf_offset: None,
493                kind: ParseWarningKind::NotReallySevenBitSafe,
494            });
495        }
496        file.header.seven_bit_safe = Some(seven_bit_safe);
497
498        file
499    }
500}
501
502impl From<crate::File> for File {
503    fn from(tfm_file: crate::File) -> Self {
504        let char_dimens: BTreeMap<Char, CharDimensions> = tfm_file
505            .char_dimens
506            .iter()
507            .map(|(c, info)| {
508                (
509                    *c,
510                    CharDimensions {
511                        width: match info.width_index {
512                            super::WidthIndex::Invalid => None,
513                            super::WidthIndex::Valid(n) => {
514                                tfm_file.widths.get(n.get() as usize).copied()
515                            }
516                        },
517                        height: if info.height_index == 0 {
518                            None
519                        } else {
520                            tfm_file.heights.get(info.height_index as usize).copied()
521                        },
522                        depth: if info.depth_index == 0 {
523                            None
524                        } else {
525                            tfm_file.depths.get(info.depth_index as usize).copied()
526                        },
527                        italic_correction: if info.italic_index == 0 {
528                            None
529                        } else {
530                            tfm_file
531                                .italic_corrections
532                                .get(info.italic_index as usize)
533                                .copied()
534                        },
535                    },
536                )
537            })
538            .collect();
539
540        let mut lig_kern_program = tfm_file.lig_kern_program;
541        lig_kern_program.pack_kerns(&tfm_file.kerns);
542        let lig_kern_entrypoints: HashMap<Char, u16> = tfm_file
543            .char_tags
544            .iter()
545            .filter_map(|(c, t)| {
546                t.ligature()
547                    .and_then(|l| lig_kern_program.unpack_entrypoint(l).ok())
548                    .map(|e| (*c, e))
549            })
550            .collect();
551
552        let char_tags = tfm_file
553            .char_tags
554            .into_iter()
555            .filter_map(|(c, tag)| match tag {
556                super::CharTag::Ligature(_) => lig_kern_entrypoints
557                    .get(&c)
558                    .map(|e| (c, CharTag::Ligature(*e))),
559                super::CharTag::List(l) => Some((c, CharTag::List(l))),
560                // If the extension index is invalid we drop the tag.
561                super::CharTag::Extension(i) => tfm_file
562                    .extensible_chars
563                    .get(i as usize)
564                    .cloned()
565                    .map(|t| (c, CharTag::Extension(t))),
566            })
567            .collect();
568
569        File {
570            header: tfm_file.header,
571            design_units: FixWord::ONE,
572            char_dimens,
573            char_tags,
574            unset_char_tags: Default::default(),
575            lig_kern_program,
576            params: tfm_file.params,
577            additional_widths: vec![],
578            additional_heights: vec![],
579            additional_depths: vec![],
580            additional_italics: vec![],
581        }
582    }
583}
584
585impl File {
586    /// Lower a File to an AST.
587    pub fn lower(&self, char_display_format: CharDisplayFormat) -> ast::Ast {
588        let mut roots = vec![];
589
590        // First output the header. This is TFtoPL.2014.48-57.
591        if let Some(font_family) = &self.header.font_family {
592            roots.push(ast::Root::Family(font_family.clone().into()))
593        }
594        if let Some(face) = self.header.face {
595            roots.push(ast::Root::Face(face.into()))
596        }
597        for (i, &u) in self.header.additional_data.iter().enumerate() {
598            let i: u8 = i.try_into().unwrap();
599            let i = i.checked_add(18).unwrap(); // TODO: gotta be a warning here
600            roots.push(ast::Root::Header((ast::DecimalU8(i), u).into()))
601        }
602        #[derive(Clone, Copy)]
603        enum FontType {
604            Vanilla,
605            TexMathSy,
606            TexMathEx,
607        }
608        let font_type = {
609            let scheme = match &self.header.character_coding_scheme {
610                None => String::new(),
611                Some(scheme) => scheme.to_uppercase(),
612            };
613            if scheme.starts_with("TEX MATH SY") {
614                FontType::TexMathSy
615            } else if scheme.starts_with("TEX MATH EX") {
616                FontType::TexMathEx
617            } else {
618                FontType::Vanilla
619            }
620        };
621        if let Some(scheme) = &self.header.character_coding_scheme {
622            roots.push(ast::Root::CodingScheme(scheme.clone().into()));
623        }
624        roots.extend([
625            ast::Root::DesignSize(
626                if self.header.design_size_valid {
627                    DesignSize::Valid(self.header.design_size)
628                } else {
629                    DesignSize::Invalid
630                }
631                .into(),
632            ),
633            ast::Root::Comment("DESIGNSIZE IS IN POINTS".into()),
634            ast::Root::Comment("OTHER SIZES ARE MULTIPLES OF DESIGNSIZE".into()),
635            ast::Root::Checksum(self.header.checksum.unwrap_or_default().into()),
636        ]);
637        if self.header.seven_bit_safe == Some(true) {
638            roots.push(ast::Root::SevenBitSafeFlag(true.into()));
639        }
640        // Next the parameters. This is TFtoPL.2014.58-61
641        let params: Vec<ast::FontDimension> = self
642            .params
643            .iter()
644            .enumerate()
645            .filter_map(|(i, &param)| {
646                let i: u16 = match (i + 1).try_into() {
647                    Ok(i) => i,
648                    Err(_) => return None,
649                };
650                // TFtoPL.2014.61
651                let named_param = match (i, font_type) {
652                    (1, _) => NamedParameter::Slant,
653                    (2, _) => NamedParameter::Space,
654                    (3, _) => NamedParameter::Stretch,
655                    (4, _) => NamedParameter::Shrink,
656                    (5, _) => NamedParameter::XHeight,
657                    (6, _) => NamedParameter::Quad,
658                    (7, _) => NamedParameter::ExtraSpace,
659                    (8, FontType::TexMathSy) => NamedParameter::Num1,
660                    (9, FontType::TexMathSy) => NamedParameter::Num2,
661                    (10, FontType::TexMathSy) => NamedParameter::Num3,
662                    (11, FontType::TexMathSy) => NamedParameter::Denom1,
663                    (12, FontType::TexMathSy) => NamedParameter::Denom2,
664                    (13, FontType::TexMathSy) => NamedParameter::Sup1,
665                    (14, FontType::TexMathSy) => NamedParameter::Sup2,
666                    (15, FontType::TexMathSy) => NamedParameter::Sup3,
667                    (16, FontType::TexMathSy) => NamedParameter::Sub1,
668                    (17, FontType::TexMathSy) => NamedParameter::Sub2,
669                    (18, FontType::TexMathSy) => NamedParameter::SupDrop,
670                    (19, FontType::TexMathSy) => NamedParameter::SubDrop,
671                    (20, FontType::TexMathSy) => NamedParameter::Delim1,
672                    (21, FontType::TexMathSy) => NamedParameter::Delim2,
673                    (22, FontType::TexMathSy) => NamedParameter::AxisHeight,
674                    (8, FontType::TexMathEx) => NamedParameter::DefaultRuleThickness,
675                    (9, FontType::TexMathEx) => NamedParameter::BigOpSpacing1,
676                    (10, FontType::TexMathEx) => NamedParameter::BigOpSpacing2,
677                    (11, FontType::TexMathEx) => NamedParameter::BigOpSpacing3,
678                    (12, FontType::TexMathEx) => NamedParameter::BigOpSpacing4,
679                    (13, FontType::TexMathEx) => NamedParameter::BigOpSpacing5,
680                    _ => {
681                        let parameter_number = ParameterNumber(i);
682                        return Some(ast::FontDimension::IndexedParam(
683                            (parameter_number, param).into(),
684                        ));
685                    }
686                };
687                Some(ast::FontDimension::NamedParam(named_param, param.into()))
688            })
689            .collect();
690        if !params.is_empty() {
691            roots.push(ast::Root::FontDimension(((), params).into()));
692        }
693
694        // Ligtable
695        if let Some(boundary_char) = self.lig_kern_program.right_boundary_char {
696            roots.push(ast::Root::BoundaryChar(boundary_char.into()));
697        }
698        let index_to_labels = {
699            let mut m = HashMap::<usize, Vec<Char>>::new();
700            for (c, tag) in &self.char_tags {
701                if let CharTag::Ligature(index) = tag {
702                    m.entry(*index as usize).or_default().push(*c);
703                }
704            }
705            m.values_mut().for_each(|v| v.sort());
706            m
707        };
708        let mut l = Vec::<ast::LigTable>::new();
709        let build_lig_kern_op = |instruction: &ligkern::lang::Instruction| match instruction
710            .operation
711        {
712            ligkern::lang::Operation::Kern(kern) => {
713                Some(ast::LigTable::Kern((instruction.right_char, kern).into()))
714            }
715            ligkern::lang::Operation::KernAtIndex(_) => {
716                panic!("tfm::pl::File lig/kern programs cannot contain `KernAtIndex` operations. Use a `Kern` operation instead.");
717            }
718            ligkern::lang::Operation::EntrypointRedirect(_, _) => None,
719            ligkern::lang::Operation::Ligature {
720                char_to_insert,
721                post_lig_operation,
722                post_lig_tag_invalid: _,
723            } => Some(ast::LigTable::Lig(
724                post_lig_operation,
725                (instruction.right_char, char_to_insert).into(),
726            )),
727        };
728
729        // When we fixed the (LIGTABLE (LABEL BOUNDARYCHAR) bug, number of failures went from 26652 -> 12004
730        let mut unreachable_elems: Option<String> = None;
731        let flush_unreachable_elems = |elems: &mut Option<String>, l: &mut Vec<ast::LigTable>| {
732            if let Some(elems) = elems.take() {
733                l.push(ast::LigTable::Comment(elems))
734            }
735        };
736        for (index, (reachable, instruction)) in self
737            .lig_kern_program
738            .reachable_iter(
739                self.char_tags
740                    .iter()
741                    .filter_map(|(c, t)| t.ligature().map(|l| (*c, l))),
742            )
743            .zip(&self.lig_kern_program.instructions)
744            .enumerate()
745        {
746            match reachable {
747                ligkern::lang::ReachableIterItem::Reachable { adjusted_skip } => {
748                    flush_unreachable_elems(&mut unreachable_elems, &mut l);
749                    if let Some(e) = self.lig_kern_program.left_boundary_char_entrypoint {
750                        if e as usize == index {
751                            l.push(ast::LigTable::Label(
752                                ast::LigTableLabel::BoundaryChar.into(),
753                            ));
754                        }
755                    }
756                    for label in index_to_labels.get(&index).unwrap_or(&vec![]) {
757                        l.push(ast::LigTable::Label(
758                            ast::LigTableLabel::Char(*label).into(),
759                        ));
760                    }
761                    if let Some(op) = build_lig_kern_op(instruction) {
762                        l.push(op);
763                    }
764                    match adjusted_skip {
765                        // Note in the first branch here we may push Skip(0)
766                        Some(i) => l.push(ast::LigTable::Skip(ast::DecimalU8(i).into())),
767                        None => {
768                            match instruction.next_instruction {
769                                None => l.push(ast::LigTable::Stop(().into())),
770                                Some(0) => {}
771                                // TODO: potentially write a warning if i is too big like in TFtoPL.2014.74.
772                                Some(i) => l.push(ast::LigTable::Skip(ast::DecimalU8(i).into())),
773                            }
774                        }
775                    }
776                }
777                ligkern::lang::ReachableIterItem::Unreachable => {
778                    let unreachable_elems = unreachable_elems.get_or_insert_with(|| {
779                        // TODO: shouldn't have to add a starting space here!
780                        // Need to fix Node::write in the CST code
781                        " THIS PART OF THE PROGRAM IS NEVER USED!\n".to_string()
782                    });
783                    if let Some(op) = build_lig_kern_op(instruction) {
784                        unreachable_elems
785                            .push_str(&format!["{}", op.lower(char_display_format).display(6, 3)]);
786                    }
787                }
788                ligkern::lang::ReachableIterItem::Passthrough => {}
789            }
790        }
791        flush_unreachable_elems(&mut unreachable_elems, &mut l);
792        if !self.lig_kern_program.instructions.is_empty() {
793            roots.push(ast::Root::LigTable(((), l).into()))
794        }
795
796        // Characters
797        let ordered_chars = {
798            let mut v: Vec<Char> = self.char_dimens.keys().copied().collect();
799            v.sort();
800            v
801        };
802        for c in &ordered_chars {
803            let data = match self.char_dimens.get(c) {
804                None => continue, // TODO: this can't happen. Fix
805                Some(data) => data,
806            };
807            let mut v = vec![];
808            match data.width {
809                None => {
810                    v.push(ast::Character::Width(None.into()));
811                }
812                Some(width) => {
813                    v.push(ast::Character::Width(Some(width).into()));
814                }
815            };
816            if let Some(height) = data.height {
817                v.push(ast::Character::Height(height.into()));
818            }
819            if let Some(depth) = data.depth {
820                v.push(ast::Character::Depth(depth.into()));
821            }
822            if let Some(italic_correction) = data.italic_correction {
823                v.push(ast::Character::ItalicCorrection(italic_correction.into()));
824            }
825
826            match self.char_tags.get(c) {
827                None => {}
828                Some(CharTag::Ligature(entrypoint)) => {
829                    let l: Vec<cst::Node> = self
830                        .lig_kern_program
831                        .instructions_for_entrypoint(*entrypoint)
832                        .map(|(_, b)| b)
833                        .filter_map(build_lig_kern_op)
834                        .map(|n| n.lower(char_display_format))
835                        .collect();
836                    if l.is_empty() {
837                        v.push(ast::Character::Comment("\n".into()));
838                    } else {
839                        v.push(ast::Character::Comment(format![
840                            "\n{}",
841                            cst::Cst(l).display(6, 3)
842                        ]));
843                    }
844                }
845                Some(CharTag::List(c)) => v.push(ast::Character::NextLarger((*c).into())),
846                Some(CharTag::Extension(recipe)) => {
847                    // TFtoPL.2014.86
848                    let mut r = vec![];
849                    if let Some(top) = recipe.top {
850                        r.push(ast::ExtensibleCharacter::Top(top.into()));
851                    }
852                    if let Some(middle) = recipe.middle {
853                        r.push(ast::ExtensibleCharacter::Middle(middle.into()));
854                    }
855                    if let Some(bottom) = recipe.bottom {
856                        r.push(ast::ExtensibleCharacter::Bottom(bottom.into()));
857                    }
858                    let rep = if self.char_dimens.contains_key(&recipe.rep) {
859                        recipe.rep
860                    } else {
861                        *c
862                    };
863                    r.push(ast::ExtensibleCharacter::Replicated(rep.into()));
864                    v.push(ast::Character::ExtensibleCharacter(((), r).into()))
865                }
866            }
867            roots.push(ast::Root::Character((*c, v).into()));
868        }
869
870        ast::Ast(roots)
871    }
872
873    /// Display this file.
874    ///
875    /// This function returns a helper type that implements the [std::fmt::Display]
876    /// trait and can be used in `print!` and similar macros.
877    pub fn display(&self, indent: usize, char_display_format: CharDisplayFormat) -> Display<'_> {
878        Display {
879            pl_file: self,
880            indent,
881            char_display_format,
882        }
883    }
884}
885
886/// Helper type for displaying files.
887///
888/// Use the [File::display] method to construct this type.
889pub struct Display<'a> {
890    pl_file: &'a File,
891    indent: usize,
892    char_display_format: CharDisplayFormat,
893}
894
895impl<'a> std::fmt::Display for Display<'a> {
896    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897        let ast = self.pl_file.lower(self.char_display_format);
898        let cst = ast.lower(self.char_display_format);
899        let d = cst.display(0, self.indent);
900        d.fmt(f)
901    }
902}
903
904/// Specification for how to display characters when printing property list data.
905#[derive(Default, Debug, Clone, Copy)]
906pub enum CharDisplayFormat {
907    /// Letters and numbers are output in PL ASCII format (e.g. `C A`), and
908    /// other characters are output in octal (e.g. `O 14`).
909    /// TODO: rename this variant to AlphanumericAscii or something like that.
910    #[default]
911    Default,
912    /// Visible ASCII characters except ( and ) are output in PL ASCII format (e.g. `C A`), and
913    /// other characters are output in octal (e.g. `O 14`).
914    Ascii,
915    /// All characters are output in octal (e.g. `O 14`)
916    Octal,
917}
918
919/// Iterator over the characters in the strings but with canonical Unix line endings.
920struct Chars<'a> {
921    s: &'a str,
922}
923
924impl<'a> Chars<'a> {
925    fn new(s: &'a str) -> Self {
926        Self { s }
927    }
928}
929
930impl<'a> Iterator for Chars<'a> {
931    type Item = char;
932    fn next(&mut self) -> Option<Self::Item> {
933        let mut iter = self.s.chars();
934        match iter.next() {
935            Some(mut c) => {
936                if c == '\r' {
937                    let mut skipped = 1;
938                    loop {
939                        match iter.next() {
940                            Some('\r') => {
941                                skipped += 1;
942                                continue;
943                            }
944                            Some('\n') => {
945                                c = '\n';
946                                self.s = &self.s[skipped..];
947                                break;
948                            }
949                            _ => break,
950                        }
951                    }
952                }
953                self.s = &self.s[c.len_utf8()..];
954                Some(c)
955            }
956            None => None,
957        }
958    }
959    fn size_hint(&self) -> (usize, Option<usize>) {
960        (0, Some(self.s.len()))
961    }
962}
963
964#[cfg(test)]
965mod tests {
966    use super::*;
967    use crate::{Face, WidthIndex};
968
969    fn run_from_pl_source_code_test(source: &str, mut want: File) {
970        want.header.seven_bit_safe = Some(true);
971        let (got, errors) = File::from_pl_source_code(source);
972        assert_eq!(errors, vec![]);
973        assert_eq!(got, want);
974    }
975
976    macro_rules! from_pl_source_code_tests {
977        ( $( ($name: ident, $source: expr, $want: expr, ), )+ ) => {
978            $(
979                #[test]
980                fn $name() {
981                    let source = $source;
982                    let want = $want;
983                    run_from_pl_source_code_test(source, want);
984                }
985            )+
986        };
987    }
988
989    from_pl_source_code_tests!(
990        (
991            checksum,
992            "(CHECKSUM H 7)",
993            File {
994                header: Header {
995                    checksum: Some(0x7),
996                    ..Header::pl_default()
997                },
998                ..Default::default()
999            },
1000        ),
1001        (
1002            checksum_0,
1003            "(CHECKSUM O 0)",
1004            File {
1005                header: Header {
1006                    checksum: Some(0),
1007                    ..Header::pl_default()
1008                },
1009                ..Default::default()
1010            },
1011        ),
1012        (
1013            design_size,
1014            "(DESIGNSIZE R 3.0)",
1015            File {
1016                header: Header {
1017                    design_size: (FixWord::ONE * 3).into(),
1018                    ..Header::pl_default()
1019                },
1020                ..Default::default()
1021            },
1022        ),
1023        (
1024            design_units,
1025            "(DESIGNUNITS R 2.0)",
1026            File {
1027                design_units: FixWord::ONE * 2,
1028                ..Default::default()
1029            },
1030        ),
1031        (
1032            coding_scheme,
1033            "(CODINGSCHEME Hola Mundo)",
1034            File {
1035                header: Header {
1036                    character_coding_scheme: Some("Hola Mundo".into()),
1037                    ..Header::pl_default()
1038                },
1039                ..Default::default()
1040            },
1041        ),
1042        (
1043            font_family,
1044            "(FAMILY Hello World)",
1045            File {
1046                header: Header {
1047                    font_family: Some("Hello World".into()),
1048                    ..Header::pl_default()
1049                },
1050                ..Default::default()
1051            },
1052        ),
1053        (
1054            face,
1055            "(FACE H 29)",
1056            File {
1057                header: Header {
1058                    face: Some(Face::Other(0x29)),
1059                    ..Header::pl_default()
1060                },
1061                ..Default::default()
1062            },
1063        ),
1064        (
1065            seven_bit_safe_flag,
1066            "(SEVENBITSAFEFLAG FALSE)",
1067            File {
1068                header: Header {
1069                    seven_bit_safe: Some(false),
1070                    ..Header::pl_default()
1071                },
1072                ..Default::default()
1073            },
1074        ),
1075        (
1076            additional_header_data,
1077            "(HEADER D 20 H 1234567)",
1078            File {
1079                header: Header {
1080                    additional_data: vec![0, 0, 0x1234567],
1081                    ..Header::pl_default()
1082                },
1083                ..Default::default()
1084            },
1085        ),
1086        (
1087            boundary_char,
1088            "(BOUNDARYCHAR C a)",
1089            File {
1090                lig_kern_program: ligkern::lang::Program {
1091                    instructions: vec![],
1092                    right_boundary_char: Some('a'.try_into().unwrap()),
1093                    left_boundary_char_entrypoint: None,
1094                    passthrough: Default::default(),
1095                },
1096                ..Default::default()
1097            },
1098        ),
1099        (
1100            named_param,
1101            "(FONTDIMEN (STRETCH D 13.0))",
1102            File {
1103                params: vec![FixWord::ZERO, FixWord::ZERO, FixWord::ONE * 13],
1104                ..Default::default()
1105            },
1106        ),
1107        (
1108            indexed_param,
1109            "(FONTDIMEN (PARAMETER D 2 D 15.0))",
1110            File {
1111                params: vec![FixWord::ZERO, FixWord::ONE * 15],
1112                ..Default::default()
1113            },
1114        ),
1115        (
1116            kern,
1117            "(LIGTABLE (KRN C r D 15.0))",
1118            File {
1119                lig_kern_program: ligkern::lang::Program {
1120                    instructions: vec![ligkern::lang::Instruction {
1121                        next_instruction: None,
1122                        right_char: 'r'.try_into().unwrap(),
1123                        operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1124                    },],
1125                    right_boundary_char: None,
1126                    left_boundary_char_entrypoint: None,
1127                    passthrough: Default::default(),
1128                },
1129                ..Default::default()
1130            },
1131        ),
1132        (
1133            kern_with_stop,
1134            "(LIGTABLE (KRN C r D 15.0) (STOP) (KRN C t D 15.0))",
1135            File {
1136                lig_kern_program: ligkern::lang::Program {
1137                    instructions: vec![
1138                        ligkern::lang::Instruction {
1139                            next_instruction: None,
1140                            right_char: 'r'.try_into().unwrap(),
1141                            operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1142                        },
1143                        ligkern::lang::Instruction {
1144                            next_instruction: None,
1145                            right_char: 't'.try_into().unwrap(),
1146                            operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1147                        },
1148                    ],
1149                    right_boundary_char: None,
1150                    left_boundary_char_entrypoint: None,
1151                    passthrough: Default::default(),
1152                },
1153                ..Default::default()
1154            },
1155        ),
1156        (
1157            kern_with_skip,
1158            "(LIGTABLE (KRN C r D 15.0) (SKIP D 3))",
1159            File {
1160                lig_kern_program: ligkern::lang::Program {
1161                    instructions: vec![ligkern::lang::Instruction {
1162                        next_instruction: Some(3),
1163                        right_char: 'r'.try_into().unwrap(),
1164                        operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1165                    },],
1166                    right_boundary_char: None,
1167                    left_boundary_char_entrypoint: None,
1168                    passthrough: Default::default(),
1169                },
1170                ..Default::default()
1171            },
1172        ),
1173        (
1174            lig,
1175            "(LIGTABLE (LIG/> C r C t))",
1176            File {
1177                lig_kern_program: ligkern::lang::Program {
1178                    instructions: vec![ligkern::lang::Instruction {
1179                        next_instruction: None,
1180                        right_char: 'r'.try_into().unwrap(),
1181                        operation: ligkern::lang::Operation::Ligature {
1182                            char_to_insert: 't'.try_into().unwrap(),
1183                            post_lig_operation:
1184                                ligkern::lang::PostLigOperation::RetainRightMoveToRight,
1185                            post_lig_tag_invalid: false,
1186                        },
1187                    },],
1188                    right_boundary_char: None,
1189                    left_boundary_char_entrypoint: None,
1190                    passthrough: Default::default(),
1191                },
1192                ..Default::default()
1193            },
1194        ),
1195        (
1196            lig_kern_entrypoints,
1197            "(LIGTABLE (LABEL C e) (KRN C r D 15.0) (LABEL C d))",
1198            File {
1199                lig_kern_program: ligkern::lang::Program {
1200                    instructions: vec![ligkern::lang::Instruction {
1201                        next_instruction: None,
1202                        right_char: 'r'.try_into().unwrap(),
1203                        operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1204                    },],
1205                    right_boundary_char: None,
1206                    left_boundary_char_entrypoint: None,
1207                    passthrough: Default::default(),
1208                },
1209                char_tags: BTreeMap::from([
1210                    ('e'.try_into().unwrap(), CharTag::Ligature(0),),
1211                    ('d'.try_into().unwrap(), CharTag::Ligature(1),),
1212                ]),
1213                ..Default::default()
1214            },
1215        ),
1216        (
1217            lig_kern_boundary_char_entrypoint,
1218            "(LIGTABLE (LABEL BOUNDARYCHAR) (KRN C r D 15.0))",
1219            File {
1220                lig_kern_program: ligkern::lang::Program {
1221                    instructions: vec![ligkern::lang::Instruction {
1222                        next_instruction: None,
1223                        right_char: 'r'.try_into().unwrap(),
1224                        operation: ligkern::lang::Operation::Kern(FixWord::ONE * 15),
1225                    },],
1226                    right_boundary_char: None,
1227                    left_boundary_char_entrypoint: Some(0),
1228                    passthrough: Default::default(),
1229                },
1230                ..Default::default()
1231            },
1232        ),
1233        (
1234            char_width,
1235            "(CHARACTER C r (CHARWD D 15.0))",
1236            File {
1237                char_dimens: BTreeMap::from([(
1238                    'r'.try_into().unwrap(),
1239                    CharDimensions {
1240                        width: Some(FixWord::ONE * 15),
1241                        ..Default::default()
1242                    }
1243                )]),
1244                ..Default::default()
1245            },
1246        ),
1247        (
1248            char_height,
1249            "(CHARACTER C r (CHARHT D 15.0))",
1250            File {
1251                char_dimens: BTreeMap::from([(
1252                    'r'.try_into().unwrap(),
1253                    CharDimensions {
1254                        height: Some(FixWord::ONE * 15),
1255                        ..Default::default()
1256                    }
1257                )]),
1258                ..Default::default()
1259            },
1260        ),
1261        (
1262            char_depth,
1263            "(CHARACTER C r (CHARDP D 15.0))",
1264            File {
1265                char_dimens: BTreeMap::from([(
1266                    'r'.try_into().unwrap(),
1267                    CharDimensions {
1268                        depth: Some(FixWord::ONE * 15),
1269                        ..Default::default()
1270                    }
1271                )]),
1272                ..Default::default()
1273            },
1274        ),
1275        (
1276            char_italic_correction,
1277            "(CHARACTER C r (CHARIC D 15.0))",
1278            File {
1279                char_dimens: BTreeMap::from([(
1280                    'r'.try_into().unwrap(),
1281                    CharDimensions {
1282                        italic_correction: Some(FixWord::ONE * 15),
1283                        ..Default::default()
1284                    }
1285                )]),
1286                ..Default::default()
1287            },
1288        ),
1289        (
1290            char_next_larger,
1291            "(CHARACTER C A) (CHARACTER C r (NEXTLARGER C A))",
1292            File {
1293                char_dimens: BTreeMap::from([
1294                    (
1295                        'r'.try_into().unwrap(),
1296                        CharDimensions {
1297                            ..Default::default()
1298                        }
1299                    ),
1300                    (
1301                        'A'.try_into().unwrap(),
1302                        CharDimensions {
1303                            ..Default::default()
1304                        }
1305                    ),
1306                ]),
1307                char_tags: BTreeMap::from([(
1308                    'r'.try_into().unwrap(),
1309                    CharTag::List('A'.try_into().unwrap()),
1310                )]),
1311                ..Default::default()
1312            },
1313        ),
1314        (
1315            char_extensible_recipe_empty,
1316            "(CHARACTER C r (VARCHAR))",
1317            File {
1318                char_dimens: BTreeMap::from([
1319                    (Char(0), Default::default(),),
1320                    ('r'.try_into().unwrap(), Default::default(),),
1321                ]),
1322                char_tags: BTreeMap::from([(
1323                    'r'.try_into().unwrap(),
1324                    CharTag::Extension(Default::default()),
1325                )]),
1326                ..Default::default()
1327            },
1328        ),
1329        (
1330            char_extensible_recipe_data,
1331            "(CHARACTER C r (VARCHAR (TOP O 1) (MID O 2) (BOT O 3) (REP O 4)))",
1332            File {
1333                char_dimens: BTreeMap::from([
1334                    (Char(1), Default::default(),),
1335                    (Char(2), Default::default(),),
1336                    (Char(3), Default::default(),),
1337                    (Char(4), Default::default(),),
1338                    ('r'.try_into().unwrap(), Default::default(),),
1339                ]),
1340                char_tags: BTreeMap::from([(
1341                    'r'.try_into().unwrap(),
1342                    CharTag::Extension(ExtensibleRecipe {
1343                        top: Some(Char(1)),
1344                        middle: Some(Char(2)),
1345                        bottom: Some(Char(3)),
1346                        rep: Char(4),
1347                    }),
1348                )]),
1349                ..Default::default()
1350            },
1351        ),
1352    );
1353
1354    fn run_from_tfm_file_test(tfm_file: crate::File, want: File) {
1355        let got: File = tfm_file.into();
1356        assert_eq!(got, want);
1357    }
1358
1359    macro_rules! from_tfm_file_tests {
1360        ( $( ($name: ident, $tfm_file: expr, $pl_file: expr, ), )+ ) => {
1361            $(
1362                #[test]
1363                fn $name() {
1364                    let tfm_file = $tfm_file;
1365                    let want = $pl_file;
1366                    run_from_tfm_file_test(tfm_file, want);
1367                }
1368            )+
1369        };
1370    }
1371
1372    from_tfm_file_tests!((
1373        gap_in_chars,
1374        crate::File {
1375            char_dimens: BTreeMap::from([
1376                (
1377                    Char('A'.try_into().unwrap()),
1378                    crate::CharDimensions {
1379                        width_index: WidthIndex::Valid(1.try_into().unwrap()),
1380                        height_index: 0,
1381                        depth_index: 0,
1382                        italic_index: 0,
1383                    }
1384                ),
1385                (
1386                    Char('C'.try_into().unwrap()),
1387                    crate::CharDimensions {
1388                        width_index: WidthIndex::Valid(2.try_into().unwrap()),
1389                        height_index: 0,
1390                        depth_index: 0,
1391                        italic_index: 0,
1392                    }
1393                ),
1394                (
1395                    Char('E'.try_into().unwrap()),
1396                    crate::CharDimensions {
1397                        width_index: WidthIndex::Invalid,
1398                        height_index: 0,
1399                        depth_index: 0,
1400                        italic_index: 0,
1401                    }
1402                ),
1403            ]),
1404            widths: vec![FixWord::ZERO, FixWord::ONE, FixWord::ONE * 2],
1405            ..Default::default()
1406        },
1407        File {
1408            char_dimens: BTreeMap::from([
1409                (
1410                    'A'.try_into().unwrap(),
1411                    CharDimensions {
1412                        width: Some(FixWord::ONE),
1413                        ..Default::default()
1414                    }
1415                ),
1416                (
1417                    'C'.try_into().unwrap(),
1418                    CharDimensions {
1419                        width: Some(FixWord::ONE * 2),
1420                        ..Default::default()
1421                    }
1422                ),
1423                (
1424                    'E'.try_into().unwrap(),
1425                    CharDimensions {
1426                        width: None,
1427                        ..Default::default()
1428                    }
1429                ),
1430            ]),
1431            ..<File as From<crate::File>>::from(Default::default())
1432        },
1433    ),);
1434}