#[allow(dead_code)]
mod annotations;
mod render;
use crate::error::{Error, Result};
use lopdf::Document;
pub use render::detect_bbox_by_rendering;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub left: f64,
pub bottom: f64,
pub right: f64,
pub top: f64,
}
impl BoundingBox {
pub fn new(left: f64, bottom: f64, right: f64, top: f64) -> Result<Self> {
if left >= right {
return Err(Error::InvalidBoundingBox(format!(
"left ({}) must be less than right ({})",
left, right
)));
}
if bottom >= top {
return Err(Error::InvalidBoundingBox(format!(
"bottom ({}) must be less than top ({})",
bottom, top
)));
}
Ok(Self {
left,
bottom,
right,
top,
})
}
pub fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 4 {
return Err(Error::InvalidBoundingBox(format!(
"expected 4 values, got {}",
parts.len()
)));
}
let left = parts[0]
.parse::<f64>()
.map_err(|e| Error::InvalidBoundingBox(format!("invalid left value: {}", e)))?;
let bottom = parts[1]
.parse::<f64>()
.map_err(|e| Error::InvalidBoundingBox(format!("invalid bottom value: {}", e)))?;
let right = parts[2]
.parse::<f64>()
.map_err(|e| Error::InvalidBoundingBox(format!("invalid right value: {}", e)))?;
let top = parts[3]
.parse::<f64>()
.map_err(|e| Error::InvalidBoundingBox(format!("invalid top value: {}", e)))?;
Self::new(left, bottom, right, top)
}
pub fn width(&self) -> f64 {
self.right - self.left
}
pub fn height(&self) -> f64 {
self.top - self.bottom
}
pub fn with_margins(&self, margins: &crate::margins::Margins) -> Self {
Self {
left: self.left - margins.left,
bottom: self.bottom - margins.bottom,
right: self.right + margins.right,
top: self.top + margins.top,
}
}
pub fn clamp_to_page(&self, page_width: f64, page_height: f64) -> Self {
Self {
left: self.left.max(0.0),
bottom: self.bottom.max(0.0),
right: self.right.min(page_width),
top: self.top.min(page_height),
}
}
pub fn union(&self, other: &BoundingBox) -> Self {
Self {
left: self.left.min(other.left),
bottom: self.bottom.min(other.bottom),
right: self.right.max(other.right),
top: self.top.max(other.top),
}
}
}
pub fn detect_bbox(doc: &mut Document, page_num: usize) -> Result<BoundingBox> {
let mut pdf_bytes = Vec::new();
doc.save_to(&mut pdf_bytes)
.map_err(|e| Error::PdfParse(format!("failed to serialize PDF: {}", e)))?;
detect_bbox_by_rendering(&pdf_bytes, page_num, Some(72.0))
}
#[allow(dead_code)]
pub(crate) fn get_media_box(page: &lopdf::Dictionary) -> Result<BoundingBox> {
let media_box = page
.get(b"MediaBox")
.map_err(|e| Error::PdfParse(format!("MediaBox not found: {}", e)))?
.as_array()
.map_err(|e| Error::PdfParse(format!("MediaBox is not an array: {}", e)))?;
if media_box.len() != 4 {
return Err(Error::PdfParse(format!(
"MediaBox has wrong length: {}",
media_box.len()
)));
}
let left = media_box[0]
.as_f32()
.map(|f| f as f64)
.or_else(|_| media_box[0].as_i64().map(|i| i as f64))
.map_err(|e| Error::PdfParse(format!("invalid MediaBox left: {}", e)))?;
let bottom = media_box[1]
.as_f32()
.map(|f| f as f64)
.or_else(|_| media_box[1].as_i64().map(|i| i as f64))
.map_err(|e| Error::PdfParse(format!("invalid MediaBox bottom: {}", e)))?;
let right = media_box[2]
.as_f32()
.map(|f| f as f64)
.or_else(|_| media_box[2].as_i64().map(|i| i as f64))
.map_err(|e| Error::PdfParse(format!("invalid MediaBox right: {}", e)))?;
let top = media_box[3]
.as_f32()
.map(|f| f as f64)
.or_else(|_| media_box[3].as_i64().map(|i| i as f64))
.map_err(|e| Error::PdfParse(format!("invalid MediaBox top: {}", e)))?;
BoundingBox::new(left, bottom, right, top)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bbox_new() {
let bbox = BoundingBox::new(10.0, 20.0, 100.0, 200.0).unwrap();
assert_eq!(bbox.left, 10.0);
assert_eq!(bbox.bottom, 20.0);
assert_eq!(bbox.right, 100.0);
assert_eq!(bbox.top, 200.0);
}
#[test]
fn test_bbox_invalid() {
assert!(BoundingBox::new(100.0, 20.0, 10.0, 200.0).is_err());
assert!(BoundingBox::new(10.0, 200.0, 100.0, 20.0).is_err());
}
#[test]
fn test_bbox_dimensions() {
let bbox = BoundingBox::new(10.0, 20.0, 110.0, 220.0).unwrap();
assert_eq!(bbox.width(), 100.0);
assert_eq!(bbox.height(), 200.0);
}
#[test]
fn test_bbox_from_str() {
let bbox = BoundingBox::from_str("10 20 100 200").unwrap();
assert_eq!(bbox.left, 10.0);
assert_eq!(bbox.bottom, 20.0);
assert_eq!(bbox.right, 100.0);
assert_eq!(bbox.top, 200.0);
}
#[test]
fn test_bbox_with_margins() {
use crate::margins::Margins;
let bbox = BoundingBox::new(10.0, 20.0, 100.0, 200.0).unwrap();
let margins = Margins::uniform(5.0);
let expanded = bbox.with_margins(&margins);
assert_eq!(expanded.left, 5.0);
assert_eq!(expanded.bottom, 15.0);
assert_eq!(expanded.right, 105.0);
assert_eq!(expanded.top, 205.0);
}
#[test]
fn test_bbox_union() {
let bbox1 = BoundingBox::new(10.0, 20.0, 100.0, 200.0).unwrap();
let bbox2 = BoundingBox::new(5.0, 30.0, 90.0, 210.0).unwrap();
let union = bbox1.union(&bbox2);
assert_eq!(union.left, 5.0); assert_eq!(union.bottom, 20.0); assert_eq!(union.right, 100.0); assert_eq!(union.top, 210.0); }
}