mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
* fix: recognize paragraph drafts as unsaved work (Issue #811) * fix(color): honour the /Lab colour space /WhitePoint (#1203) The old path fed every Lab value to the bundled ICC transform as if the space's white point were D50, so a /Lab space with any other /WhitePoint (e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel and a D65 blue came out 45 levels too red. Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2 §8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as Poppler does. Missing or degenerate white points fall back to a flat white and hostile components are clamped without panicking. Add a render regression that checks the five reported flat patches of the D65 white-point fixture against their CIE sRGB values. * Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc An /Indexed palette over Lab passes its entries as byte/255 (manual_scale), which the new Lab conversion read as raw L*a*b* and drew near black. Map them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no longer referenced) with its ATTRIBUTION, NOTICE and assets README entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity) 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:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
13637b8440
commit
bde9dc52fe
@@ -1905,6 +1905,98 @@ mod tests {
|
||||
assert!(remainder.is_empty());
|
||||
assert!(pixels.iter().all(|pixel| *pixel == [0, 0, 255, 255]));
|
||||
}
|
||||
/// #1203: a /Lab colour space must honour its own /WhitePoint. The old path
|
||||
/// fed every Lab value to the ICC transform as if the white point were D50,
|
||||
/// shifting non-D50 pages several levels per channel (for example a D65
|
||||
/// blue came out 45 levels too red). Conversion and expected values follow
|
||||
/// ISO 32000-2 §8.6.5.4 with the space's own /WhitePoint [0.9505 1 1.089].
|
||||
#[test]
|
||||
fn lab_honours_the_space_white_point() {
|
||||
use super::*;
|
||||
let render = |l: f32, a: f32, b: f32| -> [u8; 4] {
|
||||
let content = format!("/L cs {l} {a} {b} sc 0 0 200 200 re f");
|
||||
let bytes = format!(
|
||||
"%PDF-1.7\n1 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 200 200] /Resources << /ColorSpace << /L [/Lab << /WhitePoint [0.9505 1 1.089] /Range [-100 100 -100 100] >>] >> >> /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();
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
assert!(page.error.is_none(), "Lab ({l} {a} {b}): {:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (200, 200));
|
||||
page.rgba[(100 * page.width as usize + 100) * 4..][..4].try_into().unwrap()
|
||||
};
|
||||
|
||||
for (l, a, b, expected) in [
|
||||
(40.0, 20.0, -60.0, [19, 90, 194, 255]),
|
||||
(70.0, -50.0, 30.0, [77, 193, 114, 255]),
|
||||
(60.0, 0.0, -40.0, [87, 148, 215, 255]),
|
||||
(50.0, 60.0, 40.0, [217, 61, 55, 255]),
|
||||
(30.0, 40.0, 20.0, [130, 37, 42, 255]),
|
||||
] {
|
||||
let got = render(l, a, b);
|
||||
for (channel, (got, want)) in got.iter().zip(expected).enumerate() {
|
||||
assert!((*got).abs_diff(want) <= 2, "Lab ({l} {a} {b}) channel {channel}: got {got:?}, expected {expected:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// #1203: an `/Indexed` palette over Lab looks its entries up as
|
||||
/// `byte / 255`, which must map onto L* 0..100 and the a*/b* /Range. They
|
||||
/// were read as raw L*a*b* (L* below 1), so such palettes drew near black.
|
||||
#[test]
|
||||
fn indexed_palettes_over_lab_keep_their_colours() {
|
||||
use super::*;
|
||||
let lab = "[/Lab << /WhitePoint [0.9505 1 1.089] /Range [-100 100 -100 100] >>]";
|
||||
let render = |content: &str| -> [u8; 4] {
|
||||
let bytes = format!(
|
||||
"%PDF-1.7\n1 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 200 200] /Resources << /ColorSpace << /L {lab} /I [/Indexed {lab} 1 <000000 80CCB2>] >> >> /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();
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
assert!(page.error.is_none(), "{content}: {:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (200, 200));
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
|
||||
// Palette entry 1 is <80 CC B2>: L* = 128/255·100, a* = -100 + 204/255·200,
|
||||
// b* = -100 + 178/255·200, about Lab (50.2 60 39.6): a warm red.
|
||||
let indexed = render("/I cs 1 sc 0 0 200 200 re f");
|
||||
let direct = render(&format!(
|
||||
"/L cs {} {} {} sc 0 0 200 200 re f",
|
||||
128.0 / 255.0 * 100.0,
|
||||
-100.0 + 204.0 / 255.0 * 200.0,
|
||||
-100.0 + 178.0 / 255.0 * 200.0
|
||||
));
|
||||
for (channel, (got, want)) in indexed.iter().zip(direct).enumerate() {
|
||||
assert!(got.abs_diff(want) <= 1, "channel {channel}: indexed {indexed:?}, direct {direct:?}");
|
||||
}
|
||||
assert!(indexed[0] > 180, "the palette red must not render near black: {indexed:?}");
|
||||
}
|
||||
#[test]
|
||||
fn watchdog_skips_a_stuck_page_and_keeps_rendering() {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user