pdfluent-forms 1.0.0-beta.17

AcroForm engine for PDF interactive forms.
Documentation
//! Glyph-width metrics for the Standard-14 fonts, indexed by WinAnsi code.
//!
//! Generated from the Adobe Core-14 AFM data already vendored in
//! `pdf-interpret/src/font/generated/metrics.rs` (glyph-name keyed), remapped
//! through WinAnsiEncoding to a flat `code - 32` index. Values are font units
//! per 1000 em, matching AFM `WX`.
//!
//! Courier is fixed-pitch 600 across all faces and all codes, so it gets a
//! constant instead of a table. Unknown faces fall back to Helvetica, the
//! `/DA` default in practice (Acrobat's own fallback per ISO 32000 ยง12.7.3.3).

/// Fixed advance width for all four Courier faces (font units / 1000 em).
const COURIER_WIDTH: u16 = 600;

pub(crate) const HELVETICA: [u16; 224] = [
    278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
    667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
    350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350,
    222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556,
    556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556,
    537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667,
    667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722,
    722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278,
    278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
];
pub(crate) const HELVETICA_BOLD: [u16; 224] = [
    278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
    667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
    350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350,
    278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556,
    556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611,
    556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667,
    667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722,
    722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278,
    278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
];
pub(crate) const HELVETICA_OBLIQUE: [u16; 224] = [
    278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
    667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
    350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350,
    222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556,
    556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556,
    537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667,
    667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722,
    722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278,
    278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
];
pub(crate) const HELVETICA_BOLD_OBLIQUE: [u16; 224] = [
    278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
    667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
    350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350,
    278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556,
    556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611,
    556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667,
    667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722,
    722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278,
    278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
];
pub(crate) const TIMES_ROMAN: [u16; 224] = [
    250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500,
    500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611,
    556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722,
    722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500,
    278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541,
    350, 500, 350, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 350, 611, 350, 350,
    333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 350, 444, 722, 250, 333, 500, 500,
    500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300, 300, 333, 500, 453,
    250, 333, 300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722, 889, 667, 611, 611,
    611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722,
    722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
    500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
];
pub(crate) const TIMES_BOLD: [u16; 224] = [
    250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667,
    611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722,
    722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556,
    278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520,
    350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 350, 667, 350, 350,
    333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 444, 722, 250, 333, 500,
    500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300, 300, 333, 556,
    540, 250, 333, 300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722, 1000, 722, 667,
    667, 667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722,
    722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278,
    278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
];
pub(crate) const TIMES_ITALIC: [u16; 224] = [
    250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500,
    500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611,
    611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611,
    556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444,
    278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541,
    350, 500, 350, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 350, 556, 350, 350, 333,
    333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 350, 389, 556, 250, 389, 500, 500, 500,
    500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300, 300, 333, 500, 523, 250,
    333, 300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611,
    611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556,
    611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500,
    500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
];
pub(crate) const TIMES_BOLD_ITALIC: [u16; 224] = [
    250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500,
    500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667,
    667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667,
    611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500,
    278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570,
    350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 350, 611, 350, 350,
    333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 389, 611, 250, 389, 500,
    500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333, 400, 570, 300, 300, 333, 576,
    500, 250, 333, 300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667, 944, 667, 667,
    667, 667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722,
    722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278,
    278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
];

/// A resolved Standard-14 face for width measurement.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StandardFace {
    Helvetica,
    HelveticaBold,
    HelveticaOblique,
    HelveticaBoldOblique,
    TimesRoman,
    TimesBold,
    TimesItalic,
    TimesBoldItalic,
    Courier,
}

impl StandardFace {
    /// Map a `/DA` font resource name or a BaseFont name to a measurable face.
    ///
    /// Recognises both the conventional AcroForm resource aliases (`Helv`,
    /// `TiRo`, `Cour`, `HeBO`, ...) and full PostScript base names
    /// (`Helvetica-BoldOblique`, `Times-Roman`, `Courier-Bold`, ...).
    /// Anything unrecognised measures as Helvetica โ€” the same fallback the
    /// reference implementations use when /DR resolution fails.
    pub(crate) fn from_font_name(name: &str) -> Self {
        let n = name.trim_start_matches('/');
        // Conventional short aliases first (case-sensitive, as written by
        // Acrobat and LiveCycle).
        match n {
            "Helv" => return Self::Helvetica,
            "HeBo" => return Self::HelveticaBold,
            "HeOb" => return Self::HelveticaOblique,
            "HeBO" => return Self::HelveticaBoldOblique,
            "TiRo" => return Self::TimesRoman,
            "TiBo" => return Self::TimesBold,
            "TiIt" => return Self::TimesItalic,
            "TiBI" => return Self::TimesBoldItalic,
            "Cour" | "CoBo" | "CoOb" | "CoBO" => return Self::Courier,
            _ => {}
        }
        let lower = n.to_ascii_lowercase();
        if lower.starts_with("courier") || lower.contains("mono") {
            return Self::Courier;
        }
        if lower.starts_with("times") {
            let bold = lower.contains("bold");
            let italic = lower.contains("italic") || lower.contains("oblique");
            return match (bold, italic) {
                (true, true) => Self::TimesBoldItalic,
                (true, false) => Self::TimesBold,
                (false, true) => Self::TimesItalic,
                (false, false) => Self::TimesRoman,
            };
        }
        // Helvetica + Arial + everything else.
        let bold = lower.contains("bold");
        let italic = lower.contains("italic") || lower.contains("oblique");
        match (bold, italic) {
            (true, true) => Self::HelveticaBoldOblique,
            (true, false) => Self::HelveticaBold,
            (false, true) => Self::HelveticaOblique,
            (false, false) => Self::Helvetica,
        }
    }

    /// The PostScript BaseFont name for this face (Standard-14).
    pub(crate) fn base_font_name(self) -> &'static str {
        match self {
            Self::Helvetica => "Helvetica",
            Self::HelveticaBold => "Helvetica-Bold",
            Self::HelveticaOblique => "Helvetica-Oblique",
            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
            Self::TimesRoman => "Times-Roman",
            Self::TimesBold => "Times-Bold",
            Self::TimesItalic => "Times-Italic",
            Self::TimesBoldItalic => "Times-BoldItalic",
            Self::Courier => "Courier",
        }
    }

    /// Advance width of one WinAnsi-encoded byte (font units / 1000 em).
    ///
    /// Codes below 0x20 have no printable glyph in the Standard-14 and
    /// measure 0; unmapped codes measure 0 as well, mirroring AFM absence.
    pub(crate) fn glyph_width(self, code: u8) -> u16 {
        if self == Self::Courier {
            return COURIER_WIDTH;
        }
        if code < 32 {
            return 0;
        }
        let idx = (code - 32) as usize;
        let table: &[u16; 224] = match self {
            Self::Helvetica => &HELVETICA,
            Self::HelveticaBold => &HELVETICA_BOLD,
            Self::HelveticaOblique => &HELVETICA_OBLIQUE,
            Self::HelveticaBoldOblique => &HELVETICA_BOLD_OBLIQUE,
            Self::TimesRoman => &TIMES_ROMAN,
            Self::TimesBold => &TIMES_BOLD,
            Self::TimesItalic => &TIMES_ITALIC,
            Self::TimesBoldItalic => &TIMES_BOLD_ITALIC,
            Self::Courier => unreachable!("handled above"),
        };
        table[idx]
    }

    /// Width of a WinAnsi-encoded byte string at `font_size`, in text-space
    /// units (points).
    pub(crate) fn text_width(self, bytes: &[u8], font_size: f32) -> f32 {
        let units: u32 = bytes.iter().map(|&b| self.glyph_width(b) as u32).sum();
        units as f32 * font_size / 1000.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn helvetica_space_width_is_278() {
        assert_eq!(StandardFace::Helvetica.glyph_width(b' '), 278);
    }

    #[test]
    fn courier_is_fixed_pitch() {
        assert_eq!(StandardFace::Courier.glyph_width(b'W'), 600);
        assert_eq!(StandardFace::Courier.glyph_width(b'i'), 600);
    }

    #[test]
    fn euro_width_resolves_in_helvetica() {
        // WinAnsi 0x80 = Euro; AFM width for Euro in Helvetica is 556.
        assert_eq!(StandardFace::Helvetica.glyph_width(0x80), 556);
    }

    #[test]
    fn alias_and_basename_resolution() {
        assert_eq!(
            StandardFace::from_font_name("Helv"),
            StandardFace::Helvetica
        );
        assert_eq!(
            StandardFace::from_font_name("HeBO"),
            StandardFace::HelveticaBoldOblique
        );
        assert_eq!(
            StandardFace::from_font_name("Times-BoldItalic"),
            StandardFace::TimesBoldItalic
        );
        assert_eq!(
            StandardFace::from_font_name("Courier-Bold"),
            StandardFace::Courier
        );
        assert_eq!(
            StandardFace::from_font_name("Arial,Bold"),
            StandardFace::HelveticaBold
        );
        assert_eq!(
            StandardFace::from_font_name("MinionPro-Regular"),
            StandardFace::Helvetica
        );
    }

    #[test]
    fn text_width_scales_with_size() {
        let w12 = StandardFace::Helvetica.text_width(b"Hi", 12.0);
        let w24 = StandardFace::Helvetica.text_width(b"Hi", 24.0);
        assert!((w24 - 2.0 * w12).abs() < 1e-4);
    }
}