use crate::geometry::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Matrix {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
impl Matrix {
pub fn identity() -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: 0.0,
}
}
pub fn is_identity(&self) -> bool {
self.a == 1.0
&& self.b == 0.0
&& self.c == 0.0
&& self.d == 1.0
&& self.e == 0.0
&& self.f == 0.0
}
pub fn translation(tx: f32, ty: f32) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
}
}
pub fn scaling(sx: f32, sy: f32) -> Self {
Self {
a: sx,
b: 0.0,
c: 0.0,
d: sy,
e: 0.0,
f: 0.0,
}
}
pub fn multiply(&self, other: &Matrix) -> Matrix {
Matrix {
a: self.a * other.a + self.b * other.c,
b: self.a * other.b + self.b * other.d,
c: self.c * other.a + self.d * other.c,
d: self.c * other.b + self.d * other.d,
e: self.e * other.a + self.f * other.c + other.e,
f: self.e * other.b + self.f * other.d + other.f,
}
}
pub fn transform_point(&self, x: f32, y: f32) -> Point {
Point {
x: self.a * x + self.c * y + self.e,
y: self.b * x + self.d * y + self.f,
}
}
pub fn determinant(&self) -> f32 {
self.a * self.d - self.b * self.c
}
pub fn is_invertible(&self) -> bool {
self.determinant().abs() > f32::EPSILON
}
}
impl Default for Matrix {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone)]
pub struct GraphicsState {
pub ctm: Matrix,
pub text_matrix: Matrix,
pub text_line_matrix: Matrix,
pub char_space: f32,
pub word_space: f32,
pub horizontal_scaling: f32,
pub leading: f32,
pub font_name: Option<String>,
pub font_size: f32,
pub text_rise: f32,
pub render_mode: u8,
pub fill_color_space: String,
pub stroke_color_space: String,
pub fill_color_rgb: (f32, f32, f32),
pub stroke_color_rgb: (f32, f32, f32),
pub fill_color_cmyk: Option<(f32, f32, f32, f32)>,
pub stroke_color_cmyk: Option<(f32, f32, f32, f32)>,
pub line_width: f32,
pub dash_pattern: (Vec<f32>, f32),
pub line_cap: u8,
pub line_join: u8,
pub miter_limit: f32,
pub rendering_intent: String,
pub flatness: f32,
pub fill_alpha: f32,
pub stroke_alpha: f32,
pub blend_mode: String,
}
impl GraphicsState {
pub fn new() -> Self {
Self {
ctm: Matrix::identity(),
text_matrix: Matrix::identity(),
text_line_matrix: Matrix::identity(),
char_space: 0.0,
word_space: 0.0,
horizontal_scaling: 100.0,
leading: 0.0,
font_name: None,
font_size: 12.0,
text_rise: 0.0,
render_mode: 0,
fill_color_space: "DeviceGray".to_string(), stroke_color_space: "DeviceGray".to_string(), fill_color_rgb: (0.0, 0.0, 0.0), stroke_color_rgb: (0.0, 0.0, 0.0), fill_color_cmyk: None, stroke_color_cmyk: None, line_width: 1.0,
dash_pattern: (Vec::new(), 0.0), line_cap: 0, line_join: 0, miter_limit: 10.0, rendering_intent: "RelativeColorimetric".to_string(), flatness: 1.0, fill_alpha: 1.0, stroke_alpha: 1.0, blend_mode: "Normal".to_string(), }
}
pub fn is_dashed(&self) -> bool {
!self.dash_pattern.0.is_empty()
}
pub fn is_dotted(&self) -> bool {
if self.dash_pattern.0.len() >= 2 {
let on = self.dash_pattern.0[0];
let off = self.dash_pattern.0[1];
on < 5.0 && off < 5.0 && (on - off).abs() < 2.0
} else {
false
}
}
}
impl Default for GraphicsState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct GraphicsStateStack {
stack: Vec<GraphicsState>,
}
impl GraphicsStateStack {
pub fn new() -> Self {
Self {
stack: vec![GraphicsState::new()],
}
}
pub fn current(&self) -> &GraphicsState {
self.stack.last().expect("Stack should never be empty")
}
pub fn current_mut(&mut self) -> &mut GraphicsState {
self.stack.last_mut().expect("Stack should never be empty")
}
pub fn save(&mut self) {
let state = self.current().clone();
self.stack.push(state);
}
pub fn restore(&mut self) {
if self.stack.len() > 1 {
self.stack.pop();
}
}
pub fn depth(&self) -> usize {
self.stack.len()
}
}
impl Default for GraphicsStateStack {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matrix_identity() {
let m = Matrix::identity();
assert_eq!(m.a, 1.0);
assert_eq!(m.b, 0.0);
assert_eq!(m.c, 0.0);
assert_eq!(m.d, 1.0);
assert_eq!(m.e, 0.0);
assert_eq!(m.f, 0.0);
}
#[test]
fn test_matrix_translation() {
let m = Matrix::translation(10.0, 20.0);
assert_eq!(m.e, 10.0);
assert_eq!(m.f, 20.0);
let p = m.transform_point(5.0, 10.0);
assert_eq!(p.x, 15.0);
assert_eq!(p.y, 30.0);
}
#[test]
fn test_matrix_scaling() {
let m = Matrix::scaling(2.0, 3.0);
assert_eq!(m.a, 2.0);
assert_eq!(m.d, 3.0);
let p = m.transform_point(10.0, 10.0);
assert_eq!(p.x, 20.0);
assert_eq!(p.y, 30.0);
}
#[test]
fn test_matrix_multiply() {
let m1 = Matrix::translation(10.0, 20.0);
let m2 = Matrix::scaling(2.0, 2.0);
let result = m1.multiply(&m2);
let p = result.transform_point(5.0, 5.0);
assert_eq!(p.x, 30.0); assert_eq!(p.y, 50.0); }
#[test]
fn test_matrix_multiply_order() {
let m1 = Matrix::translation(10.0, 0.0);
let m2 = Matrix::scaling(2.0, 1.0);
let r1 = m1.multiply(&m2);
let r2 = m2.multiply(&m1);
let p = Point { x: 5.0, y: 0.0 };
let p1 = r1.transform_point(p.x, p.y);
let p2 = r2.transform_point(p.x, p.y);
assert_ne!(p1.x, p2.x);
}
#[test]
fn test_matrix_determinant() {
let m = Matrix::scaling(2.0, 3.0);
assert_eq!(m.determinant(), 6.0);
let m_identity = Matrix::identity();
assert_eq!(m_identity.determinant(), 1.0);
}
#[test]
fn test_matrix_invertible() {
let m = Matrix::scaling(2.0, 3.0);
assert!(m.is_invertible());
let m_degenerate = Matrix {
a: 1.0,
b: 2.0,
c: 2.0,
d: 4.0,
e: 0.0,
f: 0.0,
};
assert!(!m_degenerate.is_invertible());
}
#[test]
fn test_graphics_state_new() {
let state = GraphicsState::new();
assert_eq!(state.font_size, 12.0);
assert_eq!(state.horizontal_scaling, 100.0);
assert_eq!(state.char_space, 0.0);
assert_eq!(state.word_space, 0.0);
assert_eq!(state.leading, 0.0);
assert!(state.font_name.is_none());
}
#[test]
fn test_graphics_state_default() {
let state = GraphicsState::default();
assert_eq!(state.font_size, 12.0);
}
#[test]
fn test_graphics_state_stack_new() {
let stack = GraphicsStateStack::new();
assert_eq!(stack.depth(), 1);
assert_eq!(stack.current().font_size, 12.0);
}
#[test]
fn test_graphics_state_stack_save_restore() {
let mut stack = GraphicsStateStack::new();
stack.current_mut().font_size = 14.0;
assert_eq!(stack.current().font_size, 14.0);
stack.save();
assert_eq!(stack.depth(), 2);
assert_eq!(stack.current().font_size, 14.0);
stack.current_mut().font_size = 16.0;
assert_eq!(stack.current().font_size, 16.0);
stack.restore();
assert_eq!(stack.depth(), 1);
assert_eq!(stack.current().font_size, 14.0);
}
#[test]
fn test_graphics_state_stack_restore_limit() {
let mut stack = GraphicsStateStack::new();
assert_eq!(stack.depth(), 1);
stack.restore();
assert_eq!(stack.depth(), 1);
stack.save();
stack.save();
stack.save();
assert_eq!(stack.depth(), 4);
stack.restore();
stack.restore();
stack.restore();
assert_eq!(stack.depth(), 1);
stack.restore();
assert_eq!(stack.depth(), 1);
}
#[test]
fn test_graphics_state_color() {
let mut state = GraphicsState::new();
assert_eq!(state.fill_color_rgb, (0.0, 0.0, 0.0));
assert_eq!(state.stroke_color_rgb, (0.0, 0.0, 0.0));
state.fill_color_rgb = (1.0, 0.0, 0.0);
state.stroke_color_rgb = (0.0, 1.0, 0.0);
assert_eq!(state.fill_color_rgb, (1.0, 0.0, 0.0));
assert_eq!(state.stroke_color_rgb, (0.0, 1.0, 0.0));
}
#[test]
fn test_graphics_state_clone() {
let mut state1 = GraphicsState::new();
state1.font_size = 14.0;
let state2 = state1.clone();
assert_eq!(state2.font_size, 14.0);
}
#[test]
fn test_matrix_transform_origin() {
let m = Matrix::identity();
let p = m.transform_point(0.0, 0.0);
assert_eq!(p.x, 0.0);
assert_eq!(p.y, 0.0);
}
#[test]
fn test_matrix_default() {
let m = Matrix::default();
assert_eq!(m.a, 1.0);
assert_eq!(m.d, 1.0);
}
}