pdfrum_font/cid/
transform.rs1use pdfrum_cmap::Cid;
9use pdfrum_common::kurbo::Rect;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct CidTransform {
19 pub cid: u16,
21 pub a: u8,
23 pub b: u8,
25 pub c: u8,
27 pub d: u8,
29 pub e: u8,
31 pub f: u8,
33}
34
35include!("transform_table.rs");
36
37#[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#[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#[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 #![allow(clippy::float_cmp)]
105 use super::*;
106
107 #[test]
108 fn the_table_has_one_hundred_and_fifty_four_rows() {
109 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 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 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 assert_eq!(cid_transform_to_float(255), 0.0);
175 }
176
177 #[test]
178 fn the_rotation_rows_actually_rotate() {
179 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 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 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 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}