* test: give faux-redaction audit its own workdir (parallel-test flake) * hayro: ColorReplacement type and luminance mapping (reading colours, #897) * fmt: engine dates (upstream Greek localization miss) * hayro: apply ColorReplacement to solid text and line-art paints (#897) * render: ReadingColors view setting flows into RenderSettings (#897) * engine: per-document reading colours state (#897) * automation: view_reading_colors tool; page_render honours it (#897) * ui: Reading colours preference (Replace Document Colors, #897) * ui: seed reading colours on revision and signed-version opens (#897) * parity: a11y.replace-page-colors shipped (#897) * render: map stencil-mask paints through reading colours (#897) An ImageMask stencil's solid paint was baked into the raster bytes raw, bypassing the reading-colours hook every other solid fill goes through: under white-on-black, CCITT scans, JBIG2 text and stencil logos drew black ink on the black mapped background and vanished. Route it through replaced(paint, false) — stencils are line art in disguise — and cover it with an ImageMask fixture test sampling mapped ink on a mapped background. * render: print previews render with reading colours off (#897) Print previews went through the document's render pool, whose config now carries the reading colours: a white-on-black user saw black sheets in the Print dialog while the actual print stayed uncoloured. The request tag is the per-request channel that already crosses the render boundary, so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by rendering previews with reading colours off — a preview's job is fidelity to the printout. The main view stays recoloured. Covered on both render paths (inline PageRenderer and pool worker). * ui: seed reading colours on created-document opens (#897) open_created (comment summaries) and open_created_bytes (images, text, blank pages, clipboard) pushed new DocViews without seeding the reading colours, unlike every other open path. A created document now starts with the app-wide preference too; the seed-on-open test drives both paths. * docs: reading-colours flag echo and display-fidelity parity notes (#897) The view_reading_colors description now says a flag that differs from the mode's preset default makes the echoed mode read custom, and the a11y.replace-page-colors parity notes record that print previews and view snapshots render with the document's true colours: reading colours apply to the reading view only. * Merge follow-ups: reading colours beside preview renders and the new catalogs - Renderer::new carries both color_replacement and the new preview flag - our raster tests construct RenderRequest with preview: false - the preference dialog grew past the window: scroll the combo into view and give the fixture a valid xref so no repair banner covers the dialog - checkboxes use the crate's square-box widget (clippy disallows egui's) - Kazakh and Turkish catalogs gain the ten reading-colours strings * Reading colours: record the hayro patch in vendor/README.md; add missing translations vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` / `ColorReplacement`, the `render_pdf` signature change and the removed `[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels to el, es, pl, te and zh-hant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8cacb44728
commit
4541974ef9
|
|
@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "77b7fedb0a4f097893a4c6901ca7e35a5b86b04d5582a26ee282dde772df90cd"
|
||||
sha256 = "f0498e7e9b71f054fa2115a1d02e5cee45f070747c37fc844b299c3536711fbd"
|
||||
|
||||
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
||||
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
||||
|
|
@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "931bd6e497c0065624cad3e1bcb94ed9bfeeb97402a58e887e7e3fa0be4da298"
|
||||
sha256 = "8061baf67a35734f87bd9d91aa9c22a6121cc450b69dd9a4862d56bc61f2b4b8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
|
|
@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "707d1ffa78f0dee179bc7531fec231aba496eddbb6c82c814887913161ea0215"
|
||||
sha256 = "49f52fd09814bc9cca6f3b659bbe26a7091f7407a569cb138b5721bc8e8d5536"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
||||
|
|
@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fb9f1350f73b270a361ce087ea5f9fc899132cb1804dae92cbac5df2da4b1d18"
|
||||
sha256 = "16723806a7d1045edbe45c06250fe4ef03ec94ea5a2829e7ec6c593a42d4db9a"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
|
|
@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e182df8e86127ee2e93a7aa274139964f4bc0e47aa3e5e5af1b56bd5a352cb30"
|
||||
sha256 = "b69ecd1590a9573b25b191f6adc0450bfc45b0866a9be83aaba2d0b079c449d7"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
|
|
@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "462da2f08ec2687db4745ab6226220e4857ac785228a8593574a0ed1b4de94f0"
|
||||
sha256 = "2806f83b280606d48a9aeaa60ae6fc3ca9e4308ae427222a4413cf88c9799b38"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
|
|
@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7a41df6534ff5ec76b291c6c21cadb88cd3c384d02a00f42cd1eb0ffbb6901a4"
|
||||
sha256 = "3d6817e945690fd1a156b0d7484a762c9c1b3286de6373be58c07e0451b4dd30"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
||||
|
|
@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "536f1b899a8075171e9d61b0bd0eec11af8b705ebf29c8f578bbee75562412d3"
|
||||
sha256 = "252cdb6adc0ab7780e56f21a8edd7d2d34a8a4918c7285a0ea3f96bd3bbb0760"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
|
|
@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "c015f67b1d8a16069b676df39c0e3bfa397e1b94dd1fa1f4717b2fd53fa0335c"
|
||||
sha256 = "68eae372d31b38e6cf93a1060838cf9f7092129508e9bae0a34b1053da16916c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
||||
|
|
@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "6285280ada0ed9f01298d22ac30c6efff7f12038b32c7b30476b9548a846f999"
|
||||
sha256 = "ba2a40614bd092949ce1bacf484d18814eb13847f787b06dc2e4b1c036cef3c8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
|
|
@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "a9dff963485baf31241bc042b18e81a71e3670a27e8532d2ec21268ac6b55b28"
|
||||
sha256 = "cf19679450f1f38c38ca41cfc0c46bbb10424697fe50eeec012a36f55b87dad8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
|
|
@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "c0a8eea56e175d053517818c44ba937703ac83343c327eb19c580d12dee51a22"
|
||||
sha256 = "0c62ce0d40d9aaff929e32188ad6506dd3e861f99f560eb7c2fb60a5cd333572"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
||||
|
|
@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "6cb45c9d8193d4c841d93a11c82d52a16c13c87f49d5443360b4064139497b41"
|
||||
sha256 = "9b7ead6a6fe54b67efb765ed9dea28f24f194b3c406ec94198eb9a1ed4838f31"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
|
|
@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "9eb29f5aae67683db578174e5d5529cbd8e5dd74703f33be12172edcbafdcbbd"
|
||||
sha256 = "ffab5ead4c717d4dbfa6021d1a36f651957586948c7f69f7cedc5133883fce37"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/kk.tsv"
|
||||
|
|
@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7d20cac93b5f173323fd7d4bfc2a499ea353635ccf6ed0b330798cbed0b21a6e"
|
||||
sha256 = "6708a64bf049de7ee454966abdf564227d69e23786e9928577a97f7a1cc7e08f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
|
|
@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "771074e8ecd3a1bf47e62008150137f025160e3820885ffbe9e7d53b803d5fd6"
|
||||
sha256 = "7ccf9f318b9d3d394247975bbea19cf124edefb17910819af153a9a2a654127f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
||||
|
|
@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fda28db1e8c8b805c083f5b046fef4f89feade599d0c4ce93ba8029d68357348"
|
||||
sha256 = "a861ee9b2e4c04a36ec83df3087f0a8bcdad30b07b2385f346533177f8003fe1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
||||
|
|
@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fc002dfa644d736875bb17f7f40f121c3b59b6156bcb233a91c3b042444af16f"
|
||||
sha256 = "7f9cb9cc57ff67d57a3b12a656673b746fa3830b57f070d36085c3faaef0ec28"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/tr.tsv"
|
||||
|
|
@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "88cc3b28d57a0d3d8cd5e59e9f32bfae183a383a24505ec699b911b2d02ef651"
|
||||
sha256 = "4c63bcca851aba720662af0a1b76b1ba0140908e3ef5e9c3016d8b0642d9e689"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
|
|
@ -3328,4 +3328,4 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "f4652ba2d75a207c28f8600b788740d777d64a96e65a48b1d71a379de033903e"
|
||||
sha256 = "13071abba5e8ec37f2f93dbe1c8bd47eecac6a0d4956407535c91ba655cad936"
|
||||
|
|
|
|||
|
|
@ -3907,6 +3907,7 @@ dependencies = [
|
|||
"lopdf",
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-fonts",
|
||||
"serde",
|
||||
"skrifa 0.44.0",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ mod links;
|
|||
pub mod mcp;
|
||||
mod measure;
|
||||
mod printing;
|
||||
mod reading;
|
||||
mod redact;
|
||||
mod signing;
|
||||
mod tools;
|
||||
|
|
@ -783,6 +784,7 @@ impl Automation {
|
|||
"comment_mark" => self.comment_mark(&a)?,
|
||||
"comment_lock" => self.comment_lock(&a)?,
|
||||
"comments_hide" => self.comments_hide(&a)?,
|
||||
"view_reading_colors" => self.view_reading_colors(&a)?,
|
||||
"comments_summarize" => self.comments_summarize(&a)?,
|
||||
"comment_edit" => self.comment_edit(&a)?,
|
||||
"comment_delete" => self.comment_delete(&a)?,
|
||||
|
|
@ -1648,8 +1650,9 @@ impl Automation {
|
|||
|
||||
fn renderer(&mut self, id: DocId) -> Result<&mut PageRenderer> {
|
||||
let doc = self.session.get(id).ok_or_else(|| failed("no such document"))?;
|
||||
let colors = self.session.reading_colors(id).unwrap_or_default();
|
||||
let fresh = || {
|
||||
let config = RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() };
|
||||
let config = RenderConfig { password: doc.password.as_deref().map(Arc::from), reading_colors: colors, ..Default::default() };
|
||||
(doc.display.clone(), PageRenderer::new(doc.display.clone(), config))
|
||||
};
|
||||
let entry = self.renderers.entry(id).or_insert_with(fresh);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
//! Reading colours: Acrobat's Replace Document Colors as a tool (#897, AGENTS.md §3).
|
||||
//!
|
||||
//! A per-document view setting: solid text and line-art paints remap for readability and the
|
||||
//! page clears to the setting's background when rendering. The file itself never changes.
|
||||
|
||||
use pdfcraft_render::ReadingColors;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Args, Automation, Result, ToolError};
|
||||
|
||||
/// The modes `ReadingColors::parse` accepts, for error messages.
|
||||
const MODES: &str = "off, black-on-white, white-on-black, yellow-on-black, green-on-black, custom";
|
||||
|
||||
impl Automation {
|
||||
/// Set the document's reading colours. Colours are only meaningful with `mode: "custom"`;
|
||||
/// `parse` enforces the rest and its errors are actionable.
|
||||
pub(crate) fn view_reading_colors(&mut self, a: &Args) -> Result<Value> {
|
||||
let id = self.doc(a)?.id;
|
||||
let Some(mode) = a.opt_str("mode")? else {
|
||||
return Err(ToolError::InvalidArgs(format!("mode is required (one of: {MODES})")));
|
||||
};
|
||||
let colors = ReadingColors::parse(mode, a.opt_str("text")?, a.opt_str("background")?, a.opt_bool("only_black")?, a.opt_bool("line_art")?)
|
||||
.map_err(ToolError::InvalidArgs)?;
|
||||
if self.session.set_reading_colors(id, colors) {
|
||||
// Cached renderers baked the old setting into their config; page_render must re-read it.
|
||||
self.renderers.remove(&id);
|
||||
}
|
||||
Ok(json!({
|
||||
"mode": colors.mode_name(),
|
||||
"text": colors.text_hex(),
|
||||
"background": colors.background_hex(),
|
||||
"only_black": colors.params.is_some_and(|p| p.only_black),
|
||||
"line_art": colors.params.is_some_and(|p| p.line_art),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -719,6 +719,19 @@ pub fn tools() -> Vec<ToolDef> {
|
|||
t("comments_hide", "Hide all comments", "Hide or show every comment on the page (fields and links still draw). A view setting: the file doesn't change.")
|
||||
.cmd("comment.hide_all")
|
||||
.with(schema(json!({ "doc": doc(), "hidden": { "type": "boolean", "description": "Default true." } }), &["doc"])),
|
||||
t("view_reading_colors", "Set reading colours", "Set the document's reading colours (Acrobat's Replace Document Colors, in the Reading Options): solid text and line-art paints are remapped for readability and the page clears to the background colour in page_render. A view setting: the file doesn't change. mode is one of: off, black-on-white, white-on-black, yellow-on-black, green-on-black, or custom (which takes text and background as #RRGGBB). only_black maps only near-black paints (default false) and line_art also maps line-art paints (default true); both override the mode's preset. A flag that differs from the mode's preset default makes the returned mode read custom, because the resulting params no longer match any preset. Returns the resulting setting.")
|
||||
.cmd("a11y.reading_options")
|
||||
.with(schema(
|
||||
json!({
|
||||
"doc": doc(),
|
||||
"mode": { "type": "string", "enum": ["off", "black-on-white", "white-on-black", "yellow-on-black", "green-on-black", "custom"] },
|
||||
"text": { "type": "string", "description": "With mode custom: the text colour, #RRGGBB." },
|
||||
"background": { "type": "string", "description": "With mode custom: the background colour, #RRGGBB." },
|
||||
"only_black": { "type": "boolean", "description": "Map only near-black paints. Default false." },
|
||||
"line_art": { "type": "boolean", "description": "Also map line-art paints. Default true." },
|
||||
}),
|
||||
&["doc", "mode"],
|
||||
)),
|
||||
t("comments_summarize", "Summarize comments", "Make a PDF summarising every comment (number, author, type, date, text, replies), sorted by page, author, date or type.")
|
||||
.cmd("comment.summarize")
|
||||
.with(schema(
|
||||
|
|
|
|||
|
|
@ -5115,6 +5115,128 @@ fn architectural_and_iso_papers_through_tools() {
|
|||
assert!(a.call("doc_print", &json!({"doc": doc, "paper": "ARCH D", "path": "../escape.pdf"})).is_err());
|
||||
}
|
||||
|
||||
/// A 2-page 200×300 pt PDF in the `fixture()` style for the reading-colours test: bold black
|
||||
/// "Page N" text on a white page; page 2 also draws a 4×4 uncompressed `/DeviceRGB` image,
|
||||
/// scaled to 40×40 pt at 120, 200.
|
||||
fn reading_colors_fixture() -> Vec<u8> {
|
||||
let body1 = "BT /F1 48 Tf 20 150 Td (Page 1) Tj ET";
|
||||
let body2 = "BT /F1 48 Tf 20 150 Td (Page 2) Tj ET\nq 40 0 0 40 120 200 cm /Im1 Do Q";
|
||||
// 16 opaque pure-red pixels; images are never remapped by the reading-colours setting.
|
||||
let image: Vec<u8> = (0..16).flat_map(|_| [0xff_u8, 0x00, 0x00]).collect();
|
||||
let mut objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 200 300] >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << /Font << /F1 7 0 R >> >> >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Contents 6 0 R /Resources << /Font << /F1 7 0 R >> /XObject << /Im1 8 0 R >> >> >>".to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{body1}\nendstream", body1.len()).into_bytes(),
|
||||
format!("<< /Length {} >>\nstream\n{body2}\nendstream", body2.len()).into_bytes(),
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>".to_vec(),
|
||||
];
|
||||
let mut image_obj = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >>\nstream\n",
|
||||
image.len()
|
||||
)
|
||||
.into_bytes();
|
||||
image_obj.extend_from_slice(&image);
|
||||
image_obj.extend_from_slice(b"\nendstream");
|
||||
objs.push(image_obj);
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
out.extend_from_slice(o);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// A fresh directory whose `a.pdf` is `reading_colors_fixture()`.
|
||||
fn reading_colors_workdir() -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-automation-reading-colors-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("a.pdf"), reading_colors_fixture()).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// Decodes a `page_render` PNG (pages here are 200×300 pt) into RGBA pixels.
|
||||
fn png_pixels(content: &Content) -> Vec<u8> {
|
||||
let Content::Png { data, width, height } = content else { panic!("expected a PNG") };
|
||||
assert_eq!((*width, *height), (200, 300));
|
||||
let mut reader = png::Decoder::new(std::io::Cursor::new(data)).read_info().unwrap();
|
||||
let mut pixels = vec![0; reader.output_buffer_size().unwrap()];
|
||||
let info = reader.next_frame(&mut pixels).unwrap();
|
||||
assert_eq!((info.width, info.height), (200, 300));
|
||||
pixels
|
||||
}
|
||||
|
||||
/// The RGBA pixel at (`x`, `y`), top-left origin, of a 200-wide image.
|
||||
fn pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
|
||||
let at = ((y * 200 + x) * 4) as usize;
|
||||
pixels[at..at + 4].try_into().unwrap()
|
||||
}
|
||||
|
||||
/// The `view_reading_colors` tool changes what `page_render` draws — text and background remap,
|
||||
/// images never do — without touching the file, and its errors name what's wrong (#897).
|
||||
#[test]
|
||||
fn reading_colors_through_tools() {
|
||||
let dir = reading_colors_workdir();
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
|
||||
let render = |a: &mut Automation, page: u64| png_pixels(&a.call("page_render", &json!({ "doc": doc, "page": page, "dpi": 72 })).unwrap()[0]);
|
||||
|
||||
// Off: black text on a white page.
|
||||
let page1_off = render(&mut a, 1);
|
||||
let page2_off = render(&mut a, 2);
|
||||
assert_eq!(pixel(&page1_off, 10, 290), [255, 255, 255, 255]);
|
||||
|
||||
let echo = ok(&mut a, "view_reading_colors", json!({ "doc": doc, "mode": "white-on-black" }));
|
||||
assert_eq!(echo, json!({ "mode": "white-on-black", "text": "#ffffff", "background": "#000000", "only_black": false, "line_art": true }));
|
||||
|
||||
// The page clears to the setting's background, and the text paints with its colour.
|
||||
let page1_on = render(&mut a, 1);
|
||||
assert_eq!(pixel(&page1_on, 10, 290), [0, 0, 0, 255], "background clears to black");
|
||||
// The brightest pixel inside the text band is a stroke interior: a 48 pt bold stem covers it.
|
||||
let (tx, ty) = (20..172u32).flat_map(|x| (116..160u32).map(move |y| (x, y))).max_by_key(|&(x, y)| u32::from(pixel(&page1_on, x, y)[0])).unwrap();
|
||||
let on = pixel(&page1_on, tx, ty);
|
||||
assert!(on[0] >= 250, "a text-interior pixel renders white: {on:?}");
|
||||
let off = pixel(&page1_off, tx, ty);
|
||||
assert!(off[0] <= 60, "the same pixel is black ink without the setting: {off:?}");
|
||||
|
||||
// Images are never remapped: page 2's picture draws byte-identically in both modes.
|
||||
let page2_on = render(&mut a, 2);
|
||||
fn image_row(px: &[u8], y: u32) -> &[u8] {
|
||||
let at = ((y * 200 + 122) * 4) as usize;
|
||||
&px[at..at + 36 * 4]
|
||||
}
|
||||
assert_eq!(pixel(&page2_on, 140, 80), [255, 0, 0, 255], "the image keeps its colour");
|
||||
for y in 62..98u32 {
|
||||
assert_eq!(image_row(&page2_on, y), image_row(&page2_off, y), "image row {y}");
|
||||
}
|
||||
|
||||
// Errors name the problem and the allowed values.
|
||||
let missing = a.call("view_reading_colors", &json!({ "doc": doc, "mode": "custom", "text": "#00ff00" }));
|
||||
assert!(matches!(&missing, Err(ToolError::InvalidArgs(m)) if m.contains("background")), "{missing:?}");
|
||||
let unknown = a.call("view_reading_colors", &json!({ "doc": doc, "mode": "nope" }));
|
||||
assert!(matches!(&unknown, Err(ToolError::InvalidArgs(m)) if m.contains("off") && m.contains("custom")), "{unknown:?}");
|
||||
let no_mode = a.call("view_reading_colors", &json!({ "doc": doc }));
|
||||
assert!(matches!(&no_mode, Err(ToolError::InvalidArgs(m)) if m.contains("mode")), "{no_mode:?}");
|
||||
|
||||
// Back off: the render matches the original one again.
|
||||
assert_eq!(
|
||||
ok(&mut a, "view_reading_colors", json!({ "doc": doc, "mode": "off" })),
|
||||
json!({ "mode": "off", "text": null, "background": null, "only_black": false, "line_art": false })
|
||||
);
|
||||
assert_eq!(render(&mut a, 1), page1_off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_crops_with_nested_state_resize_save_reopen_through_tools() {
|
||||
use pdfcraft_cos::{Dict, Document, Object, Stream, write_incremental};
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ pub type SplitPart = (usize, usize, Arc<Vec<u8>>);
|
|||
use std::sync::Arc;
|
||||
|
||||
use pdfcraft_cos::{SaveOptions, write_full, write_incremental};
|
||||
use pdfcraft_render::{DocInfo, Layer, LayerOp, OpenError, RenderConfig, RenderPool, inspect};
|
||||
use pdfcraft_render::{DocInfo, Layer, LayerOp, OpenError, ReadingColors, RenderConfig, RenderPool, inspect};
|
||||
|
||||
/// Stable identifier of an open document within a session.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
|
|
@ -4098,6 +4098,24 @@ impl Session {
|
|||
true
|
||||
}
|
||||
|
||||
/// Set the document's reading colours (Acrobat's Replace Document Colors). Returns whether
|
||||
/// anything changed; callers must drop cached rasters and text for the document.
|
||||
pub fn set_reading_colors(&mut self, id: DocId, colors: ReadingColors) -> bool {
|
||||
let Some(doc) = self.docs.iter_mut().find(|d| d.id == id) else { return false };
|
||||
if doc.config.reading_colors == colors {
|
||||
return false;
|
||||
}
|
||||
doc.config.reading_colors = colors;
|
||||
doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone());
|
||||
doc.generation += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// The document's reading-colours setting; `None` when no such document is open.
|
||||
pub fn reading_colors(&self, id: DocId) -> Option<ReadingColors> {
|
||||
Some(self.get(id)?.config.reading_colors)
|
||||
}
|
||||
|
||||
/// Acrobat's Summarize Comments ("Comments only" layout): a new PDF listing every comment
|
||||
/// with its number, author, type, date, text and replies, grouped by page.
|
||||
pub fn summarize_comments(&self, id: DocId, sort: SummarySort) -> Result<Arc<Vec<u8>>, EditError> {
|
||||
|
|
|
|||
|
|
@ -2144,6 +2144,21 @@ fn comment_summary_includes_replies_to_replies() {
|
|||
assert!(text.contains(" NESTED_REPLY"), "the nested reply is indented under its parent: {text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_colors_set_and_invalidated() {
|
||||
let (mut s, id) = session_with(1);
|
||||
// Every open document holds a value; the default is Off.
|
||||
assert_eq!(s.reading_colors(id), Some(ReadingColors::default()));
|
||||
assert!(s.set_reading_colors(id, ReadingColors::WHITE_ON_BLACK));
|
||||
assert_eq!(s.reading_colors(id), Some(ReadingColors::WHITE_ON_BLACK));
|
||||
assert!(!s.set_reading_colors(id, ReadingColors::WHITE_ON_BLACK), "unchanged");
|
||||
assert!(s.set_reading_colors(id, ReadingColors::default()), "back to Off");
|
||||
assert_eq!(s.reading_colors(id), Some(ReadingColors::default()));
|
||||
// No such document.
|
||||
assert_eq!(s.reading_colors(DocId(u64::MAX)), None);
|
||||
assert!(!s.set_reading_colors(DocId(u64::MAX), ReadingColors::WHITE_ON_BLACK));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_saving_trusting_and_commenting_afterwards() {
|
||||
let p12 = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ lopdf = { workspace = true }
|
|||
kurbo = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
# The reading-colours setting is persisted with the app state and echoed by the automation tool.
|
||||
serde = { workspace = true }
|
||||
# Reads rendered pixel words as bytes; already in the tree through hayro.
|
||||
bytemuck = { version = "1" }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
mod inspect;
|
||||
mod pixels;
|
||||
mod raster;
|
||||
mod reading;
|
||||
mod structure;
|
||||
pub mod text;
|
||||
|
||||
|
|
@ -20,9 +21,10 @@ pub use inspect::{
|
|||
};
|
||||
pub use pixels::Pixels;
|
||||
pub use raster::{
|
||||
MAX_PIXELS, MAX_SIDE, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderStats, RenderWarning, RenderedPage, RequestKind, Retiring,
|
||||
Tile, device_pixels, effective_scale,
|
||||
MAX_PIXELS, MAX_SIDE, PRINT_TAG, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderStats, RenderWarning, RenderedPage, RequestKind,
|
||||
Retiring, Tile, device_pixels, effective_scale,
|
||||
};
|
||||
pub use reading::{ReadingColors, ReadingParams};
|
||||
pub use text::{PageText, TextGlyph};
|
||||
|
||||
/// Errors surfaced to the user when a document cannot be opened.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ use hayro::hayro_interpret::font::{FontData, FontQuery};
|
|||
use hayro::hayro_interpret::hayro_cmap::CidFamily;
|
||||
use hayro::hayro_interpret::{InterpreterSettings, InterpreterWarning};
|
||||
use hayro::hayro_syntax::Pdf;
|
||||
use hayro::vello_cpu::color::AlphaColor;
|
||||
use hayro::vello_cpu::color::palette::css::WHITE;
|
||||
use hayro::{RenderCache, RenderSettings, render_into, render_size};
|
||||
|
||||
use crate::Pixels;
|
||||
use crate::{Pixels, ReadingColors};
|
||||
|
||||
/// Monotonic-ish timer that is safe on wasm32 (where `std::time::Instant` panics).
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -61,6 +62,9 @@ pub struct RenderConfig {
|
|||
/// Refuse a whole-page raster that would be reduced by the renderer's hard size caps.
|
||||
/// Tiled requests remain available for pages that exceed those caps.
|
||||
pub reject_oversize: bool,
|
||||
/// View ▸ Reading colours: solid text and line-art paints are remapped for readability and
|
||||
/// the page clears to the setting's background. Display only; the document never changes.
|
||||
pub reading_colors: ReadingColors,
|
||||
}
|
||||
|
||||
impl RenderConfig {
|
||||
|
|
@ -162,6 +166,17 @@ pub struct RenderRequest {
|
|||
pub preview: bool,
|
||||
}
|
||||
|
||||
/// The tag bit of a print-preview raster. Such requests always render the document's true
|
||||
/// colours, ignoring `RenderConfig::reading_colors`: a preview's job is fidelity to the
|
||||
/// printout, and the print itself — imposed from the document — is never recoloured.
|
||||
pub const PRINT_TAG: u64 = 1 << 60;
|
||||
|
||||
/// The reading colours a request renders with: print previews ([`PRINT_TAG`]) ignore the
|
||||
/// document's setting and render with reading colours off.
|
||||
fn request_reading(config: &RenderConfig, req: &RenderRequest) -> ReadingColors {
|
||||
if req.tag & PRINT_TAG != 0 { ReadingColors::default() } else { config.reading_colors }
|
||||
}
|
||||
|
||||
/// What a render that panicked or overran the watchdog is remembered by: its page and kind. Not
|
||||
/// its scale, tile or tag: the canvas tags every zoom level and tile anew, so a wider key would
|
||||
/// let one pathological page tie up a worker for the full watchdog limit per zoom and per tile
|
||||
|
|
@ -286,6 +301,7 @@ fn render_page<'a>(
|
|||
cache: &RenderCache<'a>,
|
||||
settings: &InterpreterSettings,
|
||||
reject_oversize: bool,
|
||||
reading: ReadingColors,
|
||||
req: RenderRequest,
|
||||
) -> Result<Output, (String, bool)> {
|
||||
if req.kind == RequestKind::Text {
|
||||
|
|
@ -302,6 +318,12 @@ fn render_page<'a>(
|
|||
if !(w.is_finite() && h.is_finite()) || w < 0.5 || h < 0.5 {
|
||||
return Err(format!("page {} has an empty or invalid page box ({w}×{h} pt)", req.page + 1));
|
||||
}
|
||||
// Reading colours: the page clears to the setting's background, and solid text and
|
||||
// line-art paints are remapped (images and patterns never are). Off renders as before.
|
||||
let (bg_color, color_replacement) = match reading.params {
|
||||
Some(p) => (AlphaColor::from_rgb8(p.background[0], p.background[1], p.background[2]), Some(hayro::ColorReplacement::from(p))),
|
||||
None => (WHITE, None),
|
||||
};
|
||||
let rs = match req.tile {
|
||||
// Tiles are bounded by construction, so the page itself may be arbitrarily large.
|
||||
Some(t) => {
|
||||
|
|
@ -314,7 +336,8 @@ fn render_page<'a>(
|
|||
height: Some(th as u16),
|
||||
x_offset: t.x as f32,
|
||||
y_offset: t.y as f32,
|
||||
bg_color: WHITE,
|
||||
bg_color,
|
||||
color_replacement,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
|
@ -329,7 +352,15 @@ fn render_page<'a>(
|
|||
// hayro floors the size when none is given, losing the partial edge pixels. The
|
||||
// scale keeps each side within MAX_SIDE (give or take float noise), so it fits u16.
|
||||
let side = |pt: f32| device_pixels(pt, scale).min(MAX_SIDE as u32) as u16;
|
||||
RenderSettings { x_scale: scale, y_scale: scale, width: Some(side(w)), height: Some(side(h)), bg_color: WHITE, ..Default::default() }
|
||||
RenderSettings {
|
||||
x_scale: scale,
|
||||
y_scale: scale,
|
||||
width: Some(side(w)),
|
||||
height: Some(side(h)),
|
||||
bg_color,
|
||||
color_replacement,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
// Rendered straight into a buffer of whole pixels, which the GUI takes over as is.
|
||||
|
|
@ -424,7 +455,7 @@ impl PageRenderer {
|
|||
let cache = RenderCache::new();
|
||||
let warnings = Arc::new(Mutex::new(Vec::new()));
|
||||
let settings = warning_settings(&self.settings, warnings.clone());
|
||||
let r = render_page(pdf, &cache, &settings, self.config.reject_oversize, req);
|
||||
let r = render_page(pdf, &cache, &settings, self.config.reject_oversize, request_reading(&self.config, &req), req);
|
||||
let warnings = take_warnings(&warnings);
|
||||
if matches!(r, Err((_, true))) {
|
||||
self.pdf = parse(&self.bytes, self.config.password.as_deref());
|
||||
|
|
@ -1152,7 +1183,7 @@ fn worker(
|
|||
panic!("injected page panic");
|
||||
}
|
||||
match pdf {
|
||||
Some(pdf) => render_page(pdf, cache, &settings, config.reject_oversize, req),
|
||||
Some(pdf) => render_page(pdf, cache, &settings, config.reject_oversize, request_reading(&config, &req), req),
|
||||
None => Err(("the document could not be parsed".into(), false)),
|
||||
}
|
||||
}))
|
||||
|
|
@ -4992,6 +5023,132 @@ trailer << /Root 1 0 R >>
|
|||
}
|
||||
}
|
||||
|
||||
/// White-on-black: the page clears black, the black filled rectangle maps to the text colour,
|
||||
/// and the image's pixels are byte-identical to the plain render — photos never pass through
|
||||
/// the mapping, not even its white quadrant, which would map to black if they did.
|
||||
#[test]
|
||||
fn reading_colors_map_solid_paints_and_leave_images() {
|
||||
let content = "0 0 0 rg 10 10 30 20 re f\nq 20 0 0 10 60 20 cm /Im Do Q";
|
||||
let pdf = format!(
|
||||
"%PDF-1.4\n\
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /Contents 4 0 R /Resources << /XObject << /Im 6 0 R >> >> >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
6 0 obj << /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /ASCIIHexDecode /Length 25 >> stream\nFF000000FF00000000FFFFFFFF>\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
let req = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, preview: false };
|
||||
let render = |reading_colors: ReadingColors| {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.clone()), RenderConfig { reading_colors, ..Default::default() });
|
||||
let page = r.render(req);
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
page
|
||||
};
|
||||
let off = render(ReadingColors::default());
|
||||
let on = render(ReadingColors::WHITE_ON_BLACK);
|
||||
assert_eq!((on.width, on.height), (200, 100));
|
||||
fn pixel(page: &RenderedPage, x: u32, y: u32) -> &[u8] {
|
||||
&page.rgba[((y * page.width + x) * 4) as usize..][..4]
|
||||
}
|
||||
|
||||
// The page clears to the setting's background instead of white.
|
||||
assert_eq!(pixel(&off, 5, 5), &[255, 255, 255, 255]);
|
||||
assert_eq!(pixel(&on, 5, 5), &[0, 0, 0, 255]);
|
||||
// The black rectangle maps to the text colour.
|
||||
assert_eq!(pixel(&off, 50, 60), &[0, 0, 0, 255]);
|
||||
assert_eq!(pixel(&on, 50, 60), &[255, 255, 255, 255]);
|
||||
// The whole image area (the interior of device pixels 120..160 × 40..60) is untouched.
|
||||
for y in 44..56u32 {
|
||||
for x in 124..156u32 {
|
||||
assert_eq!(pixel(&on, x, y), pixel(&off, x, y), "image pixel {x},{y}");
|
||||
}
|
||||
}
|
||||
assert_eq!(pixel(&on, 150, 55), &[255, 255, 255, 255], "the image's white quadrant stays white");
|
||||
}
|
||||
|
||||
/// A print-preview request (`PRINT_TAG`) renders the document's true colours even though
|
||||
/// the document's config carries reading colours: a preview's job is fidelity to the
|
||||
/// printout, which is imposed from the uncoloured document. Covers both render paths —
|
||||
/// the inline `PageRenderer` and a pool worker.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn print_preview_requests_render_with_reading_colors_off() {
|
||||
let content = "0 0 0 rg 10 10 30 20 re f";
|
||||
let pdf = Arc::new(
|
||||
format!(
|
||||
"%PDF-1.4\n\
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 50] >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
fn px(page: &RenderedPage, x: u32, y: u32) -> &[u8] {
|
||||
&page.rgba[((y * page.width + x) * 4) as usize..][..4]
|
||||
}
|
||||
|
||||
// The page and its black rectangle in their true colours.
|
||||
let mut plain = PageRenderer::new(pdf.clone(), RenderConfig::default());
|
||||
let truth = plain.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, preview: false });
|
||||
assert!(truth.error.is_none(), "{:?}", truth.error);
|
||||
assert_eq!(px(&truth, 5, 5), &[255, 255, 255, 255]);
|
||||
assert_eq!(px(&truth, 50, 60), &[0, 0, 0, 255]);
|
||||
|
||||
let mut recoloured = PageRenderer::new(pdf.clone(), RenderConfig { reading_colors: ReadingColors::WHITE_ON_BLACK, ..Default::default() });
|
||||
// A plain request honours the setting: the page clears black, the rect maps white.
|
||||
let on = recoloured.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, preview: false });
|
||||
assert_eq!(px(&on, 5, 5), &[0, 0, 0, 255]);
|
||||
assert_eq!(px(&on, 50, 60), &[255, 255, 255, 255]);
|
||||
// A print-preview request ignores the setting and matches the true colours exactly.
|
||||
let preview = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: PRINT_TAG, preview: false };
|
||||
let off = recoloured.render(preview);
|
||||
assert_eq!(px(&off, 5, 5), &[255, 255, 255, 255]);
|
||||
assert_eq!(px(&off, 50, 60), &[0, 0, 0, 255]);
|
||||
assert_eq!(off.rgba, truth.rgba);
|
||||
|
||||
// The same override on the worker path the print dialog's canvas renders through.
|
||||
let pool = RenderPool::new(pdf, 1, RenderConfig { reading_colors: ReadingColors::WHITE_ON_BLACK, ..Default::default() });
|
||||
pool.set_queue(vec![preview]);
|
||||
let worker = loop {
|
||||
if let Some(p) = pool.try_recv() {
|
||||
break p;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
};
|
||||
assert!(worker.error.is_none(), "{:?}", worker.error);
|
||||
assert_eq!(worker.rgba, truth.rgba);
|
||||
}
|
||||
|
||||
/// A recoloured tile must equal the crop of the recoloured full render, or zooming would
|
||||
/// flash wrong colours at tile seams.
|
||||
#[test]
|
||||
fn reading_colors_tiles_match_full_render() {
|
||||
let mut r =
|
||||
PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig { reading_colors: ReadingColors::WHITE_ON_BLACK, ..Default::default() });
|
||||
let full = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, preview: false });
|
||||
let tile = Tile { x: 30, y: 40, w: 50, h: 30 };
|
||||
let part = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: Some(tile), scale: 2.0, tag: 0, preview: false });
|
||||
assert!(full.error.is_none() && part.error.is_none(), "{:?} {:?}", full.error, part.error);
|
||||
assert_eq!((part.width, part.height), (50, 30));
|
||||
for y in 0..30u32 {
|
||||
for x in 0..50u32 {
|
||||
let a = &full.rgba[(((y + 40) * full.width + x + 30) * 4) as usize..][..4];
|
||||
let b = &part.rgba[((y * 50 + x) * 4) as usize..][..4];
|
||||
assert_eq!(a, b, "pixel {x},{y}");
|
||||
}
|
||||
}
|
||||
// The colours are really on: the page cleared black and the blue rectangle mapped towards
|
||||
// white (its luminance is 7.2%, so 255·(1−0.0722) ≈ 237).
|
||||
assert_eq!(&full.rgba[..4], &[0, 0, 0, 255]);
|
||||
assert_eq!(&full.rgba[((60 * full.width + 50) * 4) as usize..][..4], &[237, 237, 237, 255]);
|
||||
}
|
||||
|
||||
// A tile seam must not change the raster when large-page work is stitched back together.
|
||||
#[test]
|
||||
fn stitched_render_partitions_match_a_whole_render() {
|
||||
|
|
@ -5198,9 +5355,9 @@ endstream endobj
|
|||
let pdf = parse(&Arc::new(pdf), None).expect("parses");
|
||||
let cache = RenderCache::new();
|
||||
let settings = InterpreterSettings::default();
|
||||
let preview = render_page(&pdf, &cache, &settings, false, req(true)).expect("preview renders");
|
||||
let fin = render_page(&pdf, &cache, &settings, false, req(false)).expect("final renders");
|
||||
let fresh = render_page(&pdf, &RenderCache::new(), &settings, false, req(false)).expect("final renders");
|
||||
let preview = render_page(&pdf, &cache, &settings, false, ReadingColors::default(), req(true)).expect("preview renders");
|
||||
let fin = render_page(&pdf, &cache, &settings, false, ReadingColors::default(), req(false)).expect("final renders");
|
||||
let fresh = render_page(&pdf, &RenderCache::new(), &settings, false, ReadingColors::default(), req(false)).expect("final renders");
|
||||
assert_eq!(rgb_at(&preview.2, w, 35, 35), [0, 0, 255]);
|
||||
assert_eq!(&fin.2[..], &fresh.2[..], "a final render after a preview matches one with a fresh cache");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
//! The "reading colours" view setting (Acrobat's Replace Document Colors): solid text and
|
||||
//! line-art paints are remapped for readability while rendering. Display only — the document
|
||||
//! itself never changes. The setting flows into [`hayro::RenderSettings`] through
|
||||
//! `RenderConfig`.
|
||||
|
||||
/// The reading-colours setting for one document. `params: None` means off.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ReadingColors {
|
||||
/// The colours and flags, or `None` when the setting is off.
|
||||
pub params: Option<ReadingParams>,
|
||||
}
|
||||
|
||||
/// Colours and flags of the reading-colours mapping.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ReadingParams {
|
||||
/// The sRGB colour that dark paints are mapped towards.
|
||||
pub text: [u8; 3],
|
||||
/// The sRGB colour that light paints are mapped towards.
|
||||
pub background: [u8; 3],
|
||||
/// When `true`, only near-black paints are mapped; lighter ones keep their colour.
|
||||
pub only_black: bool,
|
||||
/// When `true`, line-art paints are mapped in addition to text paints.
|
||||
pub line_art: bool,
|
||||
}
|
||||
|
||||
impl ReadingColors {
|
||||
/// Black text on a white page.
|
||||
pub const BLACK_ON_WHITE: Self =
|
||||
Self { params: Some(ReadingParams { text: [0x00, 0x00, 0x00], background: [0xff, 0xff, 0xff], only_black: false, line_art: true }) };
|
||||
/// White text on a black page.
|
||||
pub const WHITE_ON_BLACK: Self =
|
||||
Self { params: Some(ReadingParams { text: [0xff, 0xff, 0xff], background: [0x00, 0x00, 0x00], only_black: false, line_art: true }) };
|
||||
/// Yellow text on a black page.
|
||||
pub const YELLOW_ON_BLACK: Self =
|
||||
Self { params: Some(ReadingParams { text: [0xff, 0xff, 0x00], background: [0x00, 0x00, 0x00], only_black: false, line_art: true }) };
|
||||
/// Green text on a black page.
|
||||
pub const GREEN_ON_BLACK: Self =
|
||||
Self { params: Some(ReadingParams { text: [0x00, 0xff, 0x00], background: [0x00, 0x00, 0x00], only_black: false, line_art: true }) };
|
||||
|
||||
/// Parses tool/settings input. `mode` is one of `off | black-on-white | white-on-black |
|
||||
/// yellow-on-black | green-on-black | custom`; `custom` needs `text` and `background` as
|
||||
/// `#rrggbb`; the flags override the preset defaults when given. `off` ignores colours.
|
||||
/// Errors are actionable strings.
|
||||
pub fn parse(mode: &str, text: Option<&str>, background: Option<&str>, only_black: Option<bool>, line_art: Option<bool>) -> Result<Self, String> {
|
||||
// Every mode accepts the flags: they override the preset defaults, and are the custom
|
||||
// mode's own values.
|
||||
let with_flags = |mut colors: Self| {
|
||||
if let Some(p) = colors.params.as_mut() {
|
||||
if let Some(v) = only_black {
|
||||
p.only_black = v;
|
||||
}
|
||||
if let Some(v) = line_art {
|
||||
p.line_art = v;
|
||||
}
|
||||
}
|
||||
colors
|
||||
};
|
||||
match mode {
|
||||
"off" => Ok(Self::default()),
|
||||
"black-on-white" => Ok(with_flags(Self::BLACK_ON_WHITE)),
|
||||
"white-on-black" => Ok(with_flags(Self::WHITE_ON_BLACK)),
|
||||
"yellow-on-black" => Ok(with_flags(Self::YELLOW_ON_BLACK)),
|
||||
"green-on-black" => Ok(with_flags(Self::GREEN_ON_BLACK)),
|
||||
"custom" => match (text, background) {
|
||||
(None, None) => Err("custom needs text and background as #rrggbb".into()),
|
||||
(None, Some(_)) => Err("custom needs text as #rrggbb".into()),
|
||||
(Some(_), None) => Err("custom needs background as #rrggbb".into()),
|
||||
(Some(text), Some(background)) => Ok(with_flags(Self {
|
||||
params: Some(ReadingParams {
|
||||
text: parse_hex("text", text)?,
|
||||
background: parse_hex("background", background)?,
|
||||
only_black: false,
|
||||
line_art: true,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
other => {
|
||||
Err(format!("unknown mode {other:?}; expected one of: off, black-on-white, white-on-black, yellow-on-black, green-on-black, custom"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The mode name for echoes and display: `"off"`, a preset name, or `"custom"`.
|
||||
pub fn mode_name(&self) -> &'static str {
|
||||
match self.params {
|
||||
None => "off",
|
||||
Some(params) => {
|
||||
if Some(params) == Self::BLACK_ON_WHITE.params {
|
||||
"black-on-white"
|
||||
} else if Some(params) == Self::WHITE_ON_BLACK.params {
|
||||
"white-on-black"
|
||||
} else if Some(params) == Self::YELLOW_ON_BLACK.params {
|
||||
"yellow-on-black"
|
||||
} else if Some(params) == Self::GREEN_ON_BLACK.params {
|
||||
"green-on-black"
|
||||
} else {
|
||||
"custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The text colour as lowercase `#rrggbb`, or `None` when off.
|
||||
pub fn text_hex(&self) -> Option<String> {
|
||||
self.params.map(|p| hex(p.text))
|
||||
}
|
||||
|
||||
/// The background colour as lowercase `#rrggbb`, or `None` when off.
|
||||
pub fn background_hex(&self) -> Option<String> {
|
||||
self.params.map(|p| hex(p.background))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReadingParams> for hayro::ColorReplacement {
|
||||
fn from(p: ReadingParams) -> Self {
|
||||
hayro::ColorReplacement { text: p.text, background: p.background, only_black: p.only_black, line_art: p.line_art }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a colour given as `#rrggbb` — exactly this shape, either case of digits. The error
|
||||
/// names the argument and the expected shape.
|
||||
fn parse_hex(name: &str, value: &str) -> Result<[u8; 3], String> {
|
||||
let invalid = || format!("invalid {name} colour {value:?}; expected #rrggbb (six hex digits, e.g. #00ff00)");
|
||||
let Some(digits) = value.strip_prefix('#') else { return Err(invalid()) };
|
||||
if digits.len() != 6 || !digits.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return Err(invalid());
|
||||
}
|
||||
let mut rgb = [0u8; 3];
|
||||
for (i, channel) in rgb.iter_mut().enumerate() {
|
||||
// Length and charset are checked above, so the parse cannot actually fail; the mapping
|
||||
// only keeps it total without panicking.
|
||||
*channel = u8::from_str_radix(&digits[i * 2..i * 2 + 2], 16).map_err(|_| invalid())?;
|
||||
}
|
||||
Ok(rgb)
|
||||
}
|
||||
|
||||
/// Formats sRGB bytes as lowercase `#rrggbb`.
|
||||
fn hex(rgb: [u8; 3]) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_round_trips_every_mode() {
|
||||
let off = ReadingColors::parse("off", None, None, None, None).unwrap();
|
||||
assert_eq!(off, ReadingColors::default());
|
||||
assert_eq!(off.mode_name(), "off");
|
||||
assert_eq!(off.text_hex(), None);
|
||||
assert_eq!(off.background_hex(), None);
|
||||
// `off` ignores colour and flag arguments.
|
||||
assert_eq!(ReadingColors::parse("off", Some("zzz"), Some("#000000"), Some(true), Some(false)).unwrap(), off);
|
||||
|
||||
for (mode, text, background) in [
|
||||
("black-on-white", "#000000", "#ffffff"),
|
||||
("white-on-black", "#ffffff", "#000000"),
|
||||
("yellow-on-black", "#ffff00", "#000000"),
|
||||
("green-on-black", "#00ff00", "#000000"),
|
||||
] {
|
||||
let colors = ReadingColors::parse(mode, None, None, None, None).unwrap();
|
||||
assert_eq!(colors.mode_name(), mode, "{mode}");
|
||||
assert_eq!(colors.text_hex().as_deref(), Some(text), "{mode}");
|
||||
assert_eq!(colors.background_hex().as_deref(), Some(background), "{mode}");
|
||||
let params = colors.params.unwrap();
|
||||
assert!(!params.only_black && params.line_art, "{mode}");
|
||||
}
|
||||
|
||||
// The flags override the preset defaults; a changed preset is no longer one.
|
||||
let overridden = ReadingColors::parse("white-on-black", None, None, Some(true), Some(false)).unwrap();
|
||||
let params = overridden.params.unwrap();
|
||||
assert!(params.only_black && !params.line_art);
|
||||
assert_eq!(overridden.mode_name(), "custom");
|
||||
|
||||
// `custom` takes either case of hex and echoes lowercase.
|
||||
let custom = ReadingColors::parse("custom", Some("#AaBbCc"), Some("#123456"), Some(true), None).unwrap();
|
||||
assert_eq!(custom.mode_name(), "custom");
|
||||
assert_eq!(custom.text_hex().as_deref(), Some("#aabbcc"));
|
||||
assert_eq!(custom.background_hex().as_deref(), Some("#123456"));
|
||||
let params = custom.params.unwrap();
|
||||
assert!(params.only_black && params.line_art);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_unknown_mode() {
|
||||
assert_eq!(
|
||||
ReadingColors::parse("nope", None, None, None, None),
|
||||
Err("unknown mode \"nope\"; expected one of: off, black-on-white, white-on-black, yellow-on-black, green-on-black, custom".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_malformed_hex() {
|
||||
for bad in ["zzz", "000000", "#000", "#0000000", "#00g000", ""] {
|
||||
assert_eq!(
|
||||
ReadingColors::parse("custom", Some(bad), Some("#000000"), None, None),
|
||||
Err(format!("invalid text colour {bad:?}; expected #rrggbb (six hex digits, e.g. #00ff00)")),
|
||||
"{bad}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
ReadingColors::parse("custom", Some("#000000"), Some("ffffff"), None, None),
|
||||
Err("invalid background colour \"ffffff\"; expected #rrggbb (six hex digits, e.g. #00ff00)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_custom_missing_colours() {
|
||||
assert_eq!(ReadingColors::parse("custom", None, None, None, None), Err("custom needs text and background as #rrggbb".to_string()));
|
||||
assert_eq!(ReadingColors::parse("custom", None, Some("#000000"), None, None), Err("custom needs text as #rrggbb".to_string()));
|
||||
assert_eq!(ReadingColors::parse("custom", Some("#000000"), None, None, None), Err("custom needs background as #rrggbb".to_string()));
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||
|
||||
use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, TextureOptions, Vec2, pos2, vec2};
|
||||
use pdfcraft_engine::{DocId, Edit};
|
||||
use pdfcraft_render::{DestView, DocInfo, LayerOp, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile, device_pixels};
|
||||
use pdfcraft_render::{DestView, DocInfo, LayerOp, LinkTarget, PRINT_TAG, PageText, RenderPool, RenderRequest, RequestKind, Tile, device_pixels};
|
||||
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{PdfCraftApp, QuickTool, RightPanel, comments, icons, widgets};
|
||||
|
|
@ -34,8 +34,6 @@ const TILE_THRESHOLD: f32 = 4096.0;
|
|||
const TILE: u32 = 1024;
|
||||
/// Longest side of the backdrop drawn under tiles, and of a live page preview (signature drag).
|
||||
pub(crate) const BASE_SIDE: f32 = 2048.0;
|
||||
/// Print-preview rasters (distinct from thumbnails and from organize-grid renders).
|
||||
const PRINT_TAG: u64 = 1 << 60;
|
||||
/// Thumbnail slot before density scaling. The pages panel draws at most 150 logical points and
|
||||
/// the organize grid about 146; when the byte budget allows, density reaches the screen so those
|
||||
/// images are sampled down instead of stretched.
|
||||
|
|
@ -1713,6 +1711,8 @@ impl DocView {
|
|||
}
|
||||
|
||||
/// Queue sharp rasters for the print preview. Pages that leave the nearby sheets are dropped.
|
||||
/// Previews render with reading colours off (the request's tag tells the renderer): they must
|
||||
/// match the printout, which is imposed from the uncoloured document.
|
||||
pub(crate) fn queue_print_previews(&mut self, pages: &[(usize, f32)]) {
|
||||
self.print_pages.retain(|page, _| pages.iter().any(|(p, _)| p == page));
|
||||
for &(page, scale) in pages {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ impl PdfCraftApp {
|
|||
fn open_created_bytes(&mut self, name: &str, created: Result<Arc<Vec<u8>>, String>) -> Result<(), String> {
|
||||
let bytes = created?;
|
||||
let id = self.session.open_new(name, bytes).map_err(|e| e.to_string())?;
|
||||
// The reading-colours preference is app-wide: the newly created document starts with it.
|
||||
self.session.set_reading_colors(id, self.reading_colors);
|
||||
let info = &self.session.get(id).ok_or("the new document could not be opened")?.info;
|
||||
self.views.push(crate::DocView::new(id, info, self.view_defaults));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
|
|
|
|||
|
|
@ -760,6 +760,8 @@ impl PdfCraftApp {
|
|||
pub(crate) fn open_created(&mut self, name: &str, bytes: Arc<Vec<u8>>, message: &str) {
|
||||
match self.session.open_new(name, bytes) {
|
||||
Ok(id) => {
|
||||
// The reading-colours preference is app-wide: the newly created document starts with it.
|
||||
self.session.set_reading_colors(id, self.reading_colors);
|
||||
let Some(doc) = self.session.get(id) else { return };
|
||||
self.views.push(crate::DocView::new(id, &doc.info, self.view_defaults));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
|
|
|
|||
|
|
@ -2291,6 +2291,16 @@ animation effects Off معطّلة
|
|||
Original الأصل
|
||||
Greyscale تدرج رمادي
|
||||
Black and white أبيض وأسود
|
||||
Reading colours ألوان القراءة
|
||||
Replace document colours استبدال ألوان المستند
|
||||
Black on white أسود على أبيض
|
||||
White on black أبيض على أسود
|
||||
Yellow on black أصفر على أسود
|
||||
Green on black أخضر على أسود
|
||||
Page background خلفية الصفحة
|
||||
Only change the colour of black text or line art تغيير لون النص الأسود والخطوط فقط
|
||||
Change the colour of line art as well as text تغيير لون الخطوط أيضًا، وليس النص فقط
|
||||
Colours are remapped while rendering; the document itself never changes. تُستبدل الألوان عند الرسم؛ المستند نفسه لا يتغير أبدًا.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… لا يقبل برنامج تشغيل هذه الطابعة إعدادات الطباعة على الوجهين من البرامج الأخرى، لذا يُطبَّق إعداده الخاص. غيّره من خصائص…
|
||||
Browser's print dialog مربع حوار الطباعة في المتصفح
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. يُختار عدد النسخ والطباعة على الوجهين والألوان في مربع حوار الطباعة في المتصفح.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2233,6 +2233,16 @@ animation effects Off Изключени
|
|||
Original Оригинал
|
||||
Greyscale Нива на сивото
|
||||
Black and white Черно-бяло
|
||||
Reading colours Цветове за четене
|
||||
Replace document colours Замяна на цветовете на документа
|
||||
Black on white Черно на бяло
|
||||
White on black Бяло на черно
|
||||
Yellow on black Жълто на черно
|
||||
Green on black Зелено на черно
|
||||
Page background Фон на страницата
|
||||
Only change the colour of black text or line art Да се променя само цветът на черния текст и линиите
|
||||
Change the colour of line art as well as text Да се променя цветът и на линиите, не само на текста
|
||||
Colours are remapped while rendering; the document itself never changes. Цветовете се променят при изчертаването; самият документ никога не се променя.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйверът на този принтер не приема настройки за двустранен печат от други програми и прилага собствената си настройка. Променете я от Свойства…
|
||||
Browser's print dialog Диалог за печат на браузъра
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Броят копия, двустранният печат и цветът се избират в диалога за печат на браузъра.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
|
|
@ -2342,6 +2342,16 @@ animation effects Off Vypnuto
|
|||
Original Originál
|
||||
Greyscale Stupně šedi
|
||||
Black and white Černobílé
|
||||
Reading colours Barvy pro čtení
|
||||
Replace document colours Nahradit barvy dokumentu
|
||||
Black on white Černá na bílém
|
||||
White on black Bílá na černém
|
||||
Yellow on black Žlutá na černém
|
||||
Green on black Zelená na černém
|
||||
Page background Pozadí stránky
|
||||
Only change the colour of black text or line art Změnit jen barvu černého textu a čar
|
||||
Change the colour of line art as well as text Změnit barvu i čar, ne jen textu
|
||||
Colours are remapped while rendering; the document itself never changes. Barvy se mění při vykreslování; samotný dokument se nikdy nemění.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Ovladač této tiskárny nepřijímá nastavení oboustranného tisku z jiných programů, proto platí jeho vlastní nastavení. Změňte ho ve Vlastnosti…
|
||||
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Písmo tohoto pole neumí zobrazit „{c}“ ({code}), proto PdfCraft místo něj ukazuje ?. Hodnota se uloží a jiné prohlížeče PDF pole podle ní překreslí.
|
||||
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Písma, kterými PdfCraft píše, neumí zobrazit „{c}“ ({code}), proto se ve všech prohlížečích PDF zobrazí jako ?. Text se uloží.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2271,6 +2271,16 @@ animation effects Off Aus
|
|||
Original Original
|
||||
Greyscale Graustufen
|
||||
Black and white Schwarzweiß
|
||||
Reading colours Lesefarben
|
||||
Replace document colours Dokumentfarben ersetzen
|
||||
Black on white Schwarz auf weiß
|
||||
White on black Weiß auf schwarz
|
||||
Yellow on black Gelb auf schwarz
|
||||
Green on black Grün auf schwarz
|
||||
Page background Seitenhintergrund
|
||||
Only change the colour of black text or line art Nur die Farbe von schwarzem Text oder Strichzeichnungen ändern
|
||||
Change the colour of line art as well as text Auch die Farbe von Strichzeichnungen ändern, nicht nur von Text
|
||||
Colours are remapped while rendering; the document itself never changes. Farben werden beim Rendern neu zugeordnet; das Dokument selbst bleibt unverändert.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Der Treiber dieses Druckers übernimmt keine Einstellungen für beidseitigen Druck von anderen Programmen, daher gilt seine eigene Einstellung. Ändern unter Eigenschaften…
|
||||
Browser's print dialog Druckdialog des Browsers
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Exemplare, beidseitiger Druck und Farbe werden im Druckdialog des Browsers gewählt.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
|
|
@ -2321,6 +2321,16 @@ animation effects Off Ανενεργά
|
|||
Ink sensitivity Ευαισθησία μελανιού
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Δεν βρέθηκε υπογραφή στο πλαίσιο. Μετακινήστε το πλαίσιο πάνω από την υπογραφή ή αυξήστε την ευαισθησία μελανιού.
|
||||
Couldn't use the photo: {e} Δεν ήταν δυνατή η χρήση της φωτογραφίας: {e}
|
||||
Reading colours Χρώματα ανάγνωσης
|
||||
Replace document colours Αντικατάσταση χρωμάτων εγγράφου
|
||||
Page background Φόντο σελίδας
|
||||
Only change the colour of black text or line art Αλλαγή χρώματος μόνο για μαύρο κείμενο ή γραμμικά σχέδια
|
||||
Change the colour of line art as well as text Αλλαγή χρώματος και στα γραμμικά σχέδια, όχι μόνο στο κείμενο
|
||||
Colours are remapped while rendering; the document itself never changes. Τα χρώματα αντιστοιχίζονται εκ νέου κατά την απόδοση· το ίδιο το έγγραφο δεν αλλάζει ποτέ.
|
||||
Black on white Μαύρο σε λευκό
|
||||
White on black Λευκό σε μαύρο
|
||||
Yellow on black Κίτρινο σε μαύρο
|
||||
Green on black Πράσινο σε μαύρο
|
||||
Couldn't follow linked PDF: {reason} Δεν ήταν δυνατό να ανοίξει το συνδεδεμένο PDF: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. Το συνδεδεμένο PDF άνοιξε, αλλά ο ονομασμένος προορισμός του δεν εφαρμόστηκε.
|
||||
The linked PDF opened, but its page destination was out of range. Το συνδεδεμένο PDF άνοιξε, αλλά ο προορισμός σελίδας του είναι εκτός εύρους.
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -2435,6 +2435,16 @@ animation effects Off Desactivados
|
|||
Ink sensitivity Sensibilidad de la tinta
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. No se encontró ninguna firma en el marco. Mueva el marco sobre la firma o aumente la sensibilidad de la tinta.
|
||||
Couldn't use the photo: {e} No se puede usar la foto: {e}
|
||||
Reading colours Colores de lectura
|
||||
Replace document colours Reemplazar colores del documento
|
||||
Page background Fondo de página
|
||||
Only change the colour of black text or line art Cambiar solo el color del texto negro o los dibujos lineales
|
||||
Change the colour of line art as well as text Cambiar también el color de los dibujos lineales, no solo del texto
|
||||
Colours are remapped while rendering; the document itself never changes. Los colores se reasignan al mostrar la página; el documento en sí nunca cambia.
|
||||
Black on white Negro sobre blanco
|
||||
White on black Blanco sobre negro
|
||||
Yellow on black Amarillo sobre negro
|
||||
Green on black Verde sobre negro
|
||||
Couldn't follow linked PDF: {reason} No se pudo abrir el PDF vinculado: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. El PDF vinculado se abrió, pero no se aplicó su destino con nombre.
|
||||
The linked PDF opened, but its page destination was out of range. El PDF vinculado se abrió, pero su página de destino está fuera de rango.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
|
|
@ -2270,6 +2270,16 @@ animation effects Off Désactivés
|
|||
Original Original
|
||||
Greyscale Niveaux de gris
|
||||
Black and white Noir et blanc
|
||||
Reading colours Couleurs de lecture
|
||||
Replace document colours Remplacer les couleurs du document
|
||||
Black on white Noir sur blanc
|
||||
White on black Blanc sur noir
|
||||
Yellow on black Jaune sur noir
|
||||
Green on black Vert sur noir
|
||||
Page background Arrière-plan de la page
|
||||
Only change the colour of black text or line art Ne changer que la couleur du texte noir et des traits
|
||||
Change the colour of line art as well as text Changer aussi la couleur des traits, en plus du texte
|
||||
Colours are remapped while rendering; the document itself never changes. Les couleurs sont réaffectées au rendu ; le document lui-même ne change jamais.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Le pilote de cette imprimante n'accepte pas les réglages recto verso des autres programmes ; son propre réglage s'applique. Modifiez-le dans Propriétés…
|
||||
Browser's print dialog Boîte de dialogue d'impression du navigateur
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Le nombre de copies, l'impression recto verso et la couleur se choisissent dans la boîte de dialogue d'impression du navigateur.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
|
|
@ -2269,6 +2269,16 @@ animation effects Off Ki
|
|||
Original Eredeti
|
||||
Greyscale Szürkeárnyalatos
|
||||
Black and white Fekete-fehér
|
||||
Reading colours Olvasási színek
|
||||
Replace document colours Dokumentumszínek cseréje
|
||||
Black on white Fekete fehéren
|
||||
White on black Fehér feketén
|
||||
Yellow on black Sárga feketén
|
||||
Green on black Zöld feketén
|
||||
Page background Oldal háttere
|
||||
Only change the colour of black text or line art Csak a fekete szöveg és a vonalak színének módosítása
|
||||
Change the colour of line art as well as text A vonalak színének módosítása a szöveg mellett
|
||||
Colours are remapped while rendering; the document itself never changes. A színek a kirajcoláskor módosulnak; maga a dokumentum sosem változik.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… A nyomtató illesztőprogramja nem fogadja el más programoktól a kétoldalas nyomtatás beállítását, ezért a saját beállítása érvényes. Módosítás: Tulajdonságok…
|
||||
Browser's print dialog A böngésző nyomtatási párbeszédablaka
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. A példányszámot, a kétoldalas nyomtatást és a színt a böngésző nyomtatási párbeszédablakában lehet megadni.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
|
|
@ -2279,6 +2279,16 @@ animation effects Off Disattivati
|
|||
Original Originale
|
||||
Greyscale Scala di grigi
|
||||
Black and white Bianco e nero
|
||||
Reading colours Colori di lettura
|
||||
Replace document colours Sostituisci i colori del documento
|
||||
Black on white Nero su bianco
|
||||
White on black Bianco su nero
|
||||
Yellow on black Giallo su nero
|
||||
Green on black Verde su nero
|
||||
Page background Sfondo della pagina
|
||||
Only change the colour of black text or line art Modifica solo il colore del testo nero e dei tratti
|
||||
Change the colour of line art as well as text Modifica il colore anche dei tratti, oltre al testo
|
||||
Colours are remapped while rendering; the document itself never changes. I colori vengono riassociati durante il rendering; il documento in sé non cambia mai.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Il driver di questa stampante non accetta le impostazioni fronte-retro da altri programmi, quindi vale la sua impostazione. Modificala in Proprietà…
|
||||
Browser's print dialog Finestra di stampa del browser
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Copie, stampa fronte/retro e colore si scelgono nella finestra di stampa del browser.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2270,6 +2270,16 @@ animation effects Off オフ
|
|||
Original オリジナル
|
||||
Greyscale グレースケール
|
||||
Black and white 白黒
|
||||
Reading colours 読書用の色
|
||||
Replace document colours 文書の色を置き換える
|
||||
Black on white 白地に黒
|
||||
White on black 黒地に白
|
||||
Yellow on black 黒地に黄
|
||||
Green on black 黒地に緑
|
||||
Page background ページの背景
|
||||
Only change the colour of black text or line art 黒いテキストと線画の色だけを変更する
|
||||
Change the colour of line art as well as text テキストに加えて線画の色も変更する
|
||||
Colours are remapped while rendering; the document itself never changes. 色は描画時に置き換えられ、文書自体が変わることはありません。
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… このプリンターのドライバーは他のプログラムからの両面印刷の設定を受け付けないため、ドライバー自身の設定が使われます。変更先: プロパティ…
|
||||
Browser's print dialog ブラウザーの印刷ダイアログ
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. 部数、両面印刷、カラーはブラウザーの印刷ダイアログで選びます。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2304,6 +2304,16 @@ animation effects Off Өшірулі
|
|||
Ink sensitivity Сия сезімталдығы
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Жақтауда қолтаңба табылмады. Жақтауды қолтаңбаның үстіне жылжытыңыз немесе сия сезімталдығын арттырыңыз.
|
||||
Couldn't use the photo: {e} Суретті пайдалану мүмкін болмады: {e}
|
||||
Reading colours Оқу түстері
|
||||
Replace document colours Құжат түстерін ауыстыру
|
||||
Black on white Ақ фонда қара
|
||||
White on black Қара фонда ақ
|
||||
Yellow on black Қара фонда сары
|
||||
Green on black Қара фонда жасыл
|
||||
Page background Бет фоны
|
||||
Only change the colour of black text or line art Тек қара мәтіннің немесе сызықтық суреттің түсін өзгерту
|
||||
Change the colour of line art as well as text Мәтінмен қатар сызықтық суреттің түсін де өзгерту
|
||||
Colours are remapped while rendering; the document itself never changes. Түстер көрсету кезінде қайта белгіленеді; құжаттың өзі ешқашан өзгермейді.
|
||||
Couldn't follow linked PDF: {reason} Байланысты PDF ашылмады: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. Байланысты PDF ашылды, бірақ оның аталған мақсаты қолданылмады.
|
||||
The linked PDF opened, but its page destination was out of range. Байланысты PDF ашылды, бірақ оның мақсатты беті ауқымнан тыс.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 820 and column 12.
|
|
|
@ -2285,6 +2285,16 @@ animation effects Off Wyłączone
|
|||
Ink sensitivity Czułość na atrament
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. W ramce nie znaleziono podpisu. Przesuń ramkę na podpis lub zwiększ czułość na atrament.
|
||||
Couldn't use the photo: {e} Nie można użyć zdjęcia: {e}
|
||||
Reading colours Kolory do czytania
|
||||
Replace document colours Zastąp kolory dokumentu
|
||||
Page background Tło strony
|
||||
Only change the colour of black text or line art Zmieniaj kolor tylko czarnego tekstu lub grafiki liniowej
|
||||
Change the colour of line art as well as text Zmieniaj kolor grafiki liniowej, a nie tylko tekstu
|
||||
Colours are remapped while rendering; the document itself never changes. Kolory są zmieniane podczas renderowania; sam dokument nigdy się nie zmienia.
|
||||
Black on white Czarny na białym
|
||||
White on black Biały na czarnym
|
||||
Yellow on black Żółty na czarnym
|
||||
Green on black Zielony na czarnym
|
||||
Couldn't follow linked PDF: {reason} Nie można otworzyć połączonego pliku PDF: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. Połączony plik PDF został otwarty, ale jego nazwany cel nie został zastosowany.
|
||||
The linked PDF opened, but its page destination was out of range. Połączony plik PDF został otwarty, ale jego strona docelowa jest poza zakresem.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2284,6 +2284,16 @@ animation effects Off Desativados
|
|||
Original Original
|
||||
Greyscale Escala de cinza
|
||||
Black and white Preto e branco
|
||||
Reading colours Cores de leitura
|
||||
Replace document colours Substituir as cores do documento
|
||||
Black on white Preto no branco
|
||||
White on black Branco no preto
|
||||
Yellow on black Amarelo no preto
|
||||
Green on black Verde no preto
|
||||
Page background Fundo da página
|
||||
Only change the colour of black text or line art Alterar apenas a cor do texto preto e dos traços
|
||||
Change the colour of line art as well as text Alterar também a cor dos traços, além do texto
|
||||
Colours are remapped while rendering; the document itself never changes. As cores são remapeadas na renderização; o documento em si nunca muda.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… O driver desta impressora não aceita configurações de frente e verso de outros programas, então vale a configuração dele. Altere em Propriedades…
|
||||
Browser's print dialog Caixa de diálogo de impressão do navegador
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Cópias, impressão frente e verso e cor são escolhidas na caixa de diálogo de impressão do navegador.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -2183,6 +2183,16 @@ animation effects Off Выкл
|
|||
Original Оригинал
|
||||
Greyscale Оттенки серого
|
||||
Black and white Чёрно-белый
|
||||
Reading colours Цвета для чтения
|
||||
Replace document colours Заменить цвета документа
|
||||
Black on white Чёрный на белом
|
||||
White on black Белый на чёрном
|
||||
Yellow on black Жёлтый на чёрном
|
||||
Green on black Зелёный на чёрном
|
||||
Page background Фон страницы
|
||||
Only change the colour of black text or line art Менять только цвет чёрного текста и линий
|
||||
Change the colour of line art as well as text Менять цвет и линий, а не только текста
|
||||
Colours are remapped while rendering; the document itself never changes. Цвета меняются при отрисовке; сам документ никогда не изменяется.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер этого принтера не принимает настройки двусторонней печати от других программ, поэтому действует его собственная настройка. Измените её в окне Свойства…
|
||||
Browser's print dialog Диалог печати браузера
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Число копий, двустороннюю печать и цвет выбирают в диалоге печати браузера.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -2272,6 +2272,16 @@ animation effects Off Izklopljeni
|
|||
Original Izvirnik
|
||||
Greyscale Sivine
|
||||
Black and white Črno-belo
|
||||
Reading colours Barve za branje
|
||||
Replace document colours Zamenjaj barve dokumenta
|
||||
Black on white Črno na belem
|
||||
White on black Belo na črnem
|
||||
Yellow on black Rumeno na črnem
|
||||
Green on black Zeleno na črnem
|
||||
Page background Ozadje strani
|
||||
Only change the colour of black text or line art Spremeni samo barvo črnega besedila in črt
|
||||
Change the colour of line art as well as text Spremeni barvo tudi črt, ne samo besedila
|
||||
Colours are remapped while rendering; the document itself never changes. Barve se spremenijo pri izrisovanju; sam dokument se nikoli ne spremeni.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Gonilnik tega tiskalnika ne sprejema nastavitev obojestranskega tiskanja iz drugih programov, zato velja njegova lastna nastavitev. Spremenite jo v Lastnosti…
|
||||
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Pisava tega polja ne more prikazati »{c}« ({code}), zato PdfCraft namesto njega prikaže ?. Vrednost se shrani, drugi pregledovalniki PDF pa polje znova narišejo iz nje.
|
||||
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Pisave, s katerimi piše PdfCraft, ne morejo prikazati »{c}« ({code}), zato se v vseh pregledovalnikih PDF prikaže kot ?. Besedilo se shrani.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -2218,6 +2218,16 @@ animation effects Off ఆఫ్
|
|||
Ink sensitivity ఇంక్ సున్నితత్వం
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. ఫ్రేమ్లో సంతకం కనబడలేదు. ఫ్రేమ్ను సంతకం మీదకు జరపండి లేదా ఇంక్ సున్నితత్వాన్ని పెంచండి.
|
||||
Couldn't use the photo: {e} ఫోటోను ఉపయోగించలేకపోయాం: {e}
|
||||
Reading colours చదవడానికి రంగులు
|
||||
Replace document colours పత్రం రంగులను మార్చు
|
||||
Page background పేజీ నేపథ్యం
|
||||
Only change the colour of black text or line art నలుపు వచనం లేదా రేఖా చిత్రాల రంగును మాత్రమే మార్చు
|
||||
Change the colour of line art as well as text వచనంతో పాటు రేఖా చిత్రాల రంగును కూడా మార్చు
|
||||
Colours are remapped while rendering; the document itself never changes. రెండర్ చేసేటప్పుడు రంగులు మార్చబడతాయి; పత్రం మాత్రం ఎప్పుడూ మారదు.
|
||||
Black on white తెలుపుపై నలుపు
|
||||
White on black నలుపుపై తెలుపు
|
||||
Yellow on black నలుపుపై పసుపు
|
||||
Green on black నలుపుపై ఆకుపచ్చ
|
||||
Couldn't follow linked PDF: {reason} లింక్ చేసిన PDFను తెరవలేకపోయాం: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. లింక్ చేసిన PDF తెరుచుకుంది, కానీ దాని పేరున్న గమ్యం వర్తించలేదు.
|
||||
The linked PDF opened, but its page destination was out of range. లింక్ చేసిన PDF తెరుచుకుంది, కానీ దాని పేజీ గమ్యం పరిధికి వెలుపల ఉంది.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
|
|
@ -2302,6 +2302,16 @@ animation effects Off Kapalı
|
|||
Ink sensitivity Mürekkep hassasiyeti
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. Çerçevede imza bulunamadı. Çerçeveyi imzanın üzerine taşıyın veya mürekkep hassasiyetini artırın.
|
||||
Couldn't use the photo: {e} Fotoğraf kullanılamadı: {e}
|
||||
Reading colours Okuma renkleri
|
||||
Replace document colours Belge renklerini değiştir
|
||||
Black on white Beyaz üzerinde siyah
|
||||
White on black Siyah üzerinde beyaz
|
||||
Yellow on black Siyah üzerinde sarı
|
||||
Green on black Siyah üzerinde yeşil
|
||||
Page background Sayfa arka planı
|
||||
Only change the colour of black text or line art Yalnızca siyah metin veya çizgi sanatının rengini değiştir
|
||||
Change the colour of line art as well as text Metnin yanı sıra çizgi sanatının rengini de değiştir
|
||||
Colours are remapped while rendering; the document itself never changes. Renkler görüntülenirken yeniden eşlenir; belgenin kendisi asla değişmez.
|
||||
Couldn't follow linked PDF: {reason} Bağlantılı PDF açılamadı: {reason}
|
||||
The linked PDF opened, but its named destination was not applied. Bağlantılı PDF açıldı, ancak adlandırılmış hedefi uygulanmadı.
|
||||
The linked PDF opened, but its page destination was out of range. Bağlantılı PDF açıldı, ancak sayfa hedefi aralık dışında.
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -2292,6 +2292,16 @@ animation effects Off Вимкнено
|
|||
Original Оригінал
|
||||
Greyscale Відтінки сірого
|
||||
Black and white Чорно-білий
|
||||
Reading colours Кольори для читання
|
||||
Replace document colours Замінити кольори документа
|
||||
Black on white Чорний на білому
|
||||
White on black Білий на чорному
|
||||
Yellow on black Жовтий на чорному
|
||||
Green on black Зелений на чорному
|
||||
Page background Фон сторінки
|
||||
Only change the colour of black text or line art Змінювати лише колір чорного тексту та ліній
|
||||
Change the colour of line art as well as text Змінювати колір і ліній, а не лише тексту
|
||||
Colours are remapped while rendering; the document itself never changes. Кольори змінюються під час рендерення; сам документ ніколи не змінюється.
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер цього принтера не приймає налаштування двобічного друку від інших програм, тому діє його власне налаштування. Змініть його у вікні Властивості…
|
||||
Browser's print dialog Діалог друку браузера
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. Кількість копій, двосторонній друк і колір вибирають у діалозі друку браузера.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2234,6 +2234,16 @@ animation effects Off 关
|
|||
Original 原始
|
||||
Greyscale 灰度
|
||||
Black and white 黑白
|
||||
Reading colours 阅读配色
|
||||
Replace document colours 替换文档颜色
|
||||
Black on white 白底黑字
|
||||
White on black 黑底白字
|
||||
Yellow on black 黑底黄字
|
||||
Green on black 黑底绿字
|
||||
Page background 页面背景
|
||||
Only change the colour of black text or line art 仅更改黑色文本和线条的颜色
|
||||
Change the colour of line art as well as text 除文本外也更改线条的颜色
|
||||
Colours are remapped while rendering; the document itself never changes. 颜色仅在渲染时替换;文档本身不会改变。
|
||||
This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此打印机的驱动程序不接受其他程序的双面打印设置,因此使用其自身设置。更改位置:属性…
|
||||
Browser's print dialog 浏览器的打印对话框
|
||||
Copies, two-sided printing and colour are chosen in the browser's print dialog. 份数、双面打印和颜色在浏览器的打印对话框中选择。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -2179,6 +2179,16 @@ animation effects Off 關
|
|||
Ink sensitivity 墨跡靈敏度
|
||||
No signature was found in the frame. Move the frame over the signature or raise the ink sensitivity. 框內找不到簽名。請將框移到簽名上,或提高墨跡靈敏度。
|
||||
Couldn't use the photo: {e} 無法使用此相片:{e}
|
||||
Reading colours 閱讀色彩
|
||||
Replace document colours 取代文件色彩
|
||||
Page background 頁面背景
|
||||
Only change the colour of black text or line art 僅變更黑色文字或線條圖的色彩
|
||||
Change the colour of line art as well as text 除文字外,也變更線條圖的色彩
|
||||
Colours are remapped while rendering; the document itself never changes. 色彩僅在顯示時重新對應;文件本身不會變更。
|
||||
Black on white 白底黑字
|
||||
White on black 黑底白字
|
||||
Yellow on black 黑底黃字
|
||||
Green on black 黑底綠字
|
||||
Couldn't follow linked PDF: {reason} 無法開啟連結的 PDF:{reason}
|
||||
The linked PDF opened, but its named destination was not applied. 已開啟連結的 PDF,但未套用其命名目的地。
|
||||
The linked PDF opened, but its page destination was out of range. 已開啟連結的 PDF,但其頁面目的地超出範圍。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -230,6 +230,18 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
|||
close
|
||||
}
|
||||
|
||||
/// The label for a [`pdfcraft_render::ReadingColors`] mode name (`mode_name`).
|
||||
fn reading_mode_label(mode: &str) -> &'static str {
|
||||
match mode {
|
||||
"black-on-white" => "Black on white",
|
||||
"white-on-black" => "White on black",
|
||||
"yellow-on-black" => "Yellow on black",
|
||||
"green-on-black" => "Green on black",
|
||||
"custom" => "Custom",
|
||||
_ => "Off",
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferences: interface language and size, appearance, identity and JavaScript. Returns `true` to close.
|
||||
pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new(tl!("Preferences")).font(theme::semibold(18.0)));
|
||||
|
|
@ -366,6 +378,66 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
|||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
// Reading colours (Acrobat: Replace Document Colors): solid text and line-art paints are
|
||||
// remapped while rendering, and the page clears to the background colour. The preference
|
||||
// is app-wide: every open document follows it, and documents open with it.
|
||||
ui.label(egui::RichText::new(tl!("Reading colours")).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Replace document colours"));
|
||||
let mode = app.reading_colors.mode_name();
|
||||
egui::ComboBox::from_id_salt("reading-colors").selected_text(tl!(reading_mode_label(mode))).show_ui(ui, |ui| {
|
||||
for (m, label) in [
|
||||
("off", "Off"),
|
||||
("black-on-white", "Black on white"),
|
||||
("white-on-black", "White on black"),
|
||||
("yellow-on-black", "Yellow on black"),
|
||||
("green-on-black", "Green on black"),
|
||||
("custom", "Custom"),
|
||||
] {
|
||||
if ui.selectable_label(mode == m, tl!(label)).clicked() {
|
||||
match m {
|
||||
"off" => app.set_reading_colors(pdfcraft_render::ReadingColors::default()),
|
||||
"custom" => {
|
||||
// Custom starts from the colours already chosen, or black on white.
|
||||
let colors = app.reading_colors.params.map_or(pdfcraft_render::ReadingColors::BLACK_ON_WHITE, |params| {
|
||||
pdfcraft_render::ReadingColors { params: Some(params) }
|
||||
});
|
||||
app.set_reading_colors(colors);
|
||||
}
|
||||
// `parse` knows these names; a preset comes with its own flags.
|
||||
preset => {
|
||||
if let Ok(colors) = pdfcraft_render::ReadingColors::parse(preset, None, None, None, None) {
|
||||
app.set_reading_colors(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if app.reading_colors.params.is_some() {
|
||||
let mut changed = false;
|
||||
if let Some(params) = app.reading_colors.params.as_mut() {
|
||||
ui.horizontal(|ui| {
|
||||
let text = ui.label(tl!("Text"));
|
||||
changed |= ui.color_edit_button_srgb(&mut params.text).labelled_by(text.id).changed();
|
||||
let background = ui.label(tl!("Page background"));
|
||||
changed |= ui.color_edit_button_srgb(&mut params.background).labelled_by(background.id).changed();
|
||||
});
|
||||
if ui.add(crate::widgets::checkbox(&mut params.only_black, tl!("Only change the colour of black text or line art"))).changed() {
|
||||
changed = true;
|
||||
}
|
||||
if ui.add(crate::widgets::checkbox(&mut params.line_art, tl!("Change the colour of line art as well as text"))).changed() {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
app.set_reading_colors(app.reading_colors);
|
||||
}
|
||||
}
|
||||
ui.label(egui::RichText::new(tl!("Colours are remapped while rendering; the document itself never changes.")).small().color(t.text_muted));
|
||||
ui.add_space(8.0);
|
||||
// Identity: the author of new comments (Acrobat: Preferences ▸ Identity).
|
||||
ui.label(egui::RichText::new(tl!("Identity")).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
|
|
|
|||
|
|
@ -468,6 +468,9 @@ pub struct PdfCraftApp {
|
|||
/// Resolved colours, including the current OS theme when following the system.
|
||||
pub theme: ThemeKind,
|
||||
pub theme_preference: ThemePreference,
|
||||
/// The reading-colours preference (Preferences ▸ Reading colours): applied to every open
|
||||
/// document, and documents open with it (Acrobat's Replace Document Colors).
|
||||
pub reading_colors: pdfcraft_render::ReadingColors,
|
||||
/// The Linux portal is enabled only by the native host, and starts on the first System choice.
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_theme: Option<pdfcraft_platform::desktop_theme::DesktopTheme>,
|
||||
|
|
@ -800,6 +803,7 @@ impl PdfCraftApp {
|
|||
comment_prefs: Default::default(),
|
||||
theme: ThemeKind::Light,
|
||||
theme_preference: ThemePreference::Light,
|
||||
reading_colors: Default::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_theme: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -1043,6 +1047,9 @@ impl PdfCraftApp {
|
|||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
self.password_prompt = None;
|
||||
// The reading-colours preference is app-wide: a newly opened document starts with it.
|
||||
// Idempotent: the engine returns `false` when the document has the setting already.
|
||||
self.session.set_reading_colors(id, self.reading_colors);
|
||||
let doc = self.session.get(id).ok_or("the document could not be opened")?;
|
||||
let pages = doc.info.pages.len();
|
||||
// Acrobat opens straight to the Comments panel when a document has comments.
|
||||
|
|
@ -1527,6 +1534,19 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
|
||||
/// The app-wide reading-colours preference: sets it and applies it to every open document,
|
||||
/// not just the active one (Preferences ▸ Reading colours, Acrobat's Replace Document
|
||||
/// Colors). A document the change actually touches re-renders.
|
||||
pub fn set_reading_colors(&mut self, colors: pdfcraft_render::ReadingColors) {
|
||||
self.reading_colors = colors;
|
||||
for i in 0..self.views.len() {
|
||||
// Idempotent per document: the engine says whether anything changed.
|
||||
if self.session.set_reading_colors(self.views[i].id, colors) {
|
||||
self.views[i].invalidate_content();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable the Linux desktop portal in a native window. Headless contexts keep
|
||||
/// their supplied input and never connect to the user's session bus.
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -1722,6 +1742,7 @@ impl PdfCraftApp {
|
|||
"highlight_fields": self.view_defaults.highlight_fields,
|
||||
"units": self.units,
|
||||
"language": self.language,
|
||||
"reading_colors": self.reading_colors,
|
||||
// Null follows the system's text scaling.
|
||||
"ui_scale": self.ui_scale,
|
||||
"flatten_fill_sign": self.flatten_fill_sign_on_save,
|
||||
|
|
@ -1828,6 +1849,11 @@ impl PdfCraftApp {
|
|||
if let Some(on) = v["flatten_fill_sign"].as_bool() {
|
||||
self.flatten_fill_sign_on_save = on;
|
||||
}
|
||||
// Settings are untrusted: an unreadable value (a colour that isn't three bytes, say)
|
||||
// fails to deserialize and keeps Off.
|
||||
if let Ok(colors) = serde_json::from_value::<pdfcraft_render::ReadingColors>(v["reading_colors"].clone()) {
|
||||
self.reading_colors = colors;
|
||||
}
|
||||
// Settings are untrusted: a size out of range follows the system instead.
|
||||
self.ui_scale = v["ui_scale"].as_f64().and_then(ui_scale::valid);
|
||||
// Settings are untrusted: an unusable pattern keeps the default.
|
||||
|
|
|
|||
|
|
@ -507,6 +507,8 @@ impl PdfCraftApp {
|
|||
let Some((_, id)) = self.active_ids() else { return };
|
||||
match self.session.open_revision(id, n) {
|
||||
Ok(new) => {
|
||||
// The reading-colours preference is app-wide: the newly opened revision starts with it.
|
||||
self.session.set_reading_colors(new, self.reading_colors);
|
||||
let Some(doc) = self.session.get(new) else { return };
|
||||
self.views.push(DocView::new(new, &doc.info, self.view_defaults));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
|
|
@ -523,6 +525,8 @@ impl PdfCraftApp {
|
|||
let name = format!("{} (signed version)", doc.name.trim_end_matches(".pdf"));
|
||||
match self.session.open(format!("{name}.pdf"), None, std::sync::Arc::new(bytes), doc.password.clone().as_deref()) {
|
||||
Ok(new) => {
|
||||
// The reading-colours preference is app-wide: the newly opened copy starts with it.
|
||||
self.session.set_reading_colors(new, self.reading_colors);
|
||||
let Some(doc) = self.session.get(new) else { return };
|
||||
self.views.push(DocView::new(new, &doc.info, self.view_defaults));
|
||||
self.active = Some(self.views.len() - 1);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
//! Preferences ▸ Reading colours: the app-wide preference (Acrobat's Replace Document Colors),
|
||||
//! how it survives a restart, and how documents open with it.
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use pdfcraft_render::ReadingColors;
|
||||
use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
/// One valid page (proper xref, so no repair banner covers the dialog): black text on white.
|
||||
fn pdf() -> Vec<u8> {
|
||||
let objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_vec(),
|
||||
{
|
||||
let body = b"BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET";
|
||||
format!("<< /Length {} >>\nstream\n{}\nendstream", body.len(), String::from_utf8_lossy(body)).into_bytes()
|
||||
},
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
out.extend_from_slice(o);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// A small PNG for the Create ▸ Images path, which opens the document it builds.
|
||||
fn png() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
{
|
||||
let mut enc = png::Encoder::new(&mut out, 8, 4);
|
||||
enc.set_color(png::ColorType::Rgb);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&[120u8; 8 * 4 * 3]).unwrap();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The Reading colours combo of the preferences dialog: it shows the chosen mode as its value,
|
||||
/// and no other combo in the dialog shows `Off` while the setting is off.
|
||||
fn combo(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
// The preferences dialog grew taller than the window, which leaves this combo below the
|
||||
// fold where a click lands outside the dialog and dismisses it; scroll it into view and
|
||||
// let the scroll settle before clicking.
|
||||
h.get_by(|n| n.role() == Role::ComboBox && n.value().is_some_and(|v| v == "Off")).scroll_to_me();
|
||||
h.run_steps(4);
|
||||
h.get_by(|n| n.role() == Role::ComboBox && n.value().is_some_and(|v| v == "Off")).click();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_colours_preference_round_trips() {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("doc.pdf", None, pdf()).expect("opens");
|
||||
app.set_option("dialog", "preferences").unwrap();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().reading_colors, ReadingColors::default());
|
||||
// Switch the preset to white-on-black in the dialog's Reading colours combo. The open
|
||||
// document keeps repainting, so run a fixed number of steps and let the popup settle.
|
||||
combo(&mut h);
|
||||
h.run_steps(20);
|
||||
h.get_by_label("White on black").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().reading_colors, ReadingColors::WHITE_ON_BLACK);
|
||||
// The preference is app-wide: the already open document follows it too.
|
||||
let id = h.state().views[0].id;
|
||||
assert_eq!(h.state().session.reading_colors(id), Some(ReadingColors::WHITE_ON_BLACK));
|
||||
// Closing the dialog keeps everything.
|
||||
h.state_mut().set_option("dialog", "none").unwrap();
|
||||
h.run_steps(1);
|
||||
|
||||
// The choice survives a restart through the same path the settings file uses.
|
||||
let mut restored = PdfCraftApp::new();
|
||||
restored.restore(&h.state().persist());
|
||||
assert_eq!(restored.reading_colors, ReadingColors::WHITE_ON_BLACK);
|
||||
// A settings file without the field keeps Off.
|
||||
let mut fresh = PdfCraftApp::new();
|
||||
fresh.restore(r#"{"theme": "dark"}"#);
|
||||
assert_eq!(fresh.reading_colors, ReadingColors::default());
|
||||
// A corrupt value fails to deserialize and falls back to Off instead of panicking.
|
||||
fresh.restore(r#"{"reading_colors": {"params": {"text": "zzz", "background": [0, 0, 0], "only_black": false, "line_art": true}}}"#);
|
||||
assert_eq!(fresh.reading_colors, ReadingColors::default());
|
||||
fresh.restore(r#"{"reading_colors": 7}"#);
|
||||
assert_eq!(fresh.reading_colors, ReadingColors::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_colours_seed_on_open() {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.reading_colors = ReadingColors::WHITE_ON_BLACK;
|
||||
app.open_bytes("doc.pdf", None, pdf()).expect("opens");
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
// The document opened with the preference already applied.
|
||||
let id = h.state().views[0].id;
|
||||
assert_eq!(h.state().session.reading_colors(id), Some(ReadingColors::WHITE_ON_BLACK));
|
||||
// Changing the preference applies it to the open document as well.
|
||||
h.state_mut().set_reading_colors(ReadingColors::YELLOW_ON_BLACK);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().session.reading_colors(id), Some(ReadingColors::YELLOW_ON_BLACK));
|
||||
|
||||
// Revisions and the signed version open through their own session calls, bypassing the
|
||||
// ordinary open: they seed the new document the same way. A second revision comes from an
|
||||
// incremental save.
|
||||
{
|
||||
let s = &mut h.state_mut().session;
|
||||
s.apply(id, pdfcraft_engine::Edit::SetInfo { key: "Title".into(), value: "Second".into() }).unwrap();
|
||||
let saved = s.save_bytes(id).unwrap();
|
||||
s.mark_saved(id, saved, None).unwrap();
|
||||
}
|
||||
h.state_mut().open_revision(1);
|
||||
let revision = h.state().views.last().unwrap().id;
|
||||
assert_ne!(revision, id);
|
||||
assert_eq!(h.state().session.reading_colors(revision), Some(ReadingColors::YELLOW_ON_BLACK));
|
||||
let len = h.state().session.get(id).unwrap().bytes.len();
|
||||
h.state_mut().view_signed_version(len);
|
||||
let signed = h.state().views.last().unwrap().id;
|
||||
assert_ne!(signed, id);
|
||||
assert_eq!(h.state().session.reading_colors(signed), Some(ReadingColors::YELLOW_ON_BLACK));
|
||||
|
||||
// Created documents open through their own session calls too — a comment summary via
|
||||
// `open_created`, an image import via `open_created_bytes` — and seed the same way.
|
||||
h.state_mut().summarize_comments();
|
||||
let summary = h.state().views.last().unwrap().id;
|
||||
assert_ne!(summary, signed);
|
||||
assert_eq!(h.state().session.reading_colors(summary), Some(ReadingColors::YELLOW_ON_BLACK));
|
||||
h.state_mut().create_from_images(vec![("scan.png".into(), png())]);
|
||||
let created = h.state().views.last().unwrap().id;
|
||||
assert_ne!(created, summary);
|
||||
assert_eq!(h.state().session.reading_colors(created), Some(ReadingColors::YELLOW_ON_BLACK));
|
||||
|
||||
// And back off, for every open document.
|
||||
h.state_mut().set_reading_colors(ReadingColors::default());
|
||||
h.run_steps(1);
|
||||
for view in &h.state().views {
|
||||
assert_eq!(h.state().session.reading_colors(view.id), Some(ReadingColors::default()));
|
||||
}
|
||||
}
|
||||
|
|
@ -9059,11 +9059,11 @@ title = "Replace page colours (high contrast)"
|
|||
area = "M"
|
||||
tier = "P2"
|
||||
milestone = "M12"
|
||||
status = "planned"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
tools = ["view_reading_colors"]
|
||||
evidence = ["crates/automation/tests/automation.rs::reading_colors_through_tools", "crates/render/src/raster.rs::reading_colors_map_solid_paints_and_leave_images", "crates/render/src/raster.rs::reading_colors_tiles_match_full_render", "crates/render/src/raster.rs::print_preview_requests_render_with_reading_colors_off", "crates/ui-egui/tests/reading_colors.rs::reading_colours_preference_round_trips", "crates/ui-egui/tests/reading_colors.rs::reading_colours_seed_on_open"]
|
||||
notes = "View only, off by default: Preferences ▸ Reading colours holds the app-wide preference, seeded into every document as it opens (including created ones), and view_reading_colors reads and sets it per document (Acrobat's Replace Document Colors). Solid text paints and, by default, line-art paints (including stencil-mask image paints) are remapped by luminance — each lerps between the preset's text and background colour by how dark or light the original paint is — while images, shadings and tiling patterns pass through untouched. Presets: black on white, white on black, yellow on black, green on black, or a custom #RRGGBB pair; only_black limits the mapping to near-black paints (a 10% luminance threshold — our choice; Acrobat's was not observed). Display only: the document is not changed and print and export are unaffected; print previews and view snapshots render with the document's true colours (a preview matches the uncoloured printout, and a snapshot is an export surface), so reading colours apply to the reading view only. Closes #897 (duplicate #488)."
|
||||
|
||||
[[feature]]
|
||||
id = "a11y.reflow-tagged"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -83,9 +83,9 @@ features = [
|
|||
]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
# PdfCraft patch: the upstream-only `[dev-dependencies.log]` entry is removed (log is a normal
|
||||
# dependency since the logging patch), so `cargo test -p hayro` can run this crate's unit tests
|
||||
# even though a patched non-workspace crate may not require dev-dependencies.
|
||||
[lints.clippy]
|
||||
collection_is_never_read = "warn"
|
||||
dbg_macro = "warn"
|
||||
|
|
|
|||
|
|
@ -86,6 +86,53 @@ impl<'a> RenderCache<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: replacement colours for the "reading colours" view setting (Acrobat's
|
||||
/// "Replace Document Colors"). Solid paints are mapped by luminance: dark paints move towards
|
||||
/// `text`, light paints towards `background`. Images, shadings and patterns never pass through
|
||||
/// the mapping.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ColorReplacement {
|
||||
/// The sRGB colour that dark paints (low luminance) are mapped towards.
|
||||
pub text: [u8; 3],
|
||||
/// The sRGB colour that light paints (high luminance) are mapped towards.
|
||||
pub background: [u8; 3],
|
||||
/// When `true`, paints with a luminance above 10% are left unchanged, so only near-black
|
||||
/// text and line art is recoloured.
|
||||
pub only_black: bool,
|
||||
/// When `true`, line-art paints are mapped in addition to text paints.
|
||||
pub line_art: bool,
|
||||
}
|
||||
|
||||
impl ColorReplacement {
|
||||
/// Maps a solid paint colour. `is_text` distinguishes glyph fills/strokes from
|
||||
/// plain path art. Alpha is copied verbatim. Never panics.
|
||||
pub fn map(&self, rgba8: [u8; 4], is_text: bool) -> [u8; 4] {
|
||||
if !is_text && !self.line_art {
|
||||
return rgba8;
|
||||
}
|
||||
let [r, g, b, a] = rgba8;
|
||||
// Luminance of the gamma-encoded sRGB bytes, normalized to `0.0..=1.0`.
|
||||
let t = (0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b)) / 255.0;
|
||||
if self.only_black && t > 0.10 {
|
||||
return rgba8;
|
||||
}
|
||||
let [text_r, text_g, text_b] = self.text;
|
||||
let [background_r, background_g, background_b] = self.background;
|
||||
let channel = |text_ch: u8, background_ch: u8| {
|
||||
let mixed = f32::from(text_ch) * (1.0 - t) + f32::from(background_ch) * t;
|
||||
// The mixture of two channel values with weights summing to one stays within
|
||||
// `0.0..=255.0` up to float noise; the clamp keeps the cast total anyway.
|
||||
mixed.round().clamp(0.0, 255.0) as u8
|
||||
};
|
||||
[
|
||||
channel(text_r, background_r),
|
||||
channel(text_g, background_g),
|
||||
channel(text_b, background_b),
|
||||
a,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings to apply during rendering.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderSettings {
|
||||
|
|
@ -107,6 +154,9 @@ pub struct RenderSettings {
|
|||
pub x_offset: f32,
|
||||
/// See `x_offset`.
|
||||
pub y_offset: f32,
|
||||
/// PdfCraft patch: when set, solid text and line-art paint colours are remapped while
|
||||
/// rendering; `None` renders exactly as without the patch.
|
||||
pub color_replacement: Option<ColorReplacement>,
|
||||
}
|
||||
|
||||
impl Default for RenderSettings {
|
||||
|
|
@ -119,6 +169,7 @@ impl Default for RenderSettings {
|
|||
bg_color: TRANSPARENT,
|
||||
x_offset: 0.0,
|
||||
y_offset: 0.0,
|
||||
color_replacement: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,7 +245,14 @@ pub fn render_into<'a>(
|
|||
render_mode: RenderMode::OptimizeSpeed,
|
||||
};
|
||||
|
||||
let mut device = Renderer::new(pix_width, pix_height, vc_settings, cache, interpreter_settings.preview);
|
||||
let mut device = Renderer::new(
|
||||
pix_width,
|
||||
pix_height,
|
||||
vc_settings,
|
||||
cache,
|
||||
render_settings.color_replacement,
|
||||
interpreter_settings.preview,
|
||||
);
|
||||
|
||||
device.ctx.set_paint(render_settings.bg_color);
|
||||
device
|
||||
|
|
@ -230,6 +288,7 @@ pub fn render_pdf(
|
|||
pdf: &Pdf,
|
||||
scale: f32,
|
||||
settings: InterpreterSettings,
|
||||
color_replacement: Option<ColorReplacement>,
|
||||
range: Option<RangeInclusive<usize>>,
|
||||
) -> Option<Vec<Pixmap>> {
|
||||
let cache = RenderCache::new();
|
||||
|
|
@ -250,6 +309,7 @@ pub fn render_pdf(
|
|||
x_scale: scale,
|
||||
y_scale: scale,
|
||||
bg_color: WHITE,
|
||||
color_replacement,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
|
@ -267,3 +327,343 @@ pub(crate) fn derive_settings(settings: &vello_cpu::RenderSettings) -> vello_cpu
|
|||
..*settings
|
||||
}
|
||||
}
|
||||
|
||||
// PdfCraft patch: unit tests for the reading-colours mapping.
|
||||
#[cfg(test)]
|
||||
mod color_replacement_tests {
|
||||
use super::ColorReplacement;
|
||||
|
||||
fn config(
|
||||
text: [u8; 3],
|
||||
background: [u8; 3],
|
||||
only_black: bool,
|
||||
line_art: bool,
|
||||
) -> ColorReplacement {
|
||||
ColorReplacement {
|
||||
text,
|
||||
background,
|
||||
only_black,
|
||||
line_art,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_maps_to_text_colour() {
|
||||
let r = config([0x12, 0x34, 0x56], [0xff, 0xff, 0xff], false, true);
|
||||
assert_eq!(r.map([0x00, 0x00, 0x00, 0xff], true), [0x12, 0x34, 0x56, 0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_maps_to_background() {
|
||||
let r = config([0x00, 0x00, 0x00], [0xab, 0xcd, 0xef], false, true);
|
||||
assert_eq!(r.map([0xff, 0xff, 0xff, 0xff], true), [0xab, 0xcd, 0xef, 0xff]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midgrey_interpolates() {
|
||||
// 0x80 grey between black text and a white background lands near 0x80.
|
||||
let r = config([0x00, 0x00, 0x00], [0xff, 0xff, 0xff], false, true);
|
||||
let [mapped_r, mapped_g, mapped_b, mapped_a] = r.map([0x80, 0x80, 0x80, 0xff], true);
|
||||
assert!(mapped_r.abs_diff(0x80) <= 2);
|
||||
assert!(mapped_g.abs_diff(0x80) <= 2);
|
||||
assert!(mapped_b.abs_diff(0x80) <= 2);
|
||||
assert_eq!(mapped_a, 0xff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_copied_verbatim() {
|
||||
let r = config([0xff, 0x00, 0x00], [0x00, 0x00, 0xff], false, true);
|
||||
assert_eq!(r.map([0x00, 0x00, 0x00, 137], true), [0xff, 0x00, 0x00, 137]);
|
||||
assert_eq!(r.map([0xff, 0xff, 0xff, 0], true), [0x00, 0x00, 0xff, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_black_threshold() {
|
||||
// White text on black: a mapped grey v becomes 255 - v, so mapped and skipped
|
||||
// values are distinguishable.
|
||||
let r = config([0xff, 0xff, 0xff], [0x00, 0x00, 0x00], true, true);
|
||||
// Luminance t = 25/255 ≈ 0.098 is just under the 0.10 threshold: mapped.
|
||||
assert_eq!(r.map([25, 25, 25, 255], true), [230, 230, 230, 255]);
|
||||
// Luminance t = 27/255 ≈ 0.106 is just over it: left unchanged.
|
||||
assert_eq!(r.map([27, 27, 27, 255], true), [27, 27, 27, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_art_false_skips_paths_but_not_text() {
|
||||
let r = config([0xff, 0xff, 0xff], [0x00, 0x00, 0x00], false, false);
|
||||
// A plain path fill is left unchanged when line art is off.
|
||||
assert_eq!(r.map([0x00, 0x00, 0x00, 255], false), [0x00, 0x00, 0x00, 255]);
|
||||
// Text fills/strokes still map.
|
||||
assert_eq!(r.map([0x00, 0x00, 0x00, 255], true), [0xff, 0xff, 0xff, 255]);
|
||||
}
|
||||
}
|
||||
|
||||
// PdfCraft patch: end-to-end test for the reading-colours renderer hook.
|
||||
#[cfg(test)]
|
||||
mod color_replacement_render_tests {
|
||||
use super::{ColorReplacement, InterpreterSettings, Pdf, Pixmap, render_pdf};
|
||||
use vello_cpu::color::{AlphaColor, Srgb};
|
||||
|
||||
/// A one-page 200×100 pt PDF with a white background rect, black Helvetica text,
|
||||
/// a solid red `rg` rect, a CMYK-filled rect and a 4×4 uncompressed `/DeviceRGB`
|
||||
/// image. Byte-level object layout follows `fixture()` in
|
||||
/// `crates/automation/tests/automation.rs`.
|
||||
fn fixture() -> Vec<u8> {
|
||||
let body = "1 1 1 rg 0 0 200 100 re f\n\
|
||||
0 0 0 rg BT /F1 24 Tf 20 70 Td (HI) Tj ET\n\
|
||||
1 0 0 rg 20 20 50 40 re f\n\
|
||||
1 0 0 0 k 100 20 50 40 re f\n\
|
||||
q 20 0 0 20 160 20 cm /Im0 Do Q";
|
||||
// 16 opaque pure-blue pixels; blue's low luminance makes an (incorrect) mapping
|
||||
// immediately visible as a light grey.
|
||||
let image: Vec<u8> = (0..16).flat_map(|_| [0x00_u8, 0x00, 0xff]).collect();
|
||||
|
||||
let mut objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [4 0 R] /Count 1 /MediaBox [0 0 200 100] >>".to_vec(),
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << /Font << /F1 3 0 R >> \
|
||||
/XObject << /Im0 6 0 R >> >> >>"
|
||||
.to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
|
||||
];
|
||||
let mut image_obj = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB \
|
||||
/BitsPerComponent 8 /Length {} >>\nstream\n",
|
||||
image.len()
|
||||
)
|
||||
.into_bytes();
|
||||
image_obj.extend_from_slice(&image);
|
||||
image_obj.extend_from_slice(b"\nendstream");
|
||||
objs.push(image_obj);
|
||||
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
out.extend_from_slice(o);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
|
||||
objs.len() + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
fn replacement(line_art: bool) -> ColorReplacement {
|
||||
ColorReplacement {
|
||||
text: [0xff, 0xff, 0xff],
|
||||
background: [0x00, 0x00, 0x00],
|
||||
only_black: false,
|
||||
line_art,
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-page 100×50 pt PDF whose only content is a 4×4 `/ImageMask` stencil (CCITT-style
|
||||
/// line art) painted with the current fill colour, black. Each row's four samples are
|
||||
/// `0011`: under `/Decode [0 1]` a sample value of 0 marks the painted area, so the left
|
||||
/// half of the 40×40 pt image paints and the right half does not.
|
||||
fn stencil_fixture() -> Vec<u8> {
|
||||
let body = "0 0 0 rg q 40 0 0 40 30 5 cm /Im0 Do Q";
|
||||
// Four rows, one byte each (a 4-wide row uses the top nibble): samples 0,0,1,1 —
|
||||
// under Decode [0 1] the zeros paint, so the left half of every row is ink and the
|
||||
// right half is unpainted.
|
||||
let mask: [u8; 4] = [0x30, 0x30, 0x30, 0x30];
|
||||
|
||||
let objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 50] >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>"
|
||||
.to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(),
|
||||
{
|
||||
let mut o = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true \
|
||||
/BitsPerComponent 1 /Decode [0 1] /Length {} >>\nstream\n",
|
||||
mask.len()
|
||||
)
|
||||
.into_bytes();
|
||||
o.extend_from_slice(&mask);
|
||||
o.extend_from_slice(b"\nendstream");
|
||||
o
|
||||
},
|
||||
];
|
||||
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
out.extend_from_slice(o);
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
|
||||
objs.len() + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
/// Renders the stencil fixture the way the app's render crate drives hayro for a
|
||||
/// white-on-black user: the page clears to `bg` (`RenderSettings.bg_color`) while the
|
||||
/// solid paints go through `color_replacement`. `render_pdf` cannot express the mapped
|
||||
/// clear colour (it always clears white).
|
||||
fn render_stencil_on(
|
||||
bg: AlphaColor<Srgb>,
|
||||
color_replacement: Option<ColorReplacement>,
|
||||
) -> Pixmap {
|
||||
use super::{RenderCache, RenderSettings, render};
|
||||
|
||||
let pdf = Pdf::new(stencil_fixture()).unwrap();
|
||||
let cache = RenderCache::new();
|
||||
let settings = RenderSettings {
|
||||
x_scale: 1.0,
|
||||
y_scale: 1.0,
|
||||
bg_color: bg,
|
||||
color_replacement,
|
||||
..RenderSettings::default()
|
||||
};
|
||||
render(
|
||||
pdf.pages().get(0).expect("the fixture has one page"),
|
||||
&cache,
|
||||
&InterpreterSettings::default(),
|
||||
&settings,
|
||||
)
|
||||
}
|
||||
|
||||
fn render(color_replacement: Option<ColorReplacement>) -> Pixmap {
|
||||
let pdf = Pdf::new(fixture()).unwrap();
|
||||
render_pdf(
|
||||
&pdf,
|
||||
1.0,
|
||||
InterpreterSettings::default(),
|
||||
color_replacement,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.remove(0)
|
||||
}
|
||||
|
||||
/// Samples the pixel at (x, y); all fixture content is opaque, so the premultiplied
|
||||
/// value is the straight colour.
|
||||
fn sample(pixmap: &Pixmap, x: u16, y: u16) -> [u8; 4] {
|
||||
let p = pixmap.sample(x, y);
|
||||
[p.r, p.g, p.b, p.a]
|
||||
}
|
||||
|
||||
fn count_pixels(
|
||||
pixmap: &Pixmap,
|
||||
region: (u16, u16, u16, u16),
|
||||
matches: impl Fn([u8; 4]) -> bool,
|
||||
) -> u32 {
|
||||
let (x0, y0, x1, y1) = region;
|
||||
let mut count = 0;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
if matches(sample(pixmap, x, y)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_replacement_maps_text_and_line_art() {
|
||||
let is_white = |p: [u8; 4]| p == [255, 255, 255, 255];
|
||||
let is_black = |p: [u8; 4]| p == [0, 0, 0, 255];
|
||||
|
||||
let control = render(None);
|
||||
let replaced = render(Some(replacement(true)));
|
||||
|
||||
// Background: the full-page white rect is line art and maps to the
|
||||
// `background` colour exactly (t = 1.0).
|
||||
assert_eq!(sample(&control, 5, 5), [255, 255, 255, 255]);
|
||||
assert_eq!(sample(&replaced, 5, 5), [0, 0, 0, 255]);
|
||||
|
||||
// Text: solid black glyph cores in the control, fully white after replacement.
|
||||
// Region covers the "HI" glyphs (PDF baseline y=70, 24 pt → device y 10..34).
|
||||
let text_region = (16, 10, 56, 34);
|
||||
assert!(count_pixels(&control, text_region, is_black) >= 10);
|
||||
assert!(count_pixels(&replaced, text_region, is_white) >= 10);
|
||||
|
||||
// Red rect: luminance t ≈ 0.2126 maps every channel to 255·(1−t) ≈ 201 (≈0xC9),
|
||||
// a near-white neutral grey.
|
||||
assert_eq!(sample(&control, 45, 60), [255, 0, 0, 255]);
|
||||
let [r, g, b, a] = sample(&replaced, 45, 60);
|
||||
assert!(r.abs_diff(201) <= 4, "red channel {r} not ≈ 201");
|
||||
assert!(g.abs_diff(201) <= 4 && b.abs_diff(201) <= 4, "g/b not ≈ 201: {g}/{b}");
|
||||
assert_eq!(a, 255);
|
||||
|
||||
// CMYK rect (cyan): mapped to a neutral grey that differs from the control
|
||||
// colour, whatever exact value the CMYK→RGB transform produces.
|
||||
let cmyk_control = sample(&control, 125, 60);
|
||||
let cmyk_replaced = sample(&replaced, 125, 60);
|
||||
assert_ne!(cmyk_control[..3], cmyk_replaced[..3]);
|
||||
assert!(
|
||||
(i32::from(cmyk_replaced[0]) - i32::from(cmyk_replaced[1])).abs() <= 2
|
||||
&& (i32::from(cmyk_replaced[1]) - i32::from(cmyk_replaced[2])).abs() <= 2,
|
||||
"mapped CMYK not neutral: {cmyk_replaced:?}"
|
||||
);
|
||||
assert_eq!(cmyk_replaced[3], 255);
|
||||
|
||||
// Image pixels pass through untouched, identical to the control render.
|
||||
assert_eq!(sample(&control, 170, 70), [0, 0, 255, 255]);
|
||||
assert_eq!(sample(&replaced, 170, 70), [0, 0, 255, 255]);
|
||||
|
||||
// With line-art replacement off, the red rect and the background rect keep
|
||||
// their original colours while text is still replaced.
|
||||
let no_line_art = render(Some(replacement(false)));
|
||||
assert_eq!(sample(&no_line_art, 45, 60), [255, 0, 0, 255]);
|
||||
assert_eq!(sample(&no_line_art, 5, 5), [255, 255, 255, 255]);
|
||||
assert!(count_pixels(&no_line_art, text_region, is_white) >= 10);
|
||||
}
|
||||
|
||||
/// A stencil mask's ink paints with the mapped solid colour on the mapped background:
|
||||
/// it is line art in disguise, so white-on-black must leave it visible (black ink on
|
||||
/// black would disappear). The image occupies device x 30..70, y 5..45 (PDF 30..70 ×
|
||||
/// 5..45 pt); its left half is painted ink, its right half shows the page background.
|
||||
#[test]
|
||||
fn color_replacement_maps_stencil_mask_ink() {
|
||||
use vello_cpu::color::palette::css::{BLACK, WHITE};
|
||||
|
||||
let control = render_stencil_on(WHITE, None);
|
||||
let replaced = render_stencil_on(BLACK, Some(replacement(true)));
|
||||
|
||||
// Control: the page clears white and the stencil's ink keeps its black fill colour.
|
||||
assert_eq!(sample(&control, 5, 5), [255, 255, 255, 255]);
|
||||
assert_eq!(sample(&control, 40, 25), [0, 0, 0, 255]);
|
||||
// With white-on-black, the background clears black and the ink maps to white: ink
|
||||
// and background stay distinguishable, and each matches its mapped colour exactly.
|
||||
assert_eq!(sample(&replaced, 5, 5), [0, 0, 0, 255]);
|
||||
let ink = sample(&replaced, 40, 25);
|
||||
assert_eq!(ink, [255, 255, 255, 255]);
|
||||
assert_ne!(ink[..3], sample(&replaced, 5, 5)[..3], "ink must differ from the background");
|
||||
// The unpainted right half of the image area shows the mapped background.
|
||||
assert_eq!(sample(&replaced, 60, 25), [0, 0, 0, 255]);
|
||||
|
||||
// The line-art gate holds: with line-art replacement off, the ink keeps its
|
||||
// original colour while the page still clears to the background.
|
||||
let no_line_art = render_stencil_on(BLACK, Some(replacement(false)));
|
||||
assert_eq!(sample(&no_line_art, 5, 5), [0, 0, 0, 255]);
|
||||
assert_eq!(sample(&no_line_art, 40, 25), [0, 0, 0, 255]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{RenderCache, derive_settings};
|
||||
use crate::{ColorReplacement, RenderCache, derive_settings};
|
||||
use hayro_interpret::color::Color;
|
||||
use hayro_interpret::encode::EncodedShadingPattern;
|
||||
use hayro_interpret::font::Glyph;
|
||||
use hayro_interpret::pattern::Pattern;
|
||||
|
|
@ -402,6 +403,9 @@ fn may_paint_rect(rect: Rect, transform: Affine, width: u16, height: u16) -> boo
|
|||
|
||||
pub(crate) struct Renderer {
|
||||
pub(crate) ctx: RenderContext,
|
||||
/// PdfCraft patch: when set, solid text and line-art paint colours are remapped
|
||||
/// while rendering (the "reading colours" view setting).
|
||||
color_replacement: Option<ColorReplacement>,
|
||||
pub(crate) inside_pattern: bool,
|
||||
/// Soft masks drawn during this render. PdfCraft note: it belongs to one `Renderer` (one
|
||||
/// render, every nested renderer starting empty) and so to one `preview` setting: a mask
|
||||
|
|
@ -430,10 +434,12 @@ impl Renderer {
|
|||
height: u16,
|
||||
settings: RenderSettings,
|
||||
cache: &RenderCache<'_>,
|
||||
color_replacement: Option<ColorReplacement>,
|
||||
preview: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
ctx: RenderContext::new_with(width, height, settings),
|
||||
color_replacement,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: cache.outline_cache.clone(),
|
||||
|
|
@ -529,6 +535,9 @@ impl Renderer {
|
|||
self.ctx.height(),
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
// PdfCraft patch: this renders an image's alpha mask, which must keep its
|
||||
// original values, so it is never remapped by the reading-colours setting.
|
||||
color_replacement: None,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
|
|
@ -958,6 +967,25 @@ impl Renderer {
|
|||
.fill_rect(&Rect::new(0.0, 0.0, width as f64, height as f64));
|
||||
}
|
||||
|
||||
/// PdfCraft patch: remaps a solid paint colour through `color_replacement`, if one is
|
||||
/// set. `Paint::Pattern` (images, shadings, tiling patterns) always passes through
|
||||
/// untouched.
|
||||
fn replaced<'a>(&self, paint: &Paint<'a>, is_text: bool) -> Paint<'a> {
|
||||
let Some(replacement) = self.color_replacement else {
|
||||
return paint.clone();
|
||||
};
|
||||
match paint {
|
||||
Paint::Color(c) => {
|
||||
let mapped = replacement.map(c.to_rgba().to_rgba8(), is_text);
|
||||
let rgba = hayro_interpret::color::AlphaColor::from_rgba8(
|
||||
mapped[0], mapped[1], mapped[2], mapped[3],
|
||||
);
|
||||
Paint::Color(Color::from_rgba(rgba))
|
||||
}
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn set_paint(&mut self, paint: &Paint<'_>, path: &BezPath, is_stroke: bool) -> Option<BezPath> {
|
||||
let mut paint_transform = Affine::IDENTITY;
|
||||
|
|
@ -1069,6 +1097,9 @@ impl Renderer {
|
|||
pix_height,
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
// PdfCraft patch: a pattern cell is painted as one artwork, so its
|
||||
// content is never remapped by the reading-colours setting.
|
||||
color_replacement: None,
|
||||
cur_mask: None,
|
||||
inside_pattern: true,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
|
|
@ -1137,7 +1168,9 @@ impl Renderer {
|
|||
return;
|
||||
}
|
||||
|
||||
let clip_path = self.set_paint(paint, path, true);
|
||||
// PdfCraft patch: apply the reading-colours mapping before painting.
|
||||
let paint = self.replaced(paint, is_text);
|
||||
let clip_path = self.set_paint(&paint, path, true);
|
||||
if let Some(clip_path) = clip_path.as_ref() {
|
||||
self.push_clip_path_inner(clip_path, FillRule::NonZero);
|
||||
}
|
||||
|
|
@ -1153,6 +1186,9 @@ impl Renderer {
|
|||
transform: Affine,
|
||||
paint: &Paint<'_>,
|
||||
fill_rule: FillRule,
|
||||
// PdfCraft patch: distinguishes glyph fills from plain line art for the
|
||||
// reading-colours mapping.
|
||||
is_text: bool,
|
||||
) {
|
||||
self.ctx.set_fill_rule(convert_fill_rule(fill_rule));
|
||||
self.ctx.set_transform(transform);
|
||||
|
|
@ -1161,7 +1197,9 @@ impl Renderer {
|
|||
return;
|
||||
}
|
||||
|
||||
let clip_path = self.set_paint(paint, path, false);
|
||||
// PdfCraft patch: apply the reading-colours mapping before painting.
|
||||
let paint = self.replaced(paint, is_text);
|
||||
let clip_path = self.set_paint(&paint, path, false);
|
||||
if let Some(clip_path) = clip_path.as_ref() {
|
||||
self.push_clip_path_inner(clip_path, fill_rule);
|
||||
}
|
||||
|
|
@ -1189,9 +1227,14 @@ impl Renderer {
|
|||
transform * glyph_transform,
|
||||
paint,
|
||||
FillRule::NonZero,
|
||||
// PdfCraft patch: glyph outlines are text for the reading-colours mapping.
|
||||
true,
|
||||
);
|
||||
}
|
||||
Glyph::Type3(s) => {
|
||||
// PdfCraft patch: a type 3 glyph's content stream keeps painting as line art
|
||||
// (its internal ops reach `fill_path`/`stroke_path` with `is_text: false`), so
|
||||
// reading colours remap it only when line-art replacement is on.
|
||||
self.in_type3_glyph = true;
|
||||
s.interpret(self, transform, glyph_transform, paint);
|
||||
self.in_type3_glyph = false;
|
||||
|
|
@ -1293,6 +1336,11 @@ impl<'a> Device<'a> for Renderer {
|
|||
stencil.scale_factors.1 as f64,
|
||||
);
|
||||
|
||||
// PdfCraft patch: a stencil mask is line art in disguise, so its solid
|
||||
// paint goes through the reading-colours mapping (line-art gate) like
|
||||
// every other solid fill; baked in raw, its ink would stay at its
|
||||
// original colour and vanish into a mapped background.
|
||||
let paint = &self.replaced(paint, false);
|
||||
match paint {
|
||||
Paint::Color(c) => {
|
||||
let color = c.to_rgba().to_rgba8();
|
||||
|
|
@ -1363,6 +1411,10 @@ impl<'a> Device<'a> for Renderer {
|
|||
height,
|
||||
derive_settings(self.ctx.render_settings()),
|
||||
),
|
||||
// PdfCraft patch: a stencil mask must keep its original
|
||||
// coverage values, so its content is never remapped by
|
||||
// the reading-colours setting.
|
||||
color_replacement: None,
|
||||
inside_pattern: false,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
outline_cache: self.outline_cache.clone(),
|
||||
|
|
@ -1499,7 +1551,7 @@ impl<'a> Device<'a> for Renderer {
|
|||
) {
|
||||
match draw_mode {
|
||||
PathDrawMode::Fill(f) => {
|
||||
Self::fill_path(self, path, transform, paint, *f);
|
||||
Self::fill_path(self, path, transform, paint, *f, false);
|
||||
}
|
||||
PathDrawMode::Stroke(s) => {
|
||||
Self::stroke_path(self, path, transform, paint, s, false);
|
||||
|
|
@ -1524,7 +1576,9 @@ impl<'a> Device<'a> for Renderer {
|
|||
return;
|
||||
}
|
||||
|
||||
let clip_path = self.set_paint(paint, &path, false);
|
||||
// PdfCraft patch: apply the reading-colours mapping before painting.
|
||||
let paint = self.replaced(paint, false);
|
||||
let clip_path = self.set_paint(&paint, &path, false);
|
||||
if let Some(clip_path) = clip_path.as_ref() {
|
||||
self.push_clip_path_inner(clip_path, *fill_rule);
|
||||
}
|
||||
|
|
@ -1643,6 +1697,9 @@ fn render_shading_texture(
|
|||
fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, height: u16, preview: bool) -> Mask {
|
||||
let mut renderer = Renderer {
|
||||
ctx: RenderContext::new_with(width, height, derive_settings(&settings)),
|
||||
// PdfCraft patch: a soft mask must keep its original luminance values, so its
|
||||
// content is never remapped by the reading-colours setting.
|
||||
color_replacement: None,
|
||||
inside_pattern: false,
|
||||
cur_mask: None,
|
||||
soft_mask_cache: HashMap::default(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue