use std::{collections::BTreeMap, path::Path};
use lopdf::{Object, ObjectId};
use crate::{
copy::ObjectSource, lazy::PdfSource, load::map_file, repair::with_repair_retry, Result,
};
const MAX_CHAIN: usize = 256;
const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * 72.0;
const A4: Rect = Rect {
x0: 0.0,
y0: 0.0,
x1: 210.0 * POINTS_PER_MM,
y1: 297.0 * POINTS_PER_MM,
};
const NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PageDimensions {
pub width: f32,
pub height: f32,
pub rotation: u16,
}
pub fn page_dimensions(input: &Path) -> Result<Vec<PageDimensions>> {
page_dimensions_with_password(input, None)
}
pub fn page_dimensions_with_password(
input: &Path,
password: Option<&str>,
) -> Result<Vec<PageDimensions>> {
let timing = std::env::var_os("PDQ_TIMING").is_some();
let start = std::time::Instant::now();
let mmap = map_file(input)?;
with_repair_retry(&mmap, input, password, |source| {
if timing {
eprintln!("phase parse: {:?}", start.elapsed());
}
let walk_start = std::time::Instant::now();
let dimensions = dimensions_impl(source)?;
if timing {
eprintln!("phase walk: {:?}", walk_start.elapsed());
}
Ok(dimensions)
})
}
fn dimensions_impl(source: &PdfSource) -> Result<Vec<PageDimensions>> {
let page_ids = source.page_ids()?;
let mut nodes = NodeCache::default();
Ok(page_ids
.into_iter()
.map(|id| resolve_page(source, &mut nodes, id))
.collect())
}
#[derive(Debug, Clone, Copy)]
struct NodeGeometry {
media_box: Option<Rect>,
crop_box: Option<Rect>,
rotate: Option<i64>,
parent: Option<ObjectId>,
}
#[derive(Default)]
struct NodeCache(BTreeMap<ObjectId, Option<NodeGeometry>>);
impl NodeCache {
fn get(&mut self, source: &PdfSource, id: ObjectId) -> Option<NodeGeometry> {
if let Some(cached) = self.0.get(&id) {
return *cached;
}
let geometry = node_geometry(source, id);
self.0.insert(id, geometry);
geometry
}
}
fn node_geometry(source: &PdfSource, id: ObjectId) -> Option<NodeGeometry> {
let object = source.get_object_value(id).ok()?;
let dict = object.as_dict().ok()?;
Some(NodeGeometry {
media_box: dict.get(b"MediaBox").ok().and_then(|v| rect(source, v)),
crop_box: dict.get(b"CropBox").ok().and_then(|v| rect(source, v)),
rotate: dict.get(b"Rotate").ok().and_then(|v| integer(source, v)),
parent: match dict.get(b"Parent") {
Ok(Object::Reference(parent)) => Some(*parent),
_ => None,
},
})
}
fn resolve_page(source: &PdfSource, nodes: &mut NodeCache, page_id: ObjectId) -> PageDimensions {
let mut media_box = None;
let mut crop_box = None;
let mut rotate = None;
let mut current = Some(page_id);
let mut hops = 0usize;
while let Some(id) = current {
hops += 1;
if hops > MAX_CHAIN {
break;
}
let Some(node) = nodes.get(source, id) else {
break;
};
media_box = media_box.or(node.media_box);
crop_box = crop_box.or(node.crop_box);
rotate = rotate.or(node.rotate);
if media_box.is_some() && crop_box.is_some() && rotate.is_some() {
break;
}
current = node.parent;
}
let media_box = media_box.unwrap_or(A4);
let effective = crop_box.unwrap_or(media_box).intersect(media_box);
let (mut width, mut height) = if (effective.width() as f32).abs() <= NEARLY_ZERO
|| (effective.height() as f32).abs() <= NEARLY_ZERO
{
(A4.width() as f32, A4.height() as f32)
} else {
(
effective.width().max(1.0) as f32,
effective.height().max(1.0) as f32,
)
};
if !width.is_finite() || !height.is_finite() {
(width, height) = (A4.width() as f32, A4.height() as f32);
}
let rotation = match rotate.unwrap_or(0).rem_euclid(360) {
90 => 90,
180 => 180,
270 => 270,
_ => 0,
};
if rotation == 90 || rotation == 270 {
std::mem::swap(&mut width, &mut height);
}
PageDimensions {
width,
height,
rotation,
}
}
#[derive(Debug, Clone, Copy)]
struct Rect {
x0: f64,
y0: f64,
x1: f64,
y1: f64,
}
impl Rect {
fn intersect(&self, other: Rect) -> Rect {
let x0 = self.x0.max(other.x0);
let y0 = self.y0.max(other.y0);
let x1 = self.x1.min(other.x1);
let y1 = self.y1.min(other.y1);
Rect {
x0,
y0,
x1: x1.max(x0),
y1: y1.max(y0),
}
}
fn width(&self) -> f64 {
self.x1 - self.x0
}
fn height(&self) -> f64 {
self.y1 - self.y0
}
}
fn rect(source: &PdfSource, object: &Object) -> Option<Rect> {
let object = dereference(source, object)?;
let array = object.as_array().ok()?;
let mut coords = array.iter().map(|value| number(source, value));
let x0 = coords.next()?? as f64;
let y0 = coords.next()?? as f64;
let x1 = coords.next()?? as f64;
let y1 = coords.next()?? as f64;
Some(Rect {
x0: x0.min(x1),
y0: y0.min(y1),
x1: x1.max(x0),
y1: y1.max(y0),
})
}
fn number(source: &PdfSource, object: &Object) -> Option<f32> {
match dereference(source, object)? {
Object::Integer(value) => Some(value as f32),
Object::Real(value) => Some(value),
_ => None,
}
}
fn integer(source: &PdfSource, object: &Object) -> Option<i64> {
match dereference(source, object)? {
Object::Integer(value) => Some(value),
Object::Real(value) => Some(value as i64),
_ => None,
}
}
fn dereference(source: &PdfSource, object: &Object) -> Option<Object> {
let mut object = object.clone();
let mut hops = 0usize;
while let Object::Reference(id) = object {
hops += 1;
if hops > MAX_CHAIN {
return None;
}
object = source.get_object_value(id).ok()?.into_owned();
}
Some(object)
}