use crate::geometry::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoordinateSystem {
PdfStandard,
ScreenSpace,
Custom(TransformMatrix),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TransformMatrix {
pub a: f64,
pub b: f64,
pub c: f64,
pub d: f64,
pub e: f64,
pub f: f64,
}
impl Default for CoordinateSystem {
fn default() -> Self {
Self::PdfStandard
}
}
impl TransformMatrix {
pub const IDENTITY: Self = Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
};
pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
Self { a, b, c, d, e, f }
}
pub fn translate(tx: f64, ty: f64) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
}
}
pub fn scale(sx: f64, sy: f64) -> Self {
Self {
a: sx,
b: 0.0,
c: 0.0,
d: sy,
e: 0.0,
f: 0.0,
}
}
pub fn rotate(angle: f64) -> Self {
let cos = angle.cos();
let sin = angle.sin();
Self {
a: cos,
b: sin,
c: -sin,
d: cos,
e: 0.0,
f: 0.0,
}
}
pub fn flip_y(page_height: f64) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: -1.0,
e: 0.0,
f: page_height,
}
}
pub fn multiply(&self, other: &TransformMatrix) -> Self {
Self {
a: self.a * other.a + self.c * other.b,
b: self.b * other.a + self.d * other.b,
c: self.a * other.c + self.c * other.d,
d: self.b * other.c + self.d * other.d,
e: self.a * other.e + self.c * other.f + self.e,
f: self.b * other.e + self.d * other.f + self.f,
}
}
pub fn transform_point(&self, point: Point) -> Point {
Point::new(
self.a * point.x + self.c * point.y + self.e,
self.b * point.x + self.d * point.y + self.f,
)
}
pub fn to_pdf_ctm(&self) -> String {
format!(
"{:.6} {:.6} {:.6} {:.6} {:.6} {:.6} cm",
self.a, self.b, self.c, self.d, self.e, self.f
)
}
}
impl CoordinateSystem {
pub fn to_pdf_standard_matrix(&self, page_height: f64) -> TransformMatrix {
match *self {
Self::PdfStandard => TransformMatrix::IDENTITY,
Self::ScreenSpace => TransformMatrix::flip_y(page_height),
Self::Custom(matrix) => matrix,
}
}
pub fn to_pdf_standard(&self, point: Point, page_height: f64) -> Point {
let matrix = self.to_pdf_standard_matrix(page_height);
matrix.transform_point(point)
}
pub fn y_to_pdf_standard(&self, y: f64, page_height: f64) -> f64 {
match *self {
Self::PdfStandard => y,
Self::ScreenSpace => page_height - y,
Self::Custom(matrix) => {
let transformed = matrix.transform_point(Point::new(0.0, y));
transformed.y
}
}
}
pub fn grows_upward(&self) -> bool {
match *self {
Self::PdfStandard => true,
Self::ScreenSpace => false,
Self::Custom(matrix) => matrix.d > 0.0, }
}
}
#[derive(Debug)]
pub struct RenderContext {
pub coordinate_system: CoordinateSystem,
pub page_width: f64,
pub page_height: f64,
pub current_transform: TransformMatrix,
}
impl RenderContext {
pub fn new(coordinate_system: CoordinateSystem, page_width: f64, page_height: f64) -> Self {
let current_transform = coordinate_system.to_pdf_standard_matrix(page_height);
Self {
coordinate_system,
page_width,
page_height,
current_transform,
}
}
pub fn to_pdf_standard(&self, point: Point) -> Point {
self.coordinate_system
.to_pdf_standard(point, self.page_height)
}
pub fn y_to_pdf(&self, y: f64) -> f64 {
self.coordinate_system
.y_to_pdf_standard(y, self.page_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Point;
#[test]
fn test_transform_matrix_identity() {
let identity = TransformMatrix::IDENTITY;
let point = Point::new(10.0, 20.0);
let transformed = identity.transform_point(point);
assert_eq!(transformed, point);
}
#[test]
fn test_transform_matrix_translate() {
let translate = TransformMatrix::translate(5.0, 10.0);
let point = Point::new(1.0, 2.0);
let transformed = translate.transform_point(point);
assert_eq!(transformed, Point::new(6.0, 12.0));
}
#[test]
fn test_transform_matrix_scale() {
let scale = TransformMatrix::scale(2.0, 3.0);
let point = Point::new(4.0, 5.0);
let transformed = scale.transform_point(point);
assert_eq!(transformed, Point::new(8.0, 15.0));
}
#[test]
fn test_coordinate_system_pdf_standard() {
let coord_system = CoordinateSystem::PdfStandard;
let page_height = 842.0;
let point = Point::new(100.0, 200.0);
let pdf_point = coord_system.to_pdf_standard(point, page_height);
assert_eq!(pdf_point, point); }
#[test]
fn test_coordinate_system_screen_space() {
let coord_system = CoordinateSystem::ScreenSpace;
let page_height = 842.0;
let point = Point::new(100.0, 200.0);
let pdf_point = coord_system.to_pdf_standard(point, page_height);
assert_eq!(pdf_point, Point::new(100.0, 642.0)); }
#[test]
fn test_y_flip_matrix() {
let page_height = 800.0;
let flip = TransformMatrix::flip_y(page_height);
let top_screen = Point::new(0.0, 0.0);
let top_pdf = flip.transform_point(top_screen);
assert_eq!(top_pdf, Point::new(0.0, 800.0));
let bottom_screen = Point::new(0.0, 800.0);
let bottom_pdf = flip.transform_point(bottom_screen);
assert_eq!(bottom_pdf, Point::new(0.0, 0.0));
}
#[test]
fn test_render_context() {
let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
let screen_point = Point::new(100.0, 100.0);
let pdf_point = context.to_pdf_standard(screen_point);
assert_eq!(pdf_point, Point::new(100.0, 742.0));
}
#[test]
fn test_transform_matrix_rotate_90_degrees() {
let rotate = TransformMatrix::rotate(std::f64::consts::FRAC_PI_2); let point = Point::new(1.0, 0.0);
let transformed = rotate.transform_point(point);
assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
assert!((transformed.y - 1.0).abs() < 1e-10, "Y should be ~1");
}
#[test]
fn test_transform_matrix_rotate_180_degrees() {
let rotate = TransformMatrix::rotate(std::f64::consts::PI); let point = Point::new(1.0, 0.0);
let transformed = rotate.transform_point(point);
assert!((transformed.x - (-1.0)).abs() < 1e-10, "X should be ~-1");
assert!((transformed.y - 0.0).abs() < 1e-10, "Y should be ~0");
}
#[test]
fn test_transform_matrix_rotate_270_degrees() {
let rotate = TransformMatrix::rotate(3.0 * std::f64::consts::FRAC_PI_2); let point = Point::new(1.0, 0.0);
let transformed = rotate.transform_point(point);
assert!((transformed.x - 0.0).abs() < 1e-10, "X should be ~0");
assert!((transformed.y - (-1.0)).abs() < 1e-10, "Y should be ~-1");
}
#[test]
fn test_transform_matrix_multiply_identity() {
let matrix = TransformMatrix::new(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
let result = matrix.multiply(&TransformMatrix::IDENTITY);
assert_eq!(result.a, 2.0);
assert_eq!(result.b, 3.0);
assert_eq!(result.c, 4.0);
assert_eq!(result.d, 5.0);
assert_eq!(result.e, 6.0);
assert_eq!(result.f, 7.0);
}
#[test]
fn test_transform_matrix_multiply_translate_then_scale() {
let translate = TransformMatrix::translate(10.0, 20.0);
let scale = TransformMatrix::scale(2.0, 3.0);
let combined = translate.multiply(&scale);
let point = Point::new(5.0, 5.0);
let transformed = combined.transform_point(point);
assert_eq!(transformed.x, 20.0);
assert_eq!(transformed.y, 35.0);
}
#[test]
fn test_transform_matrix_multiply_scale_then_translate() {
let scale = TransformMatrix::scale(2.0, 3.0);
let translate = TransformMatrix::translate(10.0, 20.0);
let combined = scale.multiply(&translate);
let point = Point::new(5.0, 5.0);
let transformed = combined.transform_point(point);
assert_eq!(transformed.x, 30.0);
assert_eq!(transformed.y, 75.0);
}
#[test]
fn test_transform_matrix_to_pdf_ctm() {
let matrix = TransformMatrix::new(1.5, 0.5, -0.5, 2.0, 10.0, 20.0);
let ctm = matrix.to_pdf_ctm();
assert_eq!(
ctm,
"1.500000 0.500000 -0.500000 2.000000 10.000000 20.000000 cm"
);
}
#[test]
fn test_transform_matrix_to_pdf_ctm_with_precision() {
let matrix = TransformMatrix::new(
0.123456789,
0.987654321,
-0.111111111,
0.222222222,
100.123456,
200.987654,
);
let ctm = matrix.to_pdf_ctm();
assert!(ctm.contains("0.123457")); assert!(ctm.contains("0.987654")); assert!(ctm.contains("-0.111111"));
assert!(ctm.contains("0.222222"));
assert!(ctm.contains("100.123456"));
assert!(ctm.contains("200.987654"));
assert!(ctm.ends_with(" cm"));
}
#[test]
fn test_transform_matrix_flip_y_zero_height() {
let flip = TransformMatrix::flip_y(0.0);
let point = Point::new(100.0, 50.0);
let transformed = flip.transform_point(point);
assert_eq!(transformed.x, 100.0);
assert_eq!(transformed.y, -50.0);
}
#[test]
fn test_transform_matrix_flip_y_negative_height() {
let flip = TransformMatrix::flip_y(-100.0);
let point = Point::new(50.0, 25.0);
let transformed = flip.transform_point(point);
assert_eq!(transformed.x, 50.0);
assert_eq!(transformed.y, -125.0);
}
#[test]
fn test_transform_matrix_scale_zero() {
let scale = TransformMatrix::scale(0.0, 0.0);
let point = Point::new(100.0, 200.0);
let transformed = scale.transform_point(point);
assert_eq!(transformed.x, 0.0);
assert_eq!(transformed.y, 0.0);
}
#[test]
fn test_transform_matrix_scale_negative() {
let scale = TransformMatrix::scale(-1.0, -2.0);
let point = Point::new(10.0, 20.0);
let transformed = scale.transform_point(point);
assert_eq!(transformed.x, -10.0);
assert_eq!(transformed.y, -40.0);
}
#[test]
fn test_transform_matrix_translate_zero() {
let translate = TransformMatrix::translate(0.0, 0.0);
let point = Point::new(50.0, 75.0);
let transformed = translate.transform_point(point);
assert_eq!(transformed, point);
}
#[test]
fn test_transform_matrix_translate_negative() {
let translate = TransformMatrix::translate(-10.0, -20.0);
let point = Point::new(100.0, 200.0);
let transformed = translate.transform_point(point);
assert_eq!(transformed.x, 90.0);
assert_eq!(transformed.y, 180.0);
}
#[test]
fn test_coordinate_system_default() {
let default_cs = CoordinateSystem::default();
assert!(
matches!(default_cs, CoordinateSystem::PdfStandard),
"Default should be PdfStandard"
);
}
#[test]
fn test_coordinate_system_pdf_standard_identity() {
let cs = CoordinateSystem::PdfStandard;
let matrix = cs.to_pdf_standard_matrix(500.0);
assert_eq!(matrix.a, 1.0);
assert_eq!(matrix.b, 0.0);
assert_eq!(matrix.c, 0.0);
assert_eq!(matrix.d, 1.0);
assert_eq!(matrix.e, 0.0);
assert_eq!(matrix.f, 0.0);
}
#[test]
fn test_coordinate_system_screen_space_flip() {
let cs = CoordinateSystem::ScreenSpace;
let page_height = 600.0;
let matrix = cs.to_pdf_standard_matrix(page_height);
assert_eq!(matrix.a, 1.0);
assert_eq!(matrix.b, 0.0);
assert_eq!(matrix.c, 0.0);
assert_eq!(matrix.d, -1.0);
assert_eq!(matrix.e, 0.0);
assert_eq!(matrix.f, page_height);
}
#[test]
fn test_coordinate_system_custom_matrix() {
let custom_matrix = TransformMatrix::new(2.0, 0.0, 0.0, 2.0, 50.0, 100.0);
let cs = CoordinateSystem::Custom(custom_matrix);
let retrieved_matrix = cs.to_pdf_standard_matrix(500.0);
assert_eq!(retrieved_matrix.a, 2.0);
assert_eq!(retrieved_matrix.b, 0.0);
assert_eq!(retrieved_matrix.c, 0.0);
assert_eq!(retrieved_matrix.d, 2.0);
assert_eq!(retrieved_matrix.e, 50.0);
assert_eq!(retrieved_matrix.f, 100.0);
}
#[test]
fn test_coordinate_system_y_to_pdf_standard_pdf_standard() {
let cs = CoordinateSystem::PdfStandard;
let y = 200.0;
let page_height = 842.0;
let pdf_y = cs.y_to_pdf_standard(y, page_height);
assert_eq!(pdf_y, 200.0);
}
#[test]
fn test_coordinate_system_y_to_pdf_standard_screen_space() {
let cs = CoordinateSystem::ScreenSpace;
let y = 200.0;
let page_height = 842.0;
let pdf_y = cs.y_to_pdf_standard(y, page_height);
assert_eq!(pdf_y, 642.0);
}
#[test]
fn test_coordinate_system_y_to_pdf_standard_custom() {
let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -2.0, 0.0, 500.0);
let cs = CoordinateSystem::Custom(custom_matrix);
let y = 100.0;
let page_height = 600.0;
let pdf_y = cs.y_to_pdf_standard(y, page_height);
assert_eq!(pdf_y, 300.0);
}
#[test]
fn test_coordinate_system_grows_upward_pdf_standard() {
let cs = CoordinateSystem::PdfStandard;
assert!(cs.grows_upward(), "PdfStandard should grow upward");
}
#[test]
fn test_coordinate_system_grows_upward_screen_space() {
let cs = CoordinateSystem::ScreenSpace;
assert!(
!cs.grows_upward(),
"ScreenSpace should NOT grow upward (Y increases downward)"
);
}
#[test]
fn test_coordinate_system_grows_upward_custom_positive_d() {
let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 2.0, 0.0, 0.0);
let cs = CoordinateSystem::Custom(custom_matrix);
assert!(
cs.grows_upward(),
"Custom with positive d should grow upward"
);
}
#[test]
fn test_coordinate_system_grows_upward_custom_negative_d() {
let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 100.0);
let cs = CoordinateSystem::Custom(custom_matrix);
assert!(
!cs.grows_upward(),
"Custom with negative d should NOT grow upward"
);
}
#[test]
fn test_coordinate_system_grows_upward_custom_zero_d() {
let custom_matrix = TransformMatrix::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
let cs = CoordinateSystem::Custom(custom_matrix);
assert!(
!cs.grows_upward(),
"Custom with zero d should NOT grow upward"
);
}
#[test]
fn test_render_context_new_pdf_standard() {
let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
assert_eq!(context.page_width, 595.0);
assert_eq!(context.page_height, 842.0);
assert!(matches!(
context.coordinate_system,
CoordinateSystem::PdfStandard
));
assert_eq!(context.current_transform.a, 1.0);
assert_eq!(context.current_transform.b, 0.0);
assert_eq!(context.current_transform.c, 0.0);
assert_eq!(context.current_transform.d, 1.0);
assert_eq!(context.current_transform.e, 0.0);
assert_eq!(context.current_transform.f, 0.0);
}
#[test]
fn test_render_context_new_screen_space() {
let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
assert_eq!(context.page_width, 595.0);
assert_eq!(context.page_height, 842.0);
assert!(matches!(
context.coordinate_system,
CoordinateSystem::ScreenSpace
));
assert_eq!(context.current_transform.a, 1.0);
assert_eq!(context.current_transform.b, 0.0);
assert_eq!(context.current_transform.c, 0.0);
assert_eq!(context.current_transform.d, -1.0);
assert_eq!(context.current_transform.e, 0.0);
assert_eq!(context.current_transform.f, 842.0);
}
#[test]
fn test_render_context_new_custom() {
let custom_matrix = TransformMatrix::scale(2.0, 2.0);
let context = RenderContext::new(CoordinateSystem::Custom(custom_matrix), 595.0, 842.0);
assert_eq!(context.page_width, 595.0);
assert_eq!(context.page_height, 842.0);
assert_eq!(context.current_transform.a, 2.0);
assert_eq!(context.current_transform.d, 2.0);
}
#[test]
fn test_render_context_to_pdf_standard_pdf_standard() {
let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
let point = Point::new(100.0, 200.0);
let pdf_point = context.to_pdf_standard(point);
assert_eq!(pdf_point, point);
}
#[test]
fn test_render_context_to_pdf_standard_screen_space() {
let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
let point = Point::new(100.0, 200.0);
let pdf_point = context.to_pdf_standard(point);
assert_eq!(pdf_point, Point::new(100.0, 642.0));
}
#[test]
fn test_render_context_y_to_pdf_pdf_standard() {
let context = RenderContext::new(CoordinateSystem::PdfStandard, 595.0, 842.0);
let y = 300.0;
let pdf_y = context.y_to_pdf(y);
assert_eq!(pdf_y, 300.0);
}
#[test]
fn test_render_context_y_to_pdf_screen_space() {
let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, 842.0);
let y = 300.0;
let pdf_y = context.y_to_pdf(y);
assert_eq!(pdf_y, 542.0);
}
#[test]
fn test_render_context_edge_case_zero_dimensions() {
let context = RenderContext::new(CoordinateSystem::PdfStandard, 0.0, 0.0);
assert_eq!(context.page_width, 0.0);
assert_eq!(context.page_height, 0.0);
let point = Point::new(10.0, 20.0);
let pdf_point = context.to_pdf_standard(point);
assert_eq!(pdf_point, point);
}
#[test]
fn test_render_context_edge_case_negative_dimensions() {
let context = RenderContext::new(CoordinateSystem::ScreenSpace, 595.0, -842.0);
assert_eq!(context.page_width, 595.0);
assert_eq!(context.page_height, -842.0);
let point = Point::new(100.0, 200.0);
let pdf_point = context.to_pdf_standard(point);
assert_eq!(pdf_point, Point::new(100.0, -1042.0));
}
#[test]
fn test_coordinate_system_equality() {
let cs1 = CoordinateSystem::PdfStandard;
let cs2 = CoordinateSystem::PdfStandard;
assert_eq!(cs1, cs2);
let cs3 = CoordinateSystem::ScreenSpace;
let cs4 = CoordinateSystem::ScreenSpace;
assert_eq!(cs3, cs4);
assert_ne!(cs1, cs3);
let matrix1 = TransformMatrix::IDENTITY;
let matrix2 = TransformMatrix::IDENTITY;
let cs5 = CoordinateSystem::Custom(matrix1);
let cs6 = CoordinateSystem::Custom(matrix2);
assert_eq!(cs5, cs6);
}
#[test]
fn test_transform_matrix_equality() {
let m1 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
let m2 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
assert_eq!(m1, m2);
let m3 = TransformMatrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 7.0);
assert_ne!(m1, m3);
}
}