mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:36:13 -06:00
perf(render): undo PNG predictors a byte row at a time (#5430)
The vendored hayro-syntax ran PNG predictors through the bit-level BitChunks/BitWriter machinery TIFF predictors need, although PNG filters are defined on bytes (ISO 32000-2 §7.4.4.4, PNG specification §9). A 993 x 936 RGB image took about 63 ms to unfilter whatever its row filters; undone on byte slices it takes about 3 ms (Up) to 16 ms (Paeth) in a release build. It also fixes a row that ends part-way through a pixel (three 4-bit samples at an odd width): the bit-level path dropped its last byte, each later row started a byte early and the picture came out sheared. TIFF predictor 2 keeps the old path. Vendored hayro-syntax patch 29. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1d1cbc4ec9
commit
f84290b294
@@ -7004,6 +7004,9 @@ mod default_colour_spaces;
|
||||
// (vendored hayro-syntax patch 28, hayro-interpret patch 55).
|
||||
mod devicen_inks;
|
||||
|
||||
// #5430: PNG-predicted Flate images are undone byte by byte (vendored hayro-syntax patch 29).
|
||||
mod png_predicted_images;
|
||||
|
||||
// #3776: isolated DeviceCMYK groups blend in CMYK (vendored hayro-interpret patch 70, hayro
|
||||
// patch 27).
|
||||
mod isolated_cmyk_groups;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! #5430: Flate images with PNG predictors (ISO 32000-2 §7.4.4.4) are undone a byte row at a
|
||||
//! time (vendored hayro-syntax patch 29). The bit-level decoder they went through before dropped
|
||||
//! the last byte of each filtered row that ends part-way through a pixel, so every later row
|
||||
//! started a byte early and the picture came out sheared.
|
||||
|
||||
use super::rgb_at;
|
||||
use crate::{PageRenderer, RenderConfig, RenderRequest, RequestKind};
|
||||
use flate2::{Compression, write::ZlibEncoder};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The page is 150 × 200 pt, drawn at 72 dpi; the image fills it.
|
||||
const WIDTH: usize = 150;
|
||||
const HEIGHT: usize = 200;
|
||||
|
||||
/// The colour of each of the image's four rows, top first, as 4-bit samples.
|
||||
const ROWS: [[u8; 3]; 4] = [[15, 0, 0], [0, 15, 0], [0, 0, 15], [15, 15, 0]];
|
||||
|
||||
#[test]
|
||||
fn a_four_bit_rgb_image_of_odd_width_with_up_predicted_rows_is_not_sheared() {
|
||||
// Three 4-bit RGB pixels are 4.5 bytes, so each row is five bytes and ends half-way through a
|
||||
// two-byte pixel.
|
||||
let rows: Vec<Vec<u8>> = ROWS
|
||||
.iter()
|
||||
.map(|rgb| {
|
||||
let nibbles: Vec<u8> = (0..3).flat_map(|_| rgb.iter().copied()).chain([0]).collect();
|
||||
nibbles.chunks(2).map(|pair| (pair[0] << 4) | pair[1]).collect()
|
||||
})
|
||||
.collect();
|
||||
let mut predicted = Vec::new();
|
||||
let mut above = vec![0u8; 5];
|
||||
for row in &rows {
|
||||
predicted.push(2);
|
||||
predicted.extend(row.iter().zip(&above).map(|(b, a)| b.wrapping_sub(*a)));
|
||||
above.clone_from(row);
|
||||
}
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
|
||||
encoder.write_all(&predicted).unwrap();
|
||||
let data = encoder.finish().unwrap();
|
||||
|
||||
let content = "q 150 0 0 200 0 0 cm /Im0 Do Q";
|
||||
let mut pdf = 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 150 200] /Resources << /XObject << /Im0 5 0 R >> >> /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 3 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 4 \
|
||||
/Filter /FlateDecode /DecodeParms << /Predictor 15 /Colors 3 /BitsPerComponent 4 /Columns 3 >> /Length {} >> stream\n",
|
||||
content.len(),
|
||||
data.len()
|
||||
)
|
||||
.into_bytes();
|
||||
pdf.extend_from_slice(&data);
|
||||
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF");
|
||||
|
||||
let mut renderer = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false });
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (WIDTH as u32, HEIGHT as u32));
|
||||
|
||||
// Each image pixel is 50 pt square; look at the middle of each.
|
||||
for (row, rgb) in ROWS.iter().enumerate() {
|
||||
let expected = rgb.map(|sample| sample * 17);
|
||||
for column in 0..3 {
|
||||
let (x, y) = (column * 50 + 25, row * 50 + 25);
|
||||
assert_eq!(rgb_at(&page.rgba, WIDTH, x, y), expected, "image row {row}, column {column}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+235
-101
@@ -1008,15 +1008,16 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (bit_size, chunk_len) = if is_png_predictor {
|
||||
(
|
||||
8,
|
||||
// PdfCraft patch: in usize (the u8 product overflowed).
|
||||
(usize::from(params.colors) * usize::from(params.bits_per_component)).div_ceil(8),
|
||||
)
|
||||
} else {
|
||||
(params.bits_per_component, params.colors as usize)
|
||||
};
|
||||
if is_png_predictor {
|
||||
// PdfCraft patch: PNG predictors work on bytes whatever the bit depth, so they are
|
||||
// undone a row at a time on byte slices instead of through the bit-level readers.
|
||||
// In usize (the u8 product overflowed).
|
||||
let bpp = (usize::from(params.colors) * usize::from(params.bits_per_component))
|
||||
.div_ceil(8);
|
||||
return Some(unfilter_png(&data, row_len, bpp));
|
||||
}
|
||||
|
||||
let (bit_size, chunk_len) = (params.bits_per_component, params.colors as usize);
|
||||
let zero_row = vec![0; row_len];
|
||||
let mut prev_row = BitChunks::new(&zero_row, bit_size, chunk_len)?;
|
||||
let zero_col = BitChunk::new(0, chunk_len);
|
||||
@@ -1024,58 +1025,7 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
let mut writer = BitWriter::new(&mut out, bit_size)?;
|
||||
|
||||
for in_row in data.chunks_exact(total_row_len) {
|
||||
if is_png_predictor {
|
||||
let predictor = in_row[0];
|
||||
let in_data = &in_row[1..];
|
||||
let in_data_chunks = BitChunks::new(in_data, bit_size, chunk_len)?;
|
||||
|
||||
match predictor {
|
||||
1 => apply::<Sub>(
|
||||
prev_row,
|
||||
zero_col.clone(),
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
)?,
|
||||
2 => apply::<Up>(
|
||||
prev_row,
|
||||
zero_col.clone(),
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
)?,
|
||||
3 => apply::<Avg>(
|
||||
prev_row,
|
||||
zero_col.clone(),
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
)?,
|
||||
4 => apply::<Paeth>(
|
||||
prev_row,
|
||||
zero_col.clone(),
|
||||
zero_col.clone(),
|
||||
in_data_chunks,
|
||||
&mut writer,
|
||||
chunk_len,
|
||||
bit_size,
|
||||
)?,
|
||||
_ => {
|
||||
// Just copy the data.
|
||||
let mut reader = BitReader::new(in_data);
|
||||
|
||||
while let Some(data) = reader.read(bit_size) {
|
||||
writer.write(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if i == 2 {
|
||||
if i == 2 {
|
||||
apply::<Sub>(
|
||||
prev_row,
|
||||
zero_col.clone(),
|
||||
@@ -1104,6 +1054,98 @@ fn apply_predictor(data: Vec<u8>, params: &PredictorParams) -> Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch: undoes PNG row filters (ISO 32000-2 §7.4.4.4, PNG specification §9) in
|
||||
/// rows of `row_len` bytes, each after its filter-type byte, with `bpp` bytes per complete pixel
|
||||
/// (at least 1). An unknown filter type copies the row as it is. Bytes after the last complete
|
||||
/// row are dropped.
|
||||
fn unfilter_png(data: &[u8], row_len: usize, bpp: usize) -> Vec<u8> {
|
||||
let bpp = bpp.max(1);
|
||||
let in_row_len = row_len.saturating_add(1);
|
||||
let num_rows = data.len() / in_row_len;
|
||||
let mut out = vec![0; num_rows.saturating_mul(row_len)];
|
||||
let zero_row = vec![0; row_len];
|
||||
let mut prev: &[u8] = &zero_row;
|
||||
|
||||
for (cur, in_row) in out
|
||||
.chunks_exact_mut(row_len)
|
||||
.zip(data.chunks_exact(in_row_len))
|
||||
{
|
||||
let Some((&filter, src)) = in_row.split_first() else {
|
||||
break;
|
||||
};
|
||||
|
||||
match filter {
|
||||
1 => {
|
||||
let mut left: &[u8] = &zero_row;
|
||||
for (px, src) in cur.chunks_mut(bpp).zip(src.chunks(bpp)) {
|
||||
for ((c, s), l) in px.iter_mut().zip(src).zip(left) {
|
||||
*c = s.wrapping_add(*l);
|
||||
}
|
||||
left = px;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
for ((c, s), u) in cur.iter_mut().zip(src).zip(prev) {
|
||||
*c = s.wrapping_add(*u);
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
let mut left: &[u8] = &zero_row;
|
||||
for ((px, src), up) in cur
|
||||
.chunks_mut(bpp)
|
||||
.zip(src.chunks(bpp))
|
||||
.zip(prev.chunks(bpp))
|
||||
{
|
||||
for (((c, s), l), u) in px.iter_mut().zip(src).zip(left).zip(up) {
|
||||
// The sum is taken before halving, without wrapping.
|
||||
let avg = (u16::from(*l) + u16::from(*u)) / 2;
|
||||
*c = s.wrapping_add(avg as u8);
|
||||
}
|
||||
left = px;
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
let mut left: &[u8] = &zero_row;
|
||||
let mut up_left: &[u8] = &zero_row;
|
||||
for ((px, src), up) in cur
|
||||
.chunks_mut(bpp)
|
||||
.zip(src.chunks(bpp))
|
||||
.zip(prev.chunks(bpp))
|
||||
{
|
||||
for ((((c, s), l), u), ul) in
|
||||
px.iter_mut().zip(src).zip(left).zip(up).zip(up_left)
|
||||
{
|
||||
*c = s.wrapping_add(paeth_byte(*l, *u, *ul));
|
||||
}
|
||||
left = px;
|
||||
up_left = up;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for (c, s) in cur.iter_mut().zip(src) {
|
||||
*c = *s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev = cur;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// The Paeth predictor on bytes: the neighbour nearest `left + up - up_left`, preferring left,
|
||||
/// then up.
|
||||
fn paeth_byte(left: u8, up: u8, up_left: u8) -> u8 {
|
||||
let (a, b, c) = (i16::from(left), i16::from(up), i16::from(up_left));
|
||||
let pa = (b - c).abs();
|
||||
let pb = (a - c).abs();
|
||||
let pc = (a + b - c - c).abs();
|
||||
|
||||
let not_left = if pb <= pc { up } else { up_left };
|
||||
if pa <= pb && pa <= pc { left } else { not_left }
|
||||
}
|
||||
|
||||
fn apply<'a, T: Predictor>(
|
||||
prev_row: BitChunks<'a>,
|
||||
mut prev_col: BitChunk,
|
||||
@@ -1153,46 +1195,6 @@ impl Predictor for Sub {
|
||||
}
|
||||
}
|
||||
|
||||
struct Up;
|
||||
impl Predictor for Up {
|
||||
fn predict(cur_row: u16, prev_row: u16, _: u16, _: u16) -> u16 {
|
||||
cur_row.wrapping_add(prev_row)
|
||||
}
|
||||
}
|
||||
|
||||
struct Avg;
|
||||
impl Predictor for Avg {
|
||||
fn predict(cur_row: u16, prev_row: u16, prev_col: u16, _: u16) -> u16 {
|
||||
cur_row.wrapping_add(((prev_col as u32 + prev_row as u32) / 2) as u16)
|
||||
}
|
||||
}
|
||||
|
||||
struct Paeth;
|
||||
impl Predictor for Paeth {
|
||||
fn predict(cur_row: u16, prev_row: u16, prev_col: u16, top_left: u16) -> u16 {
|
||||
fn paeth(a: u16, b: u16, c: u16) -> u16 {
|
||||
let a = a as i32;
|
||||
let b = b as i32;
|
||||
let c = c as i32;
|
||||
|
||||
let p = a + b - c;
|
||||
let pa = (p - a).abs();
|
||||
let pb = (p - b).abs();
|
||||
let pc = (p - c).abs();
|
||||
|
||||
if pa <= pb && pa <= pc {
|
||||
a as u16
|
||||
} else if pb <= pc {
|
||||
b as u16
|
||||
} else {
|
||||
c as u16
|
||||
}
|
||||
}
|
||||
|
||||
cur_row.wrapping_add(paeth(prev_col, prev_row, top_left))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[rustfmt::skip]
|
||||
mod tests {
|
||||
@@ -1357,3 +1359,135 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch tests: PNG predictors are undone byte by byte (`unfilter_png`).
|
||||
#[cfg(test)]
|
||||
mod pdfcraft_png_predictor_tests {
|
||||
use crate::filter::lzw_flate::{PredictorParams, apply_predictor};
|
||||
|
||||
/// PNG unfiltering written out from the PNG specification (§9.2–9.4), one byte at a time.
|
||||
fn reference(data: &[u8], row_len: usize, bpp: usize) -> Vec<u8> {
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
for (r, row) in data.chunks_exact(row_len + 1).enumerate() {
|
||||
let start = r * row_len;
|
||||
for x in 0..row_len {
|
||||
let left = if x >= bpp { out[start + x - bpp] } else { 0 };
|
||||
let up = if r > 0 { out[start + x - row_len] } else { 0 };
|
||||
let up_left = if r > 0 && x >= bpp {
|
||||
out[start + x - row_len - bpp]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let predicted = match row[0] {
|
||||
1 => left,
|
||||
2 => up,
|
||||
3 => ((u16::from(left) + u16::from(up)) / 2) as u8,
|
||||
4 => {
|
||||
let p = i16::from(left) + i16::from(up) - i16::from(up_left);
|
||||
let (pa, pb, pc) = (
|
||||
(p - i16::from(left)).abs(),
|
||||
(p - i16::from(up)).abs(),
|
||||
(p - i16::from(up_left)).abs(),
|
||||
);
|
||||
if pa <= pb && pa <= pc {
|
||||
left
|
||||
} else if pb <= pc {
|
||||
up
|
||||
} else {
|
||||
up_left
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
out.push(row[1 + x].wrapping_add(predicted));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn png_predictors_match_the_specification_at_every_depth_and_pixel_size() {
|
||||
let mut seed: u32 = 0x2545_f491;
|
||||
let mut next = move || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed
|
||||
};
|
||||
for case in 0..400 {
|
||||
let colors = 1 + (next() % 4) as u8;
|
||||
let bits_per_component = [1u8, 2, 4, 8, 16][(next() % 5) as usize];
|
||||
let columns = 1 + (next() % 40) as usize;
|
||||
let bpp = (usize::from(colors) * usize::from(bits_per_component)).div_ceil(8);
|
||||
let row_len =
|
||||
(columns * usize::from(colors) * usize::from(bits_per_component)).div_ceil(8);
|
||||
let rows = 1 + (next() % 6) as usize;
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..rows {
|
||||
// Filter types 0–4, and 5–7, which are not PNG filters and copy the row.
|
||||
data.push((next() % 8) as u8);
|
||||
data.extend((0..row_len).map(|_| next() as u8));
|
||||
}
|
||||
// A partial row at the end is not decoded.
|
||||
let tail = (next() % 3) as usize;
|
||||
data.extend((0..tail.min(row_len)).map(|_| next() as u8));
|
||||
|
||||
let params = PredictorParams {
|
||||
predictor: 10 + (next() % 6) as u8,
|
||||
colors,
|
||||
bits_per_component,
|
||||
columns,
|
||||
early_change: false,
|
||||
};
|
||||
let got = apply_predictor(data.clone(), ¶ms).unwrap();
|
||||
assert_eq!(
|
||||
got,
|
||||
reference(&data, row_len, bpp),
|
||||
"case {case}: {colors} colours at {bits_per_component} bits, {columns} columns"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_ending_part_way_through_a_pixel_keeps_its_last_byte() {
|
||||
// Three 4-bit samples are a byte and a half, so a three-column row is five bytes and ends
|
||||
// half-way through a two-byte pixel. Each row is filtered Up.
|
||||
let data = [
|
||||
2, 10, 11, 12, 13, 14, //
|
||||
2, 20, 21, 22, 23, 24, //
|
||||
2, 30, 31, 32, 33, 34,
|
||||
];
|
||||
let params = PredictorParams {
|
||||
predictor: 15,
|
||||
colors: 3,
|
||||
bits_per_component: 4,
|
||||
columns: 3,
|
||||
early_change: false,
|
||||
};
|
||||
assert_eq!(
|
||||
apply_predictor(data.to_vec(), ¶ms).unwrap(),
|
||||
[10, 11, 12, 13, 14, 30, 32, 34, 36, 38, 60, 63, 66, 69, 72]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn png_average_and_paeth_carry_the_left_pixel_across_a_wide_row() {
|
||||
// Three 16-bit samples (6 bytes) per pixel: the left neighbour is six bytes back.
|
||||
let row_len = 6 * 3;
|
||||
let mut data = vec![3u8];
|
||||
data.extend((0..row_len).map(|i| (i * 37 + 200) as u8));
|
||||
data.push(4);
|
||||
data.extend((0..row_len).map(|i| (255 - i * 11) as u8));
|
||||
let params = PredictorParams {
|
||||
predictor: 15,
|
||||
colors: 3,
|
||||
bits_per_component: 16,
|
||||
columns: 3,
|
||||
early_change: false,
|
||||
};
|
||||
assert_eq!(
|
||||
apply_predictor(data.clone(), ¶ms).unwrap(),
|
||||
reference(&data, row_len, 6)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user