use crate::bitmap::AnyBitmap;
use crate::clip::Clip;
use crate::types::{LineCap, LineJoin, SPOT_NCOMPS, ScreenParams};
use color::TransferLut;
pub struct GraphicsState {
pub matrix: [f64; 6],
pub screen: ScreenParams,
pub stroke_alpha: f64,
pub fill_alpha: f64,
pub pattern_stroke_alpha: f64,
pub pattern_fill_alpha: f64,
pub line_width: f64,
pub line_cap: LineCap,
pub line_join: LineJoin,
pub miter_limit: f64,
pub flatness: f64,
pub line_dash: Vec<f64>,
pub line_dash_phase: f64,
pub clip: Clip,
pub soft_mask: Option<Box<AnyBitmap>>,
pub overprint_mode: i32,
pub rgb_transfer: [TransferLut; 3],
pub gray_transfer: TransferLut,
pub cmyk_transfer: [TransferLut; 4],
pub device_n_transfer: Vec<[u8; 256]>,
pub overprint_mask: u32,
pub delete_soft_mask: bool,
}
impl GraphicsState {
#[must_use]
pub fn new(width: u32, height: u32, vector_antialias: bool) -> Self {
debug_assert!(width > 0, "GraphicsState::new: width must be > 0");
debug_assert!(height > 0, "GraphicsState::new: height must be > 0");
let clip = Clip::new(
0.0,
0.0,
f64::from(width) - 0.001,
f64::from(height) - 0.001,
vector_antialias,
);
Self {
matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
screen: ScreenParams::default(),
stroke_alpha: 1.0,
fill_alpha: 1.0,
pattern_stroke_alpha: 1.0,
pattern_fill_alpha: 1.0,
line_width: 1.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
miter_limit: 10.0,
flatness: 1.0,
line_dash: Vec::new(),
line_dash_phase: 0.0,
clip,
soft_mask: None,
overprint_mode: 0,
rgb_transfer: [
TransferLut::IDENTITY,
TransferLut::IDENTITY,
TransferLut::IDENTITY,
],
gray_transfer: TransferLut::IDENTITY,
cmyk_transfer: [
TransferLut::IDENTITY,
TransferLut::IDENTITY,
TransferLut::IDENTITY,
TransferLut::IDENTITY,
],
device_n_transfer: vec![TransferLut::IDENTITY.0; SPOT_NCOMPS + 4],
overprint_mask: 0xFFFF_FFFF,
delete_soft_mask: false,
}
}
pub fn set_transfer(&mut self, r: &[u8; 256], g: &[u8; 256], b: &[u8; 256], gray: &[u8; 256]) {
let mut cc = [0u8; 256];
let mut cm = [0u8; 256];
let mut cy = [0u8; 256];
let mut ck = [0u8; 256];
for i in 0usize..256 {
cc[i] = 255 - self.rgb_transfer[0].0[255 - i]; cm[i] = 255 - self.rgb_transfer[1].0[255 - i]; cy[i] = 255 - self.rgb_transfer[2].0[255 - i]; ck[i] = 255 - self.gray_transfer.0[255 - i]; }
self.device_n_transfer[0] = cc;
self.device_n_transfer[1] = cm;
self.device_n_transfer[2] = cy;
self.device_n_transfer[3] = ck;
self.cmyk_transfer = [
TransferLut(cc),
TransferLut(cm),
TransferLut(cy),
TransferLut(ck),
];
self.rgb_transfer[0] = TransferLut(*r);
self.rgb_transfer[1] = TransferLut(*g);
self.rgb_transfer[2] = TransferLut(*b);
self.gray_transfer = TransferLut(*gray);
}
#[must_use]
pub fn save_clone(&self) -> Self {
Self {
matrix: self.matrix,
screen: self.screen,
stroke_alpha: self.stroke_alpha,
fill_alpha: self.fill_alpha,
pattern_stroke_alpha: self.pattern_stroke_alpha,
pattern_fill_alpha: self.pattern_fill_alpha,
line_width: self.line_width,
line_cap: self.line_cap,
line_join: self.line_join,
miter_limit: self.miter_limit,
flatness: self.flatness,
line_dash: self.line_dash.clone(),
line_dash_phase: self.line_dash_phase,
clip: self.clip.clone_shared(),
soft_mask: None, overprint_mode: self.overprint_mode,
rgb_transfer: self.rgb_transfer.clone(),
gray_transfer: self.gray_transfer.clone(),
cmyk_transfer: self.cmyk_transfer.clone(),
device_n_transfer: self.device_n_transfer.clone(),
overprint_mask: self.overprint_mask,
delete_soft_mask: false,
}
}
#[must_use]
pub fn transfer_set(&self) -> TransferSet<'_> {
TransferSet {
rgb: [
self.rgb_transfer[0].as_array(),
self.rgb_transfer[1].as_array(),
self.rgb_transfer[2].as_array(),
],
gray: self.gray_transfer.as_array(),
cmyk: [
self.cmyk_transfer[0].as_array(),
self.cmyk_transfer[1].as_array(),
self.cmyk_transfer[2].as_array(),
self.cmyk_transfer[3].as_array(),
],
device_n: &self.device_n_transfer,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct TransferSet<'a> {
pub rgb: [&'a [u8; 256]; 3],
pub gray: &'a [u8; 256],
pub cmyk: [&'a [u8; 256]; 4],
pub device_n: &'a [[u8; 256]],
}
impl TransferSet<'_> {
#[must_use]
pub fn is_identity_rgb(&self) -> bool {
use color::TransferLut;
let id = TransferLut::IDENTITY.as_array();
self.rgb.iter().all(|lut| {
std::ptr::eq(*lut, id) || *lut == id
})
}
#[must_use]
pub fn identity_rgb() -> TransferSet<'static> {
use color::TransferLut;
TransferSet {
rgb: [
TransferLut::IDENTITY.as_array(),
TransferLut::IDENTITY.as_array(),
TransferLut::IDENTITY.as_array(),
],
gray: TransferLut::IDENTITY.as_array(),
cmyk: [
TransferLut::IDENTITY.as_array(),
TransferLut::IDENTITY.as_array(),
TransferLut::IDENTITY.as_array(),
TransferLut::IDENTITY.as_array(),
],
device_n: {
static DN: [[u8; 256]; 8] = [TransferLut::IDENTITY.0; 8];
&DN
},
}
}
}
pub struct StateStack {
stack: Vec<GraphicsState>,
}
impl StateStack {
#[must_use]
pub fn new(initial: GraphicsState) -> Self {
Self {
stack: vec![initial],
}
}
#[must_use]
pub fn current(&self) -> &GraphicsState {
self.stack
.last()
.expect("StateStack invariant violated: stack is empty")
}
pub fn current_mut(&mut self) -> &mut GraphicsState {
self.stack
.last_mut()
.expect("StateStack invariant violated: stack is empty")
}
pub fn save(&mut self) {
let cloned = self
.stack
.last()
.expect("StateStack invariant violated: stack is empty")
.save_clone();
self.stack.push(cloned);
}
pub fn restore(&mut self) -> bool {
if self.stack.len() <= 1 {
return false;
}
drop(self.stack.pop());
true
}
#[must_use]
pub const fn depth(&self) -> usize {
self.stack.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_matrix_is_identity() {
let s = GraphicsState::new(100, 100, false);
assert!(
s.matrix
.iter()
.zip([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
.all(|(a, b)| (a - b).abs() < f64::EPSILON)
);
}
#[test]
fn default_alpha_is_one() {
let s = GraphicsState::new(100, 100, false);
assert!((s.stroke_alpha - 1.0).abs() < f64::EPSILON);
assert!((s.fill_alpha - 1.0).abs() < f64::EPSILON);
}
#[test]
fn default_overprint_mask() {
let s = GraphicsState::new(100, 100, false);
assert_eq!(s.overprint_mask, 0xFFFF_FFFF);
}
#[test]
fn default_transfer_is_identity() {
let s = GraphicsState::new(100, 100, false);
for i in 0u8..=255 {
assert_eq!(s.rgb_transfer[0].apply(i), i);
assert_eq!(s.gray_transfer.apply(i), i);
}
}
#[test]
fn clip_has_sub_pixel_inset() {
let s = GraphicsState::new(200, 100, false);
assert!(s.clip.x_max < 200.0);
assert!(s.clip.y_max < 100.0);
}
#[test]
fn save_restore_roundtrip() {
let initial = GraphicsState::new(100, 100, false);
let mut stack = StateStack::new(initial);
assert_eq!(stack.depth(), 1);
stack.save();
assert_eq!(stack.depth(), 2);
stack.current_mut().line_width = 5.0;
assert!(stack.restore());
assert_eq!(stack.depth(), 1);
assert!((stack.current().line_width - 1.0).abs() < f64::EPSILON); }
#[test]
fn restore_at_bottom_returns_false() {
let initial = GraphicsState::new(100, 100, false);
let mut stack = StateStack::new(initial);
assert!(!stack.restore());
assert_eq!(stack.depth(), 1);
}
#[test]
fn set_transfer_derives_cmyk() {
let mut s = GraphicsState::new(100, 100, false);
let inv: [u8; 256] = std::array::from_fn(|i| u8::try_from(255 - i).expect("i < 256"));
s.set_transfer(&inv, &inv, &inv, &inv);
for i in 0u8..=255 {
assert_eq!(s.cmyk_transfer[0].apply(i), i, "cmyk C[{i}]");
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "width must be > 0")]
fn new_panics_on_zero_width() {
let _ = GraphicsState::new(0, 100, false);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "height must be > 0")]
fn new_panics_on_zero_height() {
let _ = GraphicsState::new(100, 0, false);
}
#[test]
fn restore_never_pops_below_one() {
let initial = GraphicsState::new(100, 100, false);
let mut stack = StateStack::new(initial);
stack.save();
stack.save();
assert!(stack.restore());
assert!(stack.restore());
assert!(!stack.restore());
assert!(!stack.restore());
assert_eq!(stack.depth(), 1);
}
}