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}
449
450/// Color space carried through the display list for native shading output.
451///
452/// Marked `#[non_exhaustive]`; cross-crate `match` expressions need a
453/// wildcard arm.
454#[derive(Clone, Debug)]
455#[non_exhaustive]
456pub enum ShadingColorSpace {
457 DeviceGray,
458 DeviceRGB,
459 DeviceCMYK,
460 ICCBased {
461 n: u32,
462 profile_hash: ProfileHash,
463 profile_data: Arc<Vec<u8>>,
464 },
465 CalRGB {
466 white_point: [f64; 3],
467 matrix: Option<[f64; 9]>,
468 gamma: Option<[f64; 3]>,
469 },
470 CalGray {
471 white_point: [f64; 3],
472 gamma: Option<f64>,
473 },
474}
475
476impl ShadingColorSpace {
477 /// Number of color components in this color space.
478 pub fn num_components(&self) -> usize {
479 match self {
480 ShadingColorSpace::DeviceGray | ShadingColorSpace::CalGray { .. } => 1,
481 ShadingColorSpace::DeviceRGB | ShadingColorSpace::CalRGB { .. } => 3,
482 ShadingColorSpace::DeviceCMYK => 4,
483 ShadingColorSpace::ICCBased { n, .. } => *n as usize,
484 }
485 }
486}
487
488/// A single color stop in a gradient.
489///
490/// New fields may be added without notice; pattern-matching consumers
491/// should use `..` to ignore unmatched fields.
492#[derive(Clone, Debug)]
493pub struct ColorStop {
494 pub position: f64,
495 pub color: DeviceColor,
496 pub raw_components: Vec<f64>,
497}
498
499/// Parameters for axial (linear) gradient shading (Type 2).
500///
501/// New fields may be added without notice; pattern-matching consumers
502/// should use `..` to ignore unmatched fields.
503#[derive(Clone, Debug)]
504pub struct AxialShadingParams {
505 pub x0: f64,
506 pub y0: f64,
507 pub x1: f64,
508 pub y1: f64,
509 pub color_stops: Vec<ColorStop>,
510 pub extend_start: bool,
511 pub extend_end: bool,
512 pub ctm: Matrix,
513 pub bbox: Option<[f64; 4]>,
514 pub color_space: ShadingColorSpace,
515 pub overprint: bool,
516 pub painted_channels: u8,
517 /// Fill alpha from graphics state (0.0–1.0).
518 pub alpha: f64,
519 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
520 pub blend_mode: u8,
521 /// PDF `AIS` (alpha-is-shape). Default false.
522 pub alpha_is_shape: bool,
523 /// True when this shading uses a Separation/DeviceN color space with a
524 /// CMYK alternate AND at least one non-process spot colorant. The
525 /// renderer composites the per-pixel CMYK from the gradient stops with
526 /// the tracked CMYK buffer multiplicatively, preserving underlying CMYK
527 /// paints under the gradient (e.g. green checkmarks under a green→cyan
528 /// DeviceN strip survive).
529 pub spot_tint_blend: bool,
530}
531
532/// Parameters for radial gradient shading (Type 3).
533///
534/// New fields may be added without notice; pattern-matching consumers
535/// should use `..` to ignore unmatched fields.
536#[derive(Clone, Debug)]
537pub struct RadialShadingParams {
538 pub x0: f64,
539 pub y0: f64,
540 pub r0: f64,
541 pub x1: f64,
542 pub y1: f64,
543 pub r1: f64,
544 pub color_stops: Vec<ColorStop>,
545 pub extend_start: bool,
546 pub extend_end: bool,
547 pub ctm: Matrix,
548 pub bbox: Option<[f64; 4]>,
549 pub color_space: ShadingColorSpace,
550 pub overprint: bool,
551 pub painted_channels: u8,
552 /// Fill alpha from graphics state (0.0–1.0).
553 pub alpha: f64,
554 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
555 pub blend_mode: u8,
556 /// PDF `AIS` (alpha-is-shape). Default false.
557 pub alpha_is_shape: bool,
558 /// See [`AxialShadingParams::spot_tint_blend`].
559 pub spot_tint_blend: bool,
560}
561
562/// A vertex in a shading triangle mesh.
563#[derive(Clone, Debug)]
564pub struct ShadingVertex {
565 pub x: f64,
566 pub y: f64,
567 pub color: DeviceColor,
568 pub raw_components: Vec<f64>,
569}
570
571/// A triangle in a shading mesh.
572#[derive(Clone, Debug)]
573pub struct ShadingTriangle {
574 pub v0: ShadingVertex,
575 pub v1: ShadingVertex,
576 pub v2: ShadingVertex,
577}
578
579/// Parameters for Gouraud-shaded triangle mesh shading (Types 4 & 5).
580///
581/// New fields may be added without notice; pattern-matching consumers
582/// should use `..` to ignore unmatched fields.
583#[derive(Clone, Debug)]
584pub struct MeshShadingParams {
585 pub triangles: Vec<ShadingTriangle>,
586 pub ctm: Matrix,
587 pub bbox: Option<[f64; 4]>,
588 pub color_space: ShadingColorSpace,
589 pub overprint: bool,
590 pub painted_channels: u8,
591 /// Pre-sampled color LUT for function-based mesh shadings.
592 /// When present, vertex `raw_components[0]` holds a normalized `[0,1]`
593 /// function input. The renderer interpolates this per-pixel, then
594 /// indexes the LUT instead of Gouraud-interpolating DeviceColor.
595 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
596 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
597 pub alpha: f64,
598 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
599 pub blend_mode: u8,
600 /// PDF `AIS` (alpha-is-shape). Default false.
601 pub alpha_is_shape: bool,
602}
603
604/// A patch in a Coons or tensor-product patch mesh.
605#[derive(Clone, Debug)]
606pub struct ShadingPatch {
607 pub points: Vec<(f64, f64)>,
608 pub colors: [DeviceColor; 4],
609 pub raw_colors: [Vec<f64>; 4],
610}
611
612/// Parameters for Coons/tensor-product patch mesh shading (Types 6 & 7).
613///
614/// New fields may be added without notice; pattern-matching consumers
615/// should use `..` to ignore unmatched fields.
616#[derive(Clone, Debug)]
617pub struct PatchShadingParams {
618 pub patches: Vec<ShadingPatch>,
619 pub ctm: Matrix,
620 pub bbox: Option<[f64; 4]>,
621 pub color_space: ShadingColorSpace,
622 pub overprint: bool,
623 pub painted_channels: u8,
624 /// When present, vertex `raw_colors[i][0]` holds a normalized `[0,1]`
625 /// function input. The renderer interpolates this per-pixel, then
626 /// indexes the LUT for per-pixel non-linear function evaluation.
627 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
628 /// Fill alpha from graphics state (0.0–1.0). Default 1.0.
629 pub alpha: f64,
630 /// Blend mode (0=Normal, …, 15=Luminosity). Default 0.
631 pub blend_mode: u8,
632 /// PDF `AIS` (alpha-is-shape). Default false.
633 pub alpha_is_shape: bool,
634}
635
636/// Parameters for a tiled pattern fill.
637#[derive(Clone)]
638pub struct PatternFillParams {
639 /// The path to fill with the pattern.
640 pub path: PsPath,
641 /// Fill rule for the path.
642 pub fill_rule: FillRule,
643 /// Pre-rendered display list for a single tile.
644 pub tile: DisplayList,
645 /// Pattern matrix (pattern space → device space).
646 pub pattern_matrix: Matrix,
647 /// Bounding box of one tile in pattern space.
648 pub bbox: [f64; 4],
649 /// Horizontal step between tile origins.
650 pub xstep: f64,
651 /// Vertical step between tile origins.
652 pub ystep: f64,
653 /// Paint type: 1 = colored, 2 = uncolored.
654 pub paint_type: i32,
655 /// For uncolored patterns, the fill color.
656 pub underlying_color: Option<DeviceColor>,
657 /// Unique pattern ID from pattern_store (for dedup in PDF output).
658 pub pattern_id: u32,
659 /// When true, tile display list elements have CTMs in device space
660 /// (the pattern matrix is already baked into element transforms).
661 /// When false, elements are in pattern space and the renderer applies
662 /// the pattern_matrix during rendering.
663 pub device_space_tile: bool,
664 /// When true, the tile content was designed for a Y-flipped coordinate
665 /// system (pattern matrix had negative d). The pre-rendered tile must
666 /// be vertically flipped before stamping.
667 pub flip_tile_y: bool,
668 /// For pattern strokes: stroke parameters to expand the centerline path
669 /// into a fill outline for masking. When Some, `path` is a user-space
670 /// stroke centerline rather than a fill path.
671 pub stroke_params: Option<StrokeParams>,
672 /// PDF overprint mode (0 or 1). When 1, CMYK(0,0,0,0) pixels in tile
673 /// images are transparent (no ink = don't paint).
674 pub overprint_mode: i32,
675}
676
677// ---------------------------------------------------------------------------
678// Default impls
679//
680// The `Default` impls below pair with `#[non_exhaustive]` on each type:
681// downstream consumers (and other workspace crates) construct values via
682// `FillParams { color, ..Default::default() }`-style functional update so
683// new fields can be added without breaking call sites. The defaults are
684// chosen for ergonomics (alpha = 1.0, blend mode = Normal, identity CTM,
685// solid black colour, no transfer/halftone/spot state) — not as
686// semantically meaningful "blank records".
687// ---------------------------------------------------------------------------
688
689impl Default for FillParams {
690 fn default() -> Self {
691 Self {
692 color: DeviceColor::default(),
693 fill_rule: FillRule::default(),
694 ctm: Matrix::default(),
695 is_text_glyph: false,
696 overprint: false,
697 overprint_mode: 0,
698 opm_paired: false,
699 painted_channels: 0,
700 is_device_cmyk: false,
701 spot_color: None,
702 rendering_intent: 0,
703 transfer: TransferState::default(),
704 halftone: HalftoneState::default(),
705 bg_ucr: BgUcrState::default(),
706 alpha: 1.0,
707 blend_mode: 0,
708 alpha_is_shape: false,
709 }
710 }
711}
712
713impl Default for StrokeParams {
714 fn default() -> Self {
715 Self {
716 color: DeviceColor::default(),
717 line_width: 1.0,
718 line_cap: LineCap::default(),
719 line_join: LineJoin::default(),
720 miter_limit: 10.0,
721 dash_pattern: DashPattern::default(),
722 ctm: Matrix::default(),
723 stroke_adjust: false,
724 is_text_glyph: false,
725 overprint: false,
726 overprint_mode: 0,
727 opm_paired: false,
728 painted_channels: 0,
729 is_device_cmyk: false,
730 spot_color: None,
731 rendering_intent: 0,
732 transfer: TransferState::default(),
733 halftone: HalftoneState::default(),
734 bg_ucr: BgUcrState::default(),
735 alpha: 1.0,
736 blend_mode: 0,
737 alpha_is_shape: false,
738 }
739 }
740}
741
742impl Default for ClipParams {
743 fn default() -> Self {
744 Self {
745 fill_rule: FillRule::default(),
746 ctm: Matrix::default(),
747 stroke_params: None,
748 }
749 }
750}
751
752impl Default for TextParams {
753 fn default() -> Self {
754 Self {
755 text: Vec::new(),
756 start_x: 0.0,
757 start_y: 0.0,
758 font_entity: 0,
759 font_name: Vec::new(),
760 font_type: 1,
761 font_size: 0.0,
762 color: DeviceColor::default(),
763 ctm: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
764 font_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
765 paint_type: 0,
766 stroke_width: 0.0,
767 spot_color: None,
768 rendering_intent: 0,
769 transfer: TransferState::default(),
770 halftone: HalftoneState::default(),
771 bg_ucr: BgUcrState::default(),
772 fill_opacity: 1.0,
773 stroke_opacity: 1.0,
774 blend_mode: 0,
775 alpha_is_shape: false,
776 text_knockout: true,
777 }
778 }
779}
780
781impl Default for ImageColorSpace {
782 fn default() -> Self {
783 ImageColorSpace::DeviceGray
784 }
785}
786
787impl Default for ImageParams {
788 fn default() -> Self {
789 Self {
790 width: 0,
791 height: 0,
792 color_space: ImageColorSpace::default(),
793 bits_per_component: 8,
794 ctm: Matrix::default(),
795 image_matrix: Matrix::default(),
796 interpolate: false,
797 mask_color: None,
798 alpha: 1.0,
799 blend_mode: 0,
800 overprint: false,
801 overprint_mode: 0,
802 opm_paired: false,
803 painted_channels: 0,
804 alpha_is_shape: false,
805 }
806 }
807}
808
809impl Default for ShadingColorSpace {
810 fn default() -> Self {
811 ShadingColorSpace::DeviceRGB
812 }
813}
814
815impl Default for ColorStop {
816 fn default() -> Self {
817 Self {
818 position: 0.0,
819 color: DeviceColor::default(),
820 raw_components: Vec::new(),
821 }
822 }
823}
824
825impl Default for AxialShadingParams {
826 fn default() -> Self {
827 Self {
828 x0: 0.0,
829 y0: 0.0,
830 x1: 0.0,
831 y1: 0.0,
832 color_stops: Vec::new(),
833 extend_start: false,
834 extend_end: false,
835 ctm: Matrix::default(),
836 bbox: None,
837 color_space: ShadingColorSpace::default(),
838 overprint: false,
839 painted_channels: 0,
840 alpha: 1.0,
841 blend_mode: 0,
842 alpha_is_shape: false,
843 spot_tint_blend: false,
844 }
845 }
846}
847
848impl Default for RadialShadingParams {
849 fn default() -> Self {
850 Self {
851 x0: 0.0,
852 y0: 0.0,
853 r0: 0.0,
854 x1: 0.0,
855 y1: 0.0,
856 r1: 0.0,
857 color_stops: Vec::new(),
858 extend_start: false,
859 extend_end: false,
860 ctm: Matrix::default(),
861 bbox: None,
862 color_space: ShadingColorSpace::default(),
863 overprint: false,
864 painted_channels: 0,
865 alpha: 1.0,
866 blend_mode: 0,
867 alpha_is_shape: false,
868 spot_tint_blend: false,
869 }
870 }
871}
872
873impl Default for MeshShadingParams {
874 fn default() -> Self {
875 Self {
876 triangles: Vec::new(),
877 ctm: Matrix::default(),
878 bbox: None,
879 color_space: ShadingColorSpace::default(),
880 overprint: false,
881 painted_channels: 0,
882 color_lut: None,
883 alpha: 1.0,
884 blend_mode: 0,
885 alpha_is_shape: false,
886 }
887 }
888}
889
890impl Default for PatchShadingParams {
891 fn default() -> Self {
892 Self {
893 patches: Vec::new(),
894 ctm: Matrix::default(),
895 bbox: None,
896 color_space: ShadingColorSpace::default(),
897 overprint: false,
898 painted_channels: 0,
899 color_lut: None,
900 alpha: 1.0,
901 blend_mode: 0,
902 alpha_is_shape: false,
903 }
904 }
905}
906
907/// Trait for consuming rendered page pixel data.
908pub trait PageSink: Send {
909 /// Start a new page with the given pixel dimensions.
910 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String>;
911
912 /// Write one or more rows of RGBA pixel data (4 bytes per pixel, row-major).
913 fn write_rows(&mut self, rgba_rows: &[u8], num_rows: u32) -> Result<(), String>;
914
915 /// Finish the current page. May block (e.g., viewer waits for user input).
916 fn end_page(&mut self) -> Result<(), String>;
917}
918
919/// Factory for creating per-page sinks.
920pub trait PageSinkFactory: Send + Sync {
921 /// Create a new sink for a single page.
922 fn create_sink(&self, output_path: &str) -> Result<Box<dyn PageSink>, String>;
923}