tfm/lib.rs
1//! # tfm: TeX font metric data
2//!
3//! This is a crate for working with TeX font metric data.
4//! It includes:
5//!
6//! - Functions to read and write TeX font metric (.tfm) files
7//! to and from a value of type [`File`]
8//! ([`deserialize`](File::deserialize), [`serialize`](File::serialize)).
9//!
10//! - Functions to read and write property list (.pl) files
11//! to and from a value of type [`pl::File`]
12//! ([`from_pl_source_code`](pl::File::from_pl_source_code), [`display`](pl::File::display)).
13//!
14//! - Converters from .tfm to .pl files and vice-versa
15//! (using Rust's [`From`] trait to go between [`File`] and [`pl::File`]).
16//!
17//! ## Background
18//!
19//! Probably the most famous part of the implementation of TeX is the Knuth-Plass line breaking algorithm.
20//! This algorithm determines the "optimal" places to add line breaks when typesetting a paragraph of text.
21//! In order to run the algorithm one needs to provide the dimensions of all characters in the current font.
22//! These dimensions are used to size the boxes in the Knuth-Plass box and glue model.
23//!
24//! In TeX, character dimensions are provided using TeX font metric files.
25//! These are binary files.
26//! By convention they have a .tfm file extension.
27//! Unlike more modern file formats like TrueType, .tfm files only contain the font dimensions;
28//! they don't contains the glyphs.
29//! In general,
30//! .tfm files are produced by other software like Metafont,
31//! placed in some well-known directory in the TeX distribution,
32//! and then read into memory when TeX is running.
33//!
34//! Because .tfm files are binary files, it's hard to debug or tweak them.
35//! To remedy this, Knuth and his team developed another file format called a property list file
36//! (extension .pl or .plst)
37//! that contains the same information but in a modifiable text format.
38//! They then wrote two programs:
39//! `tftopl` to convert a .tfm file to a .pl file,
40//! and `pltotf` to convert a .pl file to a .tfm file.
41//!
42//! The general goal of this crate to fully re-implement all of the TeX font metric
43//! code written by Knuth and others.
44//! This includes `tftopl`, `pltotf`, and also the parts of TeX itself that contain logic
45//! for reading and interpreting .tfm files.
46//! However, unlike these monolithic software programs,
47//! this re-implementation is in the form of a modular library in which
48//! individual pieces of logic and be used and re-used.
49//!
50//! ## Basic example
51//!
52//! ```
53//! // Include the .tfm file for Computer Modern in size 10pt.
54//! let tfm_bytes = include_bytes!["../corpus/computer-modern/cmr10.tfm"];
55//!
56//! // Deserialize the .tfm file.
57//! let (tfm_file_or_error, deserialization_warnings) = tfm::File::deserialize(tfm_bytes);
58//! let mut tfm_file = tfm_file_or_error.expect("cmr10.tfm is a valid .tfm file");
59//! assert_eq![deserialization_warnings, vec![], "the data in cmr10.tfm is 100% valid, so there are no deserialization warnings"];
60//! // TODO assert_eq![tfm_file.header.design_size, tfm::FixWord::UNITY * 10]; make it 11 to be more interesting
61//! // TODO query some data
62//!
63//! // Validate the .tfm file.
64//! let validation_warnings = tfm_file.validate_and_fix();
65//! assert_eq![validation_warnings, vec![], "the data in cmr10.tfm is 100% valid, so there are no validation warnings"];
66//!
67//! // Convert the .tfm file to a .pl file and print it.
68//! let pl_file: tfm::pl::File = tfm_file.clone().into();
69//! // TODO query some data
70//! println!["cmr10.pl:\n{}", pl_file.display(/*indent=*/2, tfm::pl::CharDisplayFormat::Default)];
71//! ```
72//!
73//!
74//! ## Advanced functionality
75//!
76//! In addition to supporting the basic use cases of querying font metric data
77//! and converting between different formats,
78//! this crate has advanced functionality for performing additional tasks on font metric data.
79//! The full set of functionality can be understood by navigating through the crate documentation.
80//! But here are 3 highlights we think are interesting:
81//!
82//! - **Language analysis of .pl files**:
83//! In `pltotf`, Knuth parses .pl files in a single pass.
84//! This crate takes a common approach nowadays of parsing in multiple passes:
85//! first constructing a [concrete syntax tree](pl::cst::Cst) (or parse tree),
86//! next constructing a [fully typed and checked abstract syntax tree](pl::ast::Ast),
87//! and finally building the [`pl::File`] itself.
88//! Each of the passes is exposed, so you can e.g. just build the AST for the .pl file and
89//! do some analysis on it.
90//!
91//! - **Debug output for .tfm files**:
92//!
93//! - **Compilation of lig/kern programs**:
94//!
95//!
96//! ## Binaries
97//!
98//! The Texcraft project produces 3 binaries based on this crate:
99//!
100//! - `tftopl` and `pltotf`: re-implementations of Knuth's programs.
101//! - `tfmtools`: a new binary that has a bunch of different tools
102//! for working with TeX font metric data.
103//! Run `tfmtools help` to list all of the available tools.
104//!
105//! In the root of [the Texcraft repository](https://github.com/jamespfennell/texcraft)
106//! these tools can be run with `cargo run --bin $NAME`
107//! and built with `cargo build --bin $NAME`.
108//!
109//!
110//! ## Correctness
111//!
112//! As part of the development of this crate significant effort has been spent
113//! ensuring it exactly replicates the work of Knuth.
114//! This correctness checking is largely based around diff testing the binaries
115//! `tftopl` and `pltotf`.
116//! We verify that the Texcraft and Knuth implementations have the same output
117//! and generate the same error messages.
118//!
119//! This diff testing has been performed in a few different ways:
120//!
121//! - We have run diff tests over all ~100,000 .tfm files in CTAN.
122//! These tests verify that `tftopl` gives the correct result,
123//! and that running `pltotf` on the output .pl file gives the correct result too.
124//! Unfortunately running `pltotf` on the .pl files in CTAN is infeasible
125//! because most of these files are Perl scripts, not property list files.
126//!
127//! - We have developed a fuzz testing harness (so far just for `tftopl`)
128//! that generates highly heterogenous .tfm files and verifies that `tftopl` gives the correct result.
129//! This fuzz testing has uncovered many issues in the Texcraft implementation,
130//! and has even identified [a 30-year old bug](https://tug.org/pipermail/tex-k/2024-March/004031.html)
131//! in Knuth's implementation of `tftopl`.
132//!
133//! Any .tfm or .pl file that exposes a bug in this library is added to
134//! [our automated testing corpus](https://github.com/jamespfennell/texcraft/tree/main/crates/tfm/bin/tests/data).
135//! Running `cargo t` validates that Texcraft's binaries give the same result as Knuth's binaries
136//! (the output of Knuth's binaries is in source control).
137//! This ensures there are no regressions.
138//!
139//! If you discover a .tfm or .pl file such that the Texcraft and Knuth implementations
140//! diverge, this indicates there is a bug in this library.
141//! Please create an issue on the Texcraft GitHub repo.
142//! We will fix the bug and add your files to the testing corpus.
143
144pub mod algorithms;
145use std::{
146 collections::{BTreeSet, HashMap, HashSet},
147 num::NonZeroU8,
148};
149pub mod ligkern;
150pub mod pl;
151
152use std::collections::BTreeMap;
153
154mod debug;
155mod deserialize;
156mod serialize;
157mod validate;
158
159use common::font;
160pub use deserialize::DeserializationError;
161pub use deserialize::DeserializationWarning;
162pub use deserialize::RawFile;
163pub use deserialize::SubFileSizes;
164pub use validate::ValidationWarning;
165
166#[derive(Debug, Default)]
167pub struct Font {
168 pub file: File,
169 pub lig_kern_program: ligkern::CompiledProgram,
170}
171
172const TFM_CMR10: &[u8] = include_bytes!("../corpus/computer-modern/cmr10.tfm");
173
174impl Font {
175 pub fn build_from_file(mut file: File) -> (Self, Vec<ligkern::InfiniteLoopError>) {
176 let (lig_kern_program, lig_kern_warnings) =
177 ligkern::CompiledProgram::compile_from_tfm_file(&mut file);
178 let font = Font {
179 file,
180 lig_kern_program,
181 };
182 (font, lig_kern_warnings)
183 }
184
185 pub fn build_from_bytes(
186 tfm_bytes: &[u8],
187 ) -> Result<
188 (
189 Self,
190 Vec<DeserializationWarning>,
191 Vec<ligkern::InfiniteLoopError>,
192 ),
193 DeserializationError,
194 > {
195 let (file, deserialization_warnings) = match File::deserialize(tfm_bytes) {
196 (Err(err), _) => return Err(err),
197 (Ok(file), warnings) => (file, warnings),
198 };
199 let (font, lig_kern_warnings) = Self::build_from_file(file);
200 Ok((font, deserialization_warnings, lig_kern_warnings))
201 }
202 pub fn cmr10() -> Self {
203 Self::build_from_bytes(TFM_CMR10)
204 .expect("cmr10.tfm is valid")
205 .0
206 }
207}
208
209impl font::Format for Font {
210 fn null() -> Self {
211 Default::default()
212 }
213 fn width(&self, c: char) -> Option<common::Scaled> {
214 self.file.width_utf8(c)
215 }
216 fn height(&self, c: char) -> Option<common::Scaled> {
217 self.file.height_utf8(c)
218 }
219 fn depth(&self, c: char) -> Option<common::Scaled> {
220 self.file.depth_utf8(c)
221 }
222 fn default_space(&self) -> common::Glue {
223 common::Glue {
224 width: self.file.named_param_scaled(NamedParameter::Space).unwrap(),
225 stretch: self
226 .file
227 .named_param_scaled(NamedParameter::Stretch)
228 .unwrap(),
229 stretch_order: common::GlueOrder::Normal,
230 shrink: self
231 .file
232 .named_param_scaled(NamedParameter::Shrink)
233 .unwrap(),
234 shrink_order: common::GlueOrder::Normal,
235 }
236 }
237 fn extra_space(&self) -> common::Scaled {
238 self.file
239 .named_param_scaled(NamedParameter::ExtraSpace)
240 .unwrap()
241 }
242}
243
244impl font::TextBuilder for Font {
245 type TextIter<'a, Word: Iterator<Item = char>> = ligkern::RunIter<'a, Word>;
246
247 fn build_text<'a, Word: Iterator<Item = char>>(
248 &'a self,
249 word: Word,
250 options: font::BuildTextOptions,
251 ) -> Self::TextIter<'a, Word> {
252 self.lig_kern_program.run_with_options(
253 word,
254 ligkern::RunOptions {
255 disable_left_boundary: options.disable_left_boundary,
256 right_boundary_override: options.right_boundary_override,
257 },
258 )
259 }
260
261 fn has_replacement(&self, left: Option<char>, right: Option<char>) -> bool {
262 self.lig_kern_program.has_replacement(left, right)
263 }
264}
265
266/// Complete contents of a TeX font metric (.tfm) file.
267///
268/// The struct contain multiple vectors.
269/// In TeX and TFtoPL there is an optimization in which all of data in the vectors
270/// is stored in one large vector of 32-bit integers.
271/// The conversion from [u32] to the specific types like [FixWord] are then done when the
272/// data is needed.
273/// This makes the memory footprint of this type much more compact,
274/// and such a change may be considered in the future.
275///
276/// In fact in TeX the font data for all fonts is stored in one contiguous piece of memory
277/// (`font_info`, defined in TeX82.2021.549).
278/// This is a little too unsafe to pull off though.
279#[derive(Clone, Debug, PartialEq, Eq)]
280pub struct File {
281 /// Header.
282 pub header: Header,
283
284 /// Smallest character in the .tfm.
285 pub smallest_char: Char,
286
287 /// Character dimensions.
288 pub char_dimens: BTreeMap<Char, CharDimensions>,
289
290 /// Character tags.
291 ///
292 /// Note there is no correlation between a character having
293 /// a tag and a character having a dimension.
294 /// All four combinations of (has or hasn't dimensions) and (has or hasn't a tag)
295 /// are possible.
296 pub char_tags: BTreeMap<Char, CharTag>,
297
298 /// Tags that have been unset, but whose discriminant is still written to a .tfm file by PLtoTF.
299 pub unset_char_tags: BTreeMap<Char, u8>,
300
301 /// Character widths
302 pub widths: Vec<FixWord>,
303
304 /// Character heights
305 pub heights: Vec<FixWord>,
306
307 /// Character depths
308 pub depths: Vec<FixWord>,
309
310 /// Character italic corrections
311 pub italic_corrections: Vec<FixWord>,
312
313 /// Lig kern program.
314 pub lig_kern_program: ligkern::lang::Program,
315
316 /// Kerns. These are referenced from inside the lig kern commands.
317 pub kerns: Vec<FixWord>,
318
319 /// Extensible characters.
320 pub extensible_chars: Vec<ExtensibleRecipe>,
321
322 /// Font parameters.
323 pub params: Vec<FixWord>,
324}
325
326/// Data about one character in a .tfm file.
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct CharDimensions {
329 /// Index of the width of this character in the widths array.
330 ///
331 /// In valid TFM files, this index will always be non-zero.
332 /// This is because if the width index is zero it means there is no data for the character in the file.
333 /// In this case the other indices (height, etc.) are necessarily zero.
334 /// See TFtoPL.2014.? (where blocks of data with a zero width index are skipped)
335 /// and PLtoTF.2014.? (where missing characters are written with all indices 0).
336 ///
337 /// There is one edge case where this index can be zero.
338 /// This is if the width index is invalid.
339 /// In this case tftopl essentially sets the width index to 0.
340 ///
341 /// Note that even if a character doesn't have dimensions, it can still have a tag.
342 pub width_index: WidthIndex,
343 /// Index of the height of this character in the height array.
344 pub height_index: u8,
345 pub depth_index: u8,
346 pub italic_index: u8,
347}
348
349#[derive(Copy, Clone, Debug, PartialEq, Eq)]
350pub enum WidthIndex {
351 Invalid,
352 Valid(NonZeroU8),
353}
354
355impl WidthIndex {
356 pub fn valid(&self) -> Option<NonZeroU8> {
357 match self {
358 WidthIndex::Invalid => None,
359 WidthIndex::Valid(n) => Some(*n),
360 }
361 }
362 pub fn get(&self) -> u8 {
363 match self {
364 WidthIndex::Invalid => 0,
365 WidthIndex::Valid(n) => n.get(),
366 }
367 }
368}
369
370/// Tag of a character in a .tfm file.
371#[derive(Clone, Debug, PartialEq, Eq)]
372pub enum CharTag {
373 Ligature(u8),
374 List(Char),
375 Extension(u8),
376}
377
378impl CharTag {
379 pub fn ligature(&self) -> Option<u8> {
380 match self {
381 CharTag::Ligature(l) => Some(*l),
382 CharTag::List(_) | CharTag::Extension(_) => None,
383 }
384 }
385 pub fn list(&self) -> Option<Char> {
386 match self {
387 CharTag::List(c) => Some(*c),
388 CharTag::Ligature(_) | CharTag::Extension(_) => None,
389 }
390 }
391 pub fn extension(&self) -> Option<u8> {
392 match self {
393 CharTag::Extension(u) => Some(*u),
394 CharTag::Ligature(_) | CharTag::List(_) => None,
395 }
396 }
397}
398
399/// Extensible recipe instruction in a .tfm file.
400#[derive(Debug, Default, Clone, PartialEq, Eq)]
401pub struct ExtensibleRecipe {
402 pub top: Option<Char>,
403 pub middle: Option<Char>,
404 pub bottom: Option<Char>,
405 pub rep: Char,
406}
407
408impl ExtensibleRecipe {
409 pub fn is_seven_bit(&self) -> bool {
410 self.chars().all(|c| c.is_seven_bit())
411 }
412
413 pub fn chars(&self) -> impl Iterator<Item = Char> {
414 [self.top, self.middle, self.bottom, Some(self.rep)]
415 .into_iter()
416 .flatten()
417 }
418}
419
420impl Default for File {
421 fn default() -> Self {
422 Self {
423 header: Default::default(),
424 smallest_char: Char(1),
425 char_dimens: Default::default(),
426 char_tags: Default::default(),
427 unset_char_tags: Default::default(),
428 widths: vec![FixWord::ZERO],
429 heights: vec![FixWord::ZERO],
430 depths: vec![FixWord::ZERO],
431 italic_corrections: vec![FixWord::ZERO],
432 lig_kern_program: Default::default(),
433 kerns: vec![],
434 extensible_chars: vec![],
435 params: Default::default(),
436 }
437 }
438}
439
440/// Print debugging information about a .tfm file.
441pub fn debug<'a>(
442 path: Option<&'a str>,
443 sub_file_sizes: SubFileSizes,
444 tfm_file: Option<&'a File>,
445 raw_file: Option<&'a RawFile<'a>>,
446 sections: Vec<Section>,
447) -> impl std::fmt::Display + 'a {
448 debug::Debug {
449 sub_file_sizes,
450 path,
451 raw_file,
452 tfm_file,
453 sections,
454 }
455}
456
457impl File {
458 pub fn from_raw_file(raw_file: &RawFile) -> Self {
459 deserialize::from_raw_file(raw_file)
460 }
461
462 pub fn validate_and_fix(&mut self) -> Vec<ValidationWarning> {
463 validate::validate_and_fix(self)
464 }
465
466 pub fn deserialize(
467 b: &[u8],
468 ) -> (
469 Result<File, DeserializationError>,
470 Vec<DeserializationWarning>,
471 ) {
472 deserialize::deserialize(b)
473 }
474
475 pub fn serialize(&self) -> Vec<u8> {
476 serialize::serialize(self)
477 }
478
479 pub fn replace_lig_kern_program(
480 &mut self,
481 mut program: ligkern::lang::Program,
482 entrypoints: HashMap<Char, u16>,
483 ) {
484 let entrypoints = program.pack_entrypoints(entrypoints);
485 self.kerns = program.unpack_kerns();
486 self.lig_kern_program = program;
487 self.char_tags
488 .retain(|_, tag| !matches!(tag, CharTag::Ligature(_)));
489 for (c, ep) in entrypoints {
490 self.char_tags.insert(c, CharTag::Ligature(ep));
491 }
492 }
493
494 /// Return a map from characters to the lig/kern entrypoint for that character.
495 /// TODO: can probably return impl Iterator<Item=(Char, u8)>
496 pub fn lig_kern_entrypoints(&self) -> HashMap<Char, u8> {
497 self.char_tags
498 .iter()
499 .filter_map(|(c, d)| match *d {
500 CharTag::Ligature(l) => Some((*c, l)),
501 _ => None,
502 })
503 .collect()
504 }
505
506 fn char_info_bounds(&self) -> Option<(Char, Char)> {
507 let mut r: Option<(Char, Char)> = None;
508 for c in self.char_dimens.keys().copied() {
509 r = Some(match r {
510 None => (c, c),
511 Some((lower, upper)) => (
512 if c.0 < lower.0 { c } else { lower },
513 if c.0 < upper.0 { upper } else { c },
514 ),
515 })
516 }
517 r
518 }
519
520 /// Calculate the checksum of this .tfm file.
521 pub fn checksum(&self) -> u32 {
522 // This checksum algorithm is in PLtoTF.2014.134.
523 let (bc, ec) = self.char_info_bounds().unwrap_or((Char(1), Char(0)));
524 let mut b = [bc.0, ec.0, bc.0, ec.0];
525 for c in bc.0..=ec.0 {
526 let char_dimens = match self.char_dimens.get(&Char(c)) {
527 None => continue,
528 Some(char_dimens) => char_dimens,
529 };
530 let width = self.widths[char_dimens.width_index.get() as usize].0;
531 // TODO: adjust based on the design units
532 let width = width + (c as i32 + 4) * 0o20_000_000;
533 let add = |b: u8, m: u8| -> u8 {
534 (((b as i32) + (b as i32) + width) % (m as i32))
535 .try_into()
536 .expect("(i32 % u8) is always a u8")
537 };
538 b = [
539 add(b[0], 255),
540 add(b[1], 253),
541 add(b[2], 251),
542 add(b[3], 247),
543 ]
544 }
545 u32::from_be_bytes(b)
546 }
547
548 pub fn named_param(&self, param: NamedParameter) -> Option<FixWord> {
549 self.params.get((param.number() - 1) as usize).copied()
550 }
551
552 pub fn named_param_scaled(&self, param: NamedParameter) -> Option<common::Scaled> {
553 self.named_param(param)
554 .map(|p| p.to_scaled(self.header.design_size))
555 }
556
557 pub fn width_utf8(&self, char: char) -> Option<common::Scaled> {
558 let char: Char = char.try_into().ok()?;
559 let dimens = self.char_dimens.get(&char)?;
560 let i = dimens.width_index.valid()?.get();
561 Some(
562 self.widths
563 .get(i as usize)?
564 .to_scaled(self.header.design_size),
565 )
566 }
567 pub fn height_utf8(&self, char: char) -> Option<common::Scaled> {
568 let char: Char = char.try_into().ok()?;
569 let dimens = self.char_dimens.get(&char)?;
570 let i = dimens.height_index;
571 Some(
572 self.heights
573 .get(i as usize)?
574 .to_scaled(self.header.design_size),
575 )
576 }
577 pub fn depth_utf8(&self, char: char) -> Option<common::Scaled> {
578 let char: Char = char.try_into().ok()?;
579 let dimens = self.char_dimens.get(&char)?;
580 let i = dimens.depth_index;
581 Some(
582 self.depths
583 .get(i as usize)?
584 .to_scaled(self.header.design_size),
585 )
586 }
587}
588
589impl From<crate::pl::File> for File {
590 fn from(pl_file: crate::pl::File) -> Self {
591 let mut char_bounds: Option<(Char, Char)> = None;
592
593 let lig_kern_entrypoints = pl_file.lig_kern_entrypoints(true);
594 let mut lig_kern_program = pl_file.lig_kern_program;
595 let kerns = lig_kern_program.unpack_kerns();
596 let lig_kern_entrypoints = lig_kern_program.pack_entrypoints(lig_kern_entrypoints);
597
598 let mut widths = pl_file.additional_widths;
599 let mut heights = pl_file.additional_heights;
600 let mut depths = pl_file.additional_depths;
601 let mut italic_corrections = pl_file.additional_italics;
602 for (char, char_dimens) in &pl_file.char_dimens {
603 widths.push(char_dimens.width.unwrap_or_default());
604 match char_dimens.height {
605 None | Some(FixWord::ZERO) => {}
606 Some(height) => heights.push(height),
607 }
608 match char_dimens.depth {
609 None | Some(FixWord::ZERO) => {}
610 Some(depth) => depths.push(depth),
611 }
612 match char_dimens.italic_correction {
613 None | Some(FixWord::ZERO) => {}
614 Some(italic_correction) => italic_corrections.push(italic_correction),
615 }
616 char_bounds = Some(match char_bounds {
617 None => (*char, *char),
618 Some((lower, upper)) => (
619 if *char < lower { *char } else { lower },
620 if *char > upper { *char } else { upper },
621 ),
622 })
623 }
624 let (widths, width_to_index) = compress(&widths, 255);
625 let (heights, height_to_index) = compress(&heights, 15);
626 let (depths, depth_to_index) = compress(&depths, 15);
627 let (italic_corrections, italic_correction_to_index) = compress(&italic_corrections, 63);
628 let mut extensible_chars = vec![];
629 let char_dimens = match char_bounds {
630 None => Default::default(),
631 Some((lower, upper)) => {
632 let mut m: BTreeMap<Char, CharDimensions> = Default::default();
633 for c in lower.0..=upper.0 {
634 let pl_data = match pl_file.char_dimens.get(&Char(c)) {
635 Some(pl_data) => pl_data,
636 None => continue,
637 };
638 let width = pl_data.width.unwrap_or_default();
639 let width_index = *width_to_index.get(&width).expect(
640 "the map returned from compress(_,_) contains every input as a key",
641 );
642 m.insert(
643 Char(c),
644 CharDimensions {
645 width_index: WidthIndex::Valid(width_index),
646 height_index: match pl_data.height {
647 None => 0,
648 Some(height) => {
649 // If the height data is missing from the height_to_index map, it's because
650 // the height is 0. Similar with depths and italic corrections.
651 height_to_index
652 .get(&height)
653 .copied()
654 .map(NonZeroU8::get)
655 .unwrap_or(0)
656 }
657 },
658 depth_index: match pl_data.depth {
659 None => 0,
660 Some(depth) => depth_to_index
661 .get(&depth)
662 .copied()
663 .map(NonZeroU8::get)
664 .unwrap_or(0),
665 },
666 italic_index: match pl_data.italic_correction {
667 None => 0,
668 Some(italic_correction) => italic_correction_to_index
669 .get(&italic_correction)
670 .copied()
671 .map(NonZeroU8::get)
672 .unwrap_or(0),
673 },
674 },
675 );
676 }
677 m
678 }
679 };
680 let ordered_chars = {
681 let mut m: Vec<Char> = pl_file.char_tags.keys().copied().collect();
682 m.sort();
683 m
684 };
685 let char_tags = ordered_chars.into_iter().map(|c| {
686 (c,
687 match pl_file.char_tags.get(&c).unwrap() {
688 pl::CharTag::Ligature(_) => {
689 let entrypoint = *lig_kern_entrypoints.get(&c).expect("the map returned by crate::ligkern::lang::compress_entrypoints has a key for all chars with a lig tag");
690 CharTag::Ligature(entrypoint)
691 }
692 pl::CharTag::List(c) => CharTag::List(*c),
693 pl::CharTag::Extension(e) => {
694 let index: u8 = extensible_chars.len().try_into().unwrap();
695 extensible_chars.push(e.clone());
696 CharTag::Extension(index)
697 }
698 },
699 )
700 }).collect();
701
702 let mut file = Self {
703 header: pl_file.header,
704 smallest_char: char_bounds.map(|t| t.0).unwrap_or(Char(1)),
705 char_dimens,
706 char_tags,
707 unset_char_tags: pl_file.unset_char_tags.clone(),
708 widths,
709 heights,
710 depths,
711 italic_corrections,
712 lig_kern_program,
713 kerns,
714 extensible_chars,
715 params: pl_file.params,
716 };
717 if file.header.checksum.is_none() {
718 file.header.checksum = Some(file.checksum());
719 }
720 file
721 }
722}
723
724/// Lossy compression of numbers for TFM files.
725///
726/// The TFM file format can only store up to 15 heights, 15 depths and 63 italic corrections.
727/// If a property list file contains, e.g., more than 15 distinct heights, something has to give.
728/// PLtoTF contains a lossy compression algorithm that takes a list of values and returns another
729/// bounded list of values which approximates the original list.
730/// This is implemented in PLtoTF.2014.75-80 and re-implemented here.
731///
732/// ## How the algorithm works.
733///
734/// For a given delta, the algorithm partitions the ordered list of values such that within
735/// each partition the maximum distance between two elements is delta. All of the values within
736/// the partition are then approximated by `(interval_max+interval_min)/2`.
737/// As such, each value may be increased or decreased by up to `delta/2`.
738/// After this procedure, the list of values is replaced by the list of approximations.
739/// This compresses the list of values into a list whose size is the number of intervals.
740///
741/// Given this subroutine, the algorithm finds the smallest delta such that the number of intervals
742/// is less than the required maximum (e.g., 15 for heights).
743///
744/// There are some important features of this algorithm to note:
745///
746/// - For a given delta value there may be multiple possible partitions.
747/// The algorithm uses a greedy approach in which it maximizes the size of the first partition,
748/// then the size of the second partition, and so on.
749///
750/// - Distinct delta values can yield the same partition.
751/// For example, if the initial values are `[1, 4, 5]` then any delta in the range `[1, 3)`
752/// gives the same result (`[[1], [4, 5]]`)
753/// Whenever we check a delta, we are really checking the _interval of deltas_ that gives the same result.
754/// Both Knuth's and our implementations use this fact to speed up the search by reducing the search space.
755/// E.g. in the example above, after checking `delta=1` we _don't_ check `delta=2`.
756///
757/// ## This re-implementation
758///
759/// The re-implementation here follows PLtoTF closely enough, but with one modification.
760/// To find the optimal delta, Knuth first calculates the minimal possible delta.
761/// This is the minimum distance between adjacent elements in the ordered list of values.
762/// In general this will not be a valid solution because the number of intervals
763/// it generates will be large.
764/// So, he next finds the smallest `k` such that `2^k * min_delta` is a valid solution.
765/// Te then does a upwards linear search within the interval `[min_delta * 2^{k-1}, min_delta * 2^k]` to find
766/// the optimal delta.
767///
768/// Checking a particular delta is `O(n)`.
769/// The worst-case running time of Knuth's algorithm is then `O(n^3)` because the interval
770/// `[2^{k-1}, 2^k]` can contain `O(n^2)` distinct deltas to check in the worst-case [Note 1].
771///
772/// In the re-implementation here we realize that the problem of finding the smallest possible delta
773/// is a classic binary search problem.
774/// This is because if delta is a valid solution, any larger delta also is;
775/// and if delta is not a valid solution, any smaller delta is also not a valid solution.
776/// The re-implementation using binary search is `O(n log n)`.
777/// Moreover, the constant factors are about the same.
778///
779/// [Note 1] In the worst-case there are O(n^2) distinct deltas because each pair of elements yields a delta.
780/// Let m be the smallest delta and M the largest delta.
781/// In the initial `2^k`-based ranging scheme, the largest `K` satisfies `m 2^{K-1} < M <= m 2^K`,
782/// or `K-1 <= log_2(M/m)`. Thus there are `K=O(1)` ranges in this initial scheme.
783/// By the pigeon-hole principle, there exists a `k` such that the range `[m * 2^{k-1}, m * 2^k]`
784/// contains O(n^2) elements.
785/// In the worst-case, the solution is the maximum element of this range.
786pub fn compress(values: &[FixWord], max_size: u8) -> (Vec<FixWord>, HashMap<FixWord, NonZeroU8>) {
787 let max_size = max_size as usize;
788 let dedup_values = {
789 let s: HashSet<FixWord> = values.iter().copied().collect();
790 // remove the zero value for non-widths
791 // and then add it back in at the start
792 let mut v: Vec<FixWord> = s.into_iter().collect();
793 v.sort();
794 v
795 };
796 // After deduplication, it is possible we don't need to compress at all so we can exit early.
797 // This also handles the case when the values slice is empty.
798 if dedup_values.len() <= max_size {
799 let m: HashMap<FixWord, NonZeroU8> = dedup_values
800 .iter()
801 .enumerate()
802 .map(|(i, &w)| {
803 let i: u8 = i.try_into().expect("`dedup_values` has at most `max_size` elements, so the index is at most `max_size-1`");
804 let i: NonZeroU8 = (i+1).try_into().expect("`i<=max_size-1<=u8::MAX`, so `i+1<=u8::MAX`");
805 (w, i) })
806 .collect();
807 let mut dedup_values = dedup_values;
808 dedup_values.push(FixWord::ZERO);
809 dedup_values.rotate_right(1);
810 return (dedup_values, m);
811 }
812
813 // For the binary search we maintain lower and upper indices as usual.
814 // The optimal delta is in the interval [lower, upper].
815 //
816 // Invariant: delta<lower is never a solution.
817 // Because delta must be non-negative, we initialize it to zero.
818 let mut lower = FixWord::ZERO;
819 // Invariant: delta=upper is always solution.
820 // To initialize upper and begin the search we construct a solution that always works: a single
821 // interval encompassing the entire slice and the largest delta possible.
822 let max_delta = *dedup_values.last().unwrap() - *dedup_values.first().unwrap();
823 let mut upper = max_delta;
824 let mut solution = vec![dedup_values.len()];
825
826 let mut buffer = vec![];
827 while lower < upper {
828 // After the following line delta is potentially equal to lower. This is what we want as
829 // we know upper is a solution so to advance the search when upper=lower+1
830 // we need to check lower+1.
831 let delta = lower + (upper - lower) / 2;
832
833 let mut interval_start = *dedup_values.first().unwrap();
834 // The smallest delta such that the candidate solution will be the same.
835 // This is the maximum of all gaps that don't start a new interval.
836 let mut delta_lower = FixWord::ZERO;
837 // The largest delta such that the candidate solution will be different.
838 // This is the minimum of all gaps that start a new interval.
839 let mut delta_upper = max_delta;
840 for (i, &v) in dedup_values.iter().enumerate() {
841 let gap = v - interval_start;
842 if gap > delta {
843 // We need to start a new interval
844 if gap < delta_upper {
845 delta_upper = gap;
846 }
847 buffer.push(i);
848 // If the candidate solution is already too big, we can exit early.
849 if buffer.len() >= max_size {
850 break;
851 }
852 interval_start = v;
853 } else {
854 // We need to extend the current interval
855 // For any delta in the range [gap, delta] we would have made the same choice here.
856 if gap > delta_lower {
857 delta_lower = gap;
858 }
859 }
860 }
861 buffer.push(dedup_values.len());
862
863 if buffer.len() <= max_size {
864 // solution
865 std::mem::swap(&mut buffer, &mut solution);
866 upper = delta_lower;
867 } else {
868 // not a solution
869 lower = delta_upper;
870 }
871 buffer.clear();
872 }
873
874 let mut value_to_index = HashMap::<FixWord, NonZeroU8>::new();
875 let mut result = vec![FixWord::ZERO];
876 let mut previous = 0_usize;
877 for i in solution {
878 let interval = &dedup_values[previous..i];
879 previous = i;
880 for &v in interval {
881 let index: u8 = result.len().try_into().expect("the `result` array contains at most `1+max_size` elements, so the index it at most `max_size` which is a u8");
882 let index: NonZeroU8 = index
883 .try_into()
884 .expect("the `result` array contains at least 1 element so this is never 0");
885 value_to_index.insert(v, index);
886 }
887 let replacement = (*interval.last().unwrap() + *interval.first().unwrap()) / 2;
888 result.push(replacement);
889 }
890
891 (result, value_to_index)
892}
893
894#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
895pub enum Section {
896 /// Sub-file sizes
897 SubFileSizes,
898 /// Header
899 Header,
900 /// Character data
901 CharInfos,
902 /// Widths array
903 Widths,
904 /// Heights array
905 Heights,
906 /// Depths array
907 Depths,
908 /// Italic corrections array
909 ItalicCorrections,
910 /// Lig/kern instructions
911 LigKern,
912 /// Kerns array
913 Kerns,
914 /// Extensible recipes
915 ExtensibleRecipes,
916 /// Params array
917 Params,
918}
919
920impl Section {
921 pub const NAMES: [&'static str; 11] = [
922 "sub-file-sizes",
923 "header",
924 "char-infos",
925 "widths",
926 "heights",
927 "depths",
928 "italic-corrections",
929 "lig-kern",
930 "kerns",
931 "extensible-recipes",
932 "params",
933 ];
934 pub const ALL_SECTIONS: [Section; 11] = [
935 Section::SubFileSizes,
936 Section::Header,
937 Section::CharInfos,
938 Section::Widths,
939 Section::Heights,
940 Section::Depths,
941 Section::ItalicCorrections,
942 Section::LigKern,
943 Section::Kerns,
944 Section::ExtensibleRecipes,
945 Section::Params,
946 ];
947}
948
949impl TryFrom<&str> for Section {
950 type Error = ();
951
952 fn try_from(value: &str) -> Result<Self, Self::Error> {
953 for i in 0..Section::NAMES.len() {
954 if Section::NAMES[i] == value {
955 return Ok(Section::ALL_SECTIONS[i]);
956 }
957 }
958 Err(())
959 }
960}
961
962impl std::fmt::Display for Section {
963 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
964 write!(f, "{}", Section::NAMES[*self as usize])
965 }
966}
967
968/// The TFM header, which contains metadata about the file.
969///
970/// This is defined in TFtoPL.2014.10.
971#[derive(Clone, Debug, Default, PartialEq, Eq)]
972pub struct Header {
973 /// The font checksum, if specified.
974 ///
975 /// In .tfm files checksums are always specified because the format has no
976 /// way to omit a checksum.
977 ///
978 /// In .pl files checksums are specified if the `CHECKSUM` node appears.
979 /// If no checksum is specified in a .pl file, pltotf calculates the
980 /// correct value and writes that.
981 ///
982 /// In TeX82, this is stored in the `font_check` array (TeX82.2021.549).
983 pub checksum: Option<u32>,
984 /// In TeX82, this is stored in the `font_dsize` array (TeX82.2021.549).
985 pub design_size: FixWord,
986 pub design_size_valid: bool,
987 pub character_coding_scheme: Option<String>,
988 pub font_family: Option<String>,
989 pub seven_bit_safe: Option<bool>,
990 pub face: Option<Face>,
991 /// The TFM format allows the header to contain arbitrary additional data.
992 pub additional_data: Vec<u32>,
993}
994
995impl Header {
996 /// Returns the default header when parsing property list files.
997 ///
998 /// This is defined in PLtoTF.2014.70.
999 pub fn pl_default() -> Header {
1000 Header {
1001 checksum: None,
1002 design_size: FixWord::ONE * 10,
1003 design_size_valid: true,
1004 character_coding_scheme: Some("UNSPECIFIED".into()),
1005 font_family: Some("UNSPECIFIED".into()),
1006 seven_bit_safe: None,
1007 face: Some(0.into()),
1008 additional_data: vec![],
1009 }
1010 }
1011
1012 /// Returns the default header when parsing .tfm files.
1013 ///
1014 /// This is defined in PLtoTF.2014.70.
1015 pub fn tfm_default() -> Header {
1016 Header {
1017 checksum: Some(0),
1018 design_size: FixWord::ZERO,
1019 design_size_valid: true,
1020 character_coding_scheme: None,
1021 font_family: None,
1022 seven_bit_safe: None,
1023 face: None,
1024 additional_data: vec![],
1025 }
1026 }
1027}
1028
1029/// A character in a TFM file.
1030///
1031/// TFM and PL files only support 1-byte characters.
1032#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
1033#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1034pub struct Char(pub u8);
1035
1036impl std::fmt::Display for Char {
1037 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1038 if (self.0 as char).is_ascii_graphic() {
1039 write!(f, "{}", self.0 as char)
1040 } else {
1041 write!(f, "\\u{{{:02x}}}", self.0)
1042 }
1043 }
1044}
1045
1046impl From<u8> for Char {
1047 fn from(value: u8) -> Self {
1048 Char(value)
1049 }
1050}
1051
1052impl TryFrom<char> for Char {
1053 type Error = std::char::TryFromCharError;
1054
1055 fn try_from(value: char) -> Result<Self, Self::Error> {
1056 let u: u8 = value.try_into()?;
1057 Ok(Char(u))
1058 }
1059}
1060
1061impl From<Char> for char {
1062 fn from(value: Char) -> Self {
1063 value.0 as char
1064 }
1065}
1066
1067macro_rules! const_chars {
1068 ( $( ($name: ident, $value: expr), )+ ) => {
1069 $(
1070 pub const $name: Char = Char($value);
1071 )+
1072 };
1073}
1074
1075impl Char {
1076 const_chars![
1077 (A, b'A'),
1078 (B, b'B'),
1079 (C, b'C'),
1080 (D, b'D'),
1081 (V, b'V'),
1082 (W, b'W'),
1083 (X, b'X'),
1084 (Y, b'Y'),
1085 (Z, b'Z'),
1086 ];
1087
1088 pub fn is_seven_bit(&self) -> bool {
1089 self.0 <= 127
1090 }
1091}
1092
1093#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1094#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1095pub enum FaceWeight {
1096 Light,
1097 Medium,
1098 Bold,
1099}
1100
1101#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1102#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1103pub enum FaceSlope {
1104 Roman,
1105 Italic,
1106}
1107
1108#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1109#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1110pub enum FaceExpansion {
1111 Regular,
1112 Condensed,
1113 Extended,
1114}
1115
1116#[derive(Debug, PartialEq, Eq, Copy, Clone)]
1117#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1118pub enum Face {
1119 Valid(FaceWeight, FaceSlope, FaceExpansion),
1120 Other(u8),
1121}
1122
1123impl From<u8> for Face {
1124 fn from(value: u8) -> Self {
1125 if value >= 18 {
1126 return Face::Other(value);
1127 }
1128 let a = match (value % 6) / 2 {
1129 0 => FaceWeight::Medium,
1130 1 => FaceWeight::Bold,
1131 2 => FaceWeight::Light,
1132 _ => unreachable!(),
1133 };
1134 let b = match value % 2 {
1135 0 => FaceSlope::Roman,
1136 1 => FaceSlope::Italic,
1137 _ => unreachable!(),
1138 };
1139 let c = match value / 6 {
1140 0 => FaceExpansion::Regular,
1141 1 => FaceExpansion::Condensed,
1142 2 => FaceExpansion::Extended,
1143 _ => unreachable!(),
1144 };
1145 Face::Valid(a, b, c)
1146 }
1147}
1148
1149impl From<Face> for u8 {
1150 fn from(value: Face) -> Self {
1151 match value {
1152 Face::Valid(w, s, c) => {
1153 let a: u8 = match w {
1154 FaceWeight::Medium => 0,
1155 FaceWeight::Bold => 1,
1156 FaceWeight::Light => 2,
1157 };
1158 let b: u8 = match s {
1159 FaceSlope::Roman => 0,
1160 FaceSlope::Italic => 1,
1161 };
1162 let c: u8 = match c {
1163 FaceExpansion::Regular => 0,
1164 FaceExpansion::Condensed => 1,
1165 FaceExpansion::Extended => 2,
1166 };
1167 c * 6 + a * 2 + b
1168 }
1169 Face::Other(b) => b,
1170 }
1171 }
1172}
1173
1174/// A named TeX font metric parameter.
1175#[derive(PartialEq, Eq, Debug, Copy, Clone)]
1176#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1177pub enum NamedParameter {
1178 Slant,
1179 Space,
1180 Stretch,
1181 Shrink,
1182 XHeight,
1183 Quad,
1184 ExtraSpace,
1185 Num1,
1186 Num2,
1187 Num3,
1188 Denom1,
1189 Denom2,
1190 Sup1,
1191 Sup2,
1192 Sup3,
1193 Sub1,
1194 Sub2,
1195 SupDrop,
1196 SubDrop,
1197 Delim1,
1198 Delim2,
1199 AxisHeight,
1200 DefaultRuleThickness,
1201 BigOpSpacing1,
1202 BigOpSpacing2,
1203 BigOpSpacing3,
1204 BigOpSpacing4,
1205 BigOpSpacing5,
1206}
1207
1208impl NamedParameter {
1209 pub fn number(&self) -> u8 {
1210 match self {
1211 NamedParameter::Slant => 1,
1212 NamedParameter::Space => 2,
1213 NamedParameter::Stretch => 3,
1214 NamedParameter::Shrink => 4,
1215 NamedParameter::XHeight => 5,
1216 NamedParameter::Quad => 6,
1217 NamedParameter::ExtraSpace => 7,
1218 NamedParameter::Num1 => 8,
1219 NamedParameter::Num2 => 9,
1220 NamedParameter::Num3 => 10,
1221 NamedParameter::Denom1 => 11,
1222 NamedParameter::Denom2 => 12,
1223 NamedParameter::Sup1 => 13,
1224 NamedParameter::Sup2 => 14,
1225 NamedParameter::Sup3 => 15,
1226 NamedParameter::Sub1 => 16,
1227 NamedParameter::Sub2 => 17,
1228 NamedParameter::SupDrop => 18,
1229 NamedParameter::SubDrop => 19,
1230 NamedParameter::Delim1 => 20,
1231 NamedParameter::Delim2 => 21,
1232 NamedParameter::AxisHeight => 22,
1233 NamedParameter::DefaultRuleThickness => 8,
1234 NamedParameter::BigOpSpacing1 => 9,
1235 NamedParameter::BigOpSpacing2 => 10,
1236 NamedParameter::BigOpSpacing3 => 11,
1237 NamedParameter::BigOpSpacing4 => 12,
1238 NamedParameter::BigOpSpacing5 => 13,
1239 }
1240 }
1241}
1242
1243/// Warning from the compilation of "next larger character" instructions.
1244#[derive(PartialEq, Eq, Debug, Clone)]
1245pub enum NextLargerProgramWarning {
1246 NonExistentCharacter { original: Char, next_larger: Char },
1247 InfiniteLoop { original: Char, next_larger: Char },
1248}
1249
1250impl NextLargerProgramWarning {
1251 pub fn bad_char(&self) -> Char {
1252 match self {
1253 NextLargerProgramWarning::NonExistentCharacter {
1254 original,
1255 next_larger: _,
1256 } => *original,
1257 NextLargerProgramWarning::InfiniteLoop { original, .. } => *original,
1258 }
1259 }
1260
1261 /// Returns the warning message the TFtoPL program prints for this kind of error.
1262 pub fn tftopl_message(&self) -> String {
1263 match self {
1264 NextLargerProgramWarning::NonExistentCharacter {
1265 original: _,
1266 next_larger,
1267 } => {
1268 format![
1269 "Bad TFM file: Character list link to nonexistent character '{:03o}.",
1270 next_larger.0
1271 ]
1272 }
1273 NextLargerProgramWarning::InfiniteLoop { original, .. } => {
1274 format!["Bad TFM file: Cycle in a character list!\nCharacter '{:03o} now ends the list.", original.0]
1275 }
1276 }
1277 }
1278
1279 /// Returns the section in Knuth's TFtoPL (version 2014) in which this warning occurs.
1280 pub fn tftopl_section(&self) -> u8 {
1281 84
1282 }
1283
1284 /// Returns the section in Knuth's PLtoTF (version 2014) in which this warning occurs.
1285 pub fn pltotf_section(&self) -> u8 {
1286 match self {
1287 NextLargerProgramWarning::NonExistentCharacter { .. } => 111,
1288 NextLargerProgramWarning::InfiniteLoop { .. } => 113,
1289 }
1290 }
1291
1292 /// Returns the warning message the PLtoTF program prints for this kind of error.
1293 pub fn pltotf_message(&self) -> String {
1294 match self {
1295 NextLargerProgramWarning::NonExistentCharacter {
1296 original,
1297 next_larger: _,
1298 } => {
1299 format![
1300 "The character NEXTLARGER than '{:03o} had no CHARACTER spec.",
1301 original.0
1302 ]
1303 }
1304 NextLargerProgramWarning::InfiniteLoop { original, .. } => {
1305 format![
1306 "A cycle of NEXTLARGER characters has been broken at '{:03o}.",
1307 original.0
1308 ]
1309 }
1310 }
1311 }
1312}
1313/// Compiled program of "next larger character" instructions
1314///
1315/// The .tfm file format can associate a "next larger" character to any character in a font.
1316/// Next larger characters form sequences: i.e. B can be the next larger character for A,
1317/// and C can be the next larger character for B,
1318/// leading to the sequences A-B-C.
1319/// These next larger characters are used at certain points in TeX.
1320/// TeX occasionally traverses the entire sequence for a given starting character (e.g. A).
1321///
1322/// As with ligatures, next larger specifications can contain infinite loops -
1323/// e.g, if X is the next larger character for Y
1324/// and Y is the next larger character for X.
1325/// These loops are invalid and removed by TFtoPL and PLtoTF.
1326///
1327/// Motivated by the idea of "parse don't validate", this type represents
1328/// a compiled version of the next larger specifications in which infinite loops
1329/// are statically guaranteed not to exist.
1330///
1331/// The basic use of a valid program looks like this:
1332///
1333/// ```
1334/// # use tfm::*;
1335/// let edges = vec![
1336/// (Char::A, Char::B),
1337/// (Char::B, Char::C),
1338/// ];
1339/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1340///
1341/// assert_eq![warnings, vec![]];
1342///
1343/// let sequence_A: Vec<Char> = next_larger_program.get(Char::A).collect();
1344/// assert_eq!(sequence_A, vec![Char::B, Char::C]);
1345///
1346/// let sequence_B: Vec<Char> = next_larger_program.get(Char::B).collect();
1347/// assert_eq!(sequence_B, vec![Char::C]);
1348///
1349/// let sequence_C: Vec<Char> = next_larger_program.get(Char::C).collect();
1350/// assert_eq!(sequence_C, vec![]);
1351///
1352/// // Character that is not in the program.
1353/// let sequence_D: Vec<Char> = next_larger_program.get(Char::D).collect();
1354/// assert_eq!(sequence_D, vec![]);
1355/// ```
1356///
1357/// ## Warnings
1358///
1359/// There are two types of error that can occur when constructing the next
1360/// larger program.
1361/// Both of these errors are handled gracefully, so we officially refer to them as warnings.
1362/// The constructor returns them as values of type [`NextLargerProgramWarning`].
1363///
1364/// ### Infinite loops
1365///
1366/// The first error is that next larger programs can contain infinite loops -
1367/// e.g, if X is the next larger character for Y
1368/// and Y is the next larger character for X.
1369/// In this case the loop is broken by removing the next larger program for the
1370/// character with the largest 8-bit code, in this case Y.
1371/// A [`NextLargerProgramWarning::InfiniteLoop`] warning is returned
1372/// from the program constructor.
1373///
1374/// ```
1375/// # use tfm::*;
1376/// let edges = vec![
1377/// (Char::X, Char::Y),
1378/// (Char::Y, Char::X),
1379/// ];
1380/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1381///
1382/// assert_eq!(warnings, vec![NextLargerProgramWarning::InfiniteLoop{
1383/// original: Char::Y,
1384/// next_larger: Char::X,
1385/// }]);
1386///
1387/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1388/// assert_eq!(sequence_X, vec![Char::Y]);
1389///
1390/// let sequence_Y: Vec<Char> = next_larger_program.get(Char::Y).collect();
1391/// assert_eq!(sequence_Y, vec![]);
1392/// ```
1393///
1394/// ### Non-existent characters
1395///
1396/// The second error is that characters referred to in the next larger program
1397/// may not be defined in the .tfm or .pl file.
1398/// For example, a .pl file may contain the snippet `(CHARACTER C X (NEXTLARGER C Y))`
1399/// without defining the character Y.
1400/// The constructor [`NextLargerProgram::new`] accepts a function for checking if a
1401/// character exists.
1402/// In all cases a [`NextLargerProgramWarning::NonExistentCharacter`] warning is returned
1403/// if a non-existent character is encountered.
1404///
1405/// The behavior of the resulting next larger program is configured using the
1406/// `drop_non_existent_characters` argument.
1407/// If this is false, then the behavior is the same as PLtoTF and the program still
1408/// contains the character.
1409///
1410/// ```
1411/// # use tfm::*;
1412/// let edges = vec![
1413/// (Char::X, Char::Y),
1414/// ];
1415/// let character_exists = |c| {
1416/// if c == Char::Y {
1417/// false
1418/// } else {
1419/// true
1420/// }
1421/// };
1422/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), character_exists, false);
1423///
1424/// assert_eq!(warnings, vec![NextLargerProgramWarning::NonExistentCharacter{
1425/// original: Char::X,
1426/// next_larger: Char::Y,
1427/// }]);
1428///
1429/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1430/// assert_eq!(sequence_X, vec![Char::Y]);
1431/// ```
1432///
1433/// If `drop_non_existent_characters` is true, next larger instructions pointing at non-existent
1434/// characters are dropped.
1435/// This is how TFtoPL behaves.
1436///
1437///
1438/// ```
1439/// # use tfm::*;
1440/// let edges = vec![
1441/// (Char::X, Char::Y),
1442/// ];
1443/// let character_exists = |c| {
1444/// if c == Char::Y {
1445/// false
1446/// } else {
1447/// true
1448/// }
1449/// };
1450/// let (next_larger_program, warnings) = NextLargerProgram::new(edges.into_iter(), character_exists, true);
1451///
1452/// assert_eq!(warnings, vec![NextLargerProgramWarning::NonExistentCharacter{
1453/// original: Char::X,
1454/// next_larger: Char::Y,
1455/// }]);
1456///
1457/// let sequence_X: Vec<Char> = next_larger_program.get(Char::X).collect();
1458/// assert_eq!(sequence_X, vec![]);
1459/// ```
1460///
1461#[derive(Clone, Debug)]
1462pub struct NextLargerProgram {
1463 entrypoints: HashMap<Char, u8>,
1464 next_larger: Vec<(Char, NonZeroU8)>,
1465}
1466
1467impl NextLargerProgram {
1468 /// Build a new next larger program from an iterator over edges.
1469 pub fn new<I: Iterator<Item = (Char, Char)>, F: Fn(Char) -> bool>(
1470 edges: I,
1471 character_exists: F,
1472 drop_non_existent_characters: bool,
1473 ) -> (Self, Vec<NextLargerProgramWarning>) {
1474 // This function implements functionality in TFtoPL.2014.84 and PLtoTF.2014.{110,111,113}.
1475 let mut warnings: Vec<NextLargerProgramWarning> = vec![];
1476
1477 let mut node_to_larger = HashMap::<Char, Char>::new();
1478 let mut node_to_num_smaller = HashMap::<Char, usize>::new();
1479 for (smaller, larger) in edges {
1480 if !character_exists(larger) {
1481 warnings.push(NextLargerProgramWarning::NonExistentCharacter {
1482 original: smaller,
1483 next_larger: larger,
1484 });
1485 if drop_non_existent_characters {
1486 continue;
1487 }
1488 }
1489 node_to_larger.insert(smaller, larger);
1490 node_to_num_smaller.entry(smaller).or_default();
1491 *node_to_num_smaller.entry(larger).or_default() += 1;
1492 }
1493
1494 let mut leaves: Vec<Char> = vec![];
1495 let mut non_leaves: BTreeSet<Char> = Default::default();
1496 for (node, num_smaller) in &node_to_num_smaller {
1497 if *num_smaller == 0 {
1498 leaves.push(*node);
1499 } else {
1500 non_leaves.insert(*node);
1501 }
1502 }
1503
1504 let mut sorted_chars = vec![];
1505 let mut infinite_loop_warnings = vec![];
1506 loop {
1507 while let Some(smaller) = leaves.pop() {
1508 if let Some(larger) = node_to_larger.get(&smaller).copied() {
1509 let num_smaller = node_to_num_smaller
1510 .get_mut(&larger)
1511 .expect("`node_to_num_smaller` contains all nodes");
1512 *num_smaller = num_smaller
1513 .checked_sub(1)
1514 .expect("the larger of a smaller must have at least one smaller");
1515 if *num_smaller == 0 {
1516 leaves.push(larger);
1517 non_leaves.remove(&larger);
1518 }
1519 }
1520 sorted_chars.push(smaller);
1521 }
1522
1523 // There could be pending nodes left because of a cycle.
1524 // We break the cycle at the largest node.
1525 let smaller = match non_leaves.last() {
1526 None => break,
1527 Some(child) => *child,
1528 };
1529 let larger = node_to_larger.remove(&smaller).expect(
1530 "General graph fact: sum_(node)#in_edges(node)=sum_(node)#out_edges(node).
1531 Fact about the next larger graph: #out_edges(node)<=1, because each char has at most one next larger char.
1532 If #out_edge(child)=0 then sum_(node)#in_edges(node)=sum_(node)#out_edges(node) < #nodes.
1533 Thus there exists another node with #in_edges(node)=0 and that node is a leaf.
1534 But `leaves.len()=0` at this line of code",
1535 );
1536 infinite_loop_warnings.push(NextLargerProgramWarning::InfiniteLoop {
1537 original: smaller,
1538 next_larger: larger,
1539 });
1540 leaves.push(larger);
1541 non_leaves.remove(&larger);
1542 }
1543 warnings.extend(infinite_loop_warnings.into_iter().rev());
1544
1545 let next_larger = {
1546 let parents: HashSet<Char> = node_to_larger.values().copied().collect();
1547 let mut node_to_position = HashMap::<Char, u8>::new();
1548 let mut next_larger: Vec<(Char, NonZeroU8)> = vec![];
1549
1550 for c in sorted_chars.iter().rev() {
1551 if !parents.contains(c) {
1552 continue;
1553 }
1554 // The present character is the parent of at least one child, aka it is the next larger
1555 // character for another character. So it needs to be in the next_larger array.
1556 let child_position: u8 = next_larger
1557 .len()
1558 .try_into()
1559 .expect("there are at most u8::MAX chars in the `next_larger` array");
1560 let offset = match node_to_larger.get(c) {
1561 // The next_larger array contains at most 256 elements: one for each char.
1562 // (Actually it contains at most 255 because one character necessarily does not
1563 // have a child node and this character does not appear in the array.)
1564 // Anyway, an offset of 256 sends the index outside the array bound, and so
1565 // subsequent calls to iterator return None.
1566 None => NonZeroU8::MAX,
1567 Some(parent) => {
1568 let parent_position = *node_to_position
1569 .get(parent)
1570 .expect("parent has already been inserted");
1571 child_position
1572 .checked_sub(parent_position)
1573 .expect("parent inserted before so its position it is strictly smaller")
1574 .try_into()
1575 .expect("parent inserted before so its position it is strictly smaller")
1576 }
1577 };
1578 next_larger.push((*c, offset));
1579 node_to_position.insert(*c, child_position);
1580 }
1581 next_larger.reverse();
1582 next_larger
1583 };
1584
1585 let entrypoints = {
1586 let node_to_position: HashMap<Char, u8> = next_larger
1587 .iter()
1588 .enumerate()
1589 .map(|(i, (c, _))| {
1590 let u: u8 = i
1591 .try_into()
1592 .expect("there are at most u8::MAX chars in the `next_larger` array");
1593 (*c, u)
1594 })
1595 .collect();
1596 let mut entrypoints = HashMap::<Char, u8>::new();
1597 for c in sorted_chars.iter().rev() {
1598 if let Some(parent) = node_to_larger.get(c) {
1599 entrypoints.insert(
1600 *c,
1601 *node_to_position
1602 .get(parent)
1603 .expect("parent has already been inserted"),
1604 );
1605 }
1606 }
1607 entrypoints
1608 };
1609
1610 (
1611 Self {
1612 entrypoints,
1613 next_larger,
1614 },
1615 warnings,
1616 )
1617 }
1618
1619 /// Get the next larger sequence for a character
1620 pub fn get(&self, c: Char) -> impl Iterator<Item = Char> + '_ {
1621 NextLargerProgramIter {
1622 current: self.entrypoints.get(&c).copied(),
1623 program: self,
1624 }
1625 }
1626
1627 /// Returns whether this program is seven-bit safe.
1628 ///
1629 /// A next larger program is seven-bit safe if the next larger sequences for
1630 /// seven-bit characters only contain seven-bit characters.
1631 /// Conversely a program is seven-bit unsafe if there is a seven-bit
1632 /// character whose next larger sequence contains a non-seven-bit character.
1633 ///
1634 /// ```
1635 /// # use tfm::*;
1636 /// let edges = vec![
1637 /// (Char(250), Char(125)),
1638 /// (Char(125), Char(126)),
1639 /// ];
1640 /// let (next_larger_program, _) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1641 /// assert_eq!(true, next_larger_program.is_seven_bit_safe());
1642 ///
1643 /// let edges = vec![
1644 /// (Char(125), Char(250)),
1645 /// ];
1646 /// let (next_larger_program, _) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1647 /// assert_eq!(false, next_larger_program.is_seven_bit_safe());
1648 /// ```
1649 pub fn is_seven_bit_safe(&self) -> bool {
1650 // For each c, we only need to check the first element in c's next larger sequence.
1651 // If there is a subsequent element d of the sequence that is seven-bit unsafe,
1652 // we will find it when considering one of d's children.
1653 // This optimization makes this function O(n), rather than worst case O(n^2).
1654 self.entrypoints
1655 .keys()
1656 .copied()
1657 .filter(Char::is_seven_bit)
1658 .filter_map(|c| self.get(c).next())
1659 .all(|c| c.is_seven_bit())
1660 }
1661}
1662
1663#[derive(Clone, Debug)]
1664struct NextLargerProgramIter<'a> {
1665 current: Option<u8>,
1666 program: &'a NextLargerProgram,
1667}
1668
1669impl<'a> Iterator for NextLargerProgramIter<'a> {
1670 type Item = Char;
1671
1672 fn next(&mut self) -> Option<Self::Item> {
1673 // Note that the iterator is statically guaranteed to terminate!
1674 // If it returns None, it has already terminated.
1675 // If it returns Some, then self.current will be incremented by a strictly
1676 // positive number.
1677 // Incrementing like this can only happen a finite number of times before overflow
1678 // occurs, and then self.current is None and the iterator is terminated.
1679 match self.current {
1680 None => None,
1681 Some(current) => match self.program.next_larger.get(current as usize) {
1682 None => None,
1683 Some((c, inc)) => {
1684 self.current = current.checked_add(inc.get());
1685 Some(*c)
1686 }
1687 },
1688 }
1689 }
1690}
1691
1692impl common::FontFormat for File {
1693 const DEFAULT_FILE_EXTENSION: &'static str = "tfm";
1694 type Error = DeserializationError;
1695
1696 fn parse(b: &[u8]) -> Result<Self, Self::Error> {
1697 File::deserialize(b).0
1698 }
1699}
1700
1701/// Fixed-width numeric type used in TFM files.
1702///
1703/// This numeric type has 11 bits for the integer part,
1704/// 20 bits for the fractional part, and a single signed bit.
1705/// The inner value is the number multiplied by 2^20.
1706/// It is called a `fix_word` in TFtoPL.
1707///
1708/// In property list files, this type is represented as a decimal number
1709/// with up to 6 digits after the decimal point.
1710/// This is a non-lossy representation
1711/// because 10^(-6) is larger than 2^(-20).
1712#[derive(Default, PartialEq, Eq, Debug, Copy, Clone, PartialOrd, Ord, Hash)]
1713#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1714pub struct FixWord(pub i32);
1715
1716impl FixWord {
1717 /// Representation of the number 0 as a [FixWord].
1718 pub const ZERO: FixWord = FixWord(0);
1719
1720 /// Representation of the number 1 as a [FixWord].
1721 pub const ONE: FixWord = FixWord(1 << 20);
1722
1723 /// Returns true if the number is less than 16.0 in magnitude according to Knuth.
1724 ///
1725 /// The number +16.0 is not allowed.
1726 /// This is covered in the E2E tests.
1727 /// See `check_fix` in TFtoPL.2014.60.
1728 pub fn is_abs_less_than_16(&self) -> bool {
1729 *self >= FixWord::ONE * -16 && *self < FixWord::ONE * 16
1730 }
1731
1732 /// Calculates the scaled value of this number at the specified design size.
1733 ///
1734 /// All dimensions in a TFM file are specified as multiples of the design size
1735 /// (with the exception of the design size itself).
1736 /// The scaled value (i.e. the value is scaled points) is the TFM value multiplied
1737 /// by the design size.
1738 ///
1739 /// The raw value and the design size both have 20 binary fractional bits,
1740 /// so the product has 40 fractional bits.
1741 /// A scaled value has only 16 fractional bits, and how the truncation is performed
1742 /// can effect the result.
1743 /// The specific multiplication algorithm Knuth wrote (TeX.2021.571-572)
1744 /// truncates the design size first and then performs the multiplication
1745 /// and then truncates again.
1746 /// Additionally, the integer division in that algorithm rounds towards zero
1747 /// even for negative numbers (e.g. -1.25 rounds to -1 instead of -2).
1748 ///
1749 /// In order to ensure the result here matched Knuth's algorithm, we reimplement
1750 /// Knuth's algorithm.
1751 pub fn to_scaled(self, design_size: FixWord) -> common::Scaled {
1752 // TeX.2021.568
1753 let mut z = common::Scaled(design_size.0 / 16);
1754 // TeX.2021.572
1755 let mut alpha = 16;
1756 // Ensure that z < 2^23 sp
1757 while z.0 >= 0o40000000 {
1758 z /= 2;
1759 alpha *= 2;
1760 }
1761 let beta = 256 / alpha; // beta = 16 * z / design_size
1762 let alpha = z * alpha; // alpha = 16 * design_size
1763
1764 // TeX.2021.571 (store_scaled)
1765 let [a, b, c, d] = self.0.to_be_bytes();
1766 assert!(a == 0 || a == 255);
1767 let sw = (((z * (d as i32)) / 0o400 + (z * (c as i32))) / 0o400 + z * (b as i32)) / beta;
1768 if a == 255 {
1769 // In this case self < 0.
1770 // We have calculated sw using only the 3 least significant bytes of self,
1771 // which is equivalent to setting self=16+self at the start. We then undo
1772 // this by setting sw = sw - 16 * design_size.
1773 sw - alpha
1774 } else {
1775 sw
1776 }
1777 }
1778}
1779
1780impl std::fmt::Display for FixWord {
1781 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1782 // TFtoPL.2014.40-43
1783 if self.0 < 0 {
1784 write!(f, "-")?;
1785 }
1786 let integer_part = (self.0 / FixWord::ONE.0).abs();
1787 write!(f, "{integer_part}.")?;
1788 let mut fp = (self.0 % FixWord::ONE.0).abs();
1789 fp = 10 * fp + 5;
1790 let mut delta = 10;
1791 loop {
1792 if delta > 0o4_000_000 {
1793 fp = fp + 0o2_000_000 - delta / 2;
1794 }
1795 write!(f, "{}", fp / 0o4_000_000)?;
1796 fp = 10 * (fp % 0o4_000_000);
1797 delta *= 10;
1798 if fp <= delta {
1799 break;
1800 }
1801 }
1802 Ok(())
1803 }
1804}
1805
1806impl std::ops::Add<FixWord> for FixWord {
1807 type Output = FixWord;
1808 fn add(self, rhs: FixWord) -> Self::Output {
1809 FixWord(self.0 + rhs.0)
1810 }
1811}
1812impl std::ops::Sub<FixWord> for FixWord {
1813 type Output = FixWord;
1814 fn sub(self, rhs: FixWord) -> Self::Output {
1815 FixWord(self.0 - rhs.0)
1816 }
1817}
1818
1819impl std::ops::Mul<i32> for FixWord {
1820 type Output = FixWord;
1821
1822 fn mul(self, rhs: i32) -> Self::Output {
1823 FixWord(self.0 * rhs)
1824 }
1825}
1826
1827impl std::ops::Div<i32> for FixWord {
1828 type Output = FixWord;
1829
1830 fn div(self, rhs: i32) -> Self::Output {
1831 FixWord(self.0 / rhs)
1832 }
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837 use super::*;
1838
1839 #[test]
1840 fn to_scaled_test() {
1841 assert_eq!(common::Scaled::ZERO, FixWord::ZERO.to_scaled(FixWord::ONE));
1842 assert_eq!(common::Scaled::ONE, FixWord::ONE.to_scaled(FixWord::ONE));
1843 }
1844
1845 fn run_compress_test(
1846 values: Vec<FixWord>,
1847 max_size: u8,
1848 want: Vec<FixWord>,
1849 want_map: Vec<u8>,
1850 ) {
1851 let (got, got_map) = compress(&values, max_size);
1852 assert_eq!(got, want);
1853 let want_map: HashMap<FixWord, NonZeroU8> = want_map
1854 .into_iter()
1855 .enumerate()
1856 .map(|(i, t)| (values[i], t.try_into().unwrap()))
1857 .collect();
1858 assert_eq!(got_map, want_map);
1859 }
1860
1861 macro_rules! compress_tests {
1862 ( $( ($name: ident, $values: expr, $max_size: expr, $want: expr, $want_map: expr, ), )+ ) => {
1863 $(
1864 #[test]
1865 fn $name () {
1866 let values = $values;
1867 let max_size = $max_size;
1868 let want = $want;
1869 let want_map = $want_map;
1870 run_compress_test(values, max_size, want, want_map);
1871 }
1872 )+
1873 };
1874 }
1875
1876 compress_tests!(
1877 (no_op_0, vec![], 1, vec![FixWord(0)], vec![],),
1878 (
1879 no_op_2,
1880 vec![FixWord::ONE * 2, FixWord::ONE],
1881 2,
1882 vec![FixWord(0), FixWord::ONE, FixWord::ONE * 2],
1883 vec![2, 1],
1884 ),
1885 (
1886 just_deduplication,
1887 vec![FixWord::ONE, FixWord::ONE],
1888 1,
1889 vec![FixWord(0), FixWord::ONE],
1890 vec![1, 1],
1891 ),
1892 (
1893 simple_compression_case,
1894 vec![FixWord::ONE, FixWord::ONE * 2],
1895 1,
1896 vec![FixWord(0), FixWord::ONE * 3 / 2],
1897 vec![1, 1],
1898 ),
1899 (
1900 simple_compression_case_2,
1901 vec![
1902 FixWord::ONE,
1903 FixWord::ONE * 2,
1904 FixWord::ONE * 200,
1905 FixWord::ONE * 201
1906 ],
1907 2,
1908 vec![FixWord(0), FixWord::ONE * 3 / 2, FixWord::ONE * 401 / 2],
1909 vec![1, 1, 2, 2],
1910 ),
1911 (
1912 lower_upper_close_edge_case_1,
1913 vec![FixWord(1), FixWord(3)],
1914 1,
1915 vec![FixWord(0), FixWord(2)],
1916 vec![1, 1],
1917 ),
1918 (
1919 lower_upper_close_edge_case_2,
1920 vec![FixWord(0), FixWord(2)],
1921 1,
1922 vec![FixWord(0), FixWord(1)],
1923 vec![1, 1],
1924 ),
1925 (
1926 lower_upper_close_edge_case_3,
1927 vec![FixWord(1), FixWord(4)],
1928 1,
1929 vec![FixWord(0), FixWord(2)],
1930 vec![1, 1],
1931 ),
1932 (
1933 lower_upper_close_edge_case_4,
1934 vec![FixWord(1), FixWord(2)],
1935 1,
1936 vec![FixWord(0), FixWord(1)],
1937 vec![1, 1],
1938 ),
1939 );
1940
1941 mod next_larger_tests {
1942 use super::*;
1943
1944 fn run(
1945 edges: Vec<(Char, Char)>,
1946 want_sequences: HashMap<Char, Vec<Char>>,
1947 want_warnings: Vec<NextLargerProgramWarning>,
1948 ) {
1949 let (program, got_warnings) = NextLargerProgram::new(edges.into_iter(), |_| true, true);
1950 assert_eq!(got_warnings, want_warnings);
1951 for u in 0..=u8::MAX {
1952 let want_sequence = want_sequences
1953 .get(&Char(u))
1954 .map(Vec::as_slice)
1955 .unwrap_or_default();
1956 let got_sequence: Vec<Char> = program.get(Char(u)).collect();
1957 assert_eq!(
1958 got_sequence, want_sequence,
1959 "got/want sequences for {u} do not match"
1960 );
1961 }
1962 }
1963
1964 fn big_infinite_loop_edges() -> Vec<(Char, Char)> {
1965 (0..=u8::MAX)
1966 .into_iter()
1967 .map(|u| (Char(u), Char(u.wrapping_add(1))))
1968 .collect()
1969 }
1970
1971 fn big_infinite_loop_sequences() -> HashMap<Char, Vec<Char>> {
1972 (0..=u8::MAX)
1973 .into_iter()
1974 .map(|u| {
1975 let v: Vec<Char> = match u.checked_add(1) {
1976 None => vec![],
1977 Some(w) => (w..=u8::MAX).into_iter().map(Char).collect(),
1978 };
1979 (Char(u), v)
1980 })
1981 .collect()
1982 }
1983
1984 macro_rules! next_larger_tests {
1985 ( $( ($name: ident, $edges: expr, $want_sequences: expr, $want_warnings: expr, ), )+ ) => {
1986 $(
1987 #[test]
1988 fn $name () {
1989 run($edges, $want_sequences, $want_warnings);
1990 }
1991 )+
1992 };
1993 }
1994
1995 next_larger_tests!(
1996 (
1997 same_node_loop,
1998 vec![(Char::A, Char::A)],
1999 HashMap::from([(Char::A, vec![])]),
2000 vec![NextLargerProgramWarning::InfiniteLoop {
2001 original: Char::A,
2002 next_larger: Char::A,
2003 }],
2004 ),
2005 (
2006 two_loops,
2007 vec![
2008 (Char::A, Char::B),
2009 (Char::B, Char::C),
2010 (Char::C, Char::B),
2011 (Char::X, Char::Y),
2012 (Char::Y, Char::Z),
2013 (Char::Z, Char::X),
2014 ],
2015 HashMap::from([
2016 (Char::A, vec![Char::B, Char::C]),
2017 (Char::B, vec![Char::C]),
2018 (Char::C, vec![]),
2019 (Char::X, vec![Char::Y, Char::Z]),
2020 (Char::Y, vec![Char::Z]),
2021 (Char::Z, vec![]),
2022 ]),
2023 vec![
2024 NextLargerProgramWarning::InfiniteLoop {
2025 original: Char::C,
2026 next_larger: Char::B,
2027 },
2028 NextLargerProgramWarning::InfiniteLoop {
2029 original: Char::Z,
2030 next_larger: Char::X,
2031 },
2032 ],
2033 ),
2034 (
2035 path_leading_to_loop,
2036 vec![(Char::A, Char::B), (Char::B, Char::C), (Char::C, Char::B),],
2037 HashMap::from([
2038 (Char::A, vec![Char::B, Char::C]),
2039 (Char::B, vec![Char::C]),
2040 (Char::C, vec![]),
2041 ]),
2042 vec![NextLargerProgramWarning::InfiniteLoop {
2043 original: Char::C,
2044 next_larger: Char::B,
2045 }],
2046 ),
2047 (
2048 big_infinite_loop,
2049 big_infinite_loop_edges(),
2050 big_infinite_loop_sequences(),
2051 vec![NextLargerProgramWarning::InfiniteLoop {
2052 original: Char(u8::MAX),
2053 next_larger: Char(0),
2054 }],
2055 ),
2056 );
2057 }
2058}