mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 03:57:57 -06:00
print.exportPdf now converts each composed page to the job's colour target and writes it into a print-ready PDF: - PrintJob gains `color` (PrintColorSettings): Managed by Printer (sRGB / Adobe RGB, profile embedded, no output intent) or a printer / paper .icc / .icm converted here and named as the document's output intent; perceptual or relative colorimetric with black point compensation; Print Adjustment brightness / contrast; print sharpening Low / Standard / High for Matte or Glossy; 16 or 8 bits per component. print.set / print.get and print templates carry it; out-of-range values are refused when set. - Each photo is sharpened for the paper right after it renders at its print size. - print/output.rs converts and Flate-compresses one page at a time (memory holds one page's device samples) into ICC-tagged images through engine::pdf; the PDF is 1.6. The 256 MiB cap counts the compressed pages. - A printer profile that can't be used (missing, too large, not a profile) fails the print before anything renders, naming the file. - contact_sheet's PDF writer goes back to private: only the contact sheet uses it. Tests: a printer profile reaches the PDF as ICCBased images plus an output intent, a junk profile fails naming the file, colour settings validate with the job, sharpening and media round-trip through print.set / print.get, a missing profile fails before rendering with no activity row; the existing print and CLI tests follow the PDF 1.6 output. Docs: LRC-PRINT-JOB and gap 15.
92 lines
3.8 KiB
Rust
92 lines
3.8 KiB
Rust
//! The Print module: page layout, composition of pages and PDF output.
|
|
//!
|
|
//! [`layout`] is the pure page model (paper, margins, cells); [`compose`] renders a layout and its
|
|
//! photos to [`PageRaster`]s; [`template`] holds the saved settings; [`output`] writes the pages
|
|
//! as a PDF.
|
|
pub mod compose;
|
|
pub mod layout;
|
|
pub mod output;
|
|
pub mod template;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Lowest and highest print resolution, pixels per inch.
|
|
pub const MIN_PPI: f32 = 72.0;
|
|
pub const MAX_PPI: f32 = 720.0;
|
|
/// Pages a single print may have.
|
|
pub const MAX_JOB_PAGES: usize = 100;
|
|
/// Most pixels one page may have (about 100 megapixels: A3 at 720 ppi is 60).
|
|
pub const MAX_PAGE_PIXELS: u64 = 100_000_000;
|
|
/// Most pixels a print job may hold across all its pages (6 bytes each in memory).
|
|
pub const MAX_JOB_PIXELS: u64 = 200_000_000;
|
|
|
|
/// One composed page, ready for output: row-major interleaved RGB, 16 bits per sample, encoded in
|
|
/// Rec. 2020 ([`lightcraft_pipeline::OutputSpace::Rec2020`]; the transfer curve is that space's).
|
|
/// The page covers its paper edge to edge: `width / ppi` by `height / ppi` inches.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct PageRaster {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub ppi: f32,
|
|
pub rgb: Vec<u16>,
|
|
}
|
|
|
|
/// How a print job is produced (Lightroom's Print Job panel): resolution, and the colour
|
|
/// management, print adjustment, print sharpening and depth of [`crate::print_color`].
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct PrintJob {
|
|
/// Print resolution in pixels per inch, 72 to 720.
|
|
pub ppi: f32,
|
|
/// Profile, rendering intent, black point compensation, print adjustment, print sharpening.
|
|
pub color: crate::print_color::PrintColorSettings,
|
|
}
|
|
|
|
impl Default for PrintJob {
|
|
fn default() -> Self {
|
|
Self { ppi: 240.0, color: crate::print_color::PrintColorSettings::default() }
|
|
}
|
|
}
|
|
|
|
impl PrintJob {
|
|
/// The resolution to render at: [`Self::ppi`] clamped to 72..=720 (240 for NaN).
|
|
pub fn effective_ppi(&self) -> f32 {
|
|
if self.ppi.is_finite() { self.ppi.clamp(MIN_PPI, MAX_PPI) } else { 240.0 }
|
|
}
|
|
|
|
/// Rejects a resolution outside the supported range (commands and templates, which should
|
|
/// say so rather than silently clamp).
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if !(self.ppi.is_finite() && (MIN_PPI..=MAX_PPI).contains(&self.ppi)) {
|
|
return Err(format!("ppi must be a number from {MIN_PPI} to {MAX_PPI}"));
|
|
}
|
|
self.color.validate()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn ppi_is_clamped_and_validated() {
|
|
assert_eq!(PrintJob::default().effective_ppi(), 240.0);
|
|
for (v, e) in [(10.0, 72.0), (5000.0, 720.0), (f32::NAN, 240.0), (f32::INFINITY, 240.0), (300.0, 300.0)] {
|
|
assert_eq!(PrintJob { ppi: v, ..Default::default() }.effective_ppi(), e);
|
|
}
|
|
assert!(PrintJob { ppi: 300.0, ..Default::default() }.validate().is_ok());
|
|
for bad in [0.0, 71.0, 721.0, f32::NAN, f32::NEG_INFINITY] {
|
|
assert!(PrintJob { ppi: bad, ..Default::default() }.validate().is_err());
|
|
}
|
|
let j: PrintJob = serde_json::from_str("{}").unwrap();
|
|
assert_eq!(j, PrintJob::default());
|
|
assert!(serde_json::from_str::<PrintJob>(r#"{"dpi": 1}"#).is_err());
|
|
// the colour settings ride along, defaults filled in, and are validated with the job
|
|
let j: PrintJob = serde_json::from_str(r#"{"ppi": 300, "color": {"intent": "relative", "brightness": 20}}"#).unwrap();
|
|
assert_eq!(j.color.intent, crate::print_color::PrintIntent::Relative);
|
|
assert!(j.color.black_point_compensation && j.validate().is_ok());
|
|
let j: PrintJob = serde_json::from_str(r#"{"color": {"contrast": 500}}"#).unwrap();
|
|
assert!(j.validate().is_err());
|
|
}
|
|
}
|