mod clip;
mod extgstate;
mod general;
mod graph;
mod marks;
mod text;
pub use clip::{ClipEntry, ClipRule, ClipStack, MAX_TEXT_OBJECTS, TextClipLimit, TextClipRun};
pub use extgstate::apply_ext_gstate;
pub(crate) use general::RenderIntent;
pub use general::{BlendMode, GeneralState};
pub use graph::StrokeParams;
pub use marks::{ContentMarks, Mark};
pub use text::TextState;
pub(crate) use text::{TextCursor, glyph_matrix, kerning_shift};
use crate::color::ColorValue;
use kurbo::Affine;
#[derive(Debug, Clone, PartialEq)]
pub struct GraphicsState {
pub ctm: Affine,
pub fill: ColorValue,
pub stroke: ColorValue,
pub stroke_params: StrokeParams,
pub clip: ClipStack,
pub general: GeneralState,
pub text: TextState,
}
impl Default for GraphicsState {
fn default() -> Self {
Self {
ctm: Affine::IDENTITY,
fill: ColorValue::default(),
stroke: ColorValue::default(),
stroke_params: StrokeParams::default(),
clip: ClipStack::new(),
general: GeneralState::default(),
text: TextState::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct StateStack {
saved: Vec<GraphicsState>,
}
impl StateStack {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn depth(&self) -> usize {
self.saved.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.saved.is_empty()
}
pub fn push(&mut self, state: &GraphicsState) {
self.saved.push(state.clone());
}
pub fn pop(&mut self, state: &mut GraphicsState) -> bool {
match self.saved.pop() {
Some(saved) => {
*state = saved;
true
}
None => false,
}
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{GraphicsState, StateStack};
use crate::color::ColorSpace;
use crate::ops::LineCap;
use kurbo::Affine;
#[test]
fn the_initial_state_is_opaque_black_and_untransformed() {
let s = GraphicsState::default();
assert_eq!(s.ctm, Affine::IDENTITY);
assert_eq!(&s.fill.components[..], &[0.0]);
assert_eq!(&s.stroke.components[..], &[0.0]);
assert!((s.general.fill_alpha - 1.0).abs() < 1e-6);
assert!(s.clip.is_empty());
assert!((s.stroke_params.width - 1.0).abs() < 1e-6);
}
#[test]
fn save_and_restore_round_trip() {
let mut stack = StateStack::new();
let mut state = GraphicsState::default();
stack.push(&state);
assert_eq!(stack.depth(), 1);
state.ctm = Affine::translate((10.0, 20.0));
state.stroke_params.cap = LineCap::Round;
state
.fill
.set_stock(ColorSpace::DeviceRgb, &[1.0, 0.0, 0.0]);
assert!(stack.pop(&mut state));
assert_eq!(state.ctm, Affine::IDENTITY);
assert_eq!(state.stroke_params.cap, LineCap::Butt);
assert_eq!(&state.fill.components[..], &[0.0]);
assert!(stack.is_empty());
}
#[test]
fn restoring_an_empty_stack_changes_nothing() {
let mut stack = StateStack::new();
let mut state = GraphicsState {
ctm: Affine::scale(2.0),
..GraphicsState::default()
};
assert!(!stack.pop(&mut state));
assert_eq!(state.ctm, Affine::scale(2.0));
}
#[test]
fn the_stack_nests_without_a_cap() {
let mut stack = StateStack::new();
let state = GraphicsState::default();
for _ in 0..1000 {
stack.push(&state);
}
assert_eq!(stack.depth(), 1000);
}
}