pub const PT_PER_IN: f32 = 72.0;
pub const MM_PER_IN: f32 = 25.4;
pub fn mm_to_pt(mm: f32) -> f32 {
mm / MM_PER_IN * PT_PER_IN
}
pub fn pt_to_mm(pt: f32) -> f32 {
pt / PT_PER_IN * MM_PER_IN
}
pub fn in_to_pt(inch: f32) -> f32 {
inch * PT_PER_IN
}
pub fn pt_to_in(pt: f32) -> f32 {
pt / PT_PER_IN
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
pub fn from_mm(w_mm: f32, h_mm: f32) -> Self {
Self::new(mm_to_pt(w_mm), mm_to_pt(h_mm))
}
pub fn is_landscape(&self) -> bool {
self.width > self.height
}
pub fn landscape(self) -> Self {
if self.is_landscape() {
self
} else {
Self::new(self.height, self.width)
}
}
pub fn portrait(self) -> Self {
if self.is_landscape() {
Self::new(self.height, self.width)
} else {
self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x0: f32,
pub y0: f32,
pub x1: f32,
pub y1: f32,
}
impl Rect {
pub const fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
Self { x0, y0, x1, y1 }
}
pub fn from_size(s: Size) -> Self {
Self::new(0.0, 0.0, s.width, s.height)
}
pub fn width(&self) -> f32 {
self.x1 - self.x0
}
pub fn height(&self) -> f32 {
self.y1 - self.y0
}
pub fn size(&self) -> Size {
Size::new(self.width(), self.height())
}
pub fn to_mediabox(&self) -> [f32; 4] {
[self.x0, self.y0, self.x1, self.y1]
}
pub fn from_mediabox(b: [f32; 4]) -> Self {
Self::new(
b[0].min(b[2]),
b[1].min(b[3]),
b[0].max(b[2]),
b[1].max(b[3]),
)
}
}
pub fn page_size(name: &str) -> Option<Size> {
let s = match name.trim().to_ascii_uppercase().as_str() {
"A3" => Size::from_mm(297.0, 420.0),
"A4" => Size::from_mm(210.0, 297.0),
"A5" => Size::from_mm(148.0, 210.0),
"A6" => Size::from_mm(105.0, 148.0),
"B5" => Size::from_mm(176.0, 250.0),
"LETTER" => Size::new(in_to_pt(8.5), in_to_pt(11.0)),
"LEGAL" => Size::new(in_to_pt(8.5), in_to_pt(14.0)),
"TABLOID" => Size::new(in_to_pt(11.0), in_to_pt(17.0)),
"TRADE_6X9" | "6X9" => Size::new(in_to_pt(6.0), in_to_pt(9.0)),
"DIGEST_5.5X8.5" | "5.5X8.5" => Size::new(in_to_pt(5.5), in_to_pt(8.5)),
"POCKET_4.25X6.87" => Size::new(in_to_pt(4.25), in_to_pt(6.87)),
_ => return None,
};
Some(s)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 0.01, "{a} !≈ {b}");
}
#[test]
fn unit_conversions_round_trip() {
approx(mm_to_pt(25.4), 72.0); approx(pt_to_mm(72.0), 25.4);
approx(in_to_pt(1.0), 72.0);
approx(pt_to_in(72.0), 1.0);
approx(pt_to_mm(mm_to_pt(123.4)), 123.4);
}
#[test]
fn a4_and_letter_in_points() {
let a4 = page_size("a4").unwrap(); approx(a4.width, 595.276);
approx(a4.height, 841.890);
let letter = page_size("Letter").unwrap();
approx(letter.width, 612.0);
approx(letter.height, 792.0);
assert!(page_size("nope").is_none());
}
#[test]
fn orientation_swaps() {
let a4 = page_size("A4").unwrap();
assert!(!a4.is_landscape());
let land = a4.landscape();
assert!(land.is_landscape());
approx(land.width, a4.height);
approx(land.portrait().width, a4.width); assert_eq!(a4.landscape().landscape(), a4.landscape()); }
#[test]
fn rect_size_and_mediabox() {
let r = Rect::from_size(Size::new(612.0, 792.0));
approx(r.width(), 612.0);
approx(r.height(), 792.0);
assert_eq!(r.to_mediabox(), [0.0, 0.0, 612.0, 792.0]);
let n = Rect::from_mediabox([612.0, 792.0, 0.0, 0.0]);
assert_eq!(n, Rect::new(0.0, 0.0, 612.0, 792.0));
approx(n.width(), 612.0);
}
}