Skip to main content

pdfrum_font/cid/
transform.rs

1//! The Adobe-Japan1 vertical CID transform table.
2//!
3//! A non-embedded Japanese font is substituted with a face that has only
4//! upright glyphs, so PDFium rotates and shifts a hundred and fifty-four of
5//! them itself. The table is per-CID and hand-tuned; there is no rule behind
6//! it.
7
8use pdfrum_cmap::Cid;
9use pdfrum_common::kurbo::Rect;
10
11/// A per-CID affine transform, packed into bytes.
12///
13/// Each of the six components is a byte read through
14/// [`cid_transform_to_float`], which maps `0..=127` to `0..≈1` and `128..=255`
15/// to a *negative* range — note the split point is 255, not 256, so byte 255
16/// is exactly zero rather than a small negative.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct CidTransform {
19    /// The CID this row applies to.
20    pub cid: u16,
21    /// Matrix `a`.
22    pub a: u8,
23    /// Matrix `b`.
24    pub b: u8,
25    /// Matrix `c`.
26    pub c: u8,
27    /// Matrix `d`.
28    pub d: u8,
29    /// Matrix `e`, in thousandths of an em once scaled.
30    pub e: u8,
31    /// Matrix `f`, in thousandths of an em once scaled.
32    pub f: u8,
33}
34
35include!("transform_table.rs");
36
37/// The transform for a CID, when the table declares one.
38///
39/// The table is sorted by CID and binary-searched, exactly as the C++ does.
40#[must_use]
41pub fn japan1_transform(cid: Cid) -> Option<CidTransform> {
42    JAPAN1_VERTICAL_CIDS
43        .binary_search_by_key(&cid.0, |t| t.cid)
44        .ok()
45        .and_then(|i| JAPAN1_VERTICAL_CIDS.get(i).copied())
46}
47
48/// Unpack one byte of a transform.
49///
50/// `(if b < 128 { b } else { b - 255 }) / 127`. The `- 255` rather than `- 256`
51/// is the detail worth checking: it makes byte 129 map to `-126/127` and byte
52/// **255 map to exactly 0**, so the table's `0xFF` entries are "no offset"
53/// rather than "a whole em backwards".
54#[must_use]
55pub fn cid_transform_to_float(b: u8) -> f32 {
56    let v = if b < 128 {
57        i32::from(b)
58    } else {
59        i32::from(b) - 255
60    };
61    v as f32 * (1.0 / 127.0)
62}
63
64/// Apply a transform to a glyph bounding box, taking the outer rectangle.
65///
66/// The translation components are scaled by 1000 because the box is already in
67/// 1000/em text space while the packed bytes are in em fractions.
68// `a`..`f` are the names the six affine components carry everywhere a matrix
69// is written down, in the PDF specification and in this table alike; spelling
70// them out would obscure which component is which.
71#[allow(clippy::many_single_char_names)]
72#[must_use]
73pub fn apply(t: CidTransform, bbox: Rect) -> Rect {
74    let (a, b, c, d) = (
75        f64::from(cid_transform_to_float(t.a)),
76        f64::from(cid_transform_to_float(t.b)),
77        f64::from(cid_transform_to_float(t.c)),
78        f64::from(cid_transform_to_float(t.d)),
79    );
80    let e = f64::from(cid_transform_to_float(t.e)) * 1000.0;
81    let f = f64::from(cid_transform_to_float(t.f)) * 1000.0;
82
83    let corners = [
84        (bbox.x0, bbox.y0),
85        (bbox.x1, bbox.y0),
86        (bbox.x1, bbox.y1),
87        (bbox.x0, bbox.y1),
88    ];
89    let mut out: Option<Rect> = None;
90    for (x, y) in corners {
91        let px = a * x + c * y + e;
92        let py = b * x + d * y + f;
93        out = Some(match out {
94            None => Rect::new(px, py, px, py),
95            Some(r) => Rect::new(r.x0.min(px), r.y0.min(py), r.x1.max(px), r.y1.max(py)),
96        });
97    }
98    out.unwrap_or(Rect::ZERO)
99}
100
101#[cfg(test)]
102mod tests {
103    // Test expectations are exact values by design.
104    #![allow(clippy::float_cmp)]
105    use super::*;
106
107    #[test]
108    fn the_table_has_one_hundred_and_fifty_four_rows() {
109        // The oracle's array has 154. Counted twice against the C++ source,
110        // and pinned here so a future extraction that silently drops rows is
111        // caught.
112        assert_eq!(JAPAN1_VERTICAL_CIDS.len(), 154);
113    }
114
115    #[test]
116    fn the_table_is_sorted_by_cid_so_the_binary_search_is_valid() {
117        for pair in JAPAN1_VERTICAL_CIDS.windows(2) {
118            let (Some(a), Some(b)) = (pair.first(), pair.get(1)) else {
119                continue;
120            };
121            assert!(a.cid < b.cid, "{} then {}", a.cid, b.cid);
122        }
123    }
124
125    #[test]
126    fn the_first_and_last_rows_are_the_oracles() {
127        let first = JAPAN1_VERTICAL_CIDS.first().expect("non-empty");
128        assert_eq!(
129            *first,
130            CidTransform {
131                cid: 97,
132                a: 129,
133                b: 0,
134                c: 0,
135                d: 127,
136                e: 55,
137                f: 0
138            }
139        );
140        let last = JAPAN1_VERTICAL_CIDS.last().expect("non-empty");
141        assert_eq!(
142            *last,
143            CidTransform {
144                cid: 8819,
145                a: 0,
146                b: 129,
147                c: 127,
148                d: 0,
149                e: 218,
150                f: 108
151            }
152        );
153    }
154
155    #[test]
156    fn lookups_hit_and_miss_correctly() {
157        assert!(japan1_transform(Cid(97)).is_some());
158        assert!(japan1_transform(Cid(7887)).is_some());
159        assert!(japan1_transform(Cid(8819)).is_some());
160        // Between the first row and the second block.
161        assert!(japan1_transform(Cid(98)).is_none());
162        assert!(japan1_transform(Cid(0)).is_none());
163        assert!(japan1_transform(Cid(u16::MAX)).is_none());
164    }
165
166    #[test]
167    fn the_byte_unpacking_splits_at_255_not_256() {
168        assert_eq!(cid_transform_to_float(0), 0.0);
169        assert!((cid_transform_to_float(127) - 1.0).abs() < 1e-6);
170        // Byte 128 is the first negative value.
171        assert!((cid_transform_to_float(128) - (-127.0 / 127.0)).abs() < 1e-6);
172        assert!((cid_transform_to_float(129) - (-126.0 / 127.0)).abs() < 1e-6);
173        // And byte 255 is exactly zero, which `- 256` would have made -1/127.
174        assert_eq!(cid_transform_to_float(255), 0.0);
175    }
176
177    #[test]
178    fn the_rotation_rows_actually_rotate() {
179        // CID 7889's matrix is `{0, 129, 127, 0, …}`: a ≈ 0, b ≈ -1, c ≈ 1,
180        // d = 0 — a quarter turn, which is what a vertical form needs.
181        let t = japan1_transform(Cid(7889)).expect("row exists");
182        assert_eq!(cid_transform_to_float(t.a), 0.0);
183        assert!(cid_transform_to_float(t.b) < -0.9);
184        assert!(cid_transform_to_float(t.c) > 0.9);
185        assert_eq!(cid_transform_to_float(t.d), 0.0);
186
187        // A tall narrow box comes out short and wide.
188        let out = apply(t, Rect::new(0.0, 0.0, 100.0, 800.0));
189        assert!(out.width() > out.height(), "{out:?}");
190    }
191
192    #[test]
193    fn the_identity_ish_rows_leave_a_box_roughly_alone() {
194        // CID 7887's matrix is `{127, 0, 0, 127, …}`: a ≈ d ≈ 1, no rotation,
195        // just a translation.
196        let t = japan1_transform(Cid(7887)).expect("row exists");
197        let out = apply(t, Rect::new(0.0, 0.0, 100.0, 800.0));
198        assert!((out.width() - 100.0).abs() < 1.0, "{out:?}");
199        assert!((out.height() - 800.0).abs() < 1.0, "{out:?}");
200        // But it is shifted.
201        assert!(out.x0 > 500.0, "{out:?}");
202    }
203
204    #[test]
205    fn applying_to_a_degenerate_box_is_still_a_box() {
206        let t = japan1_transform(Cid(97)).expect("row exists");
207        let out = apply(t, Rect::ZERO);
208        assert!(out.width().abs() < 1e-6);
209        assert!(out.height().abs() < 1e-6);
210    }
211}