Skip to main content

ipp_printer_app/
raster.rs

1//! [`RasterDriver`] trait + page-header DTO for raster print jobs.
2
3/// What a print-job callback reports back to the framework. The framework is
4/// the spooler: it decides what happens to the job based on this value, so an
5/// implementor only has to classify the result of one attempt — never write
6/// its own retry/queue logic.
7///
8/// This is what makes a printer application behave like a *printer*: a device
9/// that isn't ready (powered off, busy, paper being reloaded) must not drop the
10/// job — it returns [`JobOutcome::DeviceUnavailable`] and the framework holds
11/// the job (`job-state = processing-stopped`) and retries it until the device
12/// is back, the same way a real printer holds a job through a paper jam.
13#[derive(Debug, Clone)]
14pub enum JobOutcome {
15    /// The document was printed. The job completes.
16    Completed,
17    /// The device can't print right now but is expected to recover. The
18    /// framework keeps the job and re-invokes the callback (with backoff) until
19    /// it prints or the client cancels it, surfacing `reasons` on the printer
20    /// (`printer-state-reasons`) meanwhile. Return this for transient device
21    /// conditions — unreachable hardware, busy link, media being reloaded.
22    DeviceUnavailable {
23        /// Reasons to surface while held, e.g. [`crate::flags::PrinterReason::OFFLINE`].
24        reasons: crate::flags::PrinterReason,
25    },
26    /// Permanent failure for *this* document (corrupt/unsupported data, a size
27    /// the device can't handle, …). The job aborts; retrying wouldn't help.
28    Failed(JobFailure),
29}
30
31/// Failure of a print job, carrying IPP-visible printer reasons + a message.
32#[derive(Debug, Clone)]
33pub struct JobFailure {
34    /// Reasons OR'd into the printer's `printer-state-reasons` IPP attribute
35    /// when this job aborts.
36    pub printer_reasons: crate::flags::PrinterReason,
37    /// Human-readable message surfaced as `job-state-message`.
38    pub message: String,
39}
40
41impl JobFailure {
42    /// Build a failure with explicit `printer-state-reasons`.
43    pub fn new(printer_reasons: crate::flags::PrinterReason, message: impl Into<String>) -> Self {
44        Self {
45            printer_reasons,
46            message: message.into(),
47        }
48    }
49
50    /// Shorthand for a generic failure (`PrinterReason::OTHER`).
51    pub fn other(message: impl Into<String>) -> Self {
52        Self::new(crate::flags::PrinterReason::OTHER, message)
53    }
54}
55
56impl std::fmt::Display for JobFailure {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.message)
59    }
60}
61
62impl std::error::Error for JobFailure {}
63
64/// Page geometry parsed from a CUPS/PWG raster page header.
65#[derive(Debug, Clone)]
66pub struct JobOptions {
67    /// Page width in pixels.
68    pub width: u32,
69    /// Page height in pixels.
70    pub height: u32,
71    /// Bits per pixel (typically 1 for monochrome, 8 for grayscale, 24 for RGB).
72    pub bits_per_pixel: u32,
73    /// Bytes per scanline (already pre-padded by the raster source).
74    pub bytes_per_line: u32,
75    /// Number of copies requested. Always ≥ 1.
76    pub copies: u32,
77}
78
79impl JobOptions {
80    /// Construct from a CUPS raster v1 page header. `num_copies < 1` is
81    /// clamped to 1 per IPP convention.
82    pub fn from_cups_v1(
83        width: u32,
84        height: u32,
85        bits_per_pixel: u32,
86        bytes_per_line: u32,
87        num_copies: u32,
88    ) -> Self {
89        Self {
90            width,
91            height,
92            bits_per_pixel,
93            bytes_per_line,
94            copies: num_copies.max(1),
95        }
96    }
97}
98
99/// Driver that turns a stream of raster scanlines into device bytes.
100///
101/// Implementations are *per-job stateful* — `start_job` returns a fresh
102/// value that owns the page buffer, `write_line` accumulates scanlines,
103/// `end_page` transfers the page to the device, `end_job` releases
104/// resources. The framework's IPP `Print-Job` handler drives this trait;
105/// you only need to provide a type that knows how to talk to your device.
106///
107/// `start_job`/`start_page`/`write_line` are synchronous (they only build and
108/// fill the page buffer); the device-touching `end_page`/`end_job` are async so
109/// the driver can await a transport (USB HID, Bluetooth RFCOMM, BLE GATT)
110/// without blocking the runtime.
111#[async_trait::async_trait]
112pub trait RasterDriver: Sized + Send + 'static {
113    /// The driver's opaque device handle (e.g. an open HID descriptor).
114    type Device: Send + Sync;
115
116    /// Allocate per-job state. Called once at the top of each job.
117    fn start_job(
118        printer: &crate::printer::PrinterHandle<'_>,
119        options: &JobOptions,
120        device: &Self::Device,
121    ) -> Result<Self, JobFailure>;
122
123    /// Called once per page before any `write_line`. Default: no-op.
124    fn start_page(
125        &mut self,
126        _options: &JobOptions,
127        _page: u32,
128        _device: &Self::Device,
129    ) -> Result<(), JobFailure> {
130        Ok(())
131    }
132
133    /// Append one scanline to the page buffer.
134    fn write_line(&mut self, options: &JobOptions, y: u32, line: &[u8]) -> Result<(), JobFailure>;
135
136    /// Transfer the completed page to the device (and repeat for copies).
137    async fn end_page(
138        &mut self,
139        options: &JobOptions,
140        page: u32,
141        device: &Self::Device,
142    ) -> Result<(), JobFailure>;
143
144    /// Release per-job state. Called once at the end of the job.
145    async fn end_job(self, device: &Self::Device);
146}