mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 21:11:52 +00:00
Render: an isolated CMYK group of any size blends in CMYK, in bands (#6885)
* Render: an isolated CMYK group of any size blends in CMYK, in bands hayro patch (27) blended an isolated DeviceCMYK transparency group in CMYK only up to 2^24 device pixels (a 4096 x 4096 square); a larger one was blended in RGB. Print as image renders a whole page at once, up to 8192 px on the long side, so a page enlarged to 11 x 17 in at 600 dpi (about 43 MP) blended any group covering most of it in RGB, and a print test file's CMYK blend patches drew their X's. The same applied to exporting a large page as an image. A larger group is now blended in bands of whole rows, each within the old limit: both ink passes and the tint conversion run per band, so their memory stays as before, and only the finished group (4 bytes a pixel) is kept whole. Every band is blended before any is drawn, so a group a pass has no tints for is still drawn as before, whole. The regression test lowers the band to 7 rows (set_cmyk_band_pixels, hidden, per thread like set_skip_offscreen_paths) and checks the banded group matches the whole one across an anti-aliased edge, and is CMYK's green where magenta is under black with Difference. * Test: CMYK green through the profile is (0, 166, 81); check green against RGB's magenta
This commit is contained in:
1 parent
8559306f77
commit
e03d5a1c29
3 files changed
+126
-38
No files matched your search
@@ -280,3 +280,48 @@ fn the_color_blend_mode_over_an_empty_cmyk_group_keeps_every_k_level() {
|
||||
}
|
||||
assert!(wrong.is_empty(), "K levels drawn wrong (level, plain, blended): {wrong:?}");
|
||||
}
|
||||
|
||||
/// A group larger than the renderer blends in CMYK at once (hayro patch (27): 2^24 device
|
||||
/// pixels) used to be blended in RGB instead: a page enlarged to 11 x 17 in for Print as image
|
||||
/// at 600 dpi drew the X's of a print test file's CMYK blend patches. It is now blended in bands
|
||||
/// of whole rows. Here the band is lowered to a few rows, so the group is blended in many
|
||||
/// bands, across the anti-aliased edge of a triangle as well: the result is the same as in one
|
||||
/// piece, and still CMYK's (magenta under black with Difference is green).
|
||||
#[test]
|
||||
fn a_cmyk_group_blended_in_bands_is_the_group_blended_whole() {
|
||||
let group = "0 1 0 0 k 0 0 m 200 0 l 0 200 l f /Diff gs 0 0 0 1 k 0 0 200 200 re f";
|
||||
let pdf = format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 220 220] /Contents 4 0 R /Resources << /XObject << /X0 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length 28 >> stream
|
||||
q 1 0 0 1 10 10 cm /X0 Do Q
|
||||
endstream endobj
|
||||
5 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency /CS /DeviceCMYK /I true >> /Resources << /ExtGState << /Diff << /BM /Difference >> >> >> /Length {} >> stream
|
||||
{group}
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
group.len()
|
||||
);
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
|
||||
let request = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 2.0, tag: 0, ..Default::default() };
|
||||
let whole = r.render(request);
|
||||
// 400 device pixels a row: bands of 7 rows, the last one shorter.
|
||||
hayro::set_cmyk_band_pixels(Some(2_800));
|
||||
let banded = r.render(request);
|
||||
hayro::set_cmyk_band_pixels(None);
|
||||
assert!(whole.error.is_none() && banded.error.is_none(), "{:?} {:?}", whole.error, banded.error);
|
||||
assert_eq!((whole.width, whole.height), (banded.width, banded.height));
|
||||
let worst = whole.rgba.iter().zip(banded.rgba.iter()).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
|
||||
assert!(worst <= 1, "bands differ from the whole group by up to {worst}");
|
||||
// Inside the triangle (device (100, 340), the group's point (40, 40)): magenta under black.
|
||||
let at = |p: &RenderedPage, x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..3].to_vec();
|
||||
for page in [&whole, &banded] {
|
||||
let rgb = at(page, 100, 340);
|
||||
// CMYK's 1 0 1 0 through the CMYK profile is (0, 166, 81); blended in RGB it would be
|
||||
// magenta, red and blue high and green low.
|
||||
assert!(rgb[1] > 120 && rgb[0] < 60 && rgb[2] < 120, "blended in CMYK, green: {rgb:?}");
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -65,6 +65,9 @@ pub use renderer::{image_resampling_size, tiling_cell_pixels, tiling_cell_scale,
|
||||
/// PdfCraft patch: exported for its regression test.
|
||||
#[doc(hidden)]
|
||||
pub use renderer::set_skip_offscreen_paths;
|
||||
/// PdfCraft patch (27): exported for its regression test.
|
||||
#[doc(hidden)]
|
||||
pub use renderer::set_cmyk_band_pixels;
|
||||
/// PdfCraft patch (30): exported for its regression tests.
|
||||
#[doc(hidden)]
|
||||
pub use renderer::{OffscreenDraws, offscreen_draws};
|
||||
|
||||
Vendored
+78
-38
@@ -815,11 +815,27 @@ enum PaintSetup {
|
||||
Nothing,
|
||||
}
|
||||
|
||||
/// PdfCraft patch (27): the largest group, in device pixels, blended in CMYK (2^24, a 4096 ×
|
||||
/// 4096 square). Each pixel costs about 20 bytes more than drawing the group as before (two
|
||||
/// pass canvases, its tints, their RGB and the result); a larger group is drawn as before.
|
||||
/// PdfCraft patch (27): the most device pixels of a group blended in CMYK at once (2^24, a 4096 ×
|
||||
/// 4096 square). Each costs about 16 bytes while its band is blended (two pass canvases, its
|
||||
/// tints and their RGB); a larger group is blended in bands of whole rows under it, and only
|
||||
/// the finished group, 4 bytes a pixel, is kept whole. Before, a larger group was blended in RGB
|
||||
/// instead: a page enlarged to 11 x 17 in for Print as image at 600 dpi drew the X's of a print
|
||||
/// test file's CMYK blend patches when one group covered most of it.
|
||||
const MAX_CMYK_GROUP_PIXELS: u64 = 1 << 24;
|
||||
|
||||
thread_local! {
|
||||
/// PdfCraft patch (27): the band size [`MAX_CMYK_GROUP_PIXELS`] sets, on this thread. Only
|
||||
/// PdfCraft's regression test lowers it, to blend a small page's groups in many bands.
|
||||
static CMYK_BAND_PIXELS: std::cell::Cell<u64> = const { std::cell::Cell::new(MAX_CMYK_GROUP_PIXELS) };
|
||||
}
|
||||
|
||||
/// PdfCraft patch (27): exported for its regression test (see [`CMYK_BAND_PIXELS`]); `None`
|
||||
/// restores [`MAX_CMYK_GROUP_PIXELS`].
|
||||
#[doc(hidden)]
|
||||
pub fn set_cmyk_band_pixels(pixels: Option<u64>) {
|
||||
CMYK_BAND_PIXELS.with(|p| p.set(pixels.unwrap_or(MAX_CMYK_GROUP_PIXELS).max(1)));
|
||||
}
|
||||
|
||||
pub(crate) struct Renderer {
|
||||
pub(crate) ctx: RenderContext,
|
||||
/// PdfCraft patch: when set, solid text and line-art paint colours are remapped
|
||||
@@ -1743,6 +1759,46 @@ impl Renderer {
|
||||
Some(pix)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (27): rows `offset.1` .. `offset.1 + height` of `group` blended in CMYK, as
|
||||
/// a `width` × `height` premultiplied pixmap whose corner is device point `offset`; `None`
|
||||
/// when the group holds something a pass has no tints for, or its tints can't be converted.
|
||||
fn cmyk_group_band(
|
||||
&self,
|
||||
group: &hayro_interpret::CmykGroup<'_, '_>,
|
||||
offset: (f64, f64),
|
||||
width: u16,
|
||||
height: u16,
|
||||
) -> Option<Pixmap> {
|
||||
let cmy = self.ink_pass(group, InkPass::Cmy, offset, width, height)?;
|
||||
let k = self.ink_pass(group, InkPass::K, offset, width, height)?;
|
||||
|
||||
// A tint is a pass's premultiplied colour un-premultiplied and complemented.
|
||||
let ink = |premul: u8, alpha: u8| {
|
||||
if alpha == 0 {
|
||||
return 0;
|
||||
}
|
||||
let straight = (u32::from(premul) * 255 + u32::from(alpha) / 2) / u32::from(alpha);
|
||||
255 - straight.min(255) as u8
|
||||
};
|
||||
let pixels = usize::from(width) * usize::from(height);
|
||||
let mut tints = Vec::with_capacity(pixels * 4);
|
||||
for (p, q) in cmy.data().iter().zip(k.data()) {
|
||||
// Both passes draw the same shapes at the same opacities, so they cover the same
|
||||
// pixels by the same amounts; each pass's own alpha un-premultiplies its colour.
|
||||
tints.extend_from_slice(&[ink(p.r, p.a), ink(p.g, p.a), ink(p.b, p.a), ink(q.r, q.a)]);
|
||||
}
|
||||
let mut rgb = vec![0u8; pixels * 3];
|
||||
hayro_interpret::color::cmyk_to_srgb8(&tints, &mut rgb)?;
|
||||
|
||||
let mut result = offscreen_pixmap(width, height, false);
|
||||
for ((px, rgb), coverage) in result.data_mut().iter_mut().zip(rgb.chunks_exact(3)).zip(cmy.data()) {
|
||||
let a = u32::from(coverage.a);
|
||||
let premul = |v: u8| ((u32::from(v) * a + 127) / 255) as u8;
|
||||
*px = PremulRgba8 { r: premul(rgb[0]), g: premul(rgb[1]), b: premul(rgb[2]), a: coverage.a };
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: remaps a solid paint colour through `color_replacement`, if one is
|
||||
/// set. `Paint::Pattern` (images, shadings, tiling patterns) always passes through
|
||||
/// untouched.
|
||||
@@ -2444,42 +2500,26 @@ impl<'a> Device<'a> for Renderer {
|
||||
if width == 0 || height == 0 {
|
||||
return true;
|
||||
}
|
||||
if u64::from(width) * u64::from(height) > MAX_CMYK_GROUP_PIXELS {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(cmy) = self.ink_pass(group, InkPass::Cmy, (x0, y0), width, height) else {
|
||||
return false;
|
||||
};
|
||||
let Some(k) = self.ink_pass(group, InkPass::K, (x0, y0), width, height) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// A tint is a pass's premultiplied colour un-premultiplied and complemented.
|
||||
let ink = |premul: u8, alpha: u8| {
|
||||
if alpha == 0 {
|
||||
return 0;
|
||||
}
|
||||
let straight = (u32::from(premul) * 255 + u32::from(alpha) / 2) / u32::from(alpha);
|
||||
255 - straight.min(255) as u8
|
||||
};
|
||||
let pixels = usize::from(width) * usize::from(height);
|
||||
let mut tints = Vec::with_capacity(pixels * 4);
|
||||
for (p, q) in cmy.data().iter().zip(k.data()) {
|
||||
// Both passes draw the same shapes at the same opacities, so they cover the same
|
||||
// pixels by the same amounts; each pass's own alpha un-premultiplies its colour.
|
||||
tints.extend_from_slice(&[ink(p.r, p.a), ink(p.g, p.a), ink(p.b, p.a), ink(q.r, q.a)]);
|
||||
}
|
||||
let mut rgb = vec![0u8; pixels * 3];
|
||||
if hayro_interpret::color::cmyk_to_srgb8(&tints, &mut rgb).is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bands of whole rows, each within the band size: a group of any size blends in CMYK.
|
||||
// Every band is blended before any is drawn, so a group a pass has no tints for in one
|
||||
// band is drawn as before, whole.
|
||||
let band = (CMYK_BAND_PIXELS.with(std::cell::Cell::get) / u64::from(width)).clamp(1, u64::from(height));
|
||||
// At most `height`, so it fits a u16.
|
||||
let band = band as u16;
|
||||
let mut result = offscreen_pixmap(width, height, false);
|
||||
for ((px, rgb), coverage) in result.data_mut().iter_mut().zip(rgb.chunks_exact(3)).zip(cmy.data()) {
|
||||
let a = u32::from(coverage.a);
|
||||
let premul = |v: u8| ((u32::from(v) * a + 127) / 255) as u8;
|
||||
*px = PremulRgba8 { r: premul(rgb[0]), g: premul(rgb[1]), b: premul(rgb[2]), a: coverage.a };
|
||||
let row = usize::from(width);
|
||||
let mut top = 0u16;
|
||||
while top < height {
|
||||
let rows = band.min(height - top);
|
||||
let Some(piece) = self.cmyk_group_band(group, (x0, y0 + f64::from(top)), width, rows) else {
|
||||
return false;
|
||||
};
|
||||
let start = usize::from(top) * row;
|
||||
let Some(dest) = result.data_mut().get_mut(start..start + usize::from(rows) * row) else {
|
||||
return false;
|
||||
};
|
||||
dest.copy_from_slice(piece.data());
|
||||
top += rows;
|
||||
}
|
||||
|
||||
// The group's own soft mask and blend mode are on its layer; what the contents set
|
||||
|
||||
Reference in new issue
Block a user