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#[derive(Clone, Debug)]
82pub enum SpotColorSpace {
83 Separation {
84 name: Vec<u8>,
85 alt: SimpleColorSpace,
86 tint_table: Arc<TintLookupTable>,
87 },
88 DeviceN {
89 names: Vec<Vec<u8>>,
90 alt: SimpleColorSpace,
91 tint_table: Arc<TintLookupTable>,
92 },
93}
94
95/// Simple device color space for alt-space references.
96#[derive(Clone, Debug, PartialEq, Eq, Hash)]
97pub enum SimpleColorSpace {
98 DeviceGray,
99 DeviceRGB,
100 DeviceCMYK,
101}
102
103/// Bitmask of CMYK channels painted by an overprint operation.
104/// Bits: 0=Cyan, 1=Magenta, 2=Yellow, 3=Black.
105pub const CMYK_C: u8 = 1 << 0;
106pub const CMYK_M: u8 = 1 << 1;
107pub const CMYK_Y: u8 = 1 << 2;
108pub const CMYK_K: u8 = 1 << 3;
109pub const CMYK_ALL: u8 = CMYK_C | CMYK_M | CMYK_Y | CMYK_K;
110
111/// Map a CMYK process color name to its channel bit.
112pub fn cmyk_channel_for_name(name: &[u8]) -> u8 {
113 match name {
114 b"Cyan" => CMYK_C,
115 b"Magenta" => CMYK_M,
116 b"Yellow" => CMYK_Y,
117 b"Black" => CMYK_K,
118 b"All" => CMYK_ALL,
119 b"None" => 0,
120 _ => 0,
121 }
122}
123
124/// Parameters for filling a path.
125#[derive(Clone, Debug)]
126pub struct FillParams {
127 pub color: DeviceColor,
128 pub fill_rule: FillRule,
129 pub ctm: Matrix,
130 /// True when this fill is a text glyph from a show operator.
131 /// PDF device skips these (uses Text elements instead).
132 pub is_text_glyph: bool,
133 /// Overprint flag from graphics state (used by PDF output).
134 pub overprint: bool,
135 /// Overprint mode (0 or 1). With OPM 1 + DeviceCMYK, only non-zero channels are painted.
136 pub overprint_mode: i32,
137 /// True when /OPM was set together with /op or /OP in the same ExtGState
138 /// dict that configured this fill. Enables strict OPM-1 "preserve zero
139 /// components" behavior; when false, an all-zero CMYK source still
140 /// performs a full knockout (legacy Adobe compatibility).
141 pub opm_paired: bool,
142 /// Which CMYK channels this fill paints (bitmask of CMYK_C/M/Y/K).
143 pub painted_channels: u8,
144 /// True when color space is DeviceCMYK or ICCBased(4).
145 pub is_device_cmyk: bool,
146 /// Separation/DeviceN color for PDF output. None for device color spaces.
147 pub spot_color: Option<SpotColor>,
148 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
149 pub rendering_intent: u8,
150 /// Pre-sampled transfer function state for PDF output.
151 pub transfer: TransferState,
152 /// Pre-computed halftone screen state for PDF output.
153 pub halftone: HalftoneState,
154 /// Pre-sampled black generation / undercolor removal for PDF output.
155 pub bg_ucr: BgUcrState,
156 /// Fill opacity (0.0–1.0, default 1.0). Used by PDF transparency.
157 pub alpha: f64,
158 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
159 pub blend_mode: u8,
160}
161
162/// Parameters for a text element emitted by show operators.
163///
164/// The PDF device uses these for BT/ET/Tf/Tj text operators.
165/// The raster device ignores them (uses Fill elements for glyph paths).
166#[derive(Clone, Debug)]
167pub struct TextParams {
168 /// Character bytes (or 2-byte CID values for Type 0).
169 pub text: Vec<u8>,
170 /// Device-space X position at start of string.
171 pub start_x: f64,
172 /// Device-space Y position at start of string.
173 pub start_y: f64,
174 /// Font dict entity ID (raw u32 for VM independence).
175 pub font_entity: u32,
176 /// FontName bytes (e.g., b"Times-Roman").
177 pub font_name: Vec<u8>,
178 /// FontType (0, 1, 2, 3, 42).
179 pub font_type: i32,
180 /// Effective device-space font size.
181 pub font_size: f64,
182 /// Fill color at render time.
183 pub color: DeviceColor,
184 /// CTM at render time.
185 pub ctm: [f64; 6],
186 /// User-space font matrix (scaled to point units).
187 pub font_matrix: [f64; 6],
188 /// PaintType: 0 = fill (default), 2 = stroke (outlined glyphs).
189 pub paint_type: i32,
190 /// Device-space stroke width for PaintType 2 fonts.
191 pub stroke_width: f64,
192 /// Separation/DeviceN color for PDF output. None for device color spaces.
193 pub spot_color: Option<SpotColor>,
194 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
195 pub rendering_intent: u8,
196 /// Pre-sampled transfer function state for PDF output.
197 pub transfer: TransferState,
198 /// Pre-computed halftone screen state for PDF output.
199 pub halftone: HalftoneState,
200 /// Pre-sampled black generation / undercolor removal for PDF output.
201 pub bg_ucr: BgUcrState,
202}
203
204/// Parameters for stroking a path.
205#[derive(Clone, Debug)]
206pub struct StrokeParams {
207 pub color: DeviceColor,
208 pub line_width: f64,
209 pub line_cap: LineCap,
210 pub line_join: LineJoin,
211 pub miter_limit: f64,
212 pub dash_pattern: DashPattern,
213 pub ctm: Matrix,
214 /// When true, snap thin stroke coordinates to device pixel centers.
215 pub stroke_adjust: bool,
216 /// True when this stroke is a text glyph from a show operator (PaintType 2).
217 pub is_text_glyph: bool,
218 /// Overprint flag from graphics state (used by PDF output).
219 pub overprint: bool,
220 /// Overprint mode (0 or 1).
221 pub overprint_mode: i32,
222 /// See FillParams::opm_paired. Strict OPM-1 preserve requires both
223 /// /OPM and /op|/OP set in the same ExtGState dict.
224 pub opm_paired: bool,
225 /// Which CMYK channels this stroke paints (bitmask of CMYK_C/M/Y/K).
226 pub painted_channels: u8,
227 /// True when stroke color space is DeviceCMYK or ICCBased(4) — OPM 1 only applies to these.
228 pub is_device_cmyk: bool,
229 /// Separation/DeviceN color for PDF output. None for device color spaces.
230 pub spot_color: Option<SpotColor>,
231 /// Rendering intent (0=RelativeColorimetric, 1=Absolute, 2=Perceptual, 3=Saturation).
232 pub rendering_intent: u8,
233 /// Pre-sampled transfer function state for PDF output.
234 pub transfer: TransferState,
235 /// Pre-computed halftone screen state for PDF output.
236 pub halftone: HalftoneState,
237 /// Pre-sampled black generation / undercolor removal for PDF output.
238 pub bg_ucr: BgUcrState,
239 /// Stroke opacity (0.0–1.0, default 1.0). Used by PDF transparency.
240 pub alpha: f64,
241 /// Blend mode (0=Normal, 1=Multiply, ..., 11=Exclusion). Default 0.
242 pub blend_mode: u8,
243}
244
245/// Parameters for clipping.
246#[derive(Clone, Debug)]
247pub struct ClipParams {
248 pub fill_rule: FillRule,
249 pub ctm: Matrix,
250 /// For stroke-based clips: stroke parameters to expand the clip path
251 /// from a centerline to a stroke outline before rasterizing.
252 pub stroke_params: Option<StrokeParams>,
253}
254
255/// Pre-sampled tint transform: maps input tint values to alt-space components.
256#[derive(Clone, Debug)]
257pub struct TintLookupTable {
258 /// Number of input components (1 for Separation, N for DeviceN).
259 pub num_inputs: u32,
260 /// Number of output components (matches alternative space: 1/3/4).
261 pub num_outputs: u32,
262 /// Number of samples per dimension.
263 pub samples_per_dim: u32,
264 /// Flattened f32 data, row-major order. Length = samples_per_dim^num_inputs × num_outputs.
265 pub data: Vec<f32>,
266}
267
268impl TintLookupTable {
269 /// Linear interpolation lookup for 1D (Separation) tint transforms.
270 #[inline]
271 pub fn lookup_1d(&self, tint: f32, out: &mut [f32]) {
272 let n = self.samples_per_dim as usize;
273 let no = self.num_outputs as usize;
274 let idx = tint * (n - 1) as f32;
275 let i0 = (idx as usize).min(n - 2);
276 let frac = idx - i0 as f32;
277 let base0 = i0 * no;
278 let base1 = (i0 + 1) * no;
279 for (c, out_val) in out[..no].iter_mut().enumerate() {
280 *out_val = self.data[base0 + c] * (1.0 - frac) + self.data[base1 + c] * frac;
281 }
282 }
283
284 /// Multilinear interpolation lookup for N-D (DeviceN) tint transforms.
285 pub fn lookup_nd(&self, inputs: &[f32], out: &mut [f32]) {
286 let ni = self.num_inputs as usize;
287 let no = self.num_outputs as usize;
288 let n = self.samples_per_dim as usize;
289
290 let mut idx = [0usize; 8];
291 let mut frac = [0.0f32; 8];
292 for d in 0..ni {
293 let fi = inputs[d] * (n - 1) as f32;
294 idx[d] = (fi as usize).min(n - 2);
295 frac[d] = fi - idx[d] as f32;
296 }
297
298 let corners = 1usize << ni;
299 for out_val in out[..no].iter_mut() {
300 *out_val = 0.0;
301 }
302 for corner in 0..corners {
303 let mut weight = 1.0f32;
304 let mut linear_idx = 0usize;
305 for d in 0..ni {
306 let bit = (corner >> d) & 1;
307 let dim_idx = idx[d] + bit;
308 weight *= if bit == 1 { frac[d] } else { 1.0 - frac[d] };
309 let stride = n.pow((ni - 1 - d) as u32);
310 linear_idx += dim_idx * stride;
311 }
312 let base = linear_idx * no;
313 for (c, out_val) in out[..no].iter_mut().enumerate() {
314 *out_val += weight * self.data.get(base + c).copied().unwrap_or(0.0);
315 }
316 }
317 }
318}
319
320/// VM-free color space enum for images stored in the display list.
321#[derive(Clone, Debug)]
322pub enum ImageColorSpace {
323 DeviceGray,
324 DeviceRGB,
325 DeviceCMYK,
326 ICCBased {
327 n: u32,
328 profile_hash: ProfileHash,
329 profile_data: Arc<Vec<u8>>,
330 },
331 Indexed {
332 base: Box<ImageColorSpace>,
333 hival: u32,
334 lookup: Vec<u8>,
335 },
336 CIEBasedABC {
337 params: Arc<crate::color::CieAbcParams>,
338 },
339 CIEBasedA {
340 params: Arc<crate::color::CieAParams>,
341 },
342 /// CIE L*a*b* color space (PDF /Lab or ICCBased Lab alternate).
343 ///
344 /// Sample byte layout: 3 components (L, a, b), 8-bit each. Decode
345 /// scales bytes: L = byte/255 × 100; a = byte/255 × (`range[1]`-`range[0]`) + `range[0]`;
346 /// b = byte/255 × (`range[3]`-`range[2]`) + `range[2]`.
347 Lab {
348 white_point: [f64; 3],
349 range: [f64; 4],
350 },
351 Separation {
352 name: Vec<u8>,
353 alt_space: Box<ImageColorSpace>,
354 tint_table: Arc<TintLookupTable>,
355 },
356 DeviceN {
357 names: Vec<Vec<u8>>,
358 alt_space: Box<ImageColorSpace>,
359 tint_table: Arc<TintLookupTable>,
360 },
361 Mask {
362 color: DeviceColor,
363 polarity: bool,
364 },
365 PreconvertedRGBA,
366}
367
368impl ImageColorSpace {
369 /// Number of components per sample.
370 pub fn num_components(&self) -> u32 {
371 match self {
372 ImageColorSpace::DeviceGray => 1,
373 ImageColorSpace::DeviceRGB => 3,
374 ImageColorSpace::DeviceCMYK => 4,
375 ImageColorSpace::ICCBased { n, .. } => *n,
376 ImageColorSpace::Indexed { .. } => 1,
377 ImageColorSpace::CIEBasedABC { .. } => 3,
378 ImageColorSpace::CIEBasedA { .. } => 1,
379 ImageColorSpace::Lab { .. } => 3,
380 ImageColorSpace::Separation { .. } => 1,
381 ImageColorSpace::DeviceN { tint_table, .. } => tint_table.num_inputs,
382 ImageColorSpace::Mask { .. } => 1,
383 ImageColorSpace::PreconvertedRGBA => 4,
384 }
385 }
386}
387
388/// Parameters for drawing an image.
389#[derive(Clone, Debug)]
390pub struct ImageParams {
391 pub width: u32,
392 pub height: u32,
393 pub color_space: ImageColorSpace,
394 pub bits_per_component: u8,
395 pub ctm: Matrix,
396 pub image_matrix: Matrix,
397 pub interpolate: bool,
398 pub mask_color: Option<Vec<u8>>,
399 pub alpha: f64,
400 pub blend_mode: u8,
401 pub overprint: bool,
402 pub overprint_mode: i32,
403 /// See FillParams::opm_paired.
404 pub opm_paired: bool,
405 pub painted_channels: u8,
406}
407
408/// Color space carried through the display list for native shading output.
409#[derive(Clone, Debug)]
410pub enum ShadingColorSpace {
411 DeviceGray,
412 DeviceRGB,
413 DeviceCMYK,
414 ICCBased {
415 n: u32,
416 profile_hash: ProfileHash,
417 profile_data: Arc<Vec<u8>>,
418 },
419 CalRGB {
420 white_point: [f64; 3],
421 matrix: Option<[f64; 9]>,
422 gamma: Option<[f64; 3]>,
423 },
424 CalGray {
425 white_point: [f64; 3],
426 gamma: Option<f64>,
427 },
428}
429
430impl ShadingColorSpace {
431 /// Number of color components in this color space.
432 pub fn num_components(&self) -> usize {
433 match self {
434 ShadingColorSpace::DeviceGray | ShadingColorSpace::CalGray { .. } => 1,
435 ShadingColorSpace::DeviceRGB | ShadingColorSpace::CalRGB { .. } => 3,
436 ShadingColorSpace::DeviceCMYK => 4,
437 ShadingColorSpace::ICCBased { n, .. } => *n as usize,
438 }
439 }
440}
441
442/// A single color stop in a gradient.
443#[derive(Clone, Debug)]
444pub struct ColorStop {
445 pub position: f64,
446 pub color: DeviceColor,
447 pub raw_components: Vec<f64>,
448}
449
450/// Parameters for axial (linear) gradient shading (Type 2).
451#[derive(Clone, Debug)]
452pub struct AxialShadingParams {
453 pub x0: f64,
454 pub y0: f64,
455 pub x1: f64,
456 pub y1: f64,
457 pub color_stops: Vec<ColorStop>,
458 pub extend_start: bool,
459 pub extend_end: bool,
460 pub ctm: Matrix,
461 pub bbox: Option<[f64; 4]>,
462 pub color_space: ShadingColorSpace,
463 pub overprint: bool,
464 pub painted_channels: u8,
465 /// Fill alpha from graphics state (0.0–1.0).
466 pub alpha: f64,
467 /// True when this shading uses a Separation/DeviceN color space with a
468 /// CMYK alternate AND at least one non-process spot colorant. The
469 /// renderer composites the per-pixel CMYK from the gradient stops with
470 /// the tracked CMYK buffer multiplicatively, preserving underlying CMYK
471 /// paints under the gradient (e.g. green checkmarks under a green→cyan
472 /// DeviceN strip survive).
473 pub spot_tint_blend: bool,
474}
475
476/// Parameters for radial gradient shading (Type 3).
477#[derive(Clone, Debug)]
478pub struct RadialShadingParams {
479 pub x0: f64,
480 pub y0: f64,
481 pub r0: f64,
482 pub x1: f64,
483 pub y1: f64,
484 pub r1: f64,
485 pub color_stops: Vec<ColorStop>,
486 pub extend_start: bool,
487 pub extend_end: bool,
488 pub ctm: Matrix,
489 pub bbox: Option<[f64; 4]>,
490 pub color_space: ShadingColorSpace,
491 pub overprint: bool,
492 pub painted_channels: u8,
493 /// Fill alpha from graphics state (0.0–1.0).
494 pub alpha: f64,
495 /// See [`AxialShadingParams::spot_tint_blend`].
496 pub spot_tint_blend: bool,
497}
498
499/// A vertex in a shading triangle mesh.
500#[derive(Clone, Debug)]
501pub struct ShadingVertex {
502 pub x: f64,
503 pub y: f64,
504 pub color: DeviceColor,
505 pub raw_components: Vec<f64>,
506}
507
508/// A triangle in a shading mesh.
509#[derive(Clone, Debug)]
510pub struct ShadingTriangle {
511 pub v0: ShadingVertex,
512 pub v1: ShadingVertex,
513 pub v2: ShadingVertex,
514}
515
516/// Parameters for Gouraud-shaded triangle mesh shading (Types 4 & 5).
517#[derive(Clone, Debug)]
518pub struct MeshShadingParams {
519 pub triangles: Vec<ShadingTriangle>,
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 /// Pre-sampled color LUT for function-based mesh shadings.
526 /// When present, vertex `raw_components[0]` holds a normalized `[0,1]`
527 /// function input. The renderer interpolates this per-pixel, then
528 /// indexes the LUT instead of Gouraud-interpolating DeviceColor.
529 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
530}
531
532/// A patch in a Coons or tensor-product patch mesh.
533#[derive(Clone, Debug)]
534pub struct ShadingPatch {
535 pub points: Vec<(f64, f64)>,
536 pub colors: [DeviceColor; 4],
537 pub raw_colors: [Vec<f64>; 4],
538}
539
540/// Parameters for Coons/tensor-product patch mesh shading (Types 6 & 7).
541#[derive(Clone, Debug)]
542pub struct PatchShadingParams {
543 pub patches: Vec<ShadingPatch>,
544 pub ctm: Matrix,
545 pub bbox: Option<[f64; 4]>,
546 pub color_space: ShadingColorSpace,
547 pub overprint: bool,
548 pub painted_channels: u8,
549 /// When present, vertex `raw_colors[i][0]` holds a normalized `[0,1]`
550 /// function input. The renderer interpolates this per-pixel, then
551 /// indexes the LUT for per-pixel non-linear function evaluation.
552 pub color_lut: Option<Arc<Vec<DeviceColor>>>,
553}
554
555/// Parameters for a tiled pattern fill.
556#[derive(Clone)]
557pub struct PatternFillParams {
558 /// The path to fill with the pattern.
559 pub path: PsPath,
560 /// Fill rule for the path.
561 pub fill_rule: FillRule,
562 /// Pre-rendered display list for a single tile.
563 pub tile: DisplayList,
564 /// Pattern matrix (pattern space → device space).
565 pub pattern_matrix: Matrix,
566 /// Bounding box of one tile in pattern space.
567 pub bbox: [f64; 4],
568 /// Horizontal step between tile origins.
569 pub xstep: f64,
570 /// Vertical step between tile origins.
571 pub ystep: f64,
572 /// Paint type: 1 = colored, 2 = uncolored.
573 pub paint_type: i32,
574 /// For uncolored patterns, the fill color.
575 pub underlying_color: Option<DeviceColor>,
576 /// Unique pattern ID from pattern_store (for dedup in PDF output).
577 pub pattern_id: u32,
578 /// When true, tile display list elements have CTMs in device space
579 /// (the pattern matrix is already baked into element transforms).
580 /// When false, elements are in pattern space and the renderer applies
581 /// the pattern_matrix during rendering.
582 pub device_space_tile: bool,
583 /// When true, the tile content was designed for a Y-flipped coordinate
584 /// system (pattern matrix had negative d). The pre-rendered tile must
585 /// be vertically flipped before stamping.
586 pub flip_tile_y: bool,
587 /// For pattern strokes: stroke parameters to expand the centerline path
588 /// into a fill outline for masking. When Some, `path` is a user-space
589 /// stroke centerline rather than a fill path.
590 pub stroke_params: Option<StrokeParams>,
591 /// PDF overprint mode (0 or 1). When 1, CMYK(0,0,0,0) pixels in tile
592 /// images are transparent (no ink = don't paint).
593 pub overprint_mode: i32,
594}
595
596/// Trait for consuming rendered page pixel data.
597pub trait PageSink: Send {
598 /// Start a new page with the given pixel dimensions.
599 fn begin_page(&mut self, width: u32, height: u32) -> Result<(), String>;
600
601 /// Write one or more rows of RGBA pixel data (4 bytes per pixel, row-major).
602 fn write_rows(&mut self, rgba_rows: &[u8], num_rows: u32) -> Result<(), String>;
603
604 /// Finish the current page. May block (e.g., viewer waits for user input).
605 fn end_page(&mut self) -> Result<(), String>;
606}
607
608/// Factory for creating per-page sinks.
609pub trait PageSinkFactory: Send + Sync {
610 /// Create a new sink for a single page.
611 fn create_sink(&self, output_path: &str) -> Result<Box<dyn PageSink>, String>;
612}