1mod clip;
12mod extgstate;
13mod general;
14mod graph;
15mod marks;
16mod text;
17
18pub use clip::{ClipEntry, ClipRule, ClipStack, MAX_TEXT_OBJECTS, TextClipLimit, TextClipRun};
19pub use extgstate::apply_ext_gstate;
20pub(crate) use general::RenderIntent;
21pub use general::{BlendMode, GeneralState};
22pub use graph::StrokeParams;
23pub use marks::{ContentMarks, Mark};
24pub use text::TextState;
25pub(crate) use text::{TextCursor, glyph_matrix, kerning_shift};
26
27use crate::color::ColorValue;
28use kurbo::Affine;
29
30#[derive(Debug, Clone, PartialEq)]
35pub struct GraphicsState {
36 pub ctm: Affine,
38 pub fill: ColorValue,
40 pub stroke: ColorValue,
42 pub stroke_params: StrokeParams,
44 pub clip: ClipStack,
46 pub general: GeneralState,
48 pub text: TextState,
50}
51
52impl Default for GraphicsState {
53 fn default() -> Self {
56 Self {
57 ctm: Affine::IDENTITY,
58 fill: ColorValue::default(),
59 stroke: ColorValue::default(),
60 stroke_params: StrokeParams::default(),
61 clip: ClipStack::new(),
62 general: GeneralState::default(),
63 text: TextState::default(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Default)]
73pub struct StateStack {
74 saved: Vec<GraphicsState>,
75}
76
77impl StateStack {
78 #[must_use]
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 #[must_use]
86 pub fn depth(&self) -> usize {
87 self.saved.len()
88 }
89
90 #[must_use]
92 pub fn is_empty(&self) -> bool {
93 self.saved.is_empty()
94 }
95
96 pub fn push(&mut self, state: &GraphicsState) {
98 self.saved.push(state.clone());
99 }
100
101 pub fn pop(&mut self, state: &mut GraphicsState) -> bool {
107 match self.saved.pop() {
108 Some(saved) => {
109 *state = saved;
110 true
111 }
112 None => false,
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 #![allow(
123 clippy::unreadable_literal,
124 clippy::float_cmp,
125 clippy::indexing_slicing,
126 clippy::cast_precision_loss,
127 clippy::cast_possible_truncation,
128 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
129 )]
130
131 use super::{GraphicsState, StateStack};
132 use crate::color::ColorSpace;
133 use crate::ops::LineCap;
134 use kurbo::Affine;
135
136 #[test]
137 fn the_initial_state_is_opaque_black_and_untransformed() {
138 let s = GraphicsState::default();
139 assert_eq!(s.ctm, Affine::IDENTITY);
140 assert_eq!(&s.fill.components[..], &[0.0]);
141 assert_eq!(&s.stroke.components[..], &[0.0]);
142 assert!((s.general.fill_alpha - 1.0).abs() < 1e-6);
143 assert!(s.clip.is_empty());
144 assert!((s.stroke_params.width - 1.0).abs() < 1e-6);
145 }
146
147 #[test]
148 fn save_and_restore_round_trip() {
149 let mut stack = StateStack::new();
150 let mut state = GraphicsState::default();
151 stack.push(&state);
152 assert_eq!(stack.depth(), 1);
153
154 state.ctm = Affine::translate((10.0, 20.0));
155 state.stroke_params.cap = LineCap::Round;
156 state
157 .fill
158 .set_stock(ColorSpace::DeviceRgb, &[1.0, 0.0, 0.0]);
159
160 assert!(stack.pop(&mut state));
161 assert_eq!(state.ctm, Affine::IDENTITY);
162 assert_eq!(state.stroke_params.cap, LineCap::Butt);
163 assert_eq!(&state.fill.components[..], &[0.0]);
164 assert!(stack.is_empty());
165 }
166
167 #[test]
168 fn restoring_an_empty_stack_changes_nothing() {
169 let mut stack = StateStack::new();
170 let mut state = GraphicsState {
171 ctm: Affine::scale(2.0),
172 ..GraphicsState::default()
173 };
174 assert!(!stack.pop(&mut state));
175 assert_eq!(state.ctm, Affine::scale(2.0));
177 }
178
179 #[test]
180 fn the_stack_nests_without_a_cap() {
181 let mut stack = StateStack::new();
182 let state = GraphicsState::default();
183 for _ in 0..1000 {
184 stack.push(&state);
185 }
186 assert_eq!(stack.depth(), 1000);
187 }
188}