tfm/
validate.rs

1use super::*;
2use crate::ligkern::lang;
3
4#[derive(Debug, PartialEq, Eq)]
5pub enum ValidationWarning {
6    DesignSizeIsTooSmall,
7    DesignSizeIsNegative,
8    StringIsTooLong(usize),
9    StringContainsParenthesis,
10    StringContainsNonstandardAsciiCharacter(char),
11    ParameterIsTooBig(usize),
12    /// Unusual number of parameters.
13    ///
14    /// Math symbol fonts usually contain 22 parameters and math extension fonts 13.
15    /// This warning indicates that a different number was in the .tfm file.
16    UnusualNumberOfParameters {
17        /// True if this is a math symbols font; false if it's a math extension font.
18        is_math_symbols_font: bool,
19        /// Number of parameters in the .tfm file.
20        got: usize,
21    },
22    InvalidCharacterInExtensibleRecipe(Char),
23    InvalidWidthIndex(Char, u8),
24    InvalidHeightIndex(Char, u8),
25    InvalidDepthIndex(Char, u8),
26    InvalidItalicCorrectionIndex(Char, u8),
27    InvalidExtensibleRecipeIndex(Char, u8),
28    FirstWidthIsNonZero,
29    FirstDepthIsNonZero,
30    FirstHeightIsNonZero,
31    FirstItalicCorrectionIsNonZero,
32    WidthIsTooBig(usize),
33    HeightIsTooBig(usize),
34    DepthIsTooBig(usize),
35    ItalicCorrectionIsTooBig(usize),
36    KernIsTooBig(usize),
37    NextLargerWarning(NextLargerProgramWarning),
38    LigKernWarning(lang::ValidationWarning),
39}
40
41impl ValidationWarning {
42    /// Returns the warning message the TFtoPL program prints for this kind of error.
43    pub fn tftopl_message(&self) -> String {
44        use ValidationWarning::*;
45        match self {
46            DesignSizeIsTooSmall => "Bad TFM file: Design size too small!\nI've set it to 10 points.".to_string(),
47            DesignSizeIsNegative => "Bad TFM file: Design size negative!\nI've set it to 10 points.".to_string(),
48            StringIsTooLong(_) => "Bad TFM file: String is too long; I've shortened it drastically.".to_string(),
49            StringContainsParenthesis => "Bad TFM file: Parenthesis in string has been changed to slash.".to_string(),
50            StringContainsNonstandardAsciiCharacter(_) => "Bad TFM file: Nonstandard ASCII code has been blotted out.".to_string(),
51            ParameterIsTooBig(i) => format![
52                "Bad TFM file: Parameter {} is too big;\nI have set it to zero.",
53                i
54            ],
55            UnusualNumberOfParameters { is_math_symbols_font, got } => {
56                let (font_description, expected) = if *is_math_symbols_font {
57                    ("a math symbols", 22)
58                } else {
59                    ("an extension", 13)
60                };
61                format!["Unusual number of fontdimen parameters for {font_description} font ({got} not {expected})."]
62            },
63            InvalidCharacterInExtensibleRecipe(c) => format!["Bad TFM file: Extensible recipe involves the nonexistent character '{:03o}.", c.0],
64            // The following invalid index warning messages intentionally start with a new line.
65            // I think this is a bug in tftopl: in the range_error macro in TFtoPL.2014.47,
66            //  the print_ln(` `) invocation should be guarded behind a check on chars_on_line,
67            //  like the other macros.
68            InvalidWidthIndex(c, _) => format![" \nWidth index for character '{:03o} is too large;\nso I reset it to zero.", c.0],
69            InvalidHeightIndex(c, _) =>  format![" \nHeight index for character '{:03o} is too large;\nso I reset it to zero.", c.0],
70            InvalidDepthIndex(c, _) =>  format![" \nDepth index for character '{:03o} is too large;\nso I reset it to zero.", c.0],
71            InvalidItalicCorrectionIndex(c, _) => format![" \nItalic correction index for character '{:03o} is too large;\nso I reset it to zero.", c.0],
72            InvalidExtensibleRecipeIndex(c, _ ) => format![" \nExtensible index for character '{:03o} is too large;\nso I reset it to zero.", c.0],
73            FirstWidthIsNonZero => "Bad TFM file: width[0] should be zero.".into(),
74            FirstDepthIsNonZero => "Bad TFM file: depth[0] should be zero.".into(),
75            FirstHeightIsNonZero => "Bad TFM file: height[0] should be zero.".into(),
76            FirstItalicCorrectionIsNonZero => "Bad TFM file: italic[0] should be zero.".into(),
77            WidthIsTooBig(i) => format![
78                "Bad TFM file: Width {} is too big;\nI have set it to zero.",
79                i
80            ],
81            HeightIsTooBig(i) => format![
82                "Bad TFM file: Height {} is too big;\nI have set it to zero.",
83                i
84            ],
85            DepthIsTooBig(i) => format![
86                "Bad TFM file: Depth {} is too big;\nI have set it to zero.",
87                i
88            ],
89            ItalicCorrectionIsTooBig(i) => format![
90                "Bad TFM file: Italic correction {} is too big;\nI have set it to zero.",
91                i
92            ],
93            KernIsTooBig(i) => format![
94                "Bad TFM file: Kern {} is too big;\nI have set it to zero.",
95                i
96            ],
97            NextLargerWarning(warning) => warning.tftopl_message(),
98            LigKernWarning(warning) => warning.tftopl_message(),
99        }
100    }
101
102    /// Returns the section in Knuth's TFtoPL (version 2014) in which this warning occurs.
103    pub fn tftopl_section(&self) -> u8 {
104        use ValidationWarning::*;
105        match self {
106            DesignSizeIsNegative | DesignSizeIsTooSmall => 51,
107            StringIsTooLong(_)
108            | StringContainsParenthesis
109            | StringContainsNonstandardAsciiCharacter(_) => 52,
110            ParameterIsTooBig(_) => 60,
111            UnusualNumberOfParameters { .. } => 59,
112            InvalidCharacterInExtensibleRecipe(_) => 87,
113            InvalidWidthIndex(_, _) => 79,
114            InvalidHeightIndex(_, _) => 80,
115            InvalidDepthIndex(_, _) => 81,
116            InvalidItalicCorrectionIndex(_, _) => 82,
117            InvalidExtensibleRecipeIndex(_, _) => 85,
118            FirstWidthIsNonZero
119            | FirstDepthIsNonZero
120            | FirstHeightIsNonZero
121            | FirstItalicCorrectionIsNonZero
122            | WidthIsTooBig(_)
123            | HeightIsTooBig(_)
124            | DepthIsTooBig(_)
125            | ItalicCorrectionIsTooBig(_)
126            | KernIsTooBig(_) => 62,
127            NextLargerWarning(warning) => warning.tftopl_section(),
128            LigKernWarning(warning) => warning.tftopl_section(),
129        }
130    }
131
132    /// Returns true if this warning means the .tfm file was modified.
133    pub fn tfm_file_modified(&self) -> bool {
134        use ValidationWarning::*;
135        match self {
136            DesignSizeIsNegative
137            | DesignSizeIsTooSmall
138            | StringIsTooLong(_)
139            | StringContainsParenthesis
140            | StringContainsNonstandardAsciiCharacter(_)
141            | ParameterIsTooBig(_) => true,
142            UnusualNumberOfParameters { .. } => false,
143            InvalidCharacterInExtensibleRecipe(_)
144            | InvalidWidthIndex(_, _)
145            | InvalidHeightIndex(_, _)
146            | InvalidDepthIndex(_, _)
147            | InvalidItalicCorrectionIndex(_, _)
148            | InvalidExtensibleRecipeIndex(_, _)
149            | FirstWidthIsNonZero
150            | FirstDepthIsNonZero
151            | FirstHeightIsNonZero
152            | FirstItalicCorrectionIsNonZero
153            | WidthIsTooBig(_)
154            | HeightIsTooBig(_)
155            | DepthIsTooBig(_)
156            | ItalicCorrectionIsTooBig(_)
157            | KernIsTooBig(_)
158            | NextLargerWarning(_) => true,
159            LigKernWarning(_) => true,
160        }
161    }
162}
163
164pub fn validate_and_fix(file: &mut File) -> Vec<ValidationWarning> {
165    let mut warnings = vec![];
166
167    if let Some(scheme) = &mut file.header.character_coding_scheme {
168        validate_string(scheme, 39, &mut warnings);
169    }
170    if let Some(family) = &mut file.header.font_family {
171        validate_string(family, 19, &mut warnings);
172    }
173    if file.header.design_size < FixWord::ZERO {
174        warnings.push(ValidationWarning::DesignSizeIsNegative);
175        file.header.design_size = FixWord::ONE * 10;
176        file.header.design_size_valid = false;
177    }
178    if file.header.design_size < FixWord::ONE {
179        warnings.push(ValidationWarning::DesignSizeIsTooSmall);
180        file.header.design_size = FixWord::ONE * 10;
181        file.header.design_size_valid = false;
182    }
183
184    for (i, elem) in file.params.iter_mut().enumerate() {
185        if i == 0 {
186            continue;
187        }
188        if !elem.is_abs_less_than_16() {
189            warnings.push(ValidationWarning::ParameterIsTooBig(i + 1));
190            *elem = FixWord::ZERO
191        }
192    }
193
194    {
195        let scheme = match &file.header.character_coding_scheme {
196            None => "".to_string(),
197            Some(scheme) => scheme.to_uppercase(),
198        };
199        let num_params = file.params.len();
200        if scheme.starts_with("TEX MATH SY") && num_params != 22 {
201            warnings.push(ValidationWarning::UnusualNumberOfParameters {
202                is_math_symbols_font: true,
203                got: num_params,
204            })
205        }
206        if scheme.starts_with("TEX MATH EX") && num_params != 13 {
207            warnings.push(ValidationWarning::UnusualNumberOfParameters {
208                is_math_symbols_font: false,
209                got: num_params,
210            })
211        }
212    }
213
214    for (array, first_dimension_non_zero) in [
215        (&mut file.widths, ValidationWarning::FirstWidthIsNonZero),
216        (&mut file.heights, ValidationWarning::FirstHeightIsNonZero),
217        (&mut file.depths, ValidationWarning::FirstDepthIsNonZero),
218        (
219            &mut file.italic_corrections,
220            ValidationWarning::FirstItalicCorrectionIsNonZero,
221        ),
222    ] {
223        if let Some(first) = array.first_mut() {
224            if *first != FixWord::ZERO {
225                warnings.push(first_dimension_non_zero);
226                // We zero out the number below because we may still want to issue
227                // a warning for number too big.
228            }
229        }
230    }
231
232    for (array, dimension_too_big, zero_first_element) in [
233        (
234            &mut file.widths,
235            ValidationWarning::WidthIsTooBig as fn(usize) -> ValidationWarning,
236            true,
237        ),
238        (&mut file.heights, ValidationWarning::HeightIsTooBig, true),
239        (&mut file.depths, ValidationWarning::DepthIsTooBig, true),
240        (
241            &mut file.italic_corrections,
242            ValidationWarning::ItalicCorrectionIsTooBig,
243            true,
244        ),
245        (&mut file.kerns, ValidationWarning::KernIsTooBig, false),
246    ] {
247        for (i, elem) in array.iter_mut().enumerate() {
248            if !elem.is_abs_less_than_16() {
249                warnings.push(dimension_too_big(i));
250                *elem = FixWord::ZERO
251            }
252            if i == 0 && zero_first_element {
253                *elem = FixWord::ZERO
254            }
255        }
256    }
257
258    let (_, next_larger_warnings) = NextLargerProgram::new(
259        file.char_tags
260            .iter()
261            .filter_map(|(c, t)| t.list().map(|l| (*c, l))),
262        |c| file.char_dimens.contains_key(&c),
263        true,
264    );
265    let mut next_larger_warnings: HashMap<Char, NextLargerProgramWarning> = next_larger_warnings
266        .into_iter()
267        .map(|w| (w.bad_char(), w))
268        .collect();
269
270    let lig_kern_warnings = file.lig_kern_program.validate_and_fix(
271        file.smallest_char,
272        file.char_tags
273            .iter()
274            .filter_map(|(c, t)| t.ligature().map(|l| (*c, l))),
275        |c| file.char_dimens.contains_key(&c),
276        &file.kerns,
277    );
278    lig_kern_warnings
279        .iter()
280        .filter_map(|w| match w {
281            lang::ValidationWarning::InvalidEntrypoint(c) => Some(c),
282            _ => None,
283        })
284        .for_each(|c| {
285            file.char_tags.remove(c);
286        });
287    // There is a bug in Knuth's tftopl in which the some lig/kern warnings are printed
288    // more than once. They are is printed when the lig/kern instruction is output in the LIGTABLE
289    // list, and also each time the instruction appears in a lig/kern COMMENT for a character.
290    //
291    // The cause of the bug depends on the warning. Each is documented below.
292    let duplicated_warnings = {
293        let mut m: HashMap<usize, Vec<lang::ValidationWarning>> = Default::default();
294        lig_kern_warnings
295            .iter()
296            .cloned()
297            .filter_map(|warning| match warning {
298                // When this warning is raised in the lig/kern validator, the buggy index is never fixed. When the
299                // instruction is seen again the warning is raised again.
300                lang::ValidationWarning::KernIndexTooBig(u) => Some((u, warning.clone())),
301                // As with KernIndexTooBig, the buggy data is never fixed.
302                lang::ValidationWarning::EntrypointRedirectTooBig(u) => Some((u, warning.clone())),
303                // When this warning is raised the non-existent character is replaced by bc; i.e., the smallest
304                // character in the .tfm file. However that character may also not exist! When the instruction
305                // is seen again it will be raised again, except the character being warned about is the smallest
306                // character.
307                lang::ValidationWarning::LigatureStepForNonExistentCharacter {
308                    instruction_index,
309                    right_char: _,
310                    new_right_char,
311                } => {
312                    if file.char_dimens.contains_key(&new_right_char) {
313                        None
314                    } else {
315                        Some((
316                            instruction_index,
317                            lang::ValidationWarning::LigatureStepForNonExistentCharacter {
318                                instruction_index,
319                                right_char: new_right_char,
320                                new_right_char,
321                            },
322                        ))
323                    }
324                }
325                // Same as LigatureStepForNonExistentCharacter.
326                lang::ValidationWarning::LigatureStepProducesNonExistentCharacter {
327                    instruction_index,
328                    replacement_char: _,
329                    new_replacement_char,
330                } => {
331                    if file.char_dimens.contains_key(&new_replacement_char) {
332                        None
333                    } else {
334                        Some((
335                            instruction_index,
336                            lang::ValidationWarning::LigatureStepProducesNonExistentCharacter {
337                                instruction_index,
338                                replacement_char: new_replacement_char,
339                                new_replacement_char,
340                            },
341                        ))
342                    }
343                }
344                // Same as LigatureStepForNonExistentCharacter.
345                lang::ValidationWarning::KernStepForNonExistentCharacter {
346                    instruction_index,
347                    right_char: _,
348                    new_right_char,
349                } => {
350                    if file.char_dimens.contains_key(&new_right_char) {
351                        None
352                    } else {
353                        Some((
354                            instruction_index,
355                            lang::ValidationWarning::KernStepForNonExistentCharacter {
356                                instruction_index,
357                                right_char: new_right_char,
358                                new_right_char,
359                            },
360                        ))
361                    }
362                }
363                _ => None,
364            })
365            .for_each(|(u, w)| m.entry(u).or_default().push(w));
366        m
367    };
368    let has_infinite_loop = lig_kern_warnings
369        .iter()
370        .any(|f| matches!(f, lang::ValidationWarning::InfiniteLoop(_)));
371    if has_infinite_loop {
372        file.char_dimens.clear();
373        file.extensible_chars.clear();
374    }
375    warnings.extend(
376        lig_kern_warnings
377            .into_iter()
378            .map(ValidationWarning::LigKernWarning),
379    );
380
381    file.extensible_chars.iter_mut().for_each(|e| {
382        // TFtoPL.2014.87
383        for piece in [&mut e.top, &mut e.middle, &mut e.bottom] {
384            let c = match piece {
385                None => continue,
386                Some(c) => *c,
387            };
388            if file.char_dimens.contains_key(&c) {
389                continue;
390            }
391            warnings.push(ValidationWarning::InvalidCharacterInExtensibleRecipe(c));
392            *piece = None;
393        }
394        if !file.char_dimens.contains_key(&e.rep) {
395            warnings.push(ValidationWarning::InvalidCharacterInExtensibleRecipe(e.rep));
396        }
397    });
398
399    for (c, dimens) in &mut file.char_dimens {
400        if dimens.width_index.get() as usize >= file.widths.len() {
401            warnings.push(ValidationWarning::InvalidWidthIndex(
402                *c,
403                dimens.width_index.get(),
404            ));
405            dimens.width_index = WidthIndex::Invalid;
406        }
407        if dimens.height_index as usize >= file.heights.len() {
408            warnings.push(ValidationWarning::InvalidHeightIndex(
409                *c,
410                dimens.height_index,
411            ));
412            dimens.height_index = 0;
413        }
414        if dimens.depth_index as usize >= file.depths.len() {
415            warnings.push(ValidationWarning::InvalidDepthIndex(*c, dimens.depth_index));
416            dimens.depth_index = 0;
417        }
418        if dimens.italic_index as usize >= file.italic_corrections.len() {
419            warnings.push(ValidationWarning::InvalidItalicCorrectionIndex(
420                *c,
421                dimens.italic_index,
422            ));
423            dimens.italic_index = 0
424        }
425        match file.char_tags.get(c) {
426            Some(CharTag::List(_)) => {
427                if let Some(warning) = next_larger_warnings.remove(c) {
428                    warnings.push(ValidationWarning::NextLargerWarning(warning));
429                    file.char_tags.remove(c);
430                }
431            }
432            Some(CharTag::Extension(e)) => {
433                if *e as usize >= file.extensible_chars.len() {
434                    warnings.push(ValidationWarning::InvalidExtensibleRecipeIndex(*c, *e));
435                    file.char_tags.remove(c);
436                }
437            }
438            Some(CharTag::Ligature(l)) => {
439                if let Ok(entrypoint) = file.lig_kern_program.unpack_entrypoint(*l) {
440                    warnings.extend(
441                        file.lig_kern_program
442                            .instructions_for_entrypoint(entrypoint)
443                            .map(|t| t.0)
444                            .filter_map(|u| duplicated_warnings.get(&u))
445                            .flatten()
446                            .cloned()
447                            .map(ValidationWarning::LigKernWarning),
448                    );
449                }
450            }
451            _ => {}
452        }
453    }
454
455    warnings
456}
457
458fn validate_string(s: &mut String, max_len: usize, warnings: &mut Vec<ValidationWarning>) {
459    if s.chars().count() > max_len {
460        warnings.push(ValidationWarning::StringIsTooLong(s.len()));
461        *s = format!("{}", s.chars().next().unwrap_or(' '))
462    }
463    let new_s: String = s
464        .chars()
465        .map(|c| match c {
466            '(' | ')' => {
467                warnings.push(ValidationWarning::StringContainsParenthesis);
468                '/'
469            }
470            ' '..='~' => c.to_ascii_uppercase(),
471            _ => {
472                warnings.push(ValidationWarning::StringContainsNonstandardAsciiCharacter(
473                    c,
474                ));
475                '?'
476            }
477        })
478        .collect();
479    *s = new_s;
480}