mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
Merge PR #3018: fix(render): draw device colour in the resources' /DefaultGray, /DefaultRGB and /DefaultCMYK (vendored hayro-interpret patch 38)
vendor/README.md: main's single hayro-interpret row plus the PR's patch 38 and its tests, placed before 41
This commit is contained in:
@@ -7238,4 +7238,7 @@ trailer << /Root 1 0 R >>
|
||||
assert!(render(glyph, 3).as_chunks::<4>().0.iter().all(|p| p[..3] == [255, 255, 255]), "{glyph}: 3 Tr hides the glyph");
|
||||
}
|
||||
}
|
||||
|
||||
// #1201: /DefaultGray, /DefaultRGB and /DefaultCMYK (vendored hayro-interpret patch 38).
|
||||
mod default_colour_spaces;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
//! #1201: the `/DefaultGray`, `/DefaultRGB` and `/DefaultCMYK` colour spaces of a resource
|
||||
//! dictionary stand in for the device spaces its content selects, with colour operators, with
|
||||
//! images and with shadings (ISO 32000-2 §8.6.5.6, vendored hayro-interpret patch 38). hayro
|
||||
//! drew device colour as it was, so pages that calibrate their device colour this way (common
|
||||
//! in PDF/A) came out in the wrong colours.
|
||||
//!
|
||||
//! The defaults are synthetic ICC profiles built here, each sending device colour somewhere it
|
||||
//! cannot go uncalibrated: red to green (or blue), black to mid grey, black ink to blue.
|
||||
|
||||
use super::{ONE_PIXEL_RGB, image_cache_pdf, render_first_page, rgb_at};
|
||||
|
||||
/// The width in pixels of the 130 × 70 pt page `image_cache_pdf` makes, rendered at 72 dpi.
|
||||
const WIDTH: usize = 130;
|
||||
|
||||
const RED: [u8; 3] = [255, 0, 0];
|
||||
const GREEN: [u8; 3] = [0, 255, 0];
|
||||
const BLUE: [u8; 3] = [0, 0, 255];
|
||||
const BLACK: [u8; 3] = [0, 0, 0];
|
||||
/// Device black through `gray_profile`: half the luminance of white, sRGB-encoded.
|
||||
const MID_GREY: [u8; 3] = [188, 188, 188];
|
||||
/// Full black ink through `cmyk_profile`: CIELAB (40, 20, -60) relative to the D50 white of the
|
||||
/// profile connection space, which is XYZ (0.1378, 0.1125, 0.3956) and a mid blue in sRGB.
|
||||
const INK_BLUE: [u8; 3] = [67, 86, 194];
|
||||
|
||||
/// An s15Fixed16Number (ICC.1 §4.6).
|
||||
fn s15f16(value: f64) -> [u8; 4] {
|
||||
((value * 65536.0).round() as i32).to_be_bytes()
|
||||
}
|
||||
|
||||
/// An ICC v2 profile (ICC.1:2001-04 §6) of device class `class` for the colour space `space`,
|
||||
/// with `tags` (signature, data) in that order. The device model is left blank: hayro takes a
|
||||
/// profile whose model reads `sRGB` for standard sRGB and skips its transform.
|
||||
fn icc(class: &[u8; 4], space: &[u8; 4], pcs: &[u8; 4], tags: &[([u8; 4], Vec<u8>)]) -> Vec<u8> {
|
||||
let start = 128 + 4 + 12 * tags.len();
|
||||
let (mut table, mut data) = (Vec::new(), Vec::new());
|
||||
for (signature, tag) in tags {
|
||||
table.extend_from_slice(signature);
|
||||
table.extend_from_slice(&u32::try_from(start + data.len()).unwrap().to_be_bytes());
|
||||
table.extend_from_slice(&u32::try_from(tag.len()).unwrap().to_be_bytes());
|
||||
data.extend_from_slice(tag);
|
||||
data.resize(data.len().next_multiple_of(4), 0);
|
||||
}
|
||||
let mut profile = vec![0; 128];
|
||||
profile[0..4].copy_from_slice(&u32::try_from(start + data.len()).unwrap().to_be_bytes());
|
||||
profile[8..12].copy_from_slice(&[2, 0x10, 0, 0]);
|
||||
profile[12..16].copy_from_slice(class);
|
||||
profile[16..20].copy_from_slice(space);
|
||||
profile[20..24].copy_from_slice(pcs);
|
||||
profile[36..40].copy_from_slice(b"acsp");
|
||||
// The illuminant of the profile connection space, D50.
|
||||
for (i, value) in [0.9642, 1.0, 0.8249].into_iter().enumerate() {
|
||||
profile[68 + 4 * i..72 + 4 * i].copy_from_slice(&s15f16(value));
|
||||
}
|
||||
profile.extend_from_slice(&u32::try_from(tags.len()).unwrap().to_be_bytes());
|
||||
profile.extend(table);
|
||||
profile.extend(data);
|
||||
profile
|
||||
}
|
||||
|
||||
/// An `XYZType` tag.
|
||||
fn xyz([x, y, z]: [f64; 3]) -> Vec<u8> {
|
||||
[b"XYZ ".as_slice(), &[0; 4], &s15f16(x), &s15f16(y), &s15f16(z)].concat()
|
||||
}
|
||||
|
||||
/// A `curveType` tag: one entry is a gamma (u8Fixed8), more are points spread evenly over 0..1.
|
||||
fn curve(points: &[u16]) -> Vec<u8> {
|
||||
let mut tag = [b"curv".as_slice(), &[0; 4], &u32::try_from(points.len()).unwrap().to_be_bytes()].concat();
|
||||
for point in points {
|
||||
tag.extend_from_slice(&point.to_be_bytes());
|
||||
}
|
||||
tag
|
||||
}
|
||||
|
||||
/// sRGB's red, green and blue colorants, adapted to D50 as sRGB profiles record them.
|
||||
const SRGB_COLORANTS: [[f64; 3]; 3] = [[0.4361, 0.2225, 0.0139], [0.3851, 0.7169, 0.0971], [0.1431, 0.0606, 0.7141]];
|
||||
|
||||
/// A linear RGB profile whose red, green and blue take the sRGB colorants `primaries` names:
|
||||
/// with `[1, 2, 0]` red is drawn as sRGB green, green as blue and blue as red.
|
||||
fn rgb_profile(primaries: [usize; 3]) -> Vec<u8> {
|
||||
let mut tags = Vec::new();
|
||||
for (signature, primary) in [*b"rXYZ", *b"gXYZ", *b"bXYZ"].into_iter().zip(primaries) {
|
||||
tags.push((signature, xyz(SRGB_COLORANTS[primary])));
|
||||
}
|
||||
for signature in [*b"rTRC", *b"gTRC", *b"bTRC"] {
|
||||
tags.push((signature, curve(&[0x0100])));
|
||||
}
|
||||
icc(b"mntr", b"RGB ", b"XYZ ", &tags)
|
||||
}
|
||||
|
||||
/// A gray profile whose tone curve rises from half the luminance of white to all of it, so
|
||||
/// device black (`0 g`) is drawn mid grey.
|
||||
fn gray_profile() -> Vec<u8> {
|
||||
icc(b"mntr", b"GRAY", b"XYZ ", &[(*b"kTRC", curve(&[0x8000, 0xffff]))])
|
||||
}
|
||||
|
||||
/// A CMYK profile with an 8-bit lookup table into CIELAB (`lut8Type`, ICC.1 §6.5.7) that draws
|
||||
/// full black ink as L*a*b* (40, 20, -60) and no black ink as white, whatever the other inks.
|
||||
fn cmyk_profile() -> Vec<u8> {
|
||||
let mut lut = [b"mft1".as_slice(), &[0; 4], &[4, 3, 2, 0]].concat();
|
||||
// The matrix, only used for XYZ input: identity.
|
||||
for value in [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] {
|
||||
lut.extend_from_slice(&s15f16(value));
|
||||
}
|
||||
// An identity input table per ink.
|
||||
for _ in 0..4 {
|
||||
lut.extend(0..=255_u8);
|
||||
}
|
||||
// The grid, two points per ink, black ink varying fastest. Lab is encoded as L* · 2.55,
|
||||
// a* + 128 and b* + 128.
|
||||
for point in 0..16 {
|
||||
lut.extend_from_slice(if point % 2 == 1 { &[102, 148, 68] } else { &[255, 128, 128] });
|
||||
}
|
||||
// An identity output table per component.
|
||||
for _ in 0..3 {
|
||||
lut.extend(0..=255_u8);
|
||||
}
|
||||
icc(b"prtr", b"CMYK", b"Lab ", &[(*b"A2B0", lut)])
|
||||
}
|
||||
|
||||
/// An `/ICCBased` profile stream of `n` components.
|
||||
fn profile_stream(n: u8, profile: Vec<u8>) -> (String, Vec<u8>) {
|
||||
(format!("<< /N {n} /Length {} >>", profile.len()), profile)
|
||||
}
|
||||
|
||||
/// A stream with the entries `dict` and the content `content`.
|
||||
fn stream(dict: &str, content: &str) -> (String, Vec<u8>) {
|
||||
(format!("<< {dict} /Length {} >>", content.len()), content.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// A form XObject covering the page, with the dictionary entries `extra`.
|
||||
fn form(extra: &str, content: &str) -> (String, Vec<u8>) {
|
||||
stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 130 70] {extra}"), content)
|
||||
}
|
||||
|
||||
/// Draw `content` with `resources` on `image_cache_pdf`'s page, whose objects 5, 6, … are the
|
||||
/// streams `objects`, and give the colour at each of `points` (from the top left).
|
||||
fn colours(content: &str, resources: &str, objects: &[(String, Vec<u8>)], points: &[(usize, usize)]) -> Vec<[u8; 3]> {
|
||||
let objects = objects.iter().map(|(dict, data)| (dict.as_str(), data.as_slice())).collect::<Vec<_>>();
|
||||
let page = render_first_page(image_cache_pdf(content, resources, &objects));
|
||||
assert_eq!((page.width as usize, page.height), (WIDTH, 70));
|
||||
points.iter().map(|&(x, y)| rgb_at(&page.rgba, WIDTH, x, y)).collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_near(got: [u8; 3], want: [u8; 3], what: &str) {
|
||||
assert!(got.iter().zip(want).all(|(got, want)| got.abs_diff(want) <= 6), "{what}: got {got:?}, expected about {want:?}");
|
||||
}
|
||||
|
||||
/// Device CMYK black ink through hayro's own CMYK profile: near black.
|
||||
#[track_caller]
|
||||
fn assert_dark(got: [u8; 3], what: &str) {
|
||||
assert!(got.iter().all(|c| *c < 90), "{what}: got {got:?}, expected near black");
|
||||
}
|
||||
|
||||
/// `rg`, `RG`, `/DeviceRGB cs` and a `/ColorSpace` resource naming DeviceRGB all select
|
||||
/// DeviceRGB, so all draw in the page's DefaultRGB. Without one they draw device red as before.
|
||||
#[test]
|
||||
fn device_rgb_operators_draw_in_default_rgb() {
|
||||
let content = "1 0 0 rg 0 0 30 70 re f /DeviceRGB cs 1 0 0 sc 32 0 30 70 re f /CS0 cs 1 0 0 sc 64 0 30 70 re f \
|
||||
1 0 0 RG 20 w 111 0 m 111 70 l S";
|
||||
let points = [(15, 35), (47, 35), (79, 35), (111, 35)];
|
||||
let what = ["rg", "/DeviceRGB cs", "/CS0 cs (/DeviceRGB)", "RG"];
|
||||
let objects = [profile_stream(3, rgb_profile([1, 2, 0]))];
|
||||
let calibrated = colours(content, "<< /ColorSpace << /DefaultRGB [/ICCBased 5 0 R] /CS0 /DeviceRGB >> >>", &objects, &points);
|
||||
for (got, what) in calibrated.into_iter().zip(what) {
|
||||
assert_near(got, GREEN, &format!("{what} with DefaultRGB"));
|
||||
}
|
||||
let device = colours(content, "<< /ColorSpace << /CS0 /DeviceRGB >> >>", &objects, &points);
|
||||
for (got, what) in device.into_iter().zip(what) {
|
||||
assert_eq!(got, RED, "{what} without DefaultRGB");
|
||||
}
|
||||
}
|
||||
|
||||
/// `k`, `K` and `/DeviceCMYK cs` draw in the page's DefaultCMYK.
|
||||
#[test]
|
||||
fn device_cmyk_operators_draw_in_default_cmyk() {
|
||||
let content = "0 0 0 1 k 0 0 40 70 re f /DeviceCMYK cs 0 0 0 1 sc 45 0 40 70 re f 0 0 0 1 K 20 w 110 0 m 110 70 l S";
|
||||
let points = [(20, 35), (65, 35), (110, 35)];
|
||||
let what = ["k", "/DeviceCMYK cs", "K"];
|
||||
let objects = [profile_stream(4, cmyk_profile())];
|
||||
let calibrated = colours(content, "<< /ColorSpace << /DefaultCMYK [/ICCBased 5 0 R] >> >>", &objects, &points);
|
||||
for (got, what) in calibrated.into_iter().zip(what) {
|
||||
assert_near(got, INK_BLUE, &format!("{what} with DefaultCMYK"));
|
||||
}
|
||||
for (got, what) in colours(content, "<< >>", &objects, &points).into_iter().zip(what) {
|
||||
assert_dark(got, &format!("{what} without DefaultCMYK"));
|
||||
}
|
||||
}
|
||||
|
||||
/// `g`, `G` and `/DeviceGray cs` draw in the page's DefaultGray.
|
||||
#[test]
|
||||
fn device_gray_operators_draw_in_default_gray() {
|
||||
let content = "0 g 0 0 40 70 re f /DeviceGray cs 0 sc 45 0 40 70 re f 0 G 20 w 110 0 m 110 70 l S";
|
||||
let points = [(20, 35), (65, 35), (110, 35)];
|
||||
let what = ["g", "/DeviceGray cs", "G"];
|
||||
let objects = [profile_stream(1, gray_profile())];
|
||||
let calibrated = colours(content, "<< /ColorSpace << /DefaultGray [/ICCBased 5 0 R] >> >>", &objects, &points);
|
||||
for (got, what) in calibrated.into_iter().zip(what) {
|
||||
assert_near(got, MID_GREY, &format!("{what} with DefaultGray"));
|
||||
}
|
||||
for (got, what) in colours(content, "<< >>", &objects, &points).into_iter().zip(what) {
|
||||
assert_eq!(got, BLACK, "{what} without DefaultGray");
|
||||
}
|
||||
}
|
||||
|
||||
/// Images whose `/ColorSpace` is a device space are drawn in the default for it too: image
|
||||
/// XObjects, inline images (whose `/RGB` and `/G` abbreviate the device spaces) and an inline
|
||||
/// image naming a `/ColorSpace` resource that is DeviceRGB.
|
||||
#[test]
|
||||
fn device_images_draw_in_the_default_colour_spaces() {
|
||||
let content = "q 20 0 0 70 0 0 cm /R Do Q q 20 0 0 70 22 0 cm /G Do Q q 20 0 0 70 44 0 cm /K Do Q \
|
||||
q 20 0 0 70 66 0 cm BI /W 1 /H 1 /CS /RGB /BPC 8 /F /AHx ID ff0000> EI Q \
|
||||
q 20 0 0 70 88 0 cm BI /W 1 /H 1 /CS /CS0 /BPC 8 /F /AHx ID ff0000> EI Q \
|
||||
q 20 0 0 70 110 0 cm BI /W 1 /H 1 /CS /G /BPC 8 /F /AHx ID 00> EI Q";
|
||||
let points = [(10, 35), (32, 35), (54, 35), (76, 35), (98, 35), (120, 35)];
|
||||
let image = |space: &str, data: &[u8]| {
|
||||
let dict = format!("<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /{space} /BitsPerComponent 8 /Length {} >>", data.len());
|
||||
(dict, data.to_vec())
|
||||
};
|
||||
let objects = [
|
||||
profile_stream(3, rgb_profile([1, 2, 0])),
|
||||
profile_stream(1, gray_profile()),
|
||||
profile_stream(4, cmyk_profile()),
|
||||
image("DeviceRGB", &[255, 0, 0]),
|
||||
image("DeviceGray", &[0]),
|
||||
image("DeviceCMYK", &[0, 0, 0, 255]),
|
||||
];
|
||||
let x_objects = "/XObject << /R 8 0 R /G 9 0 R /K 10 0 R >>";
|
||||
let calibrated = colours(
|
||||
content,
|
||||
&format!(
|
||||
"<< /ColorSpace << /DefaultRGB [/ICCBased 5 0 R] /DefaultGray [/ICCBased 6 0 R] /DefaultCMYK [/ICCBased 7 0 R] /CS0 /DeviceRGB >> {x_objects} >>"
|
||||
),
|
||||
&objects,
|
||||
&points,
|
||||
);
|
||||
assert_near(calibrated[0], GREEN, "DeviceRGB image");
|
||||
assert_near(calibrated[1], MID_GREY, "DeviceGray image");
|
||||
assert_near(calibrated[2], INK_BLUE, "DeviceCMYK image");
|
||||
assert_near(calibrated[3], GREEN, "inline /RGB image");
|
||||
assert_near(calibrated[4], GREEN, "inline image in /CS0 (/DeviceRGB)");
|
||||
assert_near(calibrated[5], MID_GREY, "inline /G image");
|
||||
|
||||
let device = colours(content, &format!("<< /ColorSpace << /CS0 /DeviceRGB >> {x_objects} >>"), &objects, &points);
|
||||
assert_eq!(device[0], RED, "DeviceRGB image without DefaultRGB");
|
||||
assert_eq!(device[1], BLACK, "DeviceGray image without DefaultGray");
|
||||
assert_dark(device[2], "DeviceCMYK image without DefaultCMYK");
|
||||
assert_eq!(device[3], RED, "inline /RGB image without DefaultRGB");
|
||||
assert_eq!(device[4], RED, "inline /CS0 image without DefaultRGB");
|
||||
assert_eq!(device[5], BLACK, "inline /G image without DefaultGray");
|
||||
}
|
||||
|
||||
/// Shadings whose `/ColorSpace` is a device space are drawn in the default for it too: painted
|
||||
/// by `sh` or as a shading pattern, with their colours from a function (axial) or from their
|
||||
/// vertices (a Type 4 mesh).
|
||||
#[test]
|
||||
fn device_shadings_draw_in_the_default_colour_spaces() {
|
||||
let axial = |space: &str, colour: &str| {
|
||||
format!(
|
||||
"<< /ShadingType 2 /ColorSpace /{space} /Coords [0 0 130 0] /Extend [true true] \
|
||||
/Function << /FunctionType 2 /Domain [0 1] /C0 [{colour}] /C1 [{colour}] /N 1 >> >>"
|
||||
)
|
||||
};
|
||||
// Two red triangles over the page: a flag, x, y and red, green and blue, a byte each.
|
||||
let mut mesh: Vec<u8> = Vec::new();
|
||||
for (x, y) in [(0, 0), (255, 0), (0, 255), (255, 0), (255, 255), (0, 255)] {
|
||||
mesh.extend_from_slice(&[0, x, y, 255, 0, 0]);
|
||||
}
|
||||
let objects = [
|
||||
profile_stream(3, rgb_profile([1, 2, 0])),
|
||||
profile_stream(1, gray_profile()),
|
||||
(
|
||||
format!(
|
||||
"<< /ShadingType 4 /ColorSpace /DeviceRGB /BitsPerCoordinate 8 /BitsPerComponent 8 /BitsPerFlag 8 \
|
||||
/Decode [0 255 0 255 0 1 0 1 0 1] /Length {} >>",
|
||||
mesh.len()
|
||||
),
|
||||
mesh,
|
||||
),
|
||||
];
|
||||
let content = "q 0 0 30 70 re W n /A sh Q /Pattern cs /P scn 32 0 30 70 re f \
|
||||
q 64 0 30 70 re W n /M sh Q q 96 0 30 70 re W n /G sh Q";
|
||||
let points = [(15, 35), (47, 35), (79, 35), (111, 35)];
|
||||
let shadings = format!(
|
||||
"/Shading << /A {} /M 7 0 R /G {} >> /Pattern << /P << /PatternType 2 /Shading {} >> >>",
|
||||
axial("DeviceRGB", "1 0 0"),
|
||||
axial("DeviceGray", "0"),
|
||||
axial("DeviceRGB", "1 0 0")
|
||||
);
|
||||
let calibrated = colours(
|
||||
content,
|
||||
&format!("<< /ColorSpace << /DefaultRGB [/ICCBased 5 0 R] /DefaultGray [/ICCBased 6 0 R] >> {shadings} >>"),
|
||||
&objects,
|
||||
&points,
|
||||
);
|
||||
assert_near(calibrated[0], GREEN, "sh, an axial shading in DeviceRGB");
|
||||
assert_near(calibrated[1], GREEN, "a shading pattern in DeviceRGB");
|
||||
assert_near(calibrated[2], GREEN, "sh, a Type 4 mesh in DeviceRGB");
|
||||
assert_near(calibrated[3], MID_GREY, "sh, an axial shading in DeviceGray");
|
||||
|
||||
let device = colours(content, &format!("<< {shadings} >>"), &objects, &points);
|
||||
assert_near(device[0], RED, "sh, an axial shading without DefaultRGB");
|
||||
assert_near(device[1], RED, "a shading pattern without DefaultRGB");
|
||||
assert_near(device[2], RED, "sh, a Type 4 mesh without DefaultRGB");
|
||||
assert_near(device[3], BLACK, "sh, an axial shading without DefaultGray");
|
||||
}
|
||||
|
||||
/// One image drawn by a page with DefaultRGB and by a form that switches it off is decoded
|
||||
/// for each: the decoded-image cache keeps the two apart (hayro-interpret patch 32 shares a
|
||||
/// decode between every draw of an image).
|
||||
#[test]
|
||||
fn an_image_drawn_with_and_without_a_default_keeps_both_colours() {
|
||||
let objects = [
|
||||
profile_stream(3, rgb_profile([1, 2, 0])),
|
||||
(ONE_PIXEL_RGB.to_owned(), vec![255, 0, 0]),
|
||||
form("/Resources << /ColorSpace << /DefaultRGB /DeviceRGB >> >>", "q 40 0 0 70 70 0 cm /R Do Q"),
|
||||
];
|
||||
let got = colours(
|
||||
"q 40 0 0 70 0 0 cm /R Do Q /F Do",
|
||||
"<< /ColorSpace << /DefaultRGB [/ICCBased 5 0 R] >> /XObject << /R 6 0 R /F 7 0 R >> >>",
|
||||
&objects,
|
||||
&[(20, 35), (90, 35)],
|
||||
);
|
||||
assert_near(got[0], GREEN, "the image on the page, in DefaultRGB");
|
||||
assert_eq!(got[1], RED, "the same image in the form without DefaultRGB");
|
||||
}
|
||||
|
||||
/// A form XObject draws device colour in the default of the resources it is drawn with: its
|
||||
/// own when it has one, else the page's (with or without a `/Resources` of its own, with a
|
||||
/// `/ColorSpace` of its own that has no default, and through a form nested in it). Poppler and
|
||||
/// MuPDF draw these forms in the page's default too, though §8.6.5.6 names only the current
|
||||
/// resource dictionary. A form whose own default is a device space or not a usable default
|
||||
/// draws device colour, not the page's default.
|
||||
#[test]
|
||||
fn forms_inherit_the_default_colour_spaces_they_do_not_override() {
|
||||
let blue = rgb_profile([2, 0, 1]);
|
||||
let objects = [
|
||||
profile_stream(3, rgb_profile([1, 2, 0])),
|
||||
// hayro caches a profile by its stream's dictionary, which here would be byte for byte
|
||||
// the first profile's, so this one says more.
|
||||
(format!("<< /N 3 /Alternate /DeviceRGB /Length {} >>", blue.len()), blue),
|
||||
form("", "1 0 0 rg 22 37 20 33 re f"),
|
||||
form("/Resources << /ProcSet [/PDF] >>", "1 0 0 rg 44 37 20 33 re f"),
|
||||
form("/Resources << /ColorSpace << /DefaultRGB [/ICCBased 6 0 R] >> /XObject << /N 12 0 R >> >>", "1 0 0 rg 66 37 20 33 re f /N Do"),
|
||||
form("/Resources << /ColorSpace << /DefaultRGB /DeviceRGB >> >>", "1 0 0 rg 88 37 20 33 re f"),
|
||||
form("/Resources << /ColorSpace << /DefaultRGB [/Lab << /WhitePoint [0.9642 1 0.8249] >>] >> >>", "1 0 0 rg 110 37 20 33 re f"),
|
||||
form("", "1 0 0 rg 0 0 20 33 re f"),
|
||||
form("/Resources << /ColorSpace << /CS1 /DeviceGray >> >>", "1 0 0 rg 22 0 20 33 re f"),
|
||||
];
|
||||
let got = colours(
|
||||
"1 0 0 rg 0 37 20 33 re f /A Do /B Do /C Do /D Do /E Do /F Do",
|
||||
"<< /ColorSpace << /DefaultRGB [/ICCBased 5 0 R] >> /XObject << /A 7 0 R /B 8 0 R /C 9 0 R /D 10 0 R /E 11 0 R /F 13 0 R >> >>",
|
||||
&objects,
|
||||
&[(10, 17), (32, 17), (54, 17), (76, 17), (98, 17), (120, 17), (10, 53), (32, 53)],
|
||||
);
|
||||
assert_near(got[0], GREEN, "the page");
|
||||
assert_near(got[1], GREEN, "a form without /Resources");
|
||||
assert_near(got[2], GREEN, "a form whose /Resources has no /ColorSpace");
|
||||
assert_near(got[3], BLUE, "a form with a DefaultRGB of its own");
|
||||
assert_eq!(got[4], RED, "a form whose DefaultRGB is /DeviceRGB");
|
||||
assert_eq!(got[5], RED, "a form whose DefaultRGB is a Lab space");
|
||||
assert_near(got[6], BLUE, "a form nested in the one with its own DefaultRGB");
|
||||
assert_near(got[7], GREEN, "a form whose /ColorSpace has no DefaultRGB");
|
||||
}
|
||||
|
||||
/// A default that cannot stand in for its device space (ISO 32000-2 §8.6.5.6: a Lab, Indexed
|
||||
/// or Pattern space, another device space, one with the wrong number of components, or no
|
||||
/// colour space at all) leaves device colour as it is, and the dictionary's other defaults
|
||||
/// still apply.
|
||||
#[test]
|
||||
fn unusable_defaults_leave_device_colour_alone() {
|
||||
let objects =
|
||||
[profile_stream(1, gray_profile()), profile_stream(4, cmyk_profile()), (String::from("<< /N 3 /Length 13 >>"), b"not a profile".to_vec())];
|
||||
for default in [
|
||||
"[/Lab << /WhitePoint [0.9642 1 0.8249] >>]",
|
||||
"[/CalGray << /WhitePoint [0.9642 1 0.8249] >>]",
|
||||
"[/Indexed /DeviceRGB 0 <00FF00>]",
|
||||
"/Pattern",
|
||||
"[/Pattern /DeviceRGB]",
|
||||
"/DeviceCMYK",
|
||||
"/DeviceGray",
|
||||
"[/ICCBased 6 0 R]",
|
||||
"[/ICCBased 7 0 R]",
|
||||
"42",
|
||||
"[/NoSuchSpace]",
|
||||
"/DefaultRGB",
|
||||
] {
|
||||
let got = colours(
|
||||
"1 0 0 rg 0 0 60 70 re f 0 g 70 0 60 70 re f",
|
||||
&format!("<< /ColorSpace << /DefaultRGB {default} /DefaultGray [/ICCBased 5 0 R] >> >>"),
|
||||
&objects,
|
||||
&[(30, 35), (100, 35)],
|
||||
);
|
||||
assert_eq!(got[0], RED, "DefaultRGB {default}: device RGB is drawn as it is");
|
||||
assert_near(got[1], MID_GREY, &format!("DefaultRGB {default}: DefaultGray still applies"));
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5
-1
@@ -73,7 +73,10 @@ pub(crate) struct DecodedImageCache {
|
||||
/// `/BitsPerComponent`, filters, …) and its object (`obj`: the decryption key depends on it);
|
||||
/// - whether it is decoded as a mask, a stencil mask, raster or mask output, interpolation,
|
||||
/// the decode target size (only for an image a JPX stream decodes, which is the only
|
||||
/// decode that reads it: PdfCraft patch 32), and preview or final quality.
|
||||
/// decode that reads it: PdfCraft patch 32), and preview or final quality;
|
||||
/// - the default colour space drawn in place of the image's device space, if any (the cache
|
||||
/// key of its object, PdfCraft patch 38): the same image can be drawn by resources with a
|
||||
/// `/DefaultRGB` and by resources without one.
|
||||
///
|
||||
/// A transfer function (graphics state `/TR`) also changes the pixels and has no cheap key, so
|
||||
/// an image drawn under one is not cached; neither is an inline image, whose dictionary lives in
|
||||
@@ -88,6 +91,7 @@ pub(crate) struct DecodedImageKey {
|
||||
pub(crate) is_mask: bool,
|
||||
pub(crate) is_stencil_mask: bool,
|
||||
pub(crate) interpolate: bool,
|
||||
pub(crate) default_space: Option<u128>,
|
||||
pub(crate) target: Option<(u32, u32)>,
|
||||
pub(crate) preview: bool,
|
||||
}
|
||||
|
||||
Vendored
+135
@@ -9,12 +9,14 @@ use hayro_syntax::object::Name;
|
||||
use hayro_syntax::object::Object;
|
||||
use hayro_syntax::object::Stream;
|
||||
use hayro_syntax::object::dict::keys::*;
|
||||
use hayro_syntax::page::Resources;
|
||||
use moxcms::{
|
||||
ColorProfile, DataColorSpace, Layout, Transform8BitExecutor, TransformF32Executor,
|
||||
TransformOptions,
|
||||
};
|
||||
use smallvec::{SmallVec, ToSmallVec, smallvec};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::OnceCell;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, LazyLock, OnceLock};
|
||||
@@ -350,6 +352,139 @@ impl ColorSpace {
|
||||
self.to_alpha_color(c, opacity, manual_scale)
|
||||
.unwrap_or(AlphaColor::BLACK)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (38): whether this space can be the default colour space of `family`
|
||||
/// (ISO 32000-2 §8.6.5.6): it has as many components as the device space and is not a
|
||||
/// Lab, Indexed or Pattern space. Nor is a device space: a default that names one is
|
||||
/// ignored, so substitution never recurses.
|
||||
fn can_stand_in_for(&self, family: DeviceFamily) -> bool {
|
||||
!matches!(
|
||||
self.0.as_ref(),
|
||||
ColorSpaceType::DeviceGray
|
||||
| ColorSpaceType::DeviceRgb
|
||||
| ColorSpaceType::DeviceCmyk
|
||||
| ColorSpaceType::Lab(_)
|
||||
| ColorSpaceType::Indexed(_)
|
||||
| ColorSpaceType::Pattern(_)
|
||||
) && self.num_components() == family.num_components()
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (38): a device colour space (ISO 32000-2 §8.6.4), which the resources'
|
||||
/// default colour space for it replaces wherever content selects it (§8.6.5.6).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum DeviceFamily {
|
||||
Gray,
|
||||
Rgb,
|
||||
Cmyk,
|
||||
}
|
||||
|
||||
impl DeviceFamily {
|
||||
/// The device space a colour space operand names: `/DeviceGray`, `/DeviceRGB` or
|
||||
/// `/DeviceCMYK` (or an abbreviation read as one), alone or at the head of an array. `None`
|
||||
/// for any other space, including the ICC-based and calibrated ones drawn as device colour
|
||||
/// here (an sRGB profile, `/CalCMYK`): they are not device spaces.
|
||||
pub(crate) fn of_object(object: &Object<'_>) -> Option<Self> {
|
||||
match object {
|
||||
Object::Name(name) => Self::of_name(name),
|
||||
Object::Array(array) => array
|
||||
.flex_iter()
|
||||
.next::<Name<'_>>()
|
||||
.and_then(|name| Self::of_name(&name)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The device space `name` names, as [`DeviceFamily::of_object`].
|
||||
pub(crate) fn of_name(name: &Name<'_>) -> Option<Self> {
|
||||
match name.deref() {
|
||||
DEVICE_GRAY | G => Some(Self::Gray),
|
||||
DEVICE_RGB | RGB => Some(Self::Rgb),
|
||||
DEVICE_CMYK | CMYK => Some(Self::Cmyk),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The device space itself.
|
||||
fn space(self) -> ColorSpace {
|
||||
match self {
|
||||
Self::Gray => ColorSpace::device_gray(),
|
||||
Self::Rgb => ColorSpace::device_rgb(),
|
||||
Self::Cmyk => ColorSpace::device_cmyk(),
|
||||
}
|
||||
}
|
||||
|
||||
fn num_components(self) -> u8 {
|
||||
match self {
|
||||
Self::Gray => 1,
|
||||
Self::Rgb => 3,
|
||||
Self::Cmyk => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `/ColorSpace` resource that holds the default for this space.
|
||||
fn default_key(self) -> &'static [u8] {
|
||||
match self {
|
||||
Self::Gray => DEFAULT_GRAY,
|
||||
Self::Rgb => DEFAULT_RGB,
|
||||
Self::Cmyk => DEFAULT_CMYK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (38): the default colour spaces (`/DefaultGray`, `/DefaultRGB` and
|
||||
/// `/DefaultCMYK`, ISO 32000-2 §8.6.5.6) of the resources content is drawn with, each read the
|
||||
/// first time the content selects its device space. Like any named resource, a default comes
|
||||
/// from those resources' `/ColorSpace` or from the resources they inherit: a form XObject
|
||||
/// without one of its own draws in its page's.
|
||||
pub(crate) struct DefaultColorSpaces<'r, 'a> {
|
||||
resources: &'r Resources<'a>,
|
||||
cache: Cache,
|
||||
gray: OnceCell<Option<(ColorSpace, u128)>>,
|
||||
rgb: OnceCell<Option<(ColorSpace, u128)>>,
|
||||
cmyk: OnceCell<Option<(ColorSpace, u128)>>,
|
||||
}
|
||||
|
||||
impl<'r, 'a> DefaultColorSpaces<'r, 'a> {
|
||||
pub(crate) fn new(resources: &'r Resources<'a>, cache: &Cache) -> Self {
|
||||
Self {
|
||||
resources,
|
||||
cache: cache.clone(),
|
||||
gray: OnceCell::new(),
|
||||
rgb: OnceCell::new(),
|
||||
cmyk: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The default for `family`, with the cache key of the object it was read from. `None`
|
||||
/// when the resources have none, or theirs is not a colour space that can stand in for
|
||||
/// the device space: device colour is then drawn as it is.
|
||||
pub(crate) fn get(&self, family: DeviceFamily) -> Option<(ColorSpace, u128)> {
|
||||
let found = match family {
|
||||
DeviceFamily::Gray => &self.gray,
|
||||
DeviceFamily::Rgb => &self.rgb,
|
||||
DeviceFamily::Cmyk => &self.cmyk,
|
||||
};
|
||||
found
|
||||
.get_or_init(|| {
|
||||
let object = self
|
||||
.resources
|
||||
.get_color_space(&Name::new_unescaped(family.default_key()))?;
|
||||
let key = object.cache_key();
|
||||
let space = self
|
||||
.cache
|
||||
.get_or_insert_with(key, || ColorSpace::new(object.clone(), &self.cache))?;
|
||||
space.can_stand_in_for(family).then_some((space, key))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The colour space content that selects `family` draws in: its default, else the device
|
||||
/// space itself.
|
||||
pub(crate) fn device(&self, family: DeviceFamily) -> ColorSpace {
|
||||
self.get(family)
|
||||
.map_or_else(|| family.space(), |(space, _)| space)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRgb for ColorSpace {
|
||||
|
||||
+61
-25
@@ -1,5 +1,5 @@
|
||||
use crate::{BlendMode, FillRule};
|
||||
use crate::color::ColorSpace;
|
||||
use crate::color::{ColorSpace, DefaultColorSpaces, DeviceFamily};
|
||||
use crate::context::Context;
|
||||
use crate::convert::{convert_line_cap, convert_line_join};
|
||||
use crate::device::Device;
|
||||
@@ -336,6 +336,30 @@ fn draw_highlight_without_appearance<'a>(
|
||||
context.restore_state(device);
|
||||
}
|
||||
|
||||
/// PdfCraft patch (38): the colour space `cs` or `CS` selects by `name`: a device space, else
|
||||
/// the `/ColorSpace` resource `name`. A device space, named directly or by a resource, is
|
||||
/// replaced by its default from `defaults` (ISO 32000-2 §8.6.5.6).
|
||||
fn named_color_space<'a>(
|
||||
name: &hayro_syntax::object::Name<'_>,
|
||||
resources: &Resources<'a>,
|
||||
defaults: &DefaultColorSpaces<'_, '_>,
|
||||
context: &mut Context<'a>,
|
||||
) -> Option<ColorSpace> {
|
||||
if let Some(named) = ColorSpace::new_from_name(name) {
|
||||
let default = DeviceFamily::of_name(name).and_then(|family| defaults.get(family));
|
||||
return Some(default.map_or(named, |(space, _)| space));
|
||||
}
|
||||
let resource = resources.get_color_space(name);
|
||||
if let Some((space, _)) = resource
|
||||
.as_ref()
|
||||
.and_then(DeviceFamily::of_object)
|
||||
.and_then(|family| defaults.get(family))
|
||||
{
|
||||
return Some(space);
|
||||
}
|
||||
context.get_color_space(resources, name)
|
||||
}
|
||||
|
||||
/// Interprets the instructions that `ops` yields, until they run out or the caller cancels. It runs
|
||||
/// once per window of a content stream, between one `save_state` and its restore (see
|
||||
/// `interpret_content`).
|
||||
@@ -345,6 +369,9 @@ fn interpret_window<'a>(
|
||||
context: &mut Context<'a>,
|
||||
device: &mut impl Device<'a>,
|
||||
) {
|
||||
// PdfCraft patch (38): the resources' DefaultGray, DefaultRGB and DefaultCMYK replace the
|
||||
// device spaces that colour operators and images select (ISO 32000-2 §8.6.5.6).
|
||||
let defaults = DefaultColorSpaces::new(resources, &context.interpreter_cache.object_cache);
|
||||
while let Some(op) = ops.next() {
|
||||
// PdfCraft patch: stop when the caller cancelled (see `InterpreterSettings::cancelled`).
|
||||
if context.settings.cancelled.as_ref().is_some_and(|c| c.load(Ordering::Relaxed)) {
|
||||
@@ -353,18 +380,21 @@ fn interpret_window<'a>(
|
||||
match op {
|
||||
TypedInstruction::SaveState(_) => context.save_state(),
|
||||
TypedInstruction::StrokeColorDeviceRgb(s) => {
|
||||
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb();
|
||||
// PdfCraft patch (38): DeviceRGB, or the DefaultRGB that replaces it.
|
||||
context.get_mut().graphics_state.stroke_cs = defaults.device(DeviceFamily::Rgb);
|
||||
context.get_mut().graphics_state.stroke_color =
|
||||
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
|
||||
context.get_mut().graphics_state.stroke_pattern = None;
|
||||
}
|
||||
TypedInstruction::StrokeColorDeviceGray(s) => {
|
||||
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray();
|
||||
// PdfCraft patch (38): DeviceGray, or the DefaultGray that replaces it.
|
||||
context.get_mut().graphics_state.stroke_cs = defaults.device(DeviceFamily::Gray);
|
||||
context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()];
|
||||
context.get_mut().graphics_state.stroke_pattern = None;
|
||||
}
|
||||
TypedInstruction::StrokeColorCmyk(s) => {
|
||||
context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk();
|
||||
// PdfCraft patch (38): DeviceCMYK, or the DefaultCMYK that replaces it.
|
||||
context.get_mut().graphics_state.stroke_cs = defaults.device(DeviceFamily::Cmyk);
|
||||
context.get_mut().graphics_state.stroke_color =
|
||||
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
|
||||
context.get_mut().graphics_state.stroke_pattern = None;
|
||||
@@ -428,18 +458,21 @@ fn interpret_window<'a>(
|
||||
fill_stroke_path(context, device, FillRule::NonZero);
|
||||
}
|
||||
TypedInstruction::NonStrokeColorDeviceGray(s) => {
|
||||
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray();
|
||||
// PdfCraft patch (38): DeviceGray, or the DefaultGray that replaces it.
|
||||
context.get_mut().graphics_state.none_stroke_cs = defaults.device(DeviceFamily::Gray);
|
||||
context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()];
|
||||
context.get_mut().graphics_state.non_stroke_pattern = None;
|
||||
}
|
||||
TypedInstruction::NonStrokeColorDeviceRgb(s) => {
|
||||
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb();
|
||||
// PdfCraft patch (38): DeviceRGB, or the DefaultRGB that replaces it.
|
||||
context.get_mut().graphics_state.none_stroke_cs = defaults.device(DeviceFamily::Rgb);
|
||||
context.get_mut().graphics_state.non_stroke_color =
|
||||
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()];
|
||||
context.get_mut().graphics_state.non_stroke_pattern = None;
|
||||
}
|
||||
TypedInstruction::NonStrokeColorCmyk(s) => {
|
||||
context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk();
|
||||
// PdfCraft patch (38): DeviceCMYK, or the DefaultCMYK that replaces it.
|
||||
context.get_mut().graphics_state.none_stroke_cs = defaults.device(DeviceFamily::Cmyk);
|
||||
context.get_mut().graphics_state.non_stroke_color =
|
||||
smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()];
|
||||
context.get_mut().graphics_state.non_stroke_pattern = None;
|
||||
@@ -528,13 +561,9 @@ fn interpret_window<'a>(
|
||||
// Ignore for now.
|
||||
}
|
||||
TypedInstruction::ColorSpaceStroke(c) => {
|
||||
let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
|
||||
named
|
||||
} else {
|
||||
context
|
||||
.get_color_space(resources, c.0)
|
||||
.unwrap_or(ColorSpace::device_gray())
|
||||
};
|
||||
// PdfCraft patch (38): see `named_color_space`.
|
||||
let cs = named_color_space(c.0, resources, &defaults, context)
|
||||
.unwrap_or(ColorSpace::device_gray());
|
||||
|
||||
if !cs.is_pattern() {
|
||||
context.get_mut().graphics_state.stroke_pattern = None;
|
||||
@@ -543,13 +572,9 @@ fn interpret_window<'a>(
|
||||
context.get_mut().graphics_state.stroke_cs = cs;
|
||||
}
|
||||
TypedInstruction::ColorSpaceNonStroke(c) => {
|
||||
let cs = if let Some(named) = ColorSpace::new_from_name(c.0) {
|
||||
named
|
||||
} else {
|
||||
context
|
||||
.get_color_space(resources, c.0)
|
||||
.unwrap_or(ColorSpace::device_gray())
|
||||
};
|
||||
// PdfCraft patch (38): see `named_color_space`.
|
||||
let cs = named_color_space(c.0, resources, &defaults, context)
|
||||
.unwrap_or(ColorSpace::device_gray());
|
||||
|
||||
if !cs.is_pattern() {
|
||||
context.get_mut().graphics_state.non_stroke_pattern = None;
|
||||
@@ -574,7 +599,7 @@ fn interpret_window<'a>(
|
||||
context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| {
|
||||
resources
|
||||
.get_pattern(name)
|
||||
.and_then(|d| Pattern::new(d, context, resources))
|
||||
.and_then(|d| Pattern::new(d, context, resources, &defaults))
|
||||
});
|
||||
}
|
||||
TypedInstruction::StrokeColorNamed(n) => {
|
||||
@@ -583,7 +608,7 @@ fn interpret_window<'a>(
|
||||
context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| {
|
||||
resources
|
||||
.get_pattern(name)
|
||||
.and_then(|d| Pattern::new(d, context, resources))
|
||||
.and_then(|d| Pattern::new(d, context, resources, &defaults))
|
||||
});
|
||||
}
|
||||
TypedInstruction::BeginMarkedContentWithProperties(bdc) => {
|
||||
@@ -794,6 +819,8 @@ fn interpret_window<'a>(
|
||||
&cache,
|
||||
Some(&decoded_cache),
|
||||
transfer_function.clone(),
|
||||
// PdfCraft patch (38): an image in a device space draws in its default.
|
||||
&defaults,
|
||||
)
|
||||
}) {
|
||||
draw_xobject(&x_object, resources, context, device);
|
||||
@@ -804,14 +831,17 @@ fn interpret_window<'a>(
|
||||
let transfer_function = context.get().graphics_state.transfer_function.clone();
|
||||
let cache = context.interpreter_cache.object_cache.clone();
|
||||
// PdfCraft patch: inline images are not cached (see `DecodedImageKey`).
|
||||
// PdfCraft patch (38): an inline image in a device space, named directly or by a
|
||||
// resource, draws in its default.
|
||||
if let Some(x_object) = ImageXObject::new(
|
||||
i.0,
|
||||
|name| context.get_color_space(resources, name),
|
||||
|name| named_color_space(name, resources, &defaults, context),
|
||||
&warning_sink,
|
||||
&cache,
|
||||
None,
|
||||
false,
|
||||
transfer_function,
|
||||
Some(&defaults),
|
||||
) {
|
||||
draw_image_xobject(&x_object, context, device);
|
||||
}
|
||||
@@ -830,7 +860,13 @@ fn interpret_window<'a>(
|
||||
.get_shading(s.0)
|
||||
.and_then(|o| {
|
||||
let (dict, stream) = dict_or_stream(&o)?;
|
||||
Shading::new(dict, stream, &context.interpreter_cache.object_cache)
|
||||
// PdfCraft patch (38): a shading in a device space draws in its default.
|
||||
Shading::new(
|
||||
dict,
|
||||
stream,
|
||||
&context.interpreter_cache.object_cache,
|
||||
&defaults,
|
||||
)
|
||||
})
|
||||
.map(|s| {
|
||||
Pattern::Shading(ShadingPattern {
|
||||
|
||||
+12
-3
@@ -1,7 +1,7 @@
|
||||
//! PDF patterns.
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::color::{Color, ColorSpace};
|
||||
use crate::color::{Color, ColorSpace, DefaultColorSpaces};
|
||||
use crate::context::{Context, InterpreterCache};
|
||||
use crate::device::Device;
|
||||
use crate::font::Glyph;
|
||||
@@ -34,16 +34,20 @@ pub enum Pattern<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Pattern<'a> {
|
||||
/// PdfCraft patch (38): `defaults` are those of the resources the pattern is selected with,
|
||||
/// for a shading pattern's shading in a device space.
|
||||
pub(crate) fn new(
|
||||
object: Object<'a>,
|
||||
ctx: &Context<'a>,
|
||||
resources: &Resources<'a>,
|
||||
defaults: &DefaultColorSpaces<'_, '_>,
|
||||
) -> Option<Self> {
|
||||
match object {
|
||||
Object::Dict(dict) => Some(Self::Shading(ShadingPattern::new(
|
||||
&dict,
|
||||
&ctx.interpreter_cache.object_cache,
|
||||
ctx.get().graphics_state.non_stroke_alpha,
|
||||
defaults,
|
||||
)?)),
|
||||
Object::Stream(stream) => Some(Self::Tiling(Box::new(TilingPattern::new(
|
||||
stream, ctx, resources,
|
||||
@@ -93,11 +97,16 @@ pub struct ShadingPattern {
|
||||
}
|
||||
|
||||
impl ShadingPattern {
|
||||
pub(crate) fn new(dict: &Dict<'_>, cache: &Cache, opacity: f32) -> Option<Self> {
|
||||
pub(crate) fn new(
|
||||
dict: &Dict<'_>,
|
||||
cache: &Cache,
|
||||
opacity: f32,
|
||||
defaults: &DefaultColorSpaces<'_, '_>,
|
||||
) -> Option<Self> {
|
||||
let shading = dict.get::<Object<'_>>(SHADING).and_then(|o| {
|
||||
let (dict, stream) = dict_or_stream(&o)?;
|
||||
|
||||
Shading::new(dict, stream, cache)
|
||||
Shading::new(dict, stream, cache, defaults)
|
||||
})?;
|
||||
let matrix = dict
|
||||
.get::<[f64; 6]>(MATRIX)
|
||||
|
||||
+18
-6
@@ -4,9 +4,9 @@
|
||||
|
||||
use crate::CacheKey;
|
||||
use crate::cache::Cache;
|
||||
use crate::color::{ColorComponents, ColorSpace};
|
||||
use crate::color::{ColorComponents, ColorSpace, DefaultColorSpaces, DeviceFamily};
|
||||
use crate::function::{Function, StitchingBounds, Values, interpolate};
|
||||
use crate::util::{Float32Ext, PointExt, RectExt};
|
||||
use crate::util::{Float32Ext, PointExt, RectExt, hash128};
|
||||
use hayro_syntax::bit_reader::BitReader;
|
||||
use hayro_syntax::object::Array;
|
||||
use hayro_syntax::object::Dict;
|
||||
@@ -139,12 +139,24 @@ pub struct Shading {
|
||||
}
|
||||
|
||||
impl Shading {
|
||||
pub(crate) fn new(dict: &Dict<'_>, stream: Option<&Stream<'_>>, cache: &Cache) -> Option<Self> {
|
||||
let cache_key = dict.cache_key();
|
||||
|
||||
/// PdfCraft patch (38): a shading in a device space is drawn in its default colour space
|
||||
/// from `defaults`, the resources it is painted with (ISO 32000-2 §8.6.5.6), and keyed by
|
||||
/// it as an image is.
|
||||
pub(crate) fn new(
|
||||
dict: &Dict<'_>,
|
||||
stream: Option<&Stream<'_>>,
|
||||
cache: &Cache,
|
||||
defaults: &DefaultColorSpaces<'_, '_>,
|
||||
) -> Option<Self> {
|
||||
let shading_num = dict.get::<u8>(SHADING_TYPE)?;
|
||||
|
||||
let color_space = ColorSpace::new(dict.get(COLORSPACE)?, cache)?;
|
||||
let color_space_obj = dict.get::<Object<'_>>(COLORSPACE)?;
|
||||
let default =
|
||||
DeviceFamily::of_object(&color_space_obj).and_then(|family| defaults.get(family));
|
||||
let (color_space, cache_key) = match default {
|
||||
Some((space, key)) => (space, hash128(&(dict.cache_key(), key))),
|
||||
None => (ColorSpace::new(color_space_obj, cache)?, dict.cache_key()),
|
||||
};
|
||||
|
||||
let shading_type = match shading_num {
|
||||
1 => {
|
||||
|
||||
+35
-12
@@ -1,5 +1,5 @@
|
||||
use crate::cache::{Cache, DecodedImageCache, DecodedImageKey};
|
||||
use crate::color::{ColorComponents, ColorSpace, ToRgb};
|
||||
use crate::color::{ColorComponents, ColorSpace, DefaultColorSpaces, DeviceFamily, ToRgb};
|
||||
use crate::context::Context;
|
||||
use crate::device::Device;
|
||||
use crate::function::{Function, interpolate};
|
||||
@@ -36,6 +36,7 @@ impl<'a> XObject<'a> {
|
||||
cache: &Cache,
|
||||
decoded_cache: Option<&DecodedImageCache>,
|
||||
transfer_function: Option<ActiveTransferFunction>,
|
||||
defaults: &DefaultColorSpaces<'_, '_>,
|
||||
) -> Option<Self> {
|
||||
let dict = stream.dict();
|
||||
match dict.get::<Name<'_>>(SUBTYPE)?.deref() {
|
||||
@@ -47,6 +48,7 @@ impl<'a> XObject<'a> {
|
||||
decoded_cache,
|
||||
false,
|
||||
transfer_function,
|
||||
Some(defaults),
|
||||
)?)),
|
||||
FORM => Some(Self::FormXObject(FormXObject::new(stream)?)),
|
||||
_ => None,
|
||||
@@ -308,9 +310,15 @@ pub(crate) struct ImageXObject<'a> {
|
||||
stream: Stream<'a>,
|
||||
transfer_function: Option<ActiveTransferFunction>,
|
||||
warning_sink: WarningSinkFn,
|
||||
/// PdfCraft patch (38): the cache key of the default colour space drawn in place of the
|
||||
/// image's device space, if one is.
|
||||
default_space: Option<u128>,
|
||||
}
|
||||
|
||||
impl<'a> ImageXObject<'a> {
|
||||
/// PdfCraft patch (38): an image in a device space is drawn in its default colour space
|
||||
/// from `defaults`. Masks have none to replace, and `resolve_cs` (an inline image's named
|
||||
/// colour space) substitutes its own.
|
||||
pub(crate) fn new(
|
||||
stream: &Stream<'a>,
|
||||
resolve_cs: impl FnOnce(&Name<'_>) -> Option<ColorSpace>,
|
||||
@@ -319,6 +327,7 @@ impl<'a> ImageXObject<'a> {
|
||||
decoded_cache: Option<&DecodedImageCache>,
|
||||
mut is_mask: bool,
|
||||
transfer_function: Option<ActiveTransferFunction>,
|
||||
defaults: Option<&DefaultColorSpaces<'_, '_>>,
|
||||
) -> Option<Self> {
|
||||
let dict = stream.dict();
|
||||
|
||||
@@ -328,6 +337,7 @@ impl<'a> ImageXObject<'a> {
|
||||
.unwrap_or(false);
|
||||
is_mask |= is_stencil_mask;
|
||||
|
||||
let mut default_space = None;
|
||||
let image_cs = if is_mask {
|
||||
// Masks are always single-channel.
|
||||
Some(ColorSpace::device_gray())
|
||||
@@ -336,15 +346,25 @@ impl<'a> ImageXObject<'a> {
|
||||
.get::<Object<'_>>(CS)
|
||||
.or_else(|| dict.get::<Object<'_>>(COLORSPACE));
|
||||
|
||||
cs_obj
|
||||
.clone()
|
||||
.and_then(|c| ColorSpace::new(c, cache))
|
||||
// Inline images can also refer to color spaces by name.
|
||||
.or_else(|| {
|
||||
cs_obj
|
||||
.and_then(|c| c.into_name())
|
||||
.and_then(|n| resolve_cs(&n))
|
||||
})
|
||||
let default = cs_obj
|
||||
.as_ref()
|
||||
.and_then(DeviceFamily::of_object)
|
||||
.zip(defaults)
|
||||
.and_then(|(family, defaults)| defaults.get(family));
|
||||
if let Some((space, key)) = default {
|
||||
default_space = Some(key);
|
||||
Some(space)
|
||||
} else {
|
||||
cs_obj
|
||||
.clone()
|
||||
.and_then(|c| ColorSpace::new(c, cache))
|
||||
// Inline images can also refer to color spaces by name.
|
||||
.or_else(|| {
|
||||
cs_obj
|
||||
.and_then(|c| c.into_name())
|
||||
.and_then(|n| resolve_cs(&n))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let interpolate = dict
|
||||
@@ -371,6 +391,7 @@ impl<'a> ImageXObject<'a> {
|
||||
stream: stream.clone(),
|
||||
is_mask,
|
||||
is_stencil_mask,
|
||||
default_space,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -434,6 +455,8 @@ impl<'a> ImageXObject<'a> {
|
||||
is_mask: self.is_mask,
|
||||
is_stencil_mask: self.is_stencil_mask,
|
||||
interpolate: self.interpolate,
|
||||
// PdfCraft patch (38): the same image drawn in a default colour space or without one.
|
||||
default_space: self.default_space,
|
||||
// PdfCraft patch (32): see `decode_reads_target`.
|
||||
target: if self.decode_reads_target() {
|
||||
target_dimension
|
||||
@@ -905,7 +928,7 @@ fn resolve_alpha(
|
||||
.get::<Stream<'_>>(SMASK)
|
||||
.or_else(|| dict.get::<Stream<'_>>(MASK))
|
||||
{
|
||||
let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?;
|
||||
let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None, None)?;
|
||||
|
||||
decode_mask(&obj, target_dimension).map(|decoded| decoded.luma)
|
||||
} else if let Some(color_key_mask) = dict.get::<SmallVec<[u16; 4]>>(MASK) {
|
||||
@@ -965,7 +988,7 @@ fn resolve_matte(
|
||||
let mut matte_rgb = [0_u8; 3];
|
||||
color_space.convert_f32(&matte, &mut matte_rgb, false);
|
||||
|
||||
let mask_obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?;
|
||||
let mask_obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None, None)?;
|
||||
let alpha = decode_mask(&mask_obj, target_dimension)?.luma;
|
||||
|
||||
Some((alpha, matte_rgb))
|
||||
|
||||
Reference in New Issue
Block a user