use crate::names;
use pdfrum_cmap::{CharCode, Cid};
use pdfrum_common::{DiagKind, Diagnostics, Severity};
use pdfrum_object::{Array, Dict, Object, Resolve};
pub(crate) const WIDTH_UNSET: u16 = 0xffff;
#[derive(Debug, Clone, PartialEq)]
pub struct SimpleWidths {
pub(crate) raw: [u16; 256],
pub use_face_widths: bool,
}
impl Default for SimpleWidths {
fn default() -> Self {
Self {
raw: [WIDTH_UNSET; 256],
use_face_widths: true,
}
}
}
impl SimpleWidths {
#[must_use]
pub fn get(&self, code: u8) -> Option<f32> {
match self.raw.get(usize::from(code)) {
Some(&WIDTH_UNSET) | None => None,
Some(&w) => Some(f32::from(w)),
}
}
#[must_use]
pub fn has_declared_widths(&self) -> bool {
!self.use_face_widths
}
}
#[must_use]
pub fn load_simple(font_dict: &Dict, desc: Option<&Dict>, r: &impl Resolve) -> SimpleWidths {
let Some(widths) = font_dict.array(names::WIDTHS, r) else {
return SimpleWidths::default();
};
let mut w = SimpleWidths {
raw: [WIDTH_UNSET; 256],
use_face_widths: false,
};
if let Some(missing) = desc.and_then(|d| d.int(names::MISSING_WIDTH, r)) {
w.raw = [missing as u16; 256];
}
let start = font_dict.int(names::FIRST_CHAR, r).unwrap_or(0);
let Ok(start) = usize::try_from(start) else {
return w;
};
if start > 255 {
return w;
}
let declared_end = font_dict.int(names::LAST_CHAR, r).unwrap_or(0);
let array_end = start.saturating_add(widths.len()).saturating_sub(1);
let mut end = match usize::try_from(declared_end) {
Ok(e) if e != 0 && e < start.saturating_add(widths.len()) => e,
Ok(_) | Err(_) => array_end,
};
end = end.min(255);
for code in start..=end {
let value = widths.int_at(code - start).unwrap_or(0);
if let Some(slot) = w.raw.get_mut(code) {
*slot = value as u16;
}
}
w
}
#[derive(Debug, Clone, PartialEq)]
pub struct CidWidths {
records: Vec<[i32; 3]>,
default: i32,
ansi_widths_fixed: bool,
}
impl Default for CidWidths {
fn default() -> Self {
Self {
records: Vec::new(),
default: 1000,
ansi_widths_fixed: false,
}
}
}
impl CidWidths {
#[must_use]
pub fn load(cid_dict: &Dict, r: &impl Resolve, diags: &mut Diagnostics) -> Self {
let default = cid_dict
.int(names::DW, r)
.and_then(|v| i32::try_from(v).ok())
.unwrap_or(1000);
let records = match cid_dict.array(names::W, r) {
Some(a) => parse_metrics(&a, 1, r, diags).as_chunks::<3>().0.to_vec(),
None => Vec::new(),
};
Self {
records,
default,
ansi_widths_fixed: false,
}
}
pub fn set_ansi_widths_fixed(&mut self) {
self.ansi_widths_fixed = true;
}
#[must_use]
pub fn width(&self, code: CharCode, cid: Cid) -> f32 {
if self.ansi_widths_fixed && code.0 < 0x80 {
return if (32..127).contains(&code.0) {
500.0
} else {
0.0
};
}
let cid = i32::from(cid.0);
for rec in &self.records {
let (Some(&low), Some(&high), Some(&val)) = (rec.first(), rec.get(1), rec.get(2))
else {
continue;
};
if low <= cid && cid <= high {
return val as f32;
}
}
self.default as f32
}
#[must_use]
#[cfg(test)]
pub fn default_width(&self) -> f32 {
self.default as f32
}
#[cfg(test)]
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VerticalMetrics {
records: Vec<[i32; 5]>,
default_vy: i32,
default_w1: i32,
}
impl Default for VerticalMetrics {
fn default() -> Self {
Self {
records: Vec::new(),
default_vy: 880,
default_w1: -1000,
}
}
}
impl VerticalMetrics {
#[must_use]
pub fn load(cid_dict: &Dict, r: &impl Resolve, diags: &mut Diagnostics) -> Self {
let mut v = Self::default();
if let Some(a) = cid_dict.array(names::W2, r) {
v.records = parse_metrics(&a, 3, r, diags).as_chunks::<5>().0.to_vec();
}
if let Some(dw2) = cid_dict.array(names::DW2, r) {
if let Some(vy) = dw2.int_at(0).and_then(|v| i32::try_from(v).ok()) {
v.default_vy = vy;
}
if let Some(w1) = dw2.int_at(1).and_then(|v| i32::try_from(v).ok()) {
v.default_w1 = w1;
}
}
v
}
#[must_use]
pub fn width(&self, cid: Cid) -> f32 {
let cid = i32::from(cid.0);
for rec in &self.records {
if let (Some(&low), Some(&high), Some(&w1)) = (rec.first(), rec.get(1), rec.get(2))
&& low <= cid
&& cid <= high
{
return w1 as f32;
}
}
self.default_w1 as f32
}
#[must_use]
pub fn origin(&self, cid: Cid, horizontal: &CidWidths) -> (f32, f32) {
let icid = i32::from(cid.0);
for rec in &self.records {
if let (Some(&low), Some(&high), Some(&vx), Some(&vy)) =
(rec.first(), rec.get(1), rec.get(3), rec.get(4))
&& low <= icid
&& icid <= high
{
return (f32::from(vx as i16), f32::from(vy as i16));
}
}
let width = horizontal.width(CharCode(u32::from(cid.0)), cid);
((width / 2.0).trunc(), self.default_vy as f32)
}
#[cfg(test)]
#[must_use]
pub fn default_origin_y(&self) -> f32 {
self.default_vy as f32
}
#[cfg(test)]
#[must_use]
pub fn default_advance(&self) -> f32 {
self.default_w1 as f32
}
#[cfg(test)]
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
}
fn parse_metrics(
array: &Array,
values_per_record: usize,
r: &impl Resolve,
diags: &mut Diagnostics,
) -> Vec<i32> {
const EXPECT_FIRST: u8 = 0;
const EXPECT_LAST_OR_ARRAY: u8 = 1;
const COLLECT_VALUES: u8 = 2;
let mut out: Vec<i32> = Vec::new();
let mut state = EXPECT_FIRST;
let mut cur_element = 0usize;
let mut first_code: i32 = 0;
let mut last_code: i32 = 0;
for i in 0..array.len() {
let Some(resolved) = array.get(i, r) else {
continue;
};
let Some(obj) = resolved.as_direct() else {
continue;
};
if let Object::Array(inner) = obj {
if state != EXPECT_LAST_OR_ARRAY {
diags.record(Severity::Suspicious, DiagKind::FontWidthsTruncated, None);
return out;
}
let len = i32::try_from(inner.len()).unwrap_or(i32::MAX);
if first_code > i32::MAX - len {
state = EXPECT_FIRST;
diags.record(Severity::Suspicious, DiagKind::FontWidthsTruncated, None);
continue;
}
let mut j = 0;
while j < inner.len() {
out.push(first_code);
out.push(first_code);
for k in 0..values_per_record {
out.push(inner.int_at(j + k).unwrap_or(0) as i32);
}
first_code += 1;
j += values_per_record;
}
state = EXPECT_FIRST;
} else {
let value = obj.as_int().unwrap_or(0) as i32;
match state {
EXPECT_FIRST => {
first_code = value;
state = EXPECT_LAST_OR_ARRAY;
}
EXPECT_LAST_OR_ARRAY => {
last_code = value;
state = COLLECT_VALUES;
cur_element = 0;
}
_ => {
if cur_element == 0 {
out.push(first_code);
out.push(last_code);
}
out.push(value);
cur_element += 1;
if cur_element == values_per_record {
state = EXPECT_FIRST;
}
}
}
}
}
if state != EXPECT_FIRST {
diags.record(Severity::Suspicious, DiagKind::FontWidthsTruncated, None);
}
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
use pdfrum_object::{Name, NoResolve};
fn nums(v: &[i64]) -> Array {
Array::of(v.iter().copied().map(Object::Int))
}
fn font_dict(pairs: Vec<(&Name, Object)>) -> Dict {
Dict::from_pairs(pairs.into_iter().map(|(k, v)| (k.clone(), v)))
}
fn quiet() -> Diagnostics {
Diagnostics::default()
}
#[test]
fn no_widths_array_means_the_face_answers() {
let w = load_simple(&Dict::new(), None, &NoResolve);
assert!(w.use_face_widths);
assert!(!w.has_declared_widths());
assert_eq!(w.get(65), None);
}
#[test]
fn widths_are_indexed_from_first_char() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(65)),
(names::LAST_CHAR, Object::Int(67)),
(names::WIDTHS, Object::Array(nums(&[500, 600, 700]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(65), Some(500.0));
assert_eq!(w.get(66), Some(600.0));
assert_eq!(w.get(67), Some(700.0));
assert_eq!(w.get(68), None);
assert!(w.has_declared_widths());
}
#[test]
fn a_last_char_of_zero_is_recomputed_from_the_array() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(0)),
(names::LAST_CHAR, Object::Int(0)),
(names::WIDTHS, Object::Array(nums(&[500]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(0), Some(500.0));
assert_eq!(w.get(1), None);
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(10)),
(names::LAST_CHAR, Object::Int(0)),
(names::WIDTHS, Object::Array(nums(&[1, 2, 3]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(10), Some(1.0));
assert_eq!(w.get(12), Some(3.0));
assert_eq!(w.get(13), None);
}
#[test]
fn a_last_char_past_the_array_is_clamped_to_it() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(65)),
(names::LAST_CHAR, Object::Int(200)),
(names::WIDTHS, Object::Array(nums(&[500, 600]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(66), Some(600.0));
assert_eq!(w.get(67), None);
}
#[test]
fn a_last_char_short_of_the_array_leaves_the_tail_unread() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(65)),
(names::LAST_CHAR, Object::Int(65)),
(names::WIDTHS, Object::Array(nums(&[500, 600, 700]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(65), Some(500.0));
assert_eq!(w.get(66), None);
}
#[test]
fn a_first_char_above_255_drops_every_width() {
for first in [256, 1000, -1, -100] {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(first)),
(names::WIDTHS, Object::Array(nums(&[500]))),
]);
let w = load_simple(&d, None, &NoResolve);
assert!(w.raw.iter().all(|&x| x == WIDTH_UNSET), "first {first}");
assert!(w.has_declared_widths(), "first {first}");
}
}
#[test]
fn missing_width_fills_the_whole_table_including_outside_the_range() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(65)),
(names::LAST_CHAR, Object::Int(66)),
(names::WIDTHS, Object::Array(nums(&[500, 600]))),
]);
let desc = font_dict(vec![(names::MISSING_WIDTH, Object::Int(250))]);
let w = load_simple(&d, Some(&desc), &NoResolve);
assert_eq!(w.get(65), Some(500.0));
assert_eq!(w.get(66), Some(600.0));
assert_eq!(w.get(0), Some(250.0));
assert_eq!(w.get(255), Some(250.0));
}
#[test]
fn missing_width_alone_without_widths_does_nothing() {
let desc = font_dict(vec![(names::MISSING_WIDTH, Object::Int(250))]);
let w = load_simple(&Dict::new(), Some(&desc), &NoResolve);
assert_eq!(w.get(65), None);
assert!(w.use_face_widths);
}
#[test]
fn a_non_numeric_widths_element_reads_as_zero() {
let d = font_dict(vec![
(names::FIRST_CHAR, Object::Int(65)),
(
names::WIDTHS,
Object::Array(Array::of([
Object::Int(500),
Object::Name(Name::from("oops")),
Object::Int(700),
])),
),
]);
let w = load_simple(&d, None, &NoResolve);
assert_eq!(w.get(66), Some(0.0));
assert_eq!(w.get(67), Some(700.0));
}
#[test]
fn the_c_first_c_last_w_form_parses() {
let cid = font_dict(vec![(names::W, Object::Array(nums(&[1, 10, 500])))]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
assert_eq!(w.width(CharCode(0), Cid(1)), 500.0);
assert_eq!(w.width(CharCode(0), Cid(10)), 500.0);
assert_eq!(w.width(CharCode(0), Cid(11)), 1000.0);
}
#[test]
fn the_c_array_form_parses() {
let cid = font_dict(vec![(
names::W,
Object::Array(Array::of([
Object::Int(5),
Object::Array(nums(&[100, 200, 300])),
])),
)]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
assert_eq!(w.width(CharCode(0), Cid(5)), 100.0);
assert_eq!(w.width(CharCode(0), Cid(6)), 200.0);
assert_eq!(w.width(CharCode(0), Cid(7)), 300.0);
assert_eq!(w.width(CharCode(0), Cid(8)), 1000.0);
}
#[test]
fn both_forms_interleave() {
let cid = font_dict(vec![(
names::W,
Object::Array(Array::of([
Object::Int(1),
Object::Int(3),
Object::Int(400),
Object::Int(10),
Object::Array(nums(&[700, 800])),
])),
)]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
assert_eq!(w.width(CharCode(0), Cid(2)), 400.0);
assert_eq!(w.width(CharCode(0), Cid(10)), 700.0);
assert_eq!(w.width(CharCode(0), Cid(11)), 800.0);
}
#[test]
fn the_first_overlapping_record_wins_regardless_of_order() {
let cid = font_dict(vec![(
names::W,
Object::Array(nums(&[1, 100, 500, 50, 60, 999])),
)]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
assert_eq!(w.width(CharCode(0), Cid(55)), 500.0);
}
#[test]
fn an_array_in_the_wrong_state_aborts_the_parse() {
let cid = font_dict(vec![(
names::W,
Object::Array(Array::of([
Object::Int(1),
Object::Int(3),
Object::Int(400),
Object::Array(nums(&[1, 2])),
Object::Int(20),
Object::Int(30),
Object::Int(600),
])),
)]);
let mut diags = quiet();
let w = CidWidths::load(&cid, &NoResolve, &mut diags);
assert_eq!(w.width(CharCode(0), Cid(2)), 400.0);
assert_eq!(w.width(CharCode(0), Cid(25)), 1000.0);
assert!(diags.contains(&DiagKind::FontWidthsTruncated));
}
#[test]
fn a_trailing_partial_group_is_dropped() {
let cid = font_dict(vec![(names::W, Object::Array(nums(&[1, 3, 400, 20, 30])))]);
let mut diags = quiet();
let w = CidWidths::load(&cid, &NoResolve, &mut diags);
assert_eq!(w.len(), 1);
assert_eq!(w.width(CharCode(0), Cid(25)), 1000.0);
assert!(diags.contains(&DiagKind::FontWidthsTruncated));
}
#[test]
fn a_partial_group_inside_an_array_reads_as_zero() {
let cid = font_dict(vec![(
names::W2,
Object::Array(Array::of([
Object::Int(0),
Object::Array(nums(&[1, 2, 3, 4])),
])),
)]);
let v = VerticalMetrics::load(&cid, &NoResolve, &mut quiet());
assert_eq!(v.len(), 2);
assert_eq!(v.width(Cid(0)), 1.0);
assert_eq!(v.width(Cid(1)), 4.0);
}
#[test]
fn dw_defaults_to_1000_and_is_honoured_when_present() {
let w = CidWidths::load(&Dict::new(), &NoResolve, &mut quiet());
assert_eq!(w.default_width(), 1000.0);
let cid = font_dict(vec![(names::DW, Object::Int(742))]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
assert_eq!(w.width(CharCode(0), Cid(9)), 742.0);
}
#[test]
fn the_gb2312_rescue_fixes_ascii_widths() {
let mut w = CidWidths::load(&Dict::new(), &NoResolve, &mut quiet());
w.set_ansi_widths_fixed();
assert_eq!(w.width(CharCode(31), Cid(0)), 0.0);
assert_eq!(w.width(CharCode(32), Cid(0)), 500.0);
assert_eq!(w.width(CharCode(126), Cid(0)), 500.0);
assert_eq!(w.width(CharCode(127), Cid(0)), 0.0);
assert_eq!(w.width(CharCode(0x80), Cid(0)), 1000.0);
}
#[test]
fn dw2_defaults_to_the_iso_values() {
let v = VerticalMetrics::default();
assert_eq!(v.default_origin_y(), 880.0);
assert_eq!(v.default_advance(), -1000.0);
}
#[test]
fn w2_records_hold_five_values() {
let cid = font_dict(vec![(
names::W2,
Object::Array(nums(&[10, 20, -900, 450, 800])),
)]);
let v = VerticalMetrics::load(&cid, &NoResolve, &mut quiet());
assert_eq!(v.width(Cid(15)), -900.0);
assert_eq!(v.origin(Cid(15), &CidWidths::default()), (450.0, 800.0));
}
#[test]
fn a_cid_with_no_w2_record_takes_half_the_horizontal_width() {
let cid = font_dict(vec![(names::W, Object::Array(nums(&[1, 10, 741])))]);
let h = CidWidths::load(&cid, &NoResolve, &mut quiet());
let v = VerticalMetrics::default();
assert_eq!(v.origin(Cid(5), &h), (370.0, 880.0));
}
#[test]
fn dw2_overrides_the_defaults() {
let cid = font_dict(vec![(names::DW2, Object::Array(nums(&[700, -800])))]);
let v = VerticalMetrics::load(&cid, &NoResolve, &mut quiet());
assert_eq!(v.default_origin_y(), 700.0);
assert_eq!(v.default_advance(), -800.0);
}
#[test]
fn vertical_origins_narrow_through_i16() {
let cid = font_dict(vec![(
names::W2,
Object::Array(nums(&[0, 0, -1000, 40000, 0])),
)]);
let v = VerticalMetrics::load(&cid, &NoResolve, &mut quiet());
assert_eq!(
v.origin(Cid(0), &CidWidths::default()).0,
f32::from(40000i32 as i16)
);
}
#[test]
fn malformed_w_arrays_never_panic() {
for a in [
nums(&[]),
nums(&[1]),
nums(&[4_294_967_295, -2_147_483_648, 5]),
Array::of([Object::Array(nums(&[1, 2]))]),
Array::of([Object::Null, Object::Int(1), Object::Int(2), Object::Int(3)]),
Array::of([
Object::Int(i32::MAX.into()),
Object::Array(nums(&[1, 2, 3])),
]),
] {
let cid = font_dict(vec![(names::W, Object::Array(a))]);
let w = CidWidths::load(&cid, &NoResolve, &mut quiet());
let _ = w.width(CharCode(0), Cid(0));
let _ = w.width(CharCode(0), Cid(u16::MAX));
}
}
}