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