Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`--transparent` for `--device png`, and `render_to_rgba_with_background`
in `stet-render`.** Pages are rendered onto a transparent backdrop and
composited onto white paper only as the last step; the option skips that
step and writes straight-alpha RGBA instead, so artwork (EPS, AI, PDF) can
be placed over other content with its unpainted areas clear. Both the
PostScript and PDF input paths honour it. `render_to_rgba` and
`render_to_rgba_with_layers` keep their signatures and white paper.
- **`--cmyk-intent perceptual|relative`, and `IccCacheOptions::cmyk_source_table`.**
The CLUT bake samples the source CMYK profile's `A2B1` (relative) table,
which for a print profile is a good deal lighter in the blacks than the
`A2B0` (perceptual) table that lcms2, Ghostscript and ImageMagick use by
default: Japan Color 2001 Coated renders K100 as (51,45,43) rather than
(35,25,22). The option selects the perceptual table, which reproduces
lcms2's default to within 1 RGB level over a 60-patch CMYK sweep. The
default is unchanged (relative), and a profile without a perceptual table
falls back to the colorimetric one.

### Removed

- **`stet-wasm`: the `set_page_callback()` / `clear_page_callback()` JS
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,13 @@ zoom presets, minimap navigation, and drag-and-drop.
| `--threads <N>` | Worker-thread count (default: 75 % of cores in viewer mode, 8 otherwise) |
| `--no-icc` | Disable ICC color management entirely |
| `--no-aa` | Disable anti-aliasing |
| `--transparent` | Leave unpainted areas transparent instead of white paper (`--device png` only; straight-alpha RGBA) |
| `--output-profile <FILE>` | Generic ICC output profile (also used as source CMYK when `--cmyk-profile` is absent) |
| `--cmyk-profile <FILE>` | Pin the source CMYK ICC profile for CMYK→sRGB conversion |
| `--use-output-intent` | Honour the PDF's embedded OutputIntent as the source CMYK profile (default) |
| `--no-output-intent` | Ignore the PDF's embedded OutputIntent and use the system CMYK profile |
| `--bpc <on\|off\|auto>` | Black-point compensation (default: `auto`, currently equivalent to `on`) |
| `--cmyk-intent <perceptual\|relative>` | Which table of the source CMYK profile drives CMYK conversion (default: `relative`). A print profile's perceptual table carries a darker black — what lcms2, Ghostscript and ImageMagick use by default |
| `--password <PW>` | Password for encrypted PDF input |
| `--timeout <SECONDS>` | Abort a job running longer than this. No limit by default — PostScript is Turing-complete and legitimate jobs run for minutes. Set one for untrusted input |
| `--max-vm <MB>` | Ceiling on PostScript VM — strings, arrays, dictionaries (default 8192). Exceeding it raises `VMerror` instead of aborting. Separate from the renderer's image and band buffers, so it does **not** cap rendering resolution |
Expand Down
5 changes: 5 additions & 0 deletions crates/stet-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ Common options:
viewer mode, 8 otherwise)
--password <PW> Password for encrypted PDF input
--no-aa Disable anti-aliasing
--transparent Leave unpainted areas transparent instead of
white paper (--device png only)

Resource limits (for untrusted input):
--timeout <SECONDS> Abort a job running longer than this. No limit
Expand All @@ -158,6 +160,9 @@ Colour management:
--no-output-intent Ignore it and use the system CMYK profile
--bpc <on|off|auto> Black-point compensation (default: auto,
currently equivalent to on)
--cmyk-intent <perceptual|relative>
Which table of the source CMYK profile drives
CMYK conversion (default: relative)
```

`stet --help` prints the same list; `scripts/check-cli-docs.sh` in the
Expand Down
65 changes: 63 additions & 2 deletions crates/stet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::path::PathBuf;
use stet_core::context::Context;
use stet_core::eps::{content_is_epsf, read_eps_bounding_box, strip_dos_eps_header};
use stet_engine::eval::{parse_and_exec, parse_and_exec_file};
use stet_graphics::icc::{BpcMode, IccCacheOptions};
use stet_graphics::icc::{BpcMode, CmykSourceTable, IccCacheOptions};
use stet_ops::build_system_dict;
use stet_pdf::PdfDevice;
use stet_pdf_reader::PdfDocument;
Expand All @@ -25,6 +25,7 @@ struct IccCliConfig {
output_profile_path: Option<String>,
cmyk_profile_path: Option<String>,
bpc_mode: BpcMode,
cmyk_source_table: CmykSourceTable,
/// When true, prefer the PDF's embedded `/OutputIntents[].DestOutputProfile`
/// over the system-default CMYK profile (unless `--cmyk-profile` is also
/// set, which always wins). Off by default because it changes the sRGB
Expand Down Expand Up @@ -105,10 +106,12 @@ fn main() {
let mut device_name: Option<String> = None;
let mut no_icc = false;
let mut no_aa = false;
let mut transparent = false;
let mut output_profile_path: Option<String> = None;
let mut cmyk_profile_path: Option<String> = None;
let mut bpc_mode = BpcMode::Auto;
let mut bpc_explicit = false;
let mut cmyk_source_table = CmykSourceTable::default();
// Default: honour the PDF's declared OutputIntent as the CMYK→sRGB
// source profile. Matches Acrobat's behaviour for PDF/X files and
// eliminates profile-approximation artefacts on GWG swatches (e.g.
Expand Down Expand Up @@ -206,6 +209,11 @@ fn main() {
i += 1;
continue;
}
"--transparent" => {
transparent = true;
i += 1;
continue;
}
"--output-profile" => {
if i + 1 < args.len() {
output_profile_path = Some(args[i + 1].clone());
Expand All @@ -226,6 +234,26 @@ fn main() {
std::process::exit(1);
}
}
"--cmyk-intent" => {
if i + 1 < args.len() {
cmyk_source_table = match args[i + 1].as_str() {
"perceptual" => CmykSourceTable::Perceptual,
"relative" => CmykSourceTable::Colorimetric,
other => {
eprintln!(
"Error: --cmyk-intent must be one of: perceptual, relative (got '{}')",
other
);
std::process::exit(1);
}
};
i += 2;
continue;
} else {
eprintln!("Error: --cmyk-intent requires a value (perceptual|relative)");
std::process::exit(1);
}
}
"--bpc" => {
if i + 1 < args.len() {
bpc_mode = match args[i + 1].as_str() {
Expand Down Expand Up @@ -402,6 +430,7 @@ run stet once per file",
output_profile_path,
cmyk_profile_path,
bpc_mode,
cmyk_source_table,
use_output_intent,
};

Expand Down Expand Up @@ -496,13 +525,22 @@ writes all pages to one file",
std::process::exit(1);
}

if transparent && device != "png" {
eprintln!(
"Error: --transparent is only supported for --device png (got '{}')",
device
);
std::process::exit(1);
}

match device.as_str() {
"png" => {
run_png_mode(
dpi,
file_args,
&icc_cfg,
no_aa,
transparent,
page_filter,
false,
password.as_deref(),
Expand All @@ -523,6 +561,7 @@ writes all pages to one file",
file_args,
&icc_cfg,
no_aa,
false,
page_filter,
true,
password.as_deref(),
Expand Down Expand Up @@ -618,6 +657,7 @@ fn run_png_mode(
file_args: Vec<String>,
icc_cfg: &IccCliConfig,
no_aa: bool,
transparent: bool,
page_filter: Option<std::collections::HashSet<i32>>,
use_viewport: bool,
password: Option<&str>,
Expand All @@ -636,6 +676,7 @@ fn run_png_mode(
&file_args,
&page_filter,
no_aa,
transparent,
use_viewport,
icc_cfg,
password,
Expand Down Expand Up @@ -665,6 +706,7 @@ fn run_png_mode(
dev.set_system_cmyk_bytes(bytes.clone());
}
dev.set_no_aa(no_aa);
dev.set_transparent_background(transparent);
dev.set_use_viewport_path(use_viewport);
Box::new(dev)
}));
Expand Down Expand Up @@ -1218,6 +1260,9 @@ Common options:
75% of cores in viewer mode and 8 otherwise, where
sequential PNG writing limits the benefit of more.
--no-aa Disable anti-aliasing.
--transparent Leave unpainted areas transparent instead of
white paper (--device png only). Pixels are
written as straight-alpha RGBA.
--password <PW> Password for encrypted PDF input.

Colour management:
Expand All @@ -1232,6 +1277,12 @@ Colour management:
--no-output-intent Ignore the PDF's OutputIntent and fall
back to the system CMYK profile.
--bpc <on|off|auto> Black-point compensation mode (default auto).
--cmyk-intent <perceptual|relative>
Which table of the source CMYK profile drives
CMYK conversion (default relative). A print
profile's perceptual table carries a darker
black, and is what lcms2, Ghostscript and
ImageMagick use by default.

Subcommands:
inspect <FILE.pdf> Print a structural summary of a PDF
Expand Down Expand Up @@ -1317,6 +1368,7 @@ fn build_icc_cache(icc_cfg: &IccCliConfig) -> stet_graphics::icc::IccCache {
if icc_cfg.no_icc {
return IccCache::new_with_options(IccCacheOptions {
bpc_mode: BpcMode::Off,
cmyk_source_table: icc_cfg.cmyk_source_table,
source_cmyk_profile: None,
});
}
Expand All @@ -1330,6 +1382,7 @@ fn build_icc_cache(icc_cfg: &IccCliConfig) -> stet_graphics::icc::IccCache {
eprintln!("[ICC] Loaded source CMYK profile: {}", path);
return IccCache::new_with_options(IccCacheOptions {
bpc_mode: icc_cfg.bpc_mode,
cmyk_source_table: icc_cfg.cmyk_source_table,
source_cmyk_profile: Some(bytes),
});
}
Expand All @@ -1346,12 +1399,14 @@ fn build_icc_cache(icc_cfg: &IccCliConfig) -> stet_graphics::icc::IccCache {
eprintln!("[ICC] Loaded output profile: {}", path);
return IccCache::new_with_options(IccCacheOptions {
bpc_mode: icc_cfg.bpc_mode,
cmyk_source_table: icc_cfg.cmyk_source_table,
source_cmyk_profile: Some(bytes),
});
}

let mut cache = IccCache::new_with_options(IccCacheOptions {
bpc_mode: icc_cfg.bpc_mode,
cmyk_source_table: icc_cfg.cmyk_source_table,
source_cmyk_profile: None,
});
cache.search_system_cmyk_profile();
Expand Down Expand Up @@ -2276,11 +2331,13 @@ fn compute_fit_dims(
(out_w, out_h, dpi)
}

#[expect(clippy::too_many_arguments)]
fn render_pdf_page_to_rgba(
doc: &PdfDocument,
page: usize,
dpi: f64,
no_aa: bool,
transparent: bool,
use_viewport: bool,
target_width: Option<u32>,
target_height: Option<u32>,
Expand All @@ -2307,13 +2364,15 @@ fn render_pdf_page_to_rgba(
no_aa,
)
} else {
stet_render::render_to_rgba(
stet_render::render_to_rgba_with_background(
&display_list,
pixel_w,
pixel_h,
effective_dpi,
Some(doc.icc_cache()),
no_aa,
&stet_graphics::layer_set::LayerSet::new(),
transparent,
)
};
Ok((rgba, pixel_w, pixel_h))
Expand All @@ -2325,6 +2384,7 @@ fn run_pdf_input_png(
file_args: &[String],
page_filter: &Option<std::collections::HashSet<i32>>,
no_aa: bool,
transparent: bool,
use_viewport: bool,
icc_cfg: &IccCliConfig,
password: Option<&str>,
Expand Down Expand Up @@ -2421,6 +2481,7 @@ were selected from '{}'",
page,
dpi,
no_aa,
transparent,
use_viewport,
target_width,
target_height,
Expand Down
1 change: 1 addition & 0 deletions crates/stet-graphics/examples/icc_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ fn main() {
let opts = IccCacheOptions {
bpc_mode: mode,
source_cmyk_profile: Some(bytes.clone()),
..Default::default()
};
let cache = IccCache::new_with_options(opts);
for &(c, m, y, k) in cases {
Expand Down
Loading