stet_graphics/device.rs
1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Output device parameter types — pure data structures for rendering operations.
6
7use crate::color::{DashPattern, DeviceColor, FillRule, LineCap, LineJoin};
8use crate::display_list::DisplayList;
9use crate::icc::ProfileHash;
10use std::sync::Arc;
11use stet_fonts::geometry::{Matrix, PsPath};
12
13/// Pre-sampled transfer function (256 samples, domain `[0,1]` → range `[0,1]`).
14/// Arc for cheap clone across display list elements.
15pub type TransferTable = Arc<Vec<f64>>;
16
17/// Transfer function state captured at paint time.
18#[derive(Clone, Debug, Default)]
19pub struct TransferState {
20 /// Single-component transfer (from settransfer). None = identity.
21 pub gray: Option<TransferTable>,
22 /// Per-component color transfer \[R, G, B, Gray\] (from setcolortransfer).
23 /// When set, overrides `gray`.
24 pub color: Option<[Option<TransferTable>; 4]>,
25}
26
27impl TransferState {
28 /// Returns true if any non-identity transfer function is set.
29 pub fn has_functions(&self) -> bool {
30 if self.gray.is_some() {
31 return true;
32 }
33 if let Some(ref color) = self.color {
34 return color.iter().any(|t| t.is_some());
35 }
36 false
37 }
38}
39
40/// A pre-computed halftone screen for PDF output.
41#[derive(Clone, Debug)]
42pub struct HalftoneScreen {
43 pub frequency: f64,
44 pub angle: f64,
45 /// Spot function as PDF Type 4 calculator bytes (e.g., b"{ dup mul exch dup mul add 1 exch sub }").
46 /// None if conversion failed (falls back to sampled_2d).
47 pub type4_tokens: Option<Arc<Vec<u8>>>,
48 /// Spot function sampled on a 64×64 grid (4096 f64 values, domain `[-1,1]²`, range `[0,1]`).
49 /// Used when Type 4 decompilation fails.
50 pub sampled_2d: Option<Arc<Vec<f64>>>,
51}
52
53/// Pre-sampled black generation / undercolor removal state for PDF output.
54#[derive(Clone, Debug, Default)]
55pub struct BgUcrState {
56 /// Black generation function (256 samples, domain `[0,1]` → range `[0,1]`).
57 pub bg: Option<Arc<Vec<f64>>>,
58 /// Undercolor removal function (256 samples, domain `[0,1]` → range `[-1,1]`).
59 pub ucr: Option<Arc<Vec<f64>>>,
60}
61
62/// Pre-computed halftone state captured at paint time.
63#[derive(Clone, Debug, Default)]
64pub struct HalftoneState {
65 /// Single-component halftone (from setscreen). None = default (suppress).
66 pub gray: Option<Arc<HalftoneScreen>>,
67 /// Per-component \[R, G, B, Gray\] (from setcolorscreen). Emits Type 5 composite.
68 pub color: Option<[Option<Arc<HalftoneScreen>>; 4]>,
69}
70
71/// Native Separation/DeviceN color info for PDF output.
72#[derive(Clone, Debug)]
73pub struct SpotColor {
74 /// Tint values from the most recent setcolor (1 for Separation, N for DeviceN).
75 pub tint_values: Vec<f64>,
76 /// Color space definition for this spot color.
77 pub color_space: SpotColorSpace,
78}
79
80/// Separation or DeviceN color space with pre-sampled tint function.
81///
82/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
83/// wildcard arm.
84#[derive(Clone, Debug)]
85#[non_exhaustive]
86pub enum SpotColorSpace {
87 Separation {
88 name: Vec<u8>,
89 alt: SimpleColorSpace,
90 tint_table: Arc<TintLookupTable>,
91 },
92 DeviceN {
93 names: Vec<Vec<u8>>,
94 alt: SimpleColorSpace,
95 tint_table: Arc<TintLookupTable>,
96 },
97}
98
99/// Simple device color space for alt-space references.
100#[derive(Clone, Debug, PartialEq, Eq, Hash)]
101pub enum SimpleColorSpace {
102 DeviceGray,
103 DeviceRGB,
104 DeviceCMYK,
105}
106
107/// Bitmask of CMYK channels painted by an overprint operation.
108/// Bits: 0=Cyan, 1=Magenta, 2=Yellow, 3=Black.
109pub const CMYK_C: u8 = 1 << 0;
110pub const CMYK_M: u8 = 1 << 1;
111pub const CMYK_Y: u8 = 1 << 2;
112pub const CMYK_K: u8 = 1 << 3;
113pub const CMYK_ALL: u8 = CMYK_C | CMYK_M | CMYK_Y | CMYK_K;
114
115/// Map a CMYK process color name to its channel bit.
116pub fn cmyk_channel_for_name(name: &[u8]) -> u8 {
117 match name {
118 b"Cyan" => CMYK_C,
119 b"Magenta" => CMYK_M,
120 b"Yellow" => CMYK_Y,
121 b"Black" => CMYK_K,
122 b"All" => CMYK_ALL,
123 b"None" => 0,
124 _ => 0,
125 }
126}
127
128/// Parameters for filling a path.
129///
130/// Constructed by interpreter/parser code (stet-ops, stet-pdf-reader)
131/// and read by renderers. New fields may be added without notice; pattern-
132/// matching consumers should use `..` to ignore unmatched fields.
133#[derive(Clone, Debug)]
134pub struct FillParams {
135 pub color: DeviceColor,
136 pub fill_rule: FillRule,
137 pub ctm: Matrix,
138 /// True when this fill is a text glyph from a show operator.
139 /// PDF device skips these (uses Text elements instead).
140 pub is_text_glyph: bool,
141 /// Overprint flag from graphics state (used by PDF output).
142 pub overprint: bool,
143 /// Overprint mode (0 or 1). With OPM 1 + DeviceCMYK, only non-zero channels are painted.
144 pub overprint_mode: i32,
145 /// True when /OPM was set together with /op or /OP in the same ExtGState
146 /// dict that configured this fill. Enables strict OPM-1 "preserve zero
147 /// components" behavior; when false, an all-zero CMYK source still
148 /// performs a full knockout (legacy Adobe compatibility).
149 pub opm_paired: bool,
150 /// Which CMYK channels this fill paints (bitmask of CMYK_C/M/Y/K).
151 pub painted_channels: u8,
152 /// True when color space is DeviceCMYK or ICCBased(4).
153 pub is_device_cmyk: bool,
154 /// Separation/DeviceN color for PDF output. None for device color spaces.
155 pub spot_color: Option<SpotColor>,
156 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
157 pub rendering_intent: u8,
158 /// Pre-sampled transfer function state for PDF output.
159 pub transfer: TransferState,
160 /// Pre-computed halftone screen state for PDF output.
161 pub halftone: HalftoneState,
162 /// Pre-sampled black generation / undercolor removal for PDF output.
163 pub bg_ucr: BgUcrState,
164 /// Fill opacity (0.0–1.0, default 1.0). Used by PDF transparency.
165 pub alpha: f64,
166 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
167 pub blend_mode: u8,
168 /// PDF `AIS` (alpha-is-shape). When true, the source is interpreted as
169 /// shape rather than opacity. Default false.
170 pub alpha_is_shape: bool,
171}
172
173/// Parameters for a text element emitted by show operators.
174///
175/// The PDF device uses these for BT/ET/Tf/Tj text operators.
176/// The raster device ignores them (uses Fill elements for glyph paths).
177///
178/// New fields may be added without notice; pattern-matching consumers
179/// should use `..` to ignore unmatched fields.
180#[derive(Clone, Debug)]
181pub struct TextParams {
182 /// Character bytes (or 2-byte CID values for Type 0).
183 pub text: Vec<u8>,
184 /// Device-space X position at start of string.
185 pub start_x: f64,
186 /// Device-space Y position at start of string.
187 pub start_y: f64,
188 /// Font dict entity ID (raw u32 for VM independence).
189 pub font_entity: u32,
190 /// FontName bytes (e.g., b"Times-Roman").
191 pub font_name: Vec<u8>,
192 /// FontType (0, 1, 2, 3, 42).
193 pub font_type: i32,
194 /// Effective device-space font size.
195 pub font_size: f64,
196 /// Fill color at render time.
197 pub color: DeviceColor,
198 /// CTM at render time.
199 pub ctm: [f64; 6],
200 /// User-space font matrix (scaled to point units).
201 pub font_matrix: [f64; 6],
202 /// PaintType: 0 = fill (default), 2 = stroke (outlined glyphs).
203 pub paint_type: i32,
204 /// Device-space stroke width for PaintType 2 fonts.
205 pub stroke_width: f64,
206 /// Separation/DeviceN color for PDF output. None for device color spaces.
207 pub spot_color: Option<SpotColor>,
208 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
209 pub rendering_intent: u8,
210 /// Pre-sampled transfer function state for PDF output.
211 pub transfer: TransferState,
212 /// Pre-computed halftone screen state for PDF output.
213 pub halftone: HalftoneState,
214 /// Pre-sampled black generation / undercolor removal for PDF output.
215 pub bg_ucr: BgUcrState,
216 /// Fill opacity (0.0–1.0, default 1.0). Used by PDF transparency.
217 pub fill_opacity: f64,
218 /// Stroke opacity (0.0–1.0, default 1.0). Applies to PaintType-2 fonts.
219 pub stroke_opacity: f64,
220 /// Blend mode (0=Normal, 1=Multiply, …, 15=Luminosity). Default 0.
221 pub blend_mode: u8,
222 /// Alpha-is-shape (PDF `AIS`). Default false.
223 pub alpha_is_shape: bool,
224 /// Text knockout (PDF `TK`). Default true.
225 pub text_knockout: bool,
226}
227
228/// Parameters for stroking a path.
229///
230/// New fields may be added without notice; pattern-matching consumers
231/// should use `..` to ignore unmatched fields.
232#[derive(Clone, Debug)]
233pub struct StrokeParams {
234 pub color: DeviceColor,
235 pub line_width: f64,
236 pub line_cap: LineCap,
237 pub line_join: LineJoin,
238 pub miter_limit: f64,
239 pub dash_pattern: DashPattern,
240 pub ctm: Matrix,
241 /// When true, snap thin stroke coordinates to device pixel centers.
242 pub stroke_adjust: bool,
243 /// True when this stroke is a text glyph from a show operator (PaintType 2).
244 pub is_text_glyph: bool,
245 /// Overprint flag from graphics state (used by PDF output).
246 pub overprint: bool,
247 /// Overprint mode (0 or 1).
248 pub overprint_mode: i32,
249 /// See FillParams::opm_paired. Strict OPM-1 preserve requires both
250 /// /OPM and /op|/OP set in the same ExtGState dict.
251 pub opm_paired: bool,
252 /// Which CMYK channels this stroke paints (bitmask of CMYK_C/M/Y/K).
253 pub painted_channels: u8,
254 /// True when stroke color space is DeviceCMYK or ICCBased(4) — OPM 1 only applies to these.
255 pub is_device_cmyk: bool,
256 /// Separation/DeviceN color for PDF output. None for device color spaces.
257 pub spot_color: Option<SpotColor>,
258 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
259 pub rendering_intent: u8,
260 /// Pre-sampled transfer function state for PDF output.
261 pub transfer: TransferState,
262 /// Pre-computed halftone screen state for PDF output.
263 pub halftone: HalftoneState,
264 /// Pre-sampled black generation / undercolor removal for PDF output.
265 pub bg_ucr: BgUcrState,
266 /// Stroke opacity (0.0–1.0, default 1.0). Used by PDF transparency.
267 pub alpha: f64,
268 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
269 pub blend_mode: u8,
270 /// PDF `AIS` (alpha-is-shape). When true, the source is interpreted as
271 /// shape rather than opacity. Default false.
272 pub alpha_is_shape: bool,
273}
274
275/// Parameters for clipping.
276///
277/// New fields may be added without notice; pattern-matching consumers
278/// should use `..` to ignore unmatched fields.
279#[derive(Clone, Debug)]
280pub struct ClipParams {
281 pub fill_rule: FillRule,
282 pub ctm: Matrix,
283 /// For stroke-based clips: stroke parameters to expand the clip path
284 /// from a centerline to a stroke outline before rasterizing.
285 pub stroke_params: Option<StrokeParams>,
286}
287
288/// Pre-sampled tint transform: maps input tint values to alt-space components.
289#[derive(Clone, Debug)]
290pub struct TintLookupTable {
291 /// Number of input components (1 for Separation, N for DeviceN).
292 pub num_inputs: u32,
293 /// Number of output components (matches alternative space: 1/3/4).
294 pub num_outputs: u32,
295 /// Number of samples per dimension.
296 pub samples_per_dim: u32,
297 /// Flattened f32 data, row-major order. Length = samples_per_dim^num_inputs × num_outputs.
298 pub data: Vec<f32>,
299}
300
301impl TintLookupTable {
302 /// Linear interpolation lookup for 1D (Separation) tint transforms.
303 #[inline]
304 pub fn lookup_1d(&self, tint: f32, out: &mut [f32]) {
305 let n = self.samples_per_dim as usize;
306 let no = self.num_outputs as usize;
307 let idx = tint * (n - 1) as f32;
308 let i0 = (idx as usize).min(n - 2);
309 let frac = idx - i0 as f32;
310 let base0 = i0 * no;
311 let base1 = (i0 + 1) * no;
312 for (c, out_val) in out[..no].iter_mut().enumerate() {
313 *out_val = self.data[base0 + c] * (1.0 - frac) + self.data[base1 + c] * frac;
314 }
315 }
316
317 /// Multilinear interpolation lookup for N-D (DeviceN) tint transforms.
318 pub fn lookup_nd(&self, inputs: &[f32], out: &mut [f32]) {
319 let ni = self.num_inputs as usize;
320 let no = self.num_outputs as usize;
321 let n = self.samples_per_dim as usize;
322
323 let mut idx = [0usize; 8];
324 let mut frac = [0.0f32; 8];
325 for d in 0..ni {
326 let fi = inputs[d] * (n - 1) as f32;
327 idx[d] = (fi as usize).min(n - 2);
328 frac[d] = fi - idx[d] as f32;
329 }
330
331 let corners = 1usize << ni;
332 for out_val in out[..no].iter_mut() {
333 *out_val = 0.0;
334 }
335 for corner in 0..corners {
336 let mut weight = 1.0f32;
337 let mut linear_idx = 0usize;
338 for d in 0..ni {
339 let bit = (corner >> d) & 1;
340 let dim_idx = idx[d] + bit;
341 weight *= if bit == 1 { frac[d] } else { 1.0 - frac[d] };
342 let stride = n.pow((ni - 1 - d) as u32);
343 linear_idx += dim_idx * stride;
344 }
345 let base = linear_idx * no;
346 for (c, out_val) in out[..no].iter_mut().enumerate() {
347 *out_val += weight * self.data.get(base + c).copied().unwrap_or(0.0);
348 }
349 }
350 }
351}
352
353/// VM-free color space enum for images stored in the display list.
354///
355/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
356/// wildcard arm to remain forward-compatible.
357#[derive(Clone, Debug)]
358#[non_exhaustive]
359pub enum ImageColorSpace {
360 DeviceGray,
361 DeviceRGB,
362 DeviceCMYK,
363 ICCBased {
364 n: u32,
365 profile_hash: ProfileHash,
366 profile_data: Arc<Vec<u8>>,
367 },
368 Indexed {
369 base: Box<ImageColorSpace>,
370 hival: u32,
371 lookup: Vec<u8>,
372 },
373 CIEBasedABC {
374 params: Arc<crate::color::CieAbcParams>,
375 },
376 CIEBasedA {
377 params: Arc<crate::color::CieAParams>,
378 },
379 /// CIE L*a*b* color space (PDF /Lab or ICCBased Lab alternate).
380 ///
381 /// Sample byte layout: 3 components (L, a, b), 8-bit each. Decode
382 /// scales bytes: L = byte/255 × 100; a = byte/255 × (`range[1]`-`range[0]`) + `range[0]`;
383 /// b = byte/255 × (`range[3]`-`range[2]`) + `range[2]`.
384 Lab {
385 white_point: [f64; 3],
386 range: [f64; 4],
387 },
388 Separation {
389 name: Vec<u8>,
390 alt_space: Box<ImageColorSpace>,
391 tint_table: Arc<TintLookupTable>,
392 },
393 DeviceN {
394 names: Vec<Vec<u8>>,
395 alt_space: Box<ImageColorSpace>,
396 tint_table: Arc<TintLookupTable>,
397 },
398 Mask {
399 color: DeviceColor,
400 polarity: bool,
401 },
402 PreconvertedRGBA,
403}
404
405impl ImageColorSpace {
406 /// Number of components per sample.
407 pub fn num_components(&self) -> u32 {
408 match self {
409 ImageColorSpace::DeviceGray => 1,
410 ImageColorSpace::DeviceRGB => 3,
411 ImageColorSpace::DeviceCMYK => 4,
412 ImageColorSpace::ICCBased { n, .. } => *n,
413 ImageColorSpace::Indexed { .. } => 1,
414 ImageColorSpace::CIEBasedABC { .. } => 3,
415 ImageColorSpace::CIEBasedA { .. } => 1,
416 ImageColorSpace::Lab { .. } => 3,
417 ImageColorSpace::Separation { .. } => 1,
418 ImageColorSpace::DeviceN { tint_table, .. } => tint_table.num_inputs,
419 ImageColorSpace::Mask { .. } => 1,
420 ImageColorSpace::PreconvertedRGBA => 4,
421 }
422 }
423}
424
425/// Parameters for drawing an image.
426///
427/// New fields may be added without notice; pattern-matching consumers
428/// should use `..` to ignore unmatched fields.
429#[derive(Clone, Debug)]
430pub struct ImageParams {
431 pub width: u32,
432 pub height: u32,
433 pub color_space: ImageColorSpace,
434 pub bits_per_component: u8,
435 pub ctm: Matrix,
436 pub image_matrix: Matrix,
437 pub interpolate: bool,
438 pub mask_color: Option<Vec<u8>>,
439 pub alpha: f64,
440 pub blend_mode: u8,
441 pub overprint: bool,
442 pub overprint_mode: i32,
443 /// See FillParams::opm_paired.
444 pub opm_paired: bool,
445 pub painted_channels: u8,
446 /// PDF `AIS` (alpha-is-shape). Default false.
447 pub alpha_is_shape: bool,
448 /// Rendering intent that selects which `A2B*`/`B2A*` table the source
449 /// profile and the output-intent profile use when this image flows
450 /// through the proofing chain. Encoded as PDF byte: 0=Perceptual,
451 /// 1=RelativeColorimetric, 2=Saturation, 3=AbsoluteColorimetric.
452 /// Per ISO 32000 §11.3.4 a per-image `/Intent` overrides the gstate
453 /// `/RI`; PDF readers populate this from `/Intent` when present and
454 /// fall back to `gstate.rendering_intent` otherwise.
455 pub rendering_intent: u8,
456}
457
458/// Color space carried through the display list for native shading output.
459///
460/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
461/// wildcard arm.
462#[derive(Clone, Debug)]
463#[non_exhaustive]
464pub enum ShadingColorSpace {
465 DeviceGray,
466 DeviceRGB,
467 DeviceCMYK,
468 ICCBased {
469 n: u32,
470 profile_hash: ProfileHash,
471 profile_data: Arc<Vec<u8>>,
472 },
473 CalRGB {
474 white_point: [f64; 3],
475 matrix: Option<[f64; 9]>,
476 gamma: Option<[f64; 3]>,
477 },
478 CalGray {
479 white_point: [f64; 3],
480 gamma: Option<f64>,
481 },
482}
483
484impl ShadingColorSpace {
485 /// Number of color components in this color space.
486 pub fn num_components(&self) -> usize {
487 match self {
488 ShadingColorSpace::DeviceGray | ShadingColorSpace::CalGray { .. } => 1,
489 ShadingColorSpace::DeviceRGB | ShadingColorSpace::CalRGB { .. } => 3,
490 ShadingColorSpace::DeviceCMYK => 4,
491 ShadingColorSpace::ICCBased { n, .. } => *n as usize,
492 }
493 }
494}
495
496/// A single color stop in a gradient.
497///
498/// New fields may be added without notice; pattern-matching consumers
499/// should use `..` to ignore unmatched fields.
500#[derive(Clone, Debug)]
501pub struct ColorStop {
502 pub position: f64,
503 pub color: DeviceColor,
504 pub raw_components: Vec<f64>,
505}
506
507/// Parameters for axial (linear) gradient shading (Type 2).
508///
509/// New fields may be added without notice; pattern-matching consumers
510/// should use `..` to ignore unmatched fields.
511#[derive(Clone, Debug)]
512pub struct AxialShadingParams {
513 pub x0: f64,
514 pub y0: f64,
515 pub x1: f64,
516 pub y1: f64,
517 pub color_stops: Vec<ColorStop>,
518 pub extend_start: bool,
519 pub extend_end: bool,
520 pub ctm: Matrix,
521 pub bbox: Option<[f64; 4]>,
522 pub color_space: ShadingColorSpace,
523 pub overprint: bool,
524 pub painted_channels: u8,
525 /// Fill alpha from graphics state (0.0–1.0).
526 pub alpha: f64,
527 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
528 pub blend_mode: u8,
529 /// PDF `AIS` (alpha-is-shape). Default false.
530 pub alpha_is_shape: bool,
531 /// True when this shading uses a Separation/DeviceN color space with a
532 /// CMYK alternate AND at least one non-process spot colorant. The
533 /// renderer composites the per-pixel CMYK from the gradient stops with
534 /// the tracked CMYK buffer multiplicatively, preserving underlying CMYK
535 /// paints under the gradient (e.g. green checkmarks under a green→cyan
536 /// DeviceN strip survive).
537 pub spot_tint_blend: bool,
538}
539
540/// Parameters for radial gradient shading (Type 3).
541///
542/// New fields may be added without notice; pattern-matching consumers
543/// should use `..` to ignore unmatched fields.
544#[derive(Clone, Debug)]
545pub struct RadialShadingParams {
546 pub x0: f64,
547 pub y0: f64,
548 pub r0: f64,
549 pub x1: f64,
550 pub y1: f64,
551 pub r1: f64,
552 pub color_stops: Vec<ColorStop>,
553 pub extend_start: bool,
554 pub extend_end: bool,
555 pub ctm: Matrix,
556 pub bbox: Option<[f64; 4]>,
557 pub color_space: ShadingColorSpace,
558 pub overprint: bool,
559 pub painted_channels: u8,
560 /// Fill alpha from graphics state (0.0–1.0).
561 pub alpha: f64,
562 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
563 pub blend_mode: u8,
564 /// PDF `AIS` (alpha-is-shape). Default false.
565 pub alpha_is_shape: bool,
566 /// See [`AxialShadingParams::spot_tint_blend`].
567 pub spot_tint_blend: bool,
568}
569
570/// A vertex in a shading triangle mesh.
571#[derive(Clone, Debug)]
572pub struct ShadingVertex {
573 pub x: f64,
574 pub y: f64,
575 pub color: DeviceColor,
576 pub raw_components: Vec<f64>,
577}
578
579/// A triangle in a shading mesh.
580#[derive(Clone, Debug)]
581pub struct ShadingTriangle {
582 pub v0: ShadingVertex,
583 pub v1: ShadingVertex,
584 pub v2: ShadingVertex,
585}
586
587/// Parameters for Gouraud-shaded triangle mesh shading (Types 4 & 5).
588///
589/// New fields may be added without notice; pattern-matching consumers
590/// should use `..` to ignore unmatched fields.
591#[derive(Clone, Debug)]
592pub struct MeshShadingParams {
593 pub triangles: Vec<ShadingTriangle>,
594 pub ctm: Matrix,
595 pub bbox: Option<[f64; 4]>,
596 pub color_space: ShadingColorSpace,
597 pub overprint: bool,
598 pub painted_channels: u8,
599 /// Pre-sampled color LUT for function-based mesh shadings.
600 /// When present, vertex `raw_components[0]` holds a normalized `[0,1]`
601 /// function input. The renderer interpolates this per-pixel, then
602 /// indexes the LUT instead of Gouraud-interpolating DeviceColor.
603 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
604 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
605 pub alpha: f64,
606 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
607 pub blend_mode: u8,
608 /// PDF `AIS` (alpha-is-shape). Default false.
609 pub alpha_is_shape: bool,
610}
611
612/// A patch in a Coons or tensor-product patch mesh.
613#[derive(Clone, Debug)]
614pub struct ShadingPatch {
615 pub points: Vec<(f64, f64)>,
616 pub colors: [DeviceColor; 4],
617 pub raw_colors: [Vec<f64>; 4],
618}
619
620/// Parameters for Coons/tensor-product patch mesh shading (Types 6 & 7).
621///
622/// New fields may be added without notice; pattern-matching consumers
623/// should use `..` to ignore unmatched fields.
624#[derive(Clone, Debug)]
625pub struct PatchShadingParams {
626 pub patches: Vec<ShadingPatch>,
627 pub ctm: Matrix,
628 pub bbox: Option<[f64; 4]>,
629 pub color_space: ShadingColorSpace,
630 pub overprint: bool,
631 pub painted_channels: u8,
632 /// When present, vertex `raw_colors[i][0]` holds a normalized `[0,1]`
633 /// function input. The renderer interpolates this per-pixel, then
634 /// indexes the LUT for per-pixel non-linear function evaluation.
635 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
636 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
637 pub alpha: f64,
638 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
639 pub blend_mode: u8,
640 /// PDF `AIS` (alpha-is-shape). Default false.
641 pub alpha_is_shape: bool,
642}
643
644/// Parameters for a tiled pattern fill.
645#[derive(Clone)]
646pub struct PatternFillParams {
647 /// The path to fill with the pattern.
648 pub path: PsPath,
649 /// Fill rule for the path.
650 pub fill_rule: FillRule,
651 /// Pre-rendered display list for a single tile.
652 pub tile: DisplayList,
653 /// Pattern matrix (pattern space → device space).
654 pub pattern_matrix: Matrix,
655 /// Bounding box of one tile in pattern space.
656 pub bbox: [f64; 4],
657 /// Horizontal step between tile origins.
658 pub xstep: f64,
659 /// Vertical step between tile origins.
660 pub ystep: f64,
661 /// Paint type: 1 = colored, 2 = uncolored.
662 pub paint_type: i32,
663 /// For uncolored patterns, the fill color.
664 pub underlying_color: Option<DeviceColor>,
665 /// Unique pattern ID from pattern_store (for dedup in PDF output).
666 pub pattern_id: u32,
667 /// When true, tile display list elements have CTMs in device space
668 /// (the pattern matrix is already baked into element transforms).
669 /// When false, elements are in pattern space and the renderer applies
670 /// the pattern_matrix during rendering.
671 pub device_space_tile: bool,
672 /// When true, the tile content was designed for a Y-flipped coordinate
673 /// system (pattern matrix had negative d). The pre-rendered tile must
674 /// be vertically flipped before stamping.
675 pub flip_tile_y: bool,
676 /// For pattern strokes: stroke parameters to expand the centerline path
677 /// into a fill outline for masking. When Some, `path` is a user-space
678 /// stroke centerline rather than a fill path.
679 pub stroke_params: Option<StrokeParams>,
680 /// PDF overprint mode (0 or 1). When 1, CMYK(0,0,0,0) pixels in tile
681 /// images are transparent (no ink = don't paint).
682 pub overprint_mode: i32,
683}
684
685// ---------------------------------------------------------------------------
686// Default impls
687//
688// The `Default` impls below pair with `#[non_exhaustive]` on each type:
689// downstream consumers (and other workspace crates) construct values via
690// `FillParams { color, ..Default::default() }`-style functional update so
691// new fields can be added without breaking call sites. The defaults are
692// chosen for ergonomics (alpha = 1.0, blend mode = Normal, identity CTM,
693// solid black colour, no transfer/halftone/spot state) — not as
694// semantically meaningful "blank records".
695// ---------------------------------------------------------------------------
696
697impl Default for FillParams {
698 fn default() -> Self {
699 Self {
700 color: DeviceColor::default(),
701 fill_rule: FillRule::default(),
702 ctm: Matrix::default(),
703 is_text_glyph: false,
704 overprint: false,
705 overprint_mode: 0,
706 opm_paired: false,
707 painted_channels: 0,
708 is_device_cmyk: false,
709 spot_color: None,
710 rendering_intent: 0,
711 transfer: TransferState::default(),
712 halftone: HalftoneState::default(),
713 bg_ucr: BgUcrState::default(),
714 alpha: 1.0,
715 blend_mode: 0,
716 alpha_is_shape: false,
717 }
718 }
719}
720
721impl Default for StrokeParams {
722 fn default() -> Self {
723 Self {
724 color: DeviceColor::default(),
725 line_width: 1.0,
726 line_cap: LineCap::default(),
727 line_join: LineJoin::default(),
728 miter_limit: 10.0,
729 dash_pattern: DashPattern::default(),
730 ctm: Matrix::default(),
731 stroke_adjust: false,
732 is_text_glyph: false,
733 overprint: false,
734 overprint_mode: 0,
735 opm_paired: false,
736 painted_channels: 0,
737 is_device_cmyk: false,
738 spot_color: None,
739 rendering_intent: 0,
740 transfer: TransferState::default(),
741 halftone: HalftoneState::default(),
742 bg_ucr: BgUcrState::default(),
743 alpha: 1.0,
744 blend_mode: 0,
745 alpha_is_shape: false,
746 }
747 }
748}
749
750impl Default for ClipParams {
751 fn default() -> Self {
752 Self {
753 fill_rule: FillRule::default(),
754 ctm: Matrix::default(),
755 stroke_params: None,
756 }
757 }
758}
759
760impl Default for TextParams {
761 fn default() -> Self {
762 Self {
763 text: Vec::new(),
764 start_x: 0.0,
765 start_y: 0.0,
766 font_entity: 0,
767 font_name: Vec::new(),
768 font_type: 1,
769 font_size: 0.0,
770 color: DeviceColor::default(),
771 ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
772 font_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
773 paint_type: 0,
774 stroke_width: 0.0,
775 spot_color: None,
776 rendering_intent: 0,
777 transfer: TransferState::default(),
778 halftone: HalftoneState::default(),
779 bg_ucr: BgUcrState::default(),
780 fill_opacity: 1.0,
781 stroke_opacity: 1.0,
782 blend_mode: 0,
783 alpha_is_shape: false,
784 text_knockout: true,
785 }
786 }
787}
788
789impl Default for ImageColorSpace {
790 fn default() -> Self {
791 ImageColorSpace::DeviceGray
792 }
793}
794
795impl Default for ImageParams {
796 fn default() -> Self {
797 Self {
798 width: 0,
799 height: 0,
800 color_space: ImageColorSpace::default(),
801 bits_per_component: 8,
802 ctm: Matrix::default(),
803 image_matrix: Matrix::default(),
804 interpolate: false,
805 mask_color: None,
806 alpha: 1.0,
807 blend_mode: 0,
808 overprint: false,
809 overprint_mode: 0,
810 opm_paired: false,
811 painted_channels: 0,
812 alpha_is_shape: false,
813 rendering_intent: 0,
814 }
815 }
816}
817
818impl Default for ShadingColorSpace {
819 fn default() -> Self {
820 ShadingColorSpace::DeviceRGB
821 }
822}
823
824impl Default for ColorStop {
825 fn default() -> Self {
826 Self {
827 position: 0.0,
828 color: DeviceColor::default(),
829 raw_components: Vec::new(),
830 }
831 }
832}
833
834impl Default for AxialShadingParams {
835 fn default() -> Self {
836 Self {
837 x0: 0.0,
838 y0: 0.0,
839 x1: 0.0,
840 y1: 0.0,
841 color_stops: Vec::new(),
842 extend_start: false,
843 extend_end: false,
844 ctm: Matrix::default(),
845 bbox: None,
846 color_space: ShadingColorSpace::default(),
847 overprint: false,
848 painted_channels: 0,
849 alpha: 1.0,
850 blend_mode: 0,
851 alpha_is_shape: false,
852 spot_tint_blend: false,
853 }
854 }
855}
856
857impl Default for RadialShadingParams {
858 fn default() -> Self {
859 Self {
860 x0: 0.0,
861 y0: 0.0,
862 r0: 0.0,
863 x1: 0.0,
864 y1: 0.0,
865 r1: 0.0,
866 color_stops: Vec::new(),
867 extend_start: false,
868 extend_end: false,
869 ctm: Matrix::default(),
870 bbox: None,
871 color_space: ShadingColorSpace::default(),
872 overprint: false,
873 painted_channels: 0,
874 alpha: 1.0,
875 blend_mode: 0,
876 alpha_is_shape: false,
877 spot_tint_blend: false,
878 }
879 }
880}
881
882impl Default for MeshShadingParams {
883 fn default() -> Self {
884 Self {
885 triangles: Vec::new(),
886 ctm: Matrix::default(),
887 bbox: None,
888 color_space: ShadingColorSpace::default(),
889 overprint: false,
890 painted_channels: 0,
891 color_lut: None,
892 alpha: 1.0,
893 blend_mode: 0,
894 alpha_is_shape: false,
895 }
896 }
897}
898
899impl Default for PatchShadingParams {
900 fn default() -> Self {
901 Self {
902 patches: Vec::new(),
903 ctm: Matrix::default(),
904 bbox: None,
905 color_space: ShadingColorSpace::default(),
906 overprint: false,
907 painted_channels: 0,
908 color_lut: None,
909 alpha: 1.0,
910 blend_mode: 0,
911 alpha_is_shape: false,
912 }
913 }
914}
915
916/// Trait for consuming rendered page pixel data.
917pub trait PageSink: Send {
918 /// Start a new page with the given pixel dimensions.
919 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String>;
920
921 /// Write one or more rows of RGBA pixel data (4 bytes per pixel, row-major).
922 fn write_rows(&mut self, rgba_rows: &[u8], num_rows: u32) -> Result<(), String>;
923
924 /// Finish the current page. May block (e.g., viewer waits for user input).
925 fn end_page(&mut self) -> Result<(), String>;
926}
927
928/// Factory for creating per-page sinks.
929pub trait PageSinkFactory: Send + Sync {
930 /// Create a new sink for a single page.
931 fn create_sink(&self, output_path: &str) -> Result<Box<dyn PageSink>, String>;
932}