Skip to main content

justpdf_render/
graphics_state.rs

1use justpdf_core::color::{Color, ColorSpace};
2use tiny_skia::Mask;
3
4/// Soft mask type (Luminosity or Alpha).
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SoftMaskSubtype {
7    Luminosity,
8    Alpha,
9}
10
11/// A resolved soft mask ready for use during rendering.
12/// Contains a tiny_skia::Mask derived from rendering the mask form XObject.
13#[derive(Clone)]
14pub struct SoftMask {
15    pub mask: Mask,
16    pub subtype: SoftMaskSubtype,
17}
18
19impl std::fmt::Debug for SoftMask {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        f.debug_struct("SoftMask")
22            .field("subtype", &self.subtype)
23            .field("mask_size", &"<Mask>")
24            .finish()
25    }
26}
27
28/// 2D affine transformation matrix [a b 0; c d 0; e f 1].
29/// PDF row-vector convention: point × matrix.
30#[derive(Debug, Clone, Copy)]
31pub struct Matrix {
32    pub a: f64,
33    pub b: f64,
34    pub c: f64,
35    pub d: f64,
36    pub e: f64,
37    pub f: f64,
38}
39
40impl Matrix {
41    pub fn identity() -> Self {
42        Self {
43            a: 1.0,
44            b: 0.0,
45            c: 0.0,
46            d: 1.0,
47            e: 0.0,
48            f: 0.0,
49        }
50    }
51
52    /// self × other
53    pub fn concat(&self, other: &Matrix) -> Matrix {
54        Matrix {
55            a: self.a * other.a + self.b * other.c,
56            b: self.a * other.b + self.b * other.d,
57            c: self.c * other.a + self.d * other.c,
58            d: self.c * other.b + self.d * other.d,
59            e: self.e * other.a + self.f * other.c + other.e,
60            f: self.e * other.b + self.f * other.d + other.f,
61        }
62    }
63
64    pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
65        (
66            self.a * x + self.c * y + self.e,
67            self.b * x + self.d * y + self.f,
68        )
69    }
70
71    pub fn translate(tx: f64, ty: f64) -> Self {
72        Self {
73            a: 1.0,
74            b: 0.0,
75            c: 0.0,
76            d: 1.0,
77            e: tx,
78            f: ty,
79        }
80    }
81
82    pub fn scale(sx: f64, sy: f64) -> Self {
83        Self {
84            a: sx,
85            b: 0.0,
86            c: 0.0,
87            d: sy,
88            e: 0.0,
89            f: 0.0,
90        }
91    }
92
93    /// Convert to tiny-skia Transform.
94    pub fn to_skia(&self) -> tiny_skia::Transform {
95        tiny_skia::Transform::from_row(
96            self.a as f32,
97            self.b as f32,
98            self.c as f32,
99            self.d as f32,
100            self.e as f32,
101            self.f as f32,
102        )
103    }
104
105    /// Effective y-scale (for font size).
106    pub fn font_size_scale(&self) -> f64 {
107        (self.b * self.b + self.d * self.d).sqrt()
108    }
109}
110
111/// Text state parameters (PDF spec 9.3).
112#[derive(Debug, Clone)]
113pub struct TextState {
114    pub char_spacing: f64,
115    pub word_spacing: f64,
116    pub horiz_scaling: f64,
117    pub leading: f64,
118    pub font_name: Vec<u8>,
119    pub font_size: f64,
120    pub text_rise: f64,
121    pub render_mode: i64,
122}
123
124impl Default for TextState {
125    fn default() -> Self {
126        Self {
127            char_spacing: 0.0,
128            word_spacing: 0.0,
129            horiz_scaling: 1.0,
130            leading: 0.0,
131            font_name: Vec::new(),
132            font_size: 12.0,
133            text_rise: 0.0,
134            render_mode: 0,
135        }
136    }
137}
138
139/// Line cap style.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum LineCap {
142    Butt = 0,
143    Round = 1,
144    Square = 2,
145}
146
147/// Line join style.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum LineJoin {
150    Miter = 0,
151    Round = 1,
152    Bevel = 2,
153}
154
155/// PDF blend mode.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum PdfBlendMode {
158    Normal,
159    Multiply,
160    Screen,
161    Overlay,
162    Darken,
163    Lighten,
164    ColorDodge,
165    ColorBurn,
166    HardLight,
167    SoftLight,
168    Difference,
169    Exclusion,
170    Hue,
171    Saturation,
172    Color,
173    Luminosity,
174}
175
176impl PdfBlendMode {
177    pub fn from_name(name: &[u8]) -> Self {
178        match name {
179            b"Multiply" => Self::Multiply,
180            b"Screen" => Self::Screen,
181            b"Overlay" => Self::Overlay,
182            b"Darken" => Self::Darken,
183            b"Lighten" => Self::Lighten,
184            b"ColorDodge" => Self::ColorDodge,
185            b"ColorBurn" => Self::ColorBurn,
186            b"HardLight" => Self::HardLight,
187            b"SoftLight" => Self::SoftLight,
188            b"Difference" => Self::Difference,
189            b"Exclusion" => Self::Exclusion,
190            b"Hue" => Self::Hue,
191            b"Saturation" => Self::Saturation,
192            b"Color" => Self::Color,
193            b"Luminosity" => Self::Luminosity,
194            _ => Self::Normal,
195        }
196    }
197
198    pub fn to_skia(self) -> tiny_skia::BlendMode {
199        match self {
200            Self::Normal => tiny_skia::BlendMode::SourceOver,
201            Self::Multiply => tiny_skia::BlendMode::Multiply,
202            Self::Screen => tiny_skia::BlendMode::Screen,
203            Self::Overlay => tiny_skia::BlendMode::Overlay,
204            Self::Darken => tiny_skia::BlendMode::Darken,
205            Self::Lighten => tiny_skia::BlendMode::Lighten,
206            Self::ColorDodge => tiny_skia::BlendMode::ColorDodge,
207            Self::ColorBurn => tiny_skia::BlendMode::ColorBurn,
208            Self::HardLight => tiny_skia::BlendMode::HardLight,
209            Self::SoftLight => tiny_skia::BlendMode::SoftLight,
210            Self::Difference => tiny_skia::BlendMode::Difference,
211            Self::Exclusion => tiny_skia::BlendMode::Exclusion,
212            Self::Hue => tiny_skia::BlendMode::Hue,
213            Self::Saturation => tiny_skia::BlendMode::Saturation,
214            Self::Color => tiny_skia::BlendMode::Color,
215            Self::Luminosity => tiny_skia::BlendMode::Luminosity,
216        }
217    }
218}
219
220/// Full graphics state for rendering.
221#[derive(Debug, Clone)]
222pub struct GraphicsState {
223    pub ctm: Matrix,
224    pub text: TextState,
225    // Stroke/fill colors
226    pub fill_color: Color,
227    pub stroke_color: Color,
228    pub fill_cs: ColorSpace,
229    pub stroke_cs: ColorSpace,
230    // Line drawing parameters
231    pub line_width: f64,
232    pub line_cap: LineCap,
233    pub line_join: LineJoin,
234    pub miter_limit: f64,
235    pub dash_pattern: Vec<f64>,
236    pub dash_phase: f64,
237    // Transparency
238    pub fill_alpha: f64,
239    pub stroke_alpha: f64,
240    pub blend_mode: PdfBlendMode,
241    // Clipping
242    pub has_clip: bool,
243    // Soft mask (from ExtGState /SMask)
244    pub soft_mask: Option<SoftMask>,
245    // Fill pattern name (when color space is /Pattern)
246    pub fill_pattern_name: Option<Vec<u8>>,
247    // Stroke pattern name (when color space is /Pattern)
248    pub stroke_pattern_name: Option<Vec<u8>>,
249    // Text matrices (only valid inside BT..ET)
250    pub text_matrix: Matrix,
251    pub text_line_matrix: Matrix,
252}
253
254impl Default for GraphicsState {
255    fn default() -> Self {
256        Self {
257            ctm: Matrix::identity(),
258            text: TextState::default(),
259            fill_color: Color::gray(0.0),
260            stroke_color: Color::gray(0.0),
261            fill_cs: ColorSpace::DeviceGray,
262            stroke_cs: ColorSpace::DeviceGray,
263            line_width: 1.0,
264            line_cap: LineCap::Butt,
265            line_join: LineJoin::Miter,
266            miter_limit: 10.0,
267            dash_pattern: Vec::new(),
268            dash_phase: 0.0,
269            fill_alpha: 1.0,
270            stroke_alpha: 1.0,
271            blend_mode: PdfBlendMode::Normal,
272            has_clip: false,
273            soft_mask: None,
274            fill_pattern_name: None,
275            stroke_pattern_name: None,
276            text_matrix: Matrix::identity(),
277            text_line_matrix: Matrix::identity(),
278        }
279    }
280}
281
282impl GraphicsState {
283    pub fn fill_color_rgba(&self) -> [u8; 4] {
284        let rgb = self.fill_color.to_rgb(&self.fill_cs);
285        [
286            (rgb[0].clamp(0.0, 1.0) * 255.0) as u8,
287            (rgb[1].clamp(0.0, 1.0) * 255.0) as u8,
288            (rgb[2].clamp(0.0, 1.0) * 255.0) as u8,
289            (self.fill_alpha.clamp(0.0, 1.0) * 255.0) as u8,
290        ]
291    }
292
293    pub fn stroke_color_rgba(&self) -> [u8; 4] {
294        let rgb = self.stroke_color.to_rgb(&self.stroke_cs);
295        [
296            (rgb[0].clamp(0.0, 1.0) * 255.0) as u8,
297            (rgb[1].clamp(0.0, 1.0) * 255.0) as u8,
298            (rgb[2].clamp(0.0, 1.0) * 255.0) as u8,
299            (self.stroke_alpha.clamp(0.0, 1.0) * 255.0) as u8,
300        ]
301    }
302}