Skip to main content

plat/
print.rs

1// print -- printing, the OS-independent part: paper sizes, page layout, rows over white, the
2// job as a one-page PDF  [src/plat/print.s, src/img/pdfwrite.s; docs/modules/PRINT.md]
3//
4//   print(job) -> 0 printed / ERR_CANCEL / ERR_UNSUPPORTED / ERR_IO / ERR_NOMEM / ERR_DIMS
5//        the OS part: win_print.rs (PrintDlgExW + GDI), linux_print.rs (portal Print + PDF)
6//   page_init(paper, flags) -> PPage         the paper PAPER_* in pt (PJF_LANDSCAPE swaps
7//                                            width / height), no unprintable edge
8//   layout(job, page) -> Place               the image rectangle and the printable area on
9//                                            the page (pt, from the top-left)
10//   row_white(dst, src, n, mode)             n pixels (B, G, R, A) -> n * 3 bytes composited
11//                                            over white (PRW_* mode bits)
12//   print_pdf(job, page) -> PDF bytes / ERR_*  the job laid out on page as a one-page PDF
13//                                            (job.page_w / page_h set)
14//   print_dpi(unit, dpu) -> f64              pixels per inch from a resolution in unit (0 none
15//                                            -> 96, 1 inch, 2 cm)
16//   pdf_write_image(src, opt) -> PDF bytes / ERR_*   one image on one PDF page (pdfwrite.s)
17//   set_print_deflate(f)                     the zlib compressor of the PDF streams
18//
19// Deviation (plat-shared): plat must not depend on the image crates, so pdfwrite.s is ported
20// here and the one thing it took from img -- zlib_deflate -- comes through a hook.  An app
21// sets it to img_core::deflate::zlib_deflate (then the PDF is byte-identical to the
22// assembly's); without a hook the streams are zlib "stored" blocks (a valid, uncompressed
23// PDF).  The source is a PrintSrc (rows of B, G, R, A bytes; the gfx::Surface fields)
24// instead of a SURFACE pointer.
25
26use alloc::string::String;
27use alloc::vec::Vec;
28use core::fmt::Write;
29use core::sync::atomic::{AtomicUsize, Ordering};
30
31use rt::fx::{cvtd64, maxsd, minsd};
32
33// ---- results (the ERR_* of jfgui.inc / plugin.inc that plat returns)
34pub const ERR_NOMEM: i32 = -8;
35pub const ERR_UNSUPPORTED: i32 = -9;
36pub const ERR_DIMS: i32 = -12;
37pub const ERR_IO: i32 = -13;
38pub const ERR_CANCEL: i32 = -14;
39pub const ERR_LIMIT: i32 = -15;
40
41// ---- the source formats (FMT_* of jfgui.inc)
42pub const PRINT_XRGB: u8 = 0; // alpha byte ignored
43pub const PRINT_PARGB: u8 = 1; // premultiplied
44pub const PRINT_ARGB: u8 = 3; // straight
45
46// ---- PrintJob::flags
47pub const PJF_LANDSCAPE: u32 = 1; // Querformat
48pub const PJF_CENTER: u32 = 2; // centre in the printable area (else its top-left corner)
49pub const PJF_SMASK: u32 = 4; // PDF only: straight colours + alpha as /SMask (no white)
50// ---- PrintJob::scale
51pub const PJS_FIT: u32 = 0; // as large as the printable area allows (aspect kept)
52pub const PJS_DPI: u32 = 1; // original size: pixels / DPI inches
53pub const PJS_PERCENT: u32 = 2; // PrintJob::percent of the original size
54// ---- paper
55pub const PAPER_A4: u32 = 0;
56pub const PAPER_LETTER: u32 = 1;
57pub const PAPER_A5: u32 = 2;
58pub const PAPER_LEGAL: u32 = 3;
59pub const PAPER_A3: u32 = 4;
60pub const PAPER_N: u32 = 5;
61// ---- row_white modes
62pub const PRW_BGR: u32 = 1; // B, G, R byte order (GDI DIB) instead of R, G, B (PDF)
63pub const PRW_PREMUL: u32 = 2; // source is premultiplied
64pub const PRW_COPY: u32 = 4; // straight colours unchanged (no white; PDF with SMask)
65pub const PRW_OPAQUE: u32 = 8; // alpha ignored (XRGB)
66
67const TITLE_MAX: usize = 256; // code points of /Title
68const PDF_LEVEL_DEF: i32 = 6;
69
70/// the pixels of a print job: rows of 32-bit B, G, R, A (the fields of gfx::Surface)
71#[derive(Clone, Copy)]
72pub struct PrintSrc<'a> {
73    pub px: &'a [u8],
74    pub w: i32,
75    pub h: i32,
76    pub stride: i32,
77    pub fmt: u8, // PRINT_ARGB (straight), PRINT_PARGB or PRINT_XRGB
78}
79
80/// one print job (PRINTJOB)
81#[derive(Clone, Copy)]
82pub struct PrintJob<'a> {
83    pub src: Option<PrintSrc<'a>>,
84    pub title: &'a str, // document name (spooler job, PDF /Title); "" = "Bild" (no PDF /Title)
85    pub dpi_x: f64,     // image pixels per inch (<= 0: 96)
86    pub dpi_y: f64,     // (<= 0: dpi_x)
87    pub flags: u32,     // PJF_*
88    pub scale: u32,     // PJS_*
89    pub percent: u32,   // PJS_PERCENT: 1..10000 % of the size by DPI
90    pub margin: u32,    // on every side, 1/100 mm (the printer's unprintable edge wins when larger)
91    pub paper: u32,     // PAPER_* suggested to the dialog / used without one
92    // out
93    pub copies: u32,  // copies (informative, 0 = unknown)
94    pub page_w: f64,  // page size in pt the job was laid out on
95    pub page_h: f64,
96}
97
98impl<'a> PrintJob<'a> {
99    pub fn new(src: Option<PrintSrc<'a>>, title: &'a str) -> PrintJob<'a> {
100        PrintJob {
101            src,
102            title,
103            dpi_x: 96.0,
104            dpi_y: 96.0,
105            flags: 0,
106            scale: PJS_FIT,
107            percent: 100,
108            margin: 0,
109            paper: PAPER_A4,
110            copies: 0,
111            page_w: 0.0,
112            page_h: 0.0,
113        }
114    }
115}
116
117/// the page (PPAGE, pt = 1/72 inch)
118#[derive(Clone, Copy, Default, Debug)]
119pub struct PPage {
120    pub w: f64, // page size (orientation applied)
121    pub h: f64,
122    pub l: f64, // unprintable edge left / top / right / bottom
123    pub t: f64,
124    pub r: f64,
125    pub b: f64,
126    pub token: u32, // print system cookie (portal token)
127    pub flags: u32, // PJF_LANDSCAPE as chosen in the dialog
128}
129
130/// where the image goes (PLACE, pt from the page's top-left)
131#[derive(Clone, Copy, Default, Debug)]
132pub struct Place {
133    pub x: f64, // image rectangle
134    pub y: f64,
135    pub w: f64,
136    pub h: f64,
137    pub ax: f64, // printable area (margins applied); the image is clipped to it
138    pub ay: f64,
139    pub aw: f64,
140    pub ah: f64,
141}
142
143/// PDFOPT of pdf_write_image
144#[derive(Clone, Copy, Default)]
145pub struct PdfOpt<'a> {
146    pub page_w: f64, // MediaBox in pt
147    pub page_h: f64,
148    pub x: f64, // image rectangle, pt from the page's top-left
149    pub y: f64,
150    pub w: f64,
151    pub h: f64,
152    pub cx: f64, // clip rectangle (cw <= 0: no clip)
153    pub cy: f64,
154    pub cw: f64,
155    pub ch: f64,
156    pub title: &'a str, // /Title ("" = none)
157    pub flags: u32,     // PJF_SMASK
158    pub level: i32,     // deflate level (<= 0: 6)
159}
160
161// PAPER_* portrait width, height in pt (mm * 72 / 25.4)
162static PAPER_PT: [[f64; 2]; 5] = [
163    [595.2755905511812, 841.8897637795276], // A4 210 x 297 mm
164    [612.0, 792.0],                         // Letter 8.5 x 11 in
165    [419.5275590551181, 595.2755905511812], // A5 148 x 210 mm
166    [612.0, 1008.0],                        // Legal 8.5 x 14 in
167    [841.8897637795276, 1190.551181102362], // A3 297 x 420 mm
168];
169const MM100_PT: f64 = 0.028346456692913385; // 1/100 mm in pt (72 / 2540)
170
171/// the paper in pt, PJF_LANDSCAPE swaps; no unprintable edge  [print_page_init]
172pub fn page_init(paper: u32, flags: u32) -> PPage {
173    let p = PAPER_PT[if paper >= PAPER_N { 0 } else { paper as usize }];
174    let (w, h) = if flags & PJF_LANDSCAPE != 0 { (p[1], p[0]) } else { (p[0], p[1]) };
175    PPage { w, h, l: 0.0, t: 0.0, r: 0.0, b: 0.0, token: 0, flags: flags & PJF_LANDSCAPE }
176}
177
178/// pixels per inch from a document resolution: unit 0 none -> 96, 1 inch, 2 cm (* 2.54);
179/// dpu <= 0 -> 96  [print_dpi]
180pub fn print_dpi(unit: u32, dpu: f64) -> f64 {
181    if unit < 1 || !(dpu > 0.0) {
182        return 96.0;
183    }
184    if unit == 2 { dpu * 2.54 } else { dpu }
185}
186
187/// the image rectangle and the printable area on the page  [print_layout]
188pub fn layout(job: &PrintJob, page: &PPage) -> Place {
189    let dx = if job.dpi_x > 0.0 { job.dpi_x } else { 96.0 };
190    let dy = if job.dpi_y > 0.0 { job.dpi_y } else { dx };
191    let (pw, ph) = job.src.map_or((1, 1), |s| (s.w, s.h));
192    let mut iw = pw as f64 * 72.0 / dx; // image size in pt
193    let mut ih = ph as f64 * 72.0 / dy;
194    let m = job.margin as f64 * MM100_PT;
195    let mut pl = Place::default();
196    pl.ax = maxsd(page.l, m);
197    pl.aw = maxsd(page.w - maxsd(page.r, m) - pl.ax, 1.0);
198    pl.ay = maxsd(page.t, m);
199    pl.ah = maxsd(page.h - maxsd(page.b, m) - pl.ay, 1.0);
200    if job.scale != PJS_DPI {
201        let s = if job.scale == PJS_PERCENT {
202            job.percent.clamp(1, 10000) as f64 / 100.0
203        } else {
204            minsd(pl.aw / iw, pl.ah / ih)
205        };
206        iw *= s;
207        ih *= s;
208    }
209    pl.w = iw;
210    pl.h = ih;
211    pl.x = pl.ax;
212    pl.y = pl.ay;
213    if job.flags & PJF_CENTER != 0 {
214        pl.x += (pl.aw - iw) * 0.5;
215        pl.y += (pl.ah - ih) * 0.5;
216    }
217    pl
218}
219
220/// n pixels of src (B, G, R, A) -> n * 3 bytes of dst (R, G, B; PRW_BGR: B, G, R).  Straight:
221/// c * a / 255 + 255 - a (rounded); PRW_PREMUL: c + 255 - a; PRW_COPY / PRW_OPAQUE: c
222/// [print_row_white]
223pub fn row_white(dst: &mut [u8], src: &[u8], n: usize, mode: u32) {
224    for (d, s) in dst.chunks_exact_mut(3).zip(src.chunks_exact(4)).take(n) {
225        let (b, g, r) = (s[0] as u32, s[1] as u32, s[2] as u32);
226        let a = s[3] as u32;
227        let (b, g, r) = if mode & (PRW_OPAQUE | PRW_COPY) != 0 {
228            (b, g, r)
229        } else if mode & PRW_PREMUL != 0 {
230            let k = 255 - a;
231            ((b + k).min(255), (g + k).min(255), (r + k).min(255))
232        } else {
233            let w = (255 - a) * 255 + 128;
234            let st = |c: u32| {
235                let t = c * a + w;
236                (t + (t >> 8)) >> 8
237            };
238            (st(b), st(g), st(r))
239        };
240        if mode & PRW_BGR != 0 {
241            d[0] = b as u8;
242            d[1] = g as u8;
243            d[2] = r as u8;
244        } else {
245            d[0] = r as u8;
246            d[1] = g as u8;
247            d[2] = b as u8;
248        }
249    }
250}
251
252/// the job laid out on page as a one-page PDF (job.page_w / page_h set)  [print_pdf]
253pub fn print_pdf(job: &mut PrintJob, page: &PPage) -> Result<Vec<u8>, i32> {
254    let src = job.src.ok_or(ERR_DIMS)?;
255    let pl = layout(job, page);
256    job.page_w = page.w;
257    job.page_h = page.h;
258    let opt = PdfOpt {
259        page_w: page.w,
260        page_h: page.h,
261        x: pl.x,
262        y: pl.y,
263        w: pl.w,
264        h: pl.h,
265        cx: pl.ax, // clip = printable area
266        cy: pl.ay,
267        cw: pl.aw,
268        ch: pl.ah,
269        title: job.title,
270        flags: job.flags & PJF_SMASK,
271        level: 0,
272    };
273    pdf_write_image(&src, &opt)
274}
275
276// ---------------------------------------------------------------- the zlib hook
277
278/// a zlib compressor: (data, level) -> zlib stream (img_core::deflate::zlib_deflate)
279pub type DeflateFn = fn(&[u8], i32) -> Vec<u8>;
280
281static DEFLATE: AtomicUsize = AtomicUsize::new(0);
282
283/// the compressor of the PDF streams (None: zlib stored blocks, no compression)
284pub fn set_print_deflate(f: Option<DeflateFn>) {
285    DEFLATE.store(f.map_or(0, |f| f as usize), Ordering::SeqCst);
286}
287
288fn deflate(data: &[u8], level: i32) -> Vec<u8> {
289    let p = DEFLATE.load(Ordering::SeqCst);
290    if p != 0 {
291        // SAFETY: only set_print_deflate stores here, always a DeflateFn.
292        let f: DeflateFn = unsafe { core::mem::transmute::<usize, DeflateFn>(p) };
293        return f(data, level);
294    }
295    // zlib with stored blocks: header 78 01, blocks of at most 65535 bytes, Adler-32
296    let mut out = Vec::with_capacity(data.len() + data.len() / 65535 * 5 + 16);
297    out.extend_from_slice(&[0x78, 0x01]);
298    let mut chunks = data.chunks(65535).peekable();
299    if chunks.peek().is_none() {
300        out.extend_from_slice(&[1, 0, 0, 0xFF, 0xFF]);
301    }
302    while let Some(c) = chunks.next() {
303        out.push(chunks.peek().is_none() as u8);
304        out.extend_from_slice(&(c.len() as u16).to_le_bytes());
305        out.extend_from_slice(&(!(c.len() as u16)).to_le_bytes());
306        out.extend_from_slice(c);
307    }
308    let (mut a, mut b) = (1u32, 0u32);
309    for &x in data {
310        a = (a + x as u32) % 65521;
311        b = (b + a) % 65521;
312    }
313    out.extend_from_slice(&(b << 16 | a).to_be_bytes());
314    out
315}
316
317// ---------------------------------------------------------------- the PDF writer  [pdfwrite.s]
318
319// " a b" with 3 decimals each: millipoints rounded (cvtsd2si), "-" for negative  [pw_num]
320fn pw_num(o: &mut String, v: f64) {
321    let m = cvtd64(v * 1000.0);
322    if m < 0 {
323        o.push('-');
324    }
325    let u = m.unsigned_abs();
326    let _ = write!(o, "{}.{:03}", u / 1000, u % 1000);
327}
328
329fn pw_nums(o: &mut String, a: f64, b: f64) {
330    o.push(' ');
331    pw_num(o, a);
332    o.push(' ');
333    pw_num(o, b);
334}
335
336/// " /Title <FEFF....>": UTF-16BE hex, at most TITLE_MAX code points  [pw_title]
337fn pw_title(o: &mut String, s: &str) {
338    o.push_str(" /Title <FEFF");
339    for c in s.chars().take_while(|&c| c != '\0').take(TITLE_MAX) {
340        let mut buf = [0u16; 2];
341        for u in c.encode_utf16(&mut buf) {
342            let _ = write!(o, "{:04X}", u);
343        }
344    }
345    o.push('>');
346}
347
348/// one image on one PDF 1.4 page: objects 1 Catalog, 2 Pages, 3 Page, 4 Contents, 5 Image
349/// (/DeviceRGB 8 bit /FlateDecode, composited over white -- with PJF_SMASK and a straight
350/// source: straight colours), 6 Info, [7 /SMask alpha]; classic xref  [pdf_write_image]
351pub fn pdf_write_image(src: &PrintSrc, opt: &PdfOpt) -> Result<Vec<u8>, i32> {
352    let (w, h) = (src.w, src.h);
353    if w <= 0 || h <= 0 || src.px.is_empty() {
354        return Err(ERR_DIMS);
355    }
356    let mut mode = match src.fmt {
357        PRINT_ARGB => 0,
358        PRINT_PARGB => PRW_PREMUL,
359        PRINT_XRGB => PRW_OPAQUE,
360        _ => return Err(ERR_UNSUPPORTED),
361    };
362    if src.fmt == PRINT_ARGB && opt.flags & PJF_SMASK != 0 {
363        mode = PRW_COPY;
364    }
365    let level = if opt.level <= 0 { PDF_LEVEL_DEF } else { opt.level };
366    let (wu, hu) = (w as usize, h as usize);
367    // w and h size the buffers below: the rows must lie inside px
368    if src.stride < w.saturating_mul(4) {
369        return Err(ERR_DIMS);
370    }
371    let covered = wu.checked_mul(4).and_then(|rw| (hu - 1).checked_mul(src.stride as usize)?.checked_add(rw));
372    if covered.is_none_or(|n| n > src.px.len()) {
373        return Err(ERR_DIMS);
374    }
375    let stride = src.stride as isize;
376    let row_of = |y: usize| -> &[u8] {
377        let o = (y as isize * stride) as usize;
378        src.px.get(o..o + wu * 4).unwrap_or(&[])
379    };
380    // RGB rows
381    let rb = wu * 3;
382    let mut raw = alloc::vec![0u8; rb * hu];
383    for y in 0..hu {
384        row_white(&mut raw[y * rb..(y + 1) * rb], row_of(y), wu, mode);
385    }
386    let rgb = deflate(&raw, level);
387    // alpha plane for the soft mask
388    let alpha = if mode == PRW_COPY {
389        let mut a = Vec::with_capacity(wu * hu);
390        for y in 0..hu {
391            let r = row_of(y);
392            for x in 0..wu {
393                a.push(r.get(x * 4 + 3).copied().unwrap_or(0));
394            }
395        }
396        Some(deflate(&a, level))
397    } else {
398        None
399    };
400    drop(raw);
401    let nobj = if alpha.is_some() { 7 } else { 6 };
402    let mut off = [0usize; 8];
403    let mut f: Vec<u8> = Vec::with_capacity(rgb.len() + alpha.as_ref().map_or(0, |a| a.len()) + 4096);
404    let mut s = String::new();
405    f.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
406    off[1] = f.len();
407    f.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
408    off[2] = f.len();
409    f.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
410    // 3 Page
411    off[3] = f.len();
412    s.push_str("3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0");
413    pw_nums(&mut s, opt.page_w, opt.page_h);
414    s.push_str("] /Resources << /XObject << /Im1 5 0 R >> /ProcSet [/PDF /ImageC] >> /Contents 4 0 R >>\nendobj\n");
415    f.extend_from_slice(s.as_bytes());
416    // 4 Contents
417    let mut c = String::from("q\n");
418    if opt.cw > 0.0 {
419        pw_nums(&mut c, opt.cx, opt.page_h - opt.cy - opt.ch); // bottom of the clip in PDF space
420        pw_nums(&mut c, opt.cw, opt.ch);
421        c.push_str(" re W n\n");
422    }
423    pw_nums(&mut c, opt.w, 0.0);
424    pw_nums(&mut c, 0.0, opt.h);
425    pw_nums(&mut c, opt.x, opt.page_h - opt.y - opt.h);
426    c.push_str(" cm\n/Im1 Do\nQ\n");
427    off[4] = f.len();
428    s.clear();
429    let _ = write!(s, "4 0 obj\n<< /Length {} >>\nstream\n", c.len());
430    f.extend_from_slice(s.as_bytes());
431    f.extend_from_slice(c.as_bytes());
432    f.extend_from_slice(b"\nendstream\nendobj\n");
433    // 5 Image
434    off[5] = f.len();
435    s.clear();
436    let _ = write!(s, "5 0 obj\n<< /Type /XObject /Subtype /Image /Width {w} /Height {h}");
437    s.push_str(" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode");
438    if nobj == 7 {
439        s.push_str(" /SMask 7 0 R");
440    }
441    let _ = write!(s, " /Length {} >>\nstream\n", rgb.len());
442    f.extend_from_slice(s.as_bytes());
443    f.extend_from_slice(&rgb);
444    f.extend_from_slice(b"\nendstream\nendobj\n");
445    // 6 Info
446    off[6] = f.len();
447    s.clear();
448    s.push_str("6 0 obj\n<< /Producer (jfguiLib)");
449    if !opt.title.is_empty() && !opt.title.starts_with('\0') {
450        pw_title(&mut s, opt.title);
451    }
452    s.push_str(" >>\nendobj\n");
453    f.extend_from_slice(s.as_bytes());
454    // 7 SMask
455    if let Some(a) = &alpha {
456        off[7] = f.len();
457        s.clear();
458        let _ = write!(s, "7 0 obj\n<< /Type /XObject /Subtype /Image /Width {w} /Height {h}");
459        let _ = write!(
460            s,
461            " /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>\nstream\n",
462            a.len()
463        );
464        f.extend_from_slice(s.as_bytes());
465        f.extend_from_slice(a);
466        f.extend_from_slice(b"\nendstream\nendobj\n");
467    }
468    // xref, trailer
469    let startxref = f.len();
470    s.clear();
471    let _ = write!(s, "xref\n0 {}\n0000000000 65535 f \n", nobj + 1);
472    for o in &off[1..=nobj] {
473        let _ = write!(s, "{o:010} 00000 n \n");
474    }
475    let _ = write!(s, "trailer\n<< /Size {} /Root 1 0 R /Info 6 0 R >>\nstartxref\n{startxref}\n", nobj + 1);
476    s.push_str("%%EOF\n");
477    f.extend_from_slice(s.as_bytes());
478    Ok(f)
479}