use pdfrum_cmap::Cid;
use pdfrum_common::kurbo::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CidTransform {
pub cid: u16,
pub a: u8,
pub b: u8,
pub c: u8,
pub d: u8,
pub e: u8,
pub f: u8,
}
include!("transform_table.rs");
#[must_use]
pub fn japan1_transform(cid: Cid) -> Option<CidTransform> {
JAPAN1_VERTICAL_CIDS
.binary_search_by_key(&cid.0, |t| t.cid)
.ok()
.and_then(|i| JAPAN1_VERTICAL_CIDS.get(i).copied())
}
#[must_use]
pub fn cid_transform_to_float(b: u8) -> f32 {
let v = if b < 128 {
i32::from(b)
} else {
i32::from(b) - 255
};
v as f32 * (1.0 / 127.0)
}
#[allow(clippy::many_single_char_names)]
#[must_use]
pub fn apply(t: CidTransform, bbox: Rect) -> Rect {
let (a, b, c, d) = (
f64::from(cid_transform_to_float(t.a)),
f64::from(cid_transform_to_float(t.b)),
f64::from(cid_transform_to_float(t.c)),
f64::from(cid_transform_to_float(t.d)),
);
let e = f64::from(cid_transform_to_float(t.e)) * 1000.0;
let f = f64::from(cid_transform_to_float(t.f)) * 1000.0;
let corners = [
(bbox.x0, bbox.y0),
(bbox.x1, bbox.y0),
(bbox.x1, bbox.y1),
(bbox.x0, bbox.y1),
];
let mut out: Option<Rect> = None;
for (x, y) in corners {
let px = a * x + c * y + e;
let py = b * x + d * y + f;
out = Some(match out {
None => Rect::new(px, py, px, py),
Some(r) => Rect::new(r.x0.min(px), r.y0.min(py), r.x1.max(px), r.y1.max(py)),
});
}
out.unwrap_or(Rect::ZERO)
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
#[test]
fn the_table_has_one_hundred_and_fifty_four_rows() {
assert_eq!(JAPAN1_VERTICAL_CIDS.len(), 154);
}
#[test]
fn the_table_is_sorted_by_cid_so_the_binary_search_is_valid() {
for pair in JAPAN1_VERTICAL_CIDS.windows(2) {
let (Some(a), Some(b)) = (pair.first(), pair.get(1)) else {
continue;
};
assert!(a.cid < b.cid, "{} then {}", a.cid, b.cid);
}
}
#[test]
fn the_first_and_last_rows_are_the_oracles() {
let first = JAPAN1_VERTICAL_CIDS.first().expect("non-empty");
assert_eq!(
*first,
CidTransform {
cid: 97,
a: 129,
b: 0,
c: 0,
d: 127,
e: 55,
f: 0
}
);
let last = JAPAN1_VERTICAL_CIDS.last().expect("non-empty");
assert_eq!(
*last,
CidTransform {
cid: 8819,
a: 0,
b: 129,
c: 127,
d: 0,
e: 218,
f: 108
}
);
}
#[test]
fn lookups_hit_and_miss_correctly() {
assert!(japan1_transform(Cid(97)).is_some());
assert!(japan1_transform(Cid(7887)).is_some());
assert!(japan1_transform(Cid(8819)).is_some());
assert!(japan1_transform(Cid(98)).is_none());
assert!(japan1_transform(Cid(0)).is_none());
assert!(japan1_transform(Cid(u16::MAX)).is_none());
}
#[test]
fn the_byte_unpacking_splits_at_255_not_256() {
assert_eq!(cid_transform_to_float(0), 0.0);
assert!((cid_transform_to_float(127) - 1.0).abs() < 1e-6);
assert!((cid_transform_to_float(128) - (-127.0 / 127.0)).abs() < 1e-6);
assert!((cid_transform_to_float(129) - (-126.0 / 127.0)).abs() < 1e-6);
assert_eq!(cid_transform_to_float(255), 0.0);
}
#[test]
fn the_rotation_rows_actually_rotate() {
let t = japan1_transform(Cid(7889)).expect("row exists");
assert_eq!(cid_transform_to_float(t.a), 0.0);
assert!(cid_transform_to_float(t.b) < -0.9);
assert!(cid_transform_to_float(t.c) > 0.9);
assert_eq!(cid_transform_to_float(t.d), 0.0);
let out = apply(t, Rect::new(0.0, 0.0, 100.0, 800.0));
assert!(out.width() > out.height(), "{out:?}");
}
#[test]
fn the_identity_ish_rows_leave_a_box_roughly_alone() {
let t = japan1_transform(Cid(7887)).expect("row exists");
let out = apply(t, Rect::new(0.0, 0.0, 100.0, 800.0));
assert!((out.width() - 100.0).abs() < 1.0, "{out:?}");
assert!((out.height() - 800.0).abs() < 1.0, "{out:?}");
assert!(out.x0 > 500.0, "{out:?}");
}
#[test]
fn applying_to_a_degenerate_box_is_still_a_box() {
let t = japan1_transform(Cid(97)).expect("row exists");
let out = apply(t, Rect::ZERO);
assert!(out.width().abs() < 1e-6);
assert!(out.height().abs() < 1e-6);
}
}