Add craft-stream-codecs: the codec modules of PdfCraft's stream filters (#953)

LZW, ASCII85, ASCIIHex, RunLength, the TIFF and PNG predictors, bounded CCITT
fax decoding (through hayro-ccitt) with Group 4 encoding (through fax), and
the lossless JBIG2 encoder with its MQ coder, from pdfcraft-filters at
PdfCraft revision 80e26e674d1aa33078e83c8139875047f3289eb6.

The crate sits below craft-pdf and craft-tiff: it knows no container. The
PDF filter chain, Flate and the bounded JBIG2 decoding entry are not part of
it.
This commit is contained in:
Dylan M. Taylor
2026-11-11 19:24:06 -05:00
committed by GitHub
parent 58f6c4b9e8
commit 16936716aa
15 changed files with 3846 additions and 2 deletions
Generated
+20 -2
View File
@@ -1588,6 +1588,18 @@ dependencies = [
"zip",
]
[[package]]
name = "craft-stream-codecs"
version = "0.1.0"
dependencies = [
"fax",
"hayro-ccitt",
"hayro-jbig2",
"proptest",
"thiserror 2.0.21",
"weezl 0.2.1",
]
[[package]]
name = "craft-svg"
version = "0.1.0"
@@ -2777,7 +2789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
"weezl 0.1.12",
]
[[package]]
@@ -6731,7 +6743,7 @@ dependencies = [
"flate2",
"half",
"quick-error 2.0.1",
"weezl",
"weezl 0.1.12",
"zune-jpeg",
]
@@ -7590,6 +7602,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "weezl"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2"
[[package]]
name = "wgpu"
version = "30.0.1"
+1
View File
@@ -91,6 +91,7 @@ craft-settings = { path = "crates/settings" }
craft-silhouette-studio = { path = "crates/silhouette-studio" }
craft-svg = { path = "crates/svg" }
craft-svg-paths = { path = "crates/svg-paths" }
craft-stream-codecs = { path = "crates/stream-codecs" }
craft-text = { path = "crates/text" }
craft-tga = { path = "crates/tga" }
craft-timeline = { path = "crates/timeline" }
+5
View File
@@ -143,6 +143,11 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
origin, to RGBA8, and a run-length writer whose packets end with their scan line. Bounded on
hostile input.
- [`craft-idml`](crates/idml/README.md): IDML (InDesign Markup Language) packages: the ZIP container read with bounded part sizes and lenient part names, `designmap.xml` with its includes in place, the `<Properties>` form on craft-xml elements, value codecs and the package writer; and InDesign Tagged Text, read and written.
- [`craft-stream-codecs`](crates/stream-codecs/README.md): byte-stream and bi-level image codecs that
PDF, PostScript and TIFF share: LZW, ASCII85, ASCIIHex, RunLength, TIFF and PNG predictors,
CCITT Group 3 and 4 decoding and Group 4 encoding, lossless JBIG2 encoding. Every
decoder is bounded and returns its partial output on damage. From PdfCraft; a PDF filter chain
is built on it in `craft-pdf`.
- [`craft-timeline`](crates/timeline/README.md): editorial timeline interchange: exact media time and SMPTE timecode, import/export reports, media path and `file://` URL handling, Avid Log Exchange (ALE) and CMX 3600 EDL documents, format detection. From FilmCraft.
- [`craft-metadata`](crates/metadata/README.md): photo metadata, from LightCraft: EXIF (read and write,
maker-note lens and focus point, every tag by name), IPTC-IIM, XMP packets (read, write, and a merge
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "craft-stream-codecs"
description = "Byte-stream and bi-level image codecs that PDF, PostScript and TIFF share: LZW, ASCII85, ASCIIHex, RunLength, TIFF and PNG predictors, CCITT Group 3 and 4 decoding and Group 4 encoding, lossless JBIG2 encoding"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# CCITT Group 4 encoding (ITU-T T.6). MIT.
fax = "0.2.7"
# CCITT fax decoding, the decoder the vendored PDF reader uses. MIT OR Apache-2.0.
hayro-ccitt = { version = "0.3", default-features = false }
thiserror = { workspace = true }
[dev-dependencies]
# The JBIG2 decoder the vendored PDF reader uses: reads back what the encoder writes.
hayro-jbig2 = { version = "=0.3.0", default-features = false, features = ["std"] }
proptest = { workspace = true }
weezl = "0.2"
+78
View File
@@ -0,0 +1,78 @@
# craft-stream-codecs
Byte-stream and bi-level image codecs that PDF, PostScript and TIFF share. The crate knows no
container: a caller hands it bytes and parameters. A PDF `/Filter` chain (stage order, limits
across stages, Flate, accounting) belongs to `craft-pdf`, which builds on this crate; a TIFF
reader can use the same LZW, predictor, CCITT and JBIG2 code.
- **Licence:** MIT OR Apache-2.0. Written from ISO 32000-2 §7.4, TIFF 6.0 §14 (predictor 2) and
RFC 2083 §6 (PNG filters). CCITT Group 4 encoding (ITU-T T.6) is the `fax` crate's (MIT). CCITT
decoding (T.4 and T.6) is `hayro-ccitt`'s (MIT OR Apache-2.0), bounded here: this crate has
no CCITT decoder of its own, and no JBIG2 decoder (that is `hayro-jbig2`, which the tests
read the encoder's output back with). JBIG2 encoding
is written from the public ISO/IEC 14492 Final Committee Draft (WG1 N1359, 1999); Annex H.2
checks the arithmetic coder byte for byte.
- No `unsafe`. Never panics on input.
- From PdfCraft's `pdfcraft-filters` crate (revision `80e26e674d1aa33078e83c8139875047f3289eb6`):
its codec modules, moved whole. Its filter chain and Flate stay with the PDF code.
## API
```rust
pub struct Params { predictor, colors, bits_per_component, columns, early_change } // i64s; Default = 1, 1, 8, 1, 1
pub enum CodecError { Corrupt { filter, detail }, LimitExceeded(usize), Unsupported(String), Spent }
pub struct Failure { error, partial, discarded } // what a decoder decoded before it failed
pub type Step = Result<Vec<u8>, Failure>;
pay(work: &mut u64, steps) -> Result<(), CodecError> // an allowance shared across a run of images
lzw::decode(data, early_change, max) -> Step; lzw::encode(data, early_change) -> Vec<u8>
ascii85::decode(data, max) -> Step; ascii85::encode(data) -> Vec<u8>
asciihex::decode(data, max) -> Step; asciihex::encode(data) -> Vec<u8>
runlength::decode(data, max) -> Step; runlength::encode(data) -> Vec<u8>
predictor::decode(&params, data, name, rows) -> Step; predictor::encode(&params, data) -> Result<Vec<u8>, CodecError>
pub struct CcittParams { k, columns, rows, end_of_line, encoded_byte_align, end_of_block, black_is_1 }
decode_ccitt_within(data, &params, max_rows, work: &mut u64) -> Result<Vec<u8>, CodecError>
encode_g4(data, columns, rows) -> Result<Vec<u8>, CodecError>
encode_jbig2_generic(data, columns, rows) -> Result<Vec<u8>, CodecError>
encode_jbig2(data, columns, rows) -> Result<Vec<u8>, CodecError>
encode_jbig2_shared(&[(data, columns, rows)]) -> Result<Option<SharedJbig2>, CodecError>
```
`max` bounds a decoder's output and gives `LimitExceeded`. `Corrupt` names the codec in `filter`
by its PDF filter name. Bi-level images are rows of 1-bit samples, most significant bit first,
each row on a new byte.
## Behaviour
| Codec | Decode | Encode |
|---|---|---|
| LZW | 9–12-bit MSB-first codes, clear (256), end (257), early change on or off; a missing end code or initial clear is tolerated; a full table stops growing | greedy, leading clear, clear when the table fills, end code |
| Predictors | 2 = TIFF (1/2/4/8/16 bits per component), 10–15 = PNG with per-row tags 0–4; partial final rows decoded; bad parameters or tags are `Corrupt`; with `rows`, data that is exactly that many rows without tag bytes is taken as the samples and reported as damage | 2, 10 None, 11 Sub, 12 Up, 13 Average, 14 Paeth, 15 per-row optimum |
| ASCIIHex | white space ignored, odd digit padded with 0, `>` ends (optional), other characters are `Corrupt` | upper case, newline every 64 digits, `>` |
| ASCII85 | `<~` prefix, `z`, white space, `~>` (or bare `~`, or none), final partial group; `z` inside a group, a 1-character final group, overflow or bad characters are `Corrupt` | `z` for zero groups, newline every 75 characters, `~>` |
| RunLength | 0–127 literal, 129–255 repeat, 128 ends (optional); a truncated run is `Corrupt` | runs of 2 or more repeat, literals up to 128, 128 |
| CCITT fax | Group 3 (1-D and 2-D) and Group 4 through `hayro-ccitt`, as rows of 1-bit samples, black 0 unless `black_is_1`, at most `max_rows` rows (fewer with `rows`), the rows before any damage, up to `MAX_CCITT_COLUMNS` (1,048,576) samples a row; the rows are paid for from `work` before they are decoded | `encode_g4`: Group 4 with an end-of-block mark, 0 bits coded black; up to `MAX_G4_COLUMNS` (32,767) samples a row |
| JBIG2 | not here: `hayro-jbig2` | `encode_jbig2_generic`: one immediate lossless generic region (template 0, typical prediction, MQ coder). `encode_jbig2`: the smaller of that and symbol coding (exact copies only, so the stream decodes to the same bits). `encode_jbig2_shared`: a group of images sharing one symbol dictionary (the PDF `/JBIG2Globals` data) |
The limits of the JBIG2 encoder (symbols per plan, instances per text region, marks, runs) are
those `hayro-jbig2` reads back; they are documented on the constants in `src/jbig2/symbols.rs`.
## Tests
- Unit tests per codec with hand-computed vectors (the ISO 32000-2 LZW example, PNG and TIFF rows).
- LZW is cross-checked against `weezl` (dev-dependency) in both directions for both early-change settings.
- `ccitt.rs`: decoding gives back the rows `encode_g4` was given; Group 3 layouts over seeded
pictures; Group 4 output decoded by `hayro-ccitt` matches the input bits, with property tests
over bitmaps of noise and of runs; any input returns rather than panics.
- `mq.rs`: the arithmetic coder reproduces Annex H.2's 30 bytes from its 256 decisions.
- `jbig2.rs`, `jbig2/symbols.rs`: output decoded by `hayro-jbig2` matches the input bits for
every coding, plan and strip size, with property tests; size and limit refusals.
## Not here
A bounded JBIG2 decoding entry (`decode_jbig2_within` in PdfCraft): it needs the caller-owned
work allowance PdfCraft added to its `hayro-jbig2` (`Image::decode_within`, `charge_reading`),
which the copy vendored here does not have yet. Flate (a container's choice of zlib settings and tolerances), DCT and JPEG 2000, CCITT Group 3
encoding.
+156
View File
@@ -0,0 +1,156 @@
//! ASCII85Decode (ISO 32000-2 §7.4.3): groups of five characters `!`..`u` encode four
//! bytes base-85, `z` stands for four zero bytes, `~>` is EOD, white space is ignored and
//! a final partial group of n characters (2–5) yields n − 1 bytes. A leading `<~` and a
//! missing EOD are tolerated.
use crate::{Failure, Step, is_pdf_whitespace};
const NAME: &str = "ASCII85Decode";
fn push(out: &mut Vec<u8>, bytes: &[u8], max: usize) -> Result<(), Failure> {
if out.len() + bytes.len() > max {
return Err(Failure::limit(max));
}
out.extend_from_slice(bytes);
Ok(())
}
pub fn decode(data: &[u8], max: usize) -> Step {
let start = data.iter().position(|&b| !is_pdf_whitespace(b)).unwrap_or(data.len());
let body = data[start..].strip_prefix(b"<~").unwrap_or(&data[start..]);
let mut out = Vec::with_capacity((body.len() / 5 * 4 + 4).min(max));
let mut acc: u64 = 0;
let mut n = 0usize;
for (i, &b) in body.iter().enumerate() {
match b {
b'!'..=b'u' => {
acc = acc * 85 + u64::from(b - b'!');
n += 1;
if n == 5 {
let Ok(v) = u32::try_from(acc) else {
return Err(Failure::corrupt(NAME, format!("group ending at {i} exceeds 2^32"), out));
};
push(&mut out, &v.to_be_bytes(), max)?;
acc = 0;
n = 0;
}
}
b'z' if n == 0 => push(&mut out, &[0; 4], max)?,
b'z' => return Err(Failure::corrupt(NAME, format!("'z' inside a group at {i}"), out)),
// `~` starts the EOD marker; tolerate it without the following `>`.
b'~' => break,
b if is_pdf_whitespace(b) => {}
b => return Err(Failure::corrupt(NAME, format!("invalid character 0x{b:02x} at {i}"), out)),
}
}
match n {
0 => {}
1 => return Err(Failure::corrupt(NAME, "final group has a single character", out)),
_ => {
// Pad with `u` (84) to a full group and keep n − 1 bytes.
for _ in n..5 {
acc = acc * 85 + 84;
}
let Ok(v) = u32::try_from(acc) else {
return Err(Failure::corrupt(NAME, "final group exceeds 2^32", out));
};
push(&mut out, &v.to_be_bytes()[..n - 1], max)?;
}
}
Ok(out)
}
/// Encodes with `z` for zero groups, a newline every 75 characters, and a `~>` EOD.
pub fn encode(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len() * 5 / 4 + data.len() / 60 + 8);
let mut col = 0;
let mut put = |out: &mut Vec<u8>, s: &[u8]| {
for &c in s {
if col == 75 {
out.push(b'\n');
col = 0;
}
out.push(c);
col += 1;
}
};
for chunk in data.chunks(4) {
let mut word = [0u8; 4];
word[..chunk.len()].copy_from_slice(chunk);
let mut v = u32::from_be_bytes(word);
if v == 0 && chunk.len() == 4 {
put(&mut out, b"z");
continue;
}
let mut digits = [0u8; 5];
for d in digits.iter_mut().rev() {
*d = (v % 85) as u8 + b'!';
v /= 85;
}
put(&mut out, &digits[..chunk.len() + 1]);
}
out.extend_from_slice(b"~>");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodecError;
fn dec(d: &[u8]) -> Result<Vec<u8>, CodecError> {
decode(d, 1000).map_err(|f| f.error)
}
#[test]
fn vectors() {
// "Man " = 0x4D616E20 = 24·85⁴ + 73·85³ + 80·85² + 78·85 + 61 -> "9jqo^".
assert_eq!(dec(b"9jqo^~>").unwrap(), b"Man ");
assert_eq!(encode(b"Man "), b"9jqo^~>");
// "M" padded to 0x4D000000 -> "9`" (24, 63); decoding pads with 'u'.
assert_eq!(encode(b"M"), b"9`~>");
assert_eq!(dec(b"9`~>").unwrap(), b"M");
assert_eq!(dec(b"z~>").unwrap(), [0; 4]);
assert_eq!(encode(&[0; 4]), b"z~>");
// A partial zero group is not `z`.
assert_eq!(encode(&[0; 3]), b"!!!!~>");
assert_eq!(dec(b"!!!!~>").unwrap(), [0; 3]);
assert_eq!(dec(b"s8W-!~>").unwrap(), [0xff; 4]);
}
#[test]
fn syntax_tolerance() {
assert_eq!(dec(b"<~9jqo^~>").unwrap(), b"Man ");
assert_eq!(dec(b" \n<~9j\r\nq o\t^z~>").unwrap(), b"Man \0\0\0\0");
assert_eq!(dec(b"9jqo^").unwrap(), b"Man "); // missing EOD
assert_eq!(dec(b"9jqo^~").unwrap(), b"Man "); // bare ~
assert_eq!(dec(b"9jqo^~>garbage{").unwrap(), b"Man ");
assert_eq!(dec(b"~>").unwrap(), b"");
assert_eq!(dec(b"").unwrap(), b"");
}
#[test]
fn corrupt() {
assert!(matches!(dec(b"9jz~>"), Err(CodecError::Corrupt { .. })));
assert!(matches!(dec(b"9jqo^9~>"), Err(CodecError::Corrupt { .. })));
assert!(matches!(dec(b"uuuuu~>"), Err(CodecError::Corrupt { .. }))); // > 2^32 - 1
assert!(matches!(dec(b"9jqo^v~>"), Err(CodecError::Corrupt { .. })));
let f = decode(b"9jqo^{", 100).err().unwrap();
assert_eq!(f.partial, b"Man ");
}
#[test]
fn limit() {
assert!(matches!(decode(b"zz", 7).map_err(|f| f.error), Err(CodecError::LimitExceeded(7))));
assert!(matches!(decode(b"z9`", 4).map_err(|f| f.error), Err(CodecError::LimitExceeded(4))));
assert_eq!(decode(b"zz", 8).ok().unwrap().len(), 8);
}
#[test]
fn line_wrapping() {
let data: Vec<u8> = (0..=255u8).collect();
let e = encode(&data);
assert!(e.split(|&b| b == b'\n').all(|l| l.len() <= 77));
assert_eq!(dec(&e).unwrap_or_default().len(), 256);
}
}
+106
View File
@@ -0,0 +1,106 @@
//! ASCIIHexDecode (ISO 32000-2 §7.4.2): pairs of hex digits, white space ignored,
//! `>` is EOD, an odd final digit is padded with 0. A missing EOD is tolerated.
use crate::{Failure, Step, is_pdf_whitespace};
const NAME: &str = "ASCIIHexDecode";
fn nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
pub fn decode(data: &[u8], max: usize) -> Step {
let mut out = Vec::with_capacity((data.len() / 2).min(max));
let mut hi: Option<u8> = None;
for (i, &b) in data.iter().enumerate() {
if is_pdf_whitespace(b) {
continue;
}
if b == b'>' {
break;
}
let Some(n) = nibble(b) else {
return Err(Failure::corrupt(NAME, format!("invalid character 0x{b:02x} at offset {i}"), out));
};
match hi.take() {
None => hi = Some(n),
Some(h) => {
if out.len() == max {
return Err(Failure::limit(max));
}
out.push(h << 4 | n);
}
}
}
if let Some(h) = hi {
if out.len() == max {
return Err(Failure::limit(max));
}
out.push(h << 4);
}
Ok(out)
}
/// Upper-case hex, a newline every 64 digits, terminated by `>`.
pub fn encode(data: &[u8]) -> Vec<u8> {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = Vec::with_capacity(data.len() * 2 + data.len() / 32 + 1);
for (i, &b) in data.iter().enumerate() {
if i > 0 && i % 32 == 0 {
out.push(b'\n');
}
out.push(HEX[usize::from(b >> 4)]);
out.push(HEX[usize::from(b & 15)]);
}
out.push(b'>');
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodecError;
fn dec(d: &[u8]) -> Result<Vec<u8>, CodecError> {
decode(d, 1000).map_err(|f| f.error)
}
#[test]
fn vectors() {
assert_eq!(dec(b"48656c6C6F>").unwrap(), b"Hello");
assert_eq!(dec(b" 4 8\n6\r5\t6\x0c c\x006c 6f >").unwrap(), b"Hello");
assert_eq!(dec(b"901FA>").unwrap(), [0x90, 0x1f, 0xa0]);
assert_eq!(dec(b"901FA").unwrap(), [0x90, 0x1f, 0xa0]); // missing EOD
assert_eq!(dec(b"41>zz garbage after EOD").unwrap(), b"A");
assert_eq!(dec(b">").unwrap(), b"");
assert_eq!(dec(b"").unwrap(), b"");
}
#[test]
fn invalid_char() {
let f = decode(b"4142G43>", 1000).err().unwrap();
assert!(matches!(f.error, CodecError::Corrupt { .. }));
assert_eq!(f.partial, b"AB");
}
#[test]
fn limit() {
assert!(matches!(decode(b"414243", 2).map_err(|f| f.error), Err(CodecError::LimitExceeded(2))));
assert!(matches!(decode(b"41424", 2).map_err(|f| f.error), Err(CodecError::LimitExceeded(2))));
assert_eq!(decode(b"4142", 2).ok().unwrap(), b"AB");
}
#[test]
fn encoding() {
assert_eq!(encode(b"Hello"), b"48656C6C6F>");
assert_eq!(encode(b""), b">");
let long = encode(&[0xab; 40]);
assert_eq!(long[64], b'\n');
assert_eq!(dec(&long).unwrap(), [0xab; 40]);
}
}
+546
View File
@@ -0,0 +1,546 @@
//! CCITT fax for `CCITTFaxDecode` (ISO 32000-2 §7.4.6): Group 4 (ITU-T T.6) encoding, with `/K -1`,
//! and decoding of Group 3 (T.4) and Group 4. The coding is the `fax` crate's and the decoding the
//! `hayro-ccitt` crate's; this module checks what they are given and bounds what they give.
use fax::encoder::Encoder;
use fax::{Color, VecWriter};
use crate::CodecError;
/// The widest row [`encode_g4`] encodes. The encoder counts the positions in a row in 16 bits
/// and adds two of them, so a row stays within half that range.
pub const MAX_G4_COLUMNS: u32 = 32_767;
/// `rows` rows of `columns` 1-bit samples, most significant bit first, each row starting on a new
/// byte (as a PDF image's samples are packed), encoded as CCITT Group 4 with an end-of-block mark.
///
/// A 0 bit is coded black and a 1 bit white, so the stream decodes to the same bits with
/// `/BlackIs1 false`, the default: an image keeps its colour space and decode array. Bytes after
/// the last row are ignored, as are the bits that pad a row to a whole byte.
///
/// Errors: no rows or columns, or rows wider than [`MAX_G4_COLUMNS`] ([`CodecError::Unsupported`]);
/// fewer bytes than the rows need ([`CodecError::Corrupt`]).
pub fn encode_g4(data: &[u8], columns: u32, rows: u32) -> Result<Vec<u8>, CodecError> {
let width = match u16::try_from(columns) {
Ok(w) if w > 0 && columns <= MAX_G4_COLUMNS => w,
_ => return Err(CodecError::Unsupported(format!("CCITT Group 4 encoding of rows {columns} samples wide"))),
};
if rows == 0 {
return Err(CodecError::Unsupported("CCITT Group 4 encoding of an image without rows".into()));
}
let stride = usize::from(width).div_ceil(8);
let samples = usize::try_from(rows).ok().and_then(|r| r.checked_mul(stride)).and_then(|n| data.get(..n)).ok_or_else(|| CodecError::Corrupt {
filter: "CCITTFaxDecode",
detail: format!("{} bytes for {rows} rows of {columns} samples", data.len()),
})?;
let mut encoder = Encoder::new(VecWriter::new());
for row in samples.chunks_exact(stride) {
let pels = (0..usize::from(width)).map(|x| {
let white = row.get(x / 8).is_some_and(|b| b & (0x80 >> (x % 8)) != 0);
if white { Color::White } else { Color::Black }
});
// `VecWriter` writes to memory: its error type has no values.
if let Err(never) = encoder.encode_line(pels, width) {
match never {}
}
}
match encoder.finish() {
Ok(writer) => Ok(writer.finish()),
Err(never) => match never {},
}
}
/// The widest row [`decode_ccitt_within`] decodes: its row buffer stays within 128 KiB.
pub const MAX_CCITT_COLUMNS: u32 = 1 << 20;
/// A `CCITTFaxDecode` stream's `/DecodeParms` (ISO 32000-2 Table 11). [`Default`] gives the
/// table's defaults.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CcittParams {
/// `/K`: negative for Group 4, 0 for Group 3 one-dimensional, positive for Group 3 mixed.
pub k: i64,
pub columns: i64,
/// `/Rows`: 0 when not given.
pub rows: i64,
pub end_of_line: bool,
pub encoded_byte_align: bool,
pub end_of_block: bool,
pub black_is_1: bool,
}
impl Default for CcittParams {
fn default() -> Self {
CcittParams { k: 0, columns: 1728, rows: 0, end_of_line: false, encoded_byte_align: false, end_of_block: true, black_is_1: false }
}
}
/// What [`decode_ccitt_within`] decodes once it is paid for.
fn decode_ccitt(data: &[u8], p: &CcittParams, max_rows: u32) -> Result<Vec<u8>, CodecError> {
let columns = u32::try_from(p.columns)
.ok()
.filter(|c| (1..=MAX_CCITT_COLUMNS).contains(c))
.ok_or_else(|| CodecError::Unsupported(format!("CCITT fax rows {} samples wide", p.columns)))?;
let rows = u32::try_from(p.rows).ok().filter(|r| *r > 0).map_or(max_rows, |r| r.min(max_rows));
if rows == 0 {
return Ok(Vec::new());
}
let settings = hayro_ccitt::DecodeSettings {
columns,
rows,
end_of_block: p.end_of_block,
end_of_line: p.end_of_line,
// Group 3 rows on bytes may have end-of-line codes whatever `end_of_line` says (the filter
// always accepts them), with the fill starting each code on a byte or ending it on one (T.4's
// fill, as TIFF writes it). Read unaligned, the codes mark the rows either way; without
// codes, the rows are read on bytes. A reading that comes up short isn't tried the other
// way: data cut short would then read as other rows.
rows_are_byte_aligned: p.encoded_byte_align && (p.k < 0 || !starts_with_end_of_line(data)),
encoding: match p.k {
..0 => hayro_ccitt::EncodingMode::Group4,
0 => hayro_ccitt::EncodingMode::Group3_1D,
k => hayro_ccitt::EncodingMode::Group3_2D { k: u32::try_from(k).unwrap_or(u32::MAX) },
},
invert_black: false,
};
// u32 to usize: wasm32 and up have at least 32-bit usize.
let mut out = Rows::new(columns as usize, rows as usize, p.black_is_1);
// The rows decoded before damaged data are kept.
let _ = hayro_ccitt::decode(data, &mut out, &mut hayro_ccitt::DecoderContext::new(settings));
Ok(out.out)
}
/// The steps of reading a byte of fax code ([`decode_ccitt_within`]): noise takes 114 ns a byte of
/// rows and about two bytes of code to a byte of rows, so that and a step a byte of rows is about
/// what a step of JBIG2 decoding takes.
const CODE_WORK: u64 = 4;
/// CCITT fax `data` decoded as `CCITTFaxDecode` gives it: rows of `columns` 1-bit samples, most
/// significant bit first, each starting on a new byte, with black as 0 (as 1 with `black_is_1`).
///
/// At most `max_rows` rows are decoded (fewer when `rows` says so), so the output is bounded by
/// the caller. Damaged data gives the rows decoded before the damage, as viewers show them: a
/// caller that needs a number of rows checks it got them.
///
/// Group 3 rows on bytes (`k` from 0, `encoded_byte_align`) are read by their end-of-line codes,
/// whether the fill starts each code on a byte or ends it on one, when `data` starts with one (T.4
/// sends one before the first row when it sends them at all), and on bytes otherwise, whatever
/// `end_of_line` says. Group 4 is read as its parameters say. Data with codes but none before the
/// first row is read on bytes, as hayro's renderer draws it; with `k` 0 and the fill ending each
/// code on a byte, that may be other rows than were coded.
///
/// Errors: `columns` that aren't from 1 to [`MAX_CCITT_COLUMNS`] ([`CodecError::Unsupported`]).
///
/// Decoding spends from `work`, what decoding may still cost across a run of images, and there is
/// no way in without it: a
/// byte of rows a step, whether or not the data holds them, and [`CODE_WORK`] a byte of `data`.
/// A blank row takes a bit of code, so the rows are what a few bytes can ask for. It is all paid
/// before anything is decoded, and what `work` can't pay for spends all of it.
pub fn decode_ccitt_within(data: &[u8], p: &CcittParams, max_rows: u32, work: &mut u64) -> Result<Vec<u8>, CodecError> {
let rows = u32::try_from(p.rows).ok().filter(|r| *r > 0).map_or(max_rows, |r| r.min(max_rows));
let stride = u64::try_from(p.columns).unwrap_or(0).div_ceil(8);
crate::pay(work, stride.saturating_mul(u64::from(rows)).saturating_add((data.len() as u64).saturating_mul(CODE_WORK)))?;
decode_ccitt(data, p, max_rows)
}
/// Whether CCITT fax `data` starts with an end-of-line code: a 1 after 11 zeros or more, fill
/// included. T.4 sends one before the first row when it sends them at all, and no other code starts
/// with more than eight zeros.
fn starts_with_end_of_line(data: &[u8]) -> bool {
let Some((i, b)) = data.iter().enumerate().find(|(_, b)| **b != 0) else { return false };
i.saturating_mul(8).saturating_add(b.leading_zeros() as usize) >= 11
}
/// Rows of 1-bit samples filled from a decoder's pixels: `columns` a row, each row on a new byte, at
/// most `rows` rows. Pixels past a row's end and rows past the last are dropped.
pub(crate) struct Rows {
pub(crate) out: Vec<u8>,
row: Vec<u8>,
x: usize,
columns: usize,
rows: usize,
done: usize,
black_is_1: bool,
}
impl Rows {
pub(crate) fn new(columns: usize, rows: usize, black_is_1: bool) -> Self {
Rows { out: Vec::new(), row: vec![0; columns.div_ceil(8)], x: 0, columns, rows, done: 0, black_is_1 }
}
/// `count` pixels, white or black.
pub(crate) fn push(&mut self, white: bool, count: usize) {
let end = self.x.saturating_add(count);
if white != self.black_is_1 {
for x in self.x..end.min(self.columns) {
if let Some(b) = self.row.get_mut(x / 8) {
*b |= 0x80 >> (x % 8);
}
}
}
self.x = end;
}
/// The row filled so far kept, if fewer than `rows` are, and the next one begun.
pub(crate) fn end_row(&mut self) {
if self.done < self.rows {
self.out.extend_from_slice(&self.row);
self.done += 1;
}
self.row.fill(0);
self.x = 0;
}
}
impl hayro_ccitt::Decoder for Rows {
fn push_pixel(&mut self, white: bool) {
self.push(white, 1);
}
fn push_pixel_chunk(&mut self, white: bool, chunks: u32) {
self.push(white, (chunks as usize).saturating_mul(8));
}
fn next_line(&mut self) {
self.end_row();
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A run pays for the rows an image asks for and the code it reads, before any of it is
/// decoded: rows the data doesn't hold are paid for too, as a few bytes can ask for any number.
#[test]
fn a_run_of_images_pays_for_the_rows_it_asks_for() {
let (w, h) = (4096u32, 64u32);
let stride = (w as usize).div_ceil(8);
let data = encode_g4(&vec![0xFF; stride * h as usize], w, h).unwrap();
let p = CcittParams { k: -1, columns: i64::from(w), ..CcittParams::default() };
let whole = decode_ccitt(&data, &p, h).unwrap();
let cost = (whole.len() + 4 * data.len()) as u64;
let mut work = cost;
assert_eq!(decode_ccitt_within(&data, &p, h, &mut work).unwrap(), whole, "exactly enough");
assert_eq!(work, 0);
let mut work = cost + 7;
decode_ccitt_within(&data, &p, h, &mut work).unwrap();
assert_eq!(work, 7, "only what it took");
let mut work = cost - 1;
let refused = decode_ccitt_within(&data, &p, h, &mut work).unwrap_err();
assert!(matches!(refused, CodecError::Spent), "a step short: {refused}");
assert_eq!(work, 0, "spends all that was left");
// Nothing left: not read at all, so bad columns aren't reported as bad columns.
let bad = CcittParams { columns: 0, ..p };
assert!(decode_ccitt_within(&data, &bad, h, &mut 0).is_err_and(|e| matches!(e, CodecError::Spent)));
// No code at all, for the same rows.
let mut work = u64::MAX;
decode_ccitt_within(&[], &p, h, &mut work).unwrap();
assert_eq!(u64::MAX - work, whole.len() as u64);
// `/Rows` below what is asked for, and no rows.
let mut work = u64::MAX;
decode_ccitt_within(&data, &CcittParams { rows: 3, ..p }, h, &mut work).unwrap();
assert_eq!(u64::MAX - work, (3 * stride + 4 * data.len()) as u64);
let mut work = u64::MAX;
decode_ccitt_within(&data, &p, 0, &mut work).unwrap();
assert_eq!(u64::MAX - work, 4 * data.len() as u64);
}
/// The rows decoded by an independent decoder (`hayro-ccitt`), as 1 for white and 0 for black.
pub(crate) fn decode(data: &[u8], columns: u32, rows: u32) -> Vec<Vec<bool>> {
struct Rows(Vec<Vec<bool>>, Vec<bool>, usize);
impl hayro_ccitt::Decoder for Rows {
fn push_pixel(&mut self, white: bool) {
self.1.push(white);
}
fn push_pixel_chunk(&mut self, white: bool, chunks: u32) {
self.1.extend(std::iter::repeat_n(white, chunks as usize * 8));
}
fn next_line(&mut self) {
let mut row = std::mem::take(&mut self.1);
row.truncate(self.2);
self.0.push(row);
}
}
let settings = hayro_ccitt::DecodeSettings {
columns,
rows,
end_of_block: true,
end_of_line: false,
rows_are_byte_aligned: false,
encoding: hayro_ccitt::EncodingMode::Group4,
invert_black: false,
};
let mut out = Rows(Vec::new(), Vec::new(), columns as usize);
hayro_ccitt::decode(data, &mut out, &mut hayro_ccitt::DecoderContext::new(settings)).unwrap();
out.0
}
/// The rows of packed samples `data`, as 1 for a 1 bit.
pub(crate) fn unpack(data: &[u8], columns: u32, rows: u32) -> Vec<Vec<bool>> {
let stride = (columns as usize).div_ceil(8);
(0..rows as usize).map(|y| (0..columns as usize).map(|x| data[y * stride + x / 8] & (0x80 >> (x % 8)) != 0).collect()).collect()
}
#[test]
fn a_page_of_text_like_marks_round_trips_and_shrinks() {
let (w, h) = (1700u32, 220u32);
let stride = (w as usize).div_ceil(8);
let mut data = vec![0xFF; stride * h as usize];
// Short black strokes in lines, like glyphs on a white page.
for y in (20..h as usize).step_by(40) {
for dy in 0..12 {
for x in (100..1600).step_by(9) {
if (x / 9 + dy) % 5 != 0 {
data[(y + dy) * stride + x / 8] &= !(0x80 >> (x % 8));
}
}
}
}
let g4 = encode_g4(&data, w, h).unwrap();
assert_eq!(decode(&g4, w, h), unpack(&data, w, h));
assert!(g4.len() * 4 < data.len(), "{} bytes from {}", g4.len(), data.len());
}
#[test]
fn padding_bits_and_trailing_bytes_are_ignored() {
// 3 columns: the last 5 bits of each byte pad the row; set them to anything.
let data = [0b1010_1111, 0b0101_0000, 0b1110_1010, 0xAB, 0xCD];
let g4 = encode_g4(&data, 3, 3).unwrap();
assert_eq!(decode(&g4, 3, 3), [[true, false, true], [false, true, false], [true, true, true]]);
}
#[test]
fn what_it_cannot_encode_is_an_error() {
assert!(matches!(encode_g4(&[0; 8], 0, 1), Err(CodecError::Unsupported(_))));
assert!(matches!(encode_g4(&[0; 8], 8, 0), Err(CodecError::Unsupported(_))));
assert!(matches!(encode_g4(&vec![0; 4097], MAX_G4_COLUMNS + 1, 1), Err(CodecError::Unsupported(_))));
assert!(matches!(encode_g4(&[0; 3], 16, 2), Err(CodecError::Corrupt { .. })), "two rows of 16 need 4 bytes");
assert!(matches!(encode_g4(&[], 1, u32::MAX), Err(CodecError::Corrupt { .. })));
let widest = vec![0x55; (MAX_G4_COLUMNS as usize).div_ceil(8) * 2];
let g4 = encode_g4(&widest, MAX_G4_COLUMNS, 2).unwrap();
assert_eq!(decode(&g4, MAX_G4_COLUMNS, 2), unpack(&widest, MAX_G4_COLUMNS, 2));
}
/// Rows of `columns` samples built from `runs`: alternating white and black runs one longer
/// than those values (so never empty, even when shrinking makes them all 0), repeated until the
/// rows are full; or, with `noise`, the bytes of `runs` as they are.
fn bitmap(columns: u32, rows: u32, runs: &[u8], noise: bool) -> Vec<u8> {
let stride = (columns as usize).div_ceil(8);
let mut data = vec![0u8; stride * rows as usize];
if runs.is_empty() {
return data;
}
if noise {
data.iter_mut().zip(runs.iter().cycle()).for_each(|(d, r)| *d = *r);
return data;
}
let (mut white, mut left, mut next) = (true, 1 + usize::from(runs[0]), 1);
for y in 0..rows as usize {
for x in 0..columns as usize {
if left == 0 {
white = !white;
left = 1 + usize::from(runs[next % runs.len()]);
next += 1;
}
if white {
data[y * stride + x / 8] |= 0x80 >> (x % 8);
}
left -= 1;
}
}
data
}
fn g4(columns: u32) -> CcittParams {
CcittParams { k: -1, columns: i64::from(columns), ..CcittParams::default() }
}
#[test]
fn decoding_gives_the_rows_back() {
// 3 columns, padded with anything: the decoded padding is 0.
let data = [0b1010_1111, 0b0101_0000, 0b1110_1010];
let coded = encode_g4(&data, 3, 3).unwrap();
assert_eq!(decode_ccitt(&coded, &g4(3), 3).unwrap(), [0b1010_0000, 0b0100_0000, 0b1110_0000]);
// /BlackIs1 gives black as 1.
let p = CcittParams { black_is_1: true, ..g4(3) };
assert_eq!(decode_ccitt(&coded, &p, 3).unwrap(), [0b0100_0000, 0b1010_0000, 0b0000_0000]);
// At most /Rows rows, and at most max_rows.
assert_eq!(decode_ccitt(&coded, &CcittParams { rows: 2, ..g4(3) }, 3).unwrap().len(), 2);
assert_eq!(decode_ccitt(&coded, &g4(3), 1).unwrap().len(), 1);
assert_eq!(decode_ccitt(&coded, &CcittParams { rows: 3, ..g4(3) }, 1).unwrap().len(), 1, "max_rows bounds /Rows");
assert_eq!(decode_ccitt(&coded, &g4(3), 0).unwrap(), [0u8; 0]);
// Damaged data gives the rows before the damage; none here.
assert!(decode_ccitt(&coded[..1], &g4(3), 3).unwrap().len() < 3);
}
#[test]
fn what_it_cannot_decode_is_an_error() {
for columns in [0, -1, i64::from(MAX_CCITT_COLUMNS) + 1, i64::MAX] {
let p = CcittParams { columns, ..CcittParams::default() };
assert!(matches!(decode_ccitt(&[0; 8], &p, 4), Err(CodecError::Unsupported(_))), "{columns}");
}
assert_eq!(CcittParams::default().columns, 1728, "Table 11");
}
/// Group 3 data: rows of run lengths below 64, white first, coded one-dimensionally with T.4's
/// terminating codes (ITU-T T.4 Tables 2 and 3). Each row starts with an end-of-line code with
/// `eol`, then with `tag` a 1 bit (the next row one-dimensional, as `k` above 0 has it). Fill
/// bits (0s) go where `fill` says.
fn group_3(rows: &[Vec<u32>], eol: bool, tag: bool, fill: Fill) -> Vec<u8> {
// The codes for runs 0 to 63, in order.
const WHITE: &str = concat!(
"00110101 000111 0111 1000 1011 1100 1110 1111 10011 10100 00111 01000 001000 000011 110100 110101 101010 101011 ",
"0100111 0001100 0001000 0010111 0000011 0000100 0101000 0101011 0010011 0100100 0011000 00000010 00000011 00011010 ",
"00011011 00010010 00010011 00010100 00010101 00010110 00010111 00101000 00101001 00101010 00101011 00101100 00101101 ",
"00000100 00000101 00001010 00001011 01010010 01010011 01010100 01010101 00100100 00100101 01011000 01011001 01011010 ",
"01011011 01001010 01001011 00110010 00110011 00110100",
);
const BLACK: &str = concat!(
"0000110111 010 11 10 011 0011 0010 00011 000101 000100 0000100 0000101 0000111 00000100 00000111 000011000 0000010111 ",
"0000011000 0000001000 00001100111 00001101000 00001101100 00000110111 00000101000 00000010111 00000011000 ",
"000011001010 000011001011 000011001100 000011001101 000001101000 000001101001 000001101010 000001101011 000011010010 ",
"000011010011 000011010100 000011010101 000011010110 000011010111 000001101100 000001101101 000011011010 000011011011 ",
"000001010100 000001010101 000001010110 000001010111 000001100100 000001100101 000001010010 000001010011 000000100100 ",
"000000110111 000000111000 000000100111 000000101000 000001011000 000001011001 000000101011 000000101100 000001011010 ",
"000001100110 000001100111",
);
let code = |white: bool, run: u32| (if white { WHITE } else { BLACK }).split(' ').nth(run as usize).expect("a run below 64");
let mut bits = String::new();
for row in rows {
// The end-of-line code is 12 bits.
let ends_code = if fill == Fill::CodeEnd && eol { 12 } else { 0 };
while fill != Fill::None && !(bits.len() + ends_code).is_multiple_of(8) {
bits.push('0');
}
if eol {
bits.push_str("000000000001");
}
if tag {
bits.push('1');
}
row.iter().enumerate().for_each(|(i, run)| bits.push_str(code(i % 2 == 0, *run)));
}
bits.as_bytes().chunks(8).map(|c| c.iter().enumerate().fold(0u8, |b, (i, d)| b | (u8::from(*d == b'1') << (7 - i)))).collect()
}
/// Where [`group_3`] puts fill bits: none; so each row, its end-of-line code first, starts on a
/// byte; or so each end-of-line code ends on one (T.4's fill, as TIFF writes it).
#[derive(Clone, Copy, Debug, PartialEq)]
enum Fill {
None,
RowStart,
CodeEnd,
}
/// `rows` text-like rows of `columns` samples as runs, white first, from `seed`: long white runs
/// and short black ones; a row may start black.
fn text_like(seed: u64, columns: u32, rows: u32) -> Vec<Vec<u32>> {
let mut seed = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
let mut random = |n: u64| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % n) as u32
};
(0..rows)
.map(|_| {
let (mut runs, mut left) = (Vec::new(), columns);
while left > 0 {
let run = match runs.len() % 2 {
0 if runs.is_empty() => random(31),
0 => 1 + random(30),
_ => 1 + random(5),
};
runs.push(run.min(left));
left -= run.min(left);
}
runs
})
.collect()
}
/// Rows of runs, white first, packed as `decode_ccitt` gives them (white 1).
fn pack(runs: &[Vec<u32>], columns: u32) -> Vec<u8> {
let stride = (columns as usize).div_ceil(8);
let mut out = vec![0u8; stride * runs.len()];
for (y, row) in runs.iter().enumerate() {
let mut x = 0;
for (i, run) in row.iter().enumerate() {
if i % 2 == 0 {
(x..x + *run as usize).for_each(|x| out[y * stride + x / 8] |= 0x80 >> (x % 8));
}
x += *run as usize;
}
}
out
}
/// Group 3 rows on bytes in each layout, over many pictures, as how a misreading comes out
/// depends on the picture: no codes (as TIFF's Modified Huffman has it), and `end_of_line`
/// without them; codes, each starting on a byte or ending on one, `end_of_line` or not. Each
/// decodes to its picture, and a byte short to fewer rows, not to other rows read another way.
/// Group 4 rows on bytes keep their aligned read, `end_of_line` or not.
#[test]
fn group_3_rows_on_bytes_read_by_the_codes_in_the_data() {
for seed in 0..32u64 {
let (columns, rows) = (8 + (seed as u32 * 37) % 200, 2 + seed as u32 % 20);
let runs = text_like(seed, columns, rows);
let picture = pack(&runs, columns);
for k in [0, 1] {
for (codes, fill, end_of_line) in [
(false, Fill::RowStart, false),
(false, Fill::RowStart, true),
(true, Fill::RowStart, false),
(true, Fill::RowStart, true),
(true, Fill::CodeEnd, false),
(true, Fill::CodeEnd, true),
] {
let p = CcittParams { k, columns: i64::from(columns), end_of_line, encoded_byte_align: true, ..CcittParams::default() };
let mut data = group_3(&runs, codes, k > 0, fill);
let case = format!("seed {seed}, {columns} × {rows}, k {k}, codes {codes}, fill {fill:?}, end_of_line {end_of_line}");
assert_eq!(decode_ccitt(&data, &p, rows).unwrap(), picture, "{case}");
data.pop();
assert!(decode_ccitt(&data, &p, rows).unwrap().len() < picture.len(), "{case}, a byte short");
}
}
}
// White rows, each the one code 1 (vertical, no change) and its fill.
for end_of_line in [false, true] {
let p = CcittParams { k: -1, columns: 64, end_of_line, encoded_byte_align: true, ..CcittParams::default() };
assert_eq!(decode_ccitt(&[0x80; 16], &p, 16).unwrap(), [0xFF; 8 * 16], "Group 4, end_of_line {end_of_line}");
}
}
proptest::proptest! {
/// Any bitmap, of noise or of runs, decodes to its own bits, through an independent decoder
/// and through `decode_ccitt`.
#[test]
fn bitmaps_round_trip(
columns in 1u32..400,
rows in 1u32..24,
runs in proptest::collection::vec(0u8..=255, 1..48),
noise in proptest::bool::ANY,
) {
let data = bitmap(columns, rows, &runs, noise);
let coded = encode_g4(&data, columns, rows).unwrap();
proptest::prop_assert_eq!(decode(&coded, columns, rows), unpack(&data, columns, rows));
let decoded = decode_ccitt(&coded, &g4(columns), rows).unwrap();
proptest::prop_assert_eq!(unpack(&decoded, columns, rows), unpack(&data, columns, rows));
}
/// Whatever it is given, it returns.
#[test]
fn any_input_returns(
data in proptest::collection::vec(0u8..=255, 0..600),
columns in 0u32..40_000,
rows in 0u32..80,
k in -2i64..3,
end_of_line in proptest::bool::ANY,
encoded_byte_align in proptest::bool::ANY,
) {
let _ = encode_g4(&data, columns, rows);
let p = CcittParams { k, columns: i64::from(columns), end_of_line, encoded_byte_align, ..CcittParams::default() };
if let Ok(out) = decode_ccitt(&data, &p, rows) {
proptest::prop_assert!(out.len() <= (columns as usize).div_ceil(8) * rows as usize);
}
}
}
}
+468
View File
@@ -0,0 +1,468 @@
//! JBIG2 encoding (ITU-T T.88 | ISO/IEC 14492), for `JBIG2Decode` (ISO 32000-2 §7.4.7), lossless:
//! a generic region, template 0 with its nominal adaptive pixels, typical prediction on, MQ
//! arithmetic coding ([`crate::mq`]); or the image's marks as symbols ([`symbols`]), which a group
//! of images may share through `/JBIG2Globals`. Written from the standard's 6.2.5 (coding a generic
//! region), 7.2 (segment headers), 7.4.1, 7.4.6 and 7.4.8 (the segments) and D.3 (the embedded
//! organisation PDF uses). Decoding is in [`decode`].
mod symbols;
/// The largest image the encoders take, in pixels: the largest JBIG2 page `hayro-jbig2` decodes
/// (it decodes a page whole, and takes 20,000 × 10,000), so what they store reads back.
pub const MAX_JBIG2_PIXELS: u64 = 1 << 28;
use crate::CodecError;
use crate::mq::{Context, MqEncoder};
/// The widest row, and the most rows, [`encode_jbig2_generic`] encodes: what `hayro-jbig2`, the
/// decoder behind `JBIG2Decode` here, takes on a side. JBIG2 allows 32-bit sizes, but past this
/// nothing here could read the stream, and the optimizer, which doesn't read back what it stores,
/// replaced such an image with one that draws nothing.
pub const MAX_JBIG2_COLUMNS: u32 = 65_535;
/// Template 0's pixels (Figure 3), as (x, y) offsets from the pixel being coded, each a bit of the
/// context in this order: the four on the current row, the five fixed ones on the row above, the
/// three on the row above that, then the adaptive pixels A1–A4 at their nominal places (Table 5).
const TEMPLATE: [(i32, i32); 16] = [
(-1, 0),
(-2, 0),
(-3, 0),
(-4, 0),
(-2, -1),
(-1, -1),
(0, -1),
(1, -1),
(2, -1),
(-1, -2),
(0, -2),
(1, -2),
(3, -1),
(-3, -1),
(2, -2),
(-2, -2),
];
/// The context that codes SLTP when template 0 is used (Figure 8): those pixel values, in
/// [`TEMPLATE`]'s order. Figure 8 reads, by row from x = -4: `0 1 0 1` on the current row,
/// `0 1 1 0 0 1 0` (x = -3 to 3) above it, `1 0 0 1 1` (x = -2 to 2) above that.
const SLTP: [u8; 16] = [1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1];
/// Columns of padding either side of a row, so every template pixel of a row's pixels is in it.
const PAD: usize = 4;
/// `rows` rows of `columns` 1-bit PDF samples as lossless JBIG2, the smaller of two codings:
/// [`encode_jbig2_generic`]'s, or one that stores each distinct mark of the image once, as a
/// symbol, and places its copies (see [`symbols`]). Either way the stream is in the embedded
/// organisation and decodes to the same bits without `/JBIG2Globals`.
///
/// Errors: as [`encode_jbig2_generic`].
pub fn encode_jbig2(data: &[u8], columns: u32, rows: u32) -> Result<Vec<u8>, CodecError> {
let generic = encode_jbig2_generic(data, columns, rows)?;
let samples = rows_to_encode(data, columns, rows).ok();
Ok(match samples.and_then(|s| symbols::encode(s, columns, rows)) {
Some(coded) if coded.len() < generic.len() => coded,
_ => generic,
})
}
/// `rows` rows of `columns` 1-bit PDF samples (most significant bit first, each row starting on
/// a new byte), as a JBIG2 stream in the embedded organisation: a page information segment and an
/// immediate lossless generic region segment, which `JBIG2Decode` decodes to the same bits
/// without `/JBIG2Globals`.
///
/// A 0 sample is coded as a JBIG2 1 (black), since `JBIG2Decode` gives black as 0, as a grey image's
/// samples have it. Bytes after the last row are ignored, as are the bits that pad a row to a whole
/// byte.
///
/// Errors: no rows or columns, more of either than [`MAX_JBIG2_COLUMNS`], or more samples than
/// [`MAX_JBIG2_PIXELS`], what the decoder takes ([`CodecError::Unsupported`]); fewer bytes than the
/// rows need ([`CodecError::Corrupt`]).
pub fn encode_jbig2_generic(data: &[u8], columns: u32, rows: u32) -> Result<Vec<u8>, CodecError> {
let samples = rows_to_encode(data, columns, rows)?;
let width = columns as usize;
let stride = width.div_ceil(8);
let mut contexts: Vec<Context> = vec![0; 1 << 16];
let mut mq = MqEncoder::new();
let height = samples.len() / stride;
code_generic(&mut mq, &mut contexts, width, height, true, |y, row| {
let packed = samples.get(y * stride..).unwrap_or_default();
for (x, pixel) in row.iter_mut().enumerate() {
let sample = packed.get(x / 8).is_some_and(|b| b & (0x80 >> (x % 8)) != 0);
*pixel = u8::from(!sample);
}
});
let region = mq.finish();
let mut out = Vec::with_capacity(region.len() + 64);
segment(&mut out, 0, 48, &[], false, &page_information(columns, rows));
segment(&mut out, 1, 39, &[], false, &generic_region(columns, rows, 0, 0, &region));
Ok(out)
}
/// The samples of `rows` rows of `columns` 1-bit samples in `data`, when the encoders here take
/// that size; the errors are [`encode_jbig2_generic`]'s.
fn rows_to_encode(data: &[u8], columns: u32, rows: u32) -> Result<&[u8], CodecError> {
let too_large = columns > MAX_JBIG2_COLUMNS || rows > MAX_JBIG2_COLUMNS || u64::from(columns) * u64::from(rows) > MAX_JBIG2_PIXELS;
if columns == 0 || rows == 0 || too_large {
return Err(CodecError::Unsupported(format!("JBIG2 encoding of {columns} × {rows} samples")));
}
let stride = (columns as usize).div_ceil(8);
usize::try_from(rows)
.ok()
.and_then(|r| r.checked_mul(stride))
.and_then(|n| data.get(..n))
.ok_or_else(|| CodecError::Corrupt { filter: "JBIG2Decode", detail: format!("{} bytes for {rows} rows of {columns} samples", data.len()) })
}
/// A group of images coded by [`encode_jbig2_shared`]: the data of the `/JBIG2Globals` stream
/// they share, and each image's own stream, in the order given, or `None` for one that shares
/// nothing with the others.
#[derive(Debug)]
pub struct SharedJbig2 {
pub globals: Vec<u8>,
pub images: Vec<Option<Vec<u8>>>,
}
/// Images, each `rows` rows of `columns` 1-bit PDF samples, as lossless JBIG2 sharing one symbol
/// dictionary: the marks found in more than one of them are stored once, in a symbol dictionary
/// segment not associated with a page, which is the `/JBIG2Globals` stream's data, and each image's
/// stream, in the embedded organisation, places them from there, with its other marks coded as
/// [`encode_jbig2`]'s symbol coding codes them (see [`symbols`]). With the globals, each decodes to
/// its own bits. An image that holds none of those marks, or that symbol coding doesn't take, has
/// no stream here; it is stored alone. `None` when the images share fewer than two marks.
///
/// What each image's stream saves is left for the caller to weigh against what the globals take,
/// which the images that use them share.
///
/// Errors: as [`encode_jbig2_generic`], for the first image it would refuse.
pub fn encode_jbig2_shared(images: &[(&[u8], u32, u32)]) -> Result<Option<SharedJbig2>, CodecError> {
let images =
images.iter().map(|(data, columns, rows)| Ok((rows_to_encode(data, *columns, *rows)?, *columns, *rows))).collect::<Result<Vec<_>, _>>()?;
Ok(symbols::encode_shared(&images).map(|(globals, images)| SharedJbig2 { globals, images }))
}
/// A page information segment's data (7.4.8): the page's size, resolution unknown, eventually
/// lossless, default pixel 0 (white), OR combination, not striped.
fn page_information(columns: u32, rows: u32) -> Vec<u8> {
let mut page = Vec::with_capacity(19);
page.extend_from_slice(&columns.to_be_bytes());
page.extend_from_slice(&rows.to_be_bytes());
page.extend_from_slice(&[0; 8]);
page.push(0x01);
page.extend_from_slice(&[0, 0]);
page
}
/// A region segment information field (7.4.1): a `width` × `height` region at (`x`, `y`) on the
/// page, combined with it by OR.
fn region_information(out: &mut Vec<u8>, width: u32, height: u32, x: u32, y: u32) {
for v in [width, height, x, y] {
out.extend_from_slice(&v.to_be_bytes());
}
out.push(0);
}
/// An immediate lossless generic region segment's data (7.4.6): its region information, flags
/// (MMR 0, template 0, TPGDON 1), the AT pixels at their nominal places, then `coded`.
fn generic_region(width: u32, height: u32, x: u32, y: u32, coded: &[u8]) -> Vec<u8> {
let mut data = Vec::with_capacity(26 + coded.len());
region_information(&mut data, width, height, x, y);
data.push(0x08);
at_pixels(&mut data);
data.extend_from_slice(coded);
data
}
/// Template 0's four AT pixels at their nominal places, as AT flags (7.4.6.3, 7.4.2.1.2).
fn at_pixels(out: &mut Vec<u8>) {
for (x, y) in &TEMPLATE[12..] {
out.push(*x as i8 as u8);
out.push(*y as i8 as u8);
}
}
/// A segment (7.2) of page 1: its header (number, type, the segments it refers to, the page, data
/// length), then its data. `retained`: a later segment refers to this one. `refers_to` holds each
/// referred-to segment's number and whether a segment after this one refers to it too (its
/// retain bit, 7.2.4). Segment numbers here stay below 256, so each referred-to one takes a byte
/// (7.2.5); a segment refers to at most four, so their count and retain bits take one byte too.
fn segment(out: &mut Vec<u8>, number: u32, kind: u8, refers_to: &[(u8, bool)], retained: bool, data: &[u8]) {
segment_of(out, 1, number, kind, refers_to, retained, data);
}
/// [`segment`], of page `page`: 0 for a segment associated with no page (7.2.6), such as the
/// global ones `/JBIG2Globals` holds, which a page's segments may refer to.
fn segment_of(out: &mut Vec<u8>, page: u8, number: u32, kind: u8, refers_to: &[(u8, bool)], retained: bool, data: &[u8]) {
out.extend_from_slice(&number.to_be_bytes());
out.push(kind);
// At most four, so the count fits its three bits.
let refers_to = refers_to.get(..refers_to.len().min(4)).unwrap_or_default();
let kept = refers_to.iter().enumerate().fold(0u8, |bits, (k, (_, kept))| bits | u8::from(*kept) << (k + 1));
out.push((refers_to.len() as u8) << 5 | kept | u8::from(retained));
out.extend(refers_to.iter().map(|(n, _)| *n));
out.push(page);
// A segment's data never comes near 4 GiB here: an image is at most 2^28 samples.
out.extend_from_slice(&u32::try_from(data.len()).unwrap_or(u32::MAX).to_be_bytes());
out.extend_from_slice(data);
}
/// Codes a `width` × `height` bitmap as a generic region (6.2.5.7) with template 0 and the nominal
/// AT pixels, its decisions in `contexts` (2^16 of them), with typical prediction when `typical`
/// (TPGDON). `fill(y, row)` sets row `y`'s `width` pixels, 1 for black. Pixels outside the bitmap
/// count as 0 (white), as the decoder takes them.
fn code_generic(mq: &mut MqEncoder, contexts: &mut [Context], width: usize, height: usize, typical: bool, mut fill: impl FnMut(usize, &mut [u8])) {
let sltp = SLTP.iter().enumerate().fold(0usize, |cx, (k, v)| cx | usize::from(*v) << k);
// The two rows above the current one and the current one, as JBIG2 pixels (1 black), padded.
let blank = vec![0u8; width + 2 * PAD];
let (mut above2, mut above1) = (blank.clone(), blank.clone());
let mut row = blank;
let mut ltp = false;
for y in 0..height {
if let Some(pixels) = row.get_mut(PAD..PAD + width) {
fill(y, pixels);
}
if typical {
// Typical prediction (6.2.5.5): a row the same as the one above is coded as one decision.
let same = row == above1;
if let Some(cx) = contexts.get_mut(sltp) {
mq.encode(cx, same != ltp);
}
ltp = same;
}
if !ltp {
for x in 0..width {
let at = |dx: i32, dy: i32| {
let line = match dy {
0 => &row,
-1 => &above1,
_ => &above2,
};
let i = (PAD + x).checked_add_signed(dx as isize);
i.and_then(|i| line.get(i)).copied().unwrap_or(0)
};
let cx = TEMPLATE.iter().enumerate().fold(0usize, |cx, (k, (dx, dy))| cx | usize::from(at(*dx, *dy)) << k);
if let Some(state) = contexts.get_mut(cx) {
mq.encode(state, row.get(PAD + x) == Some(&1));
}
}
}
std::mem::swap(&mut above2, &mut above1);
std::mem::swap(&mut above1, &mut row);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The rows an independent decoder (`hayro-jbig2`) reads from `data`, as PDF samples: true
/// for white (a JBIG2 0).
pub(super) fn decode(data: &[u8]) -> Vec<Vec<bool>> {
decode_with(data, None)
}
/// [`decode`], with `/JBIG2Globals`.
pub(super) fn decode_with(data: &[u8], globals: Option<&[u8]>) -> Vec<Vec<bool>> {
struct Rows(Vec<Vec<bool>>, Vec<bool>, usize);
impl hayro_jbig2::Decoder for Rows {
fn push_pixel(&mut self, black: bool) {
self.1.push(!black);
}
fn push_pixel_chunk(&mut self, black: bool, chunks: u32) {
self.1.extend(std::iter::repeat_n(!black, chunks as usize * 8));
}
fn next_line(&mut self) {
let mut row = std::mem::take(&mut self.1);
row.truncate(self.2);
self.0.push(row);
}
}
let image = hayro_jbig2::Image::new_embedded(data, globals).unwrap();
let mut rows = Rows(Vec::new(), Vec::new(), image.width() as usize);
image.decode(&mut rows).unwrap();
rows.0
}
/// The rows of packed samples `data`, true for a 1 bit.
pub(super) fn unpack(data: &[u8], columns: u32, rows: u32) -> Vec<Vec<bool>> {
let stride = (columns as usize).div_ceil(8);
(0..rows as usize).map(|y| (0..columns as usize).map(|x| data[y * stride + x / 8] & (0x80 >> (x % 8)) != 0).collect()).collect()
}
/// Rows of `columns` samples from `runs`: alternating white and black runs one longer than
/// those values (never empty, even when shrinking makes them all 0), until the rows are full;
/// or, with `noise`, the bytes of `runs` as they are.
fn bitmap(columns: u32, rows: u32, runs: &[u8], noise: bool) -> Vec<u8> {
let stride = (columns as usize).div_ceil(8);
let mut data = vec![0u8; stride * rows as usize];
if runs.is_empty() {
return data;
}
if noise {
data.iter_mut().zip(runs.iter().cycle()).for_each(|(d, r)| *d = *r);
return data;
}
let (mut white, mut left, mut next) = (true, 1 + usize::from(runs[0]), 1);
for y in 0..rows as usize {
for x in 0..columns as usize {
if left == 0 {
white = !white;
left = 1 + usize::from(runs[next % runs.len()]);
next += 1;
}
if white {
data[y * stride + x / 8] |= 0x80 >> (x % 8);
}
left -= 1;
}
}
data
}
#[test]
fn a_page_of_text_like_marks_round_trips_and_shrinks() {
let (w, h) = (1700u32, 220u32);
let stride = (w as usize).div_ceil(8);
let mut data = vec![0xFF; stride * h as usize];
for y in (20..h as usize).step_by(40) {
for dy in 0..12 {
for x in (100..1600).step_by(9) {
if (x / 9 + dy) % 5 != 0 {
data[(y + dy) * stride + x / 8] &= !(0x80 >> (x % 8));
}
}
}
}
let jbig2 = encode_jbig2_generic(&data, w, h).unwrap();
assert_eq!(decode(&jbig2), unpack(&data, w, h));
let g4 = crate::encode_g4(&data, w, h).unwrap();
assert!(jbig2.len() < g4.len(), "JBIG2 {} bytes, Group 4 {}", jbig2.len(), g4.len());
}
#[test]
fn edges_padding_and_typical_rows_round_trip() {
// 3 columns: the last 5 bits of each byte pad the row; set them to anything.
let data = [0b1010_1111, 0b0101_0000, 0b1110_1010, 0xAB, 0xCD];
assert_eq!(decode(&encode_jbig2_generic(&data, 3, 3).unwrap()), [[true, false, true], [false, true, false], [true, true, true]]);
for (w, h, fill) in [(1, 1, 0x00), (1, 1, 0xFF), (13, 9, 0x00), (13, 9, 0xFF), (64, 40, 0x5A)] {
let data = vec![fill; (w as usize).div_ceil(8) * h as usize];
assert_eq!(decode(&encode_jbig2_generic(&data, w, h).unwrap()), unpack(&data, w, h), "{w} × {h} of {fill:#04x}");
}
}
/// Packed samples from JBIG2 pixels (1 black): a 0 sample for each 1.
pub(super) fn samples(pixels: &[Vec<u8>]) -> Vec<u8> {
let width = pixels.first().map_or(0, Vec::len);
let stride = width.div_ceil(8);
let mut data = vec![0xFF; stride * pixels.len()];
for (y, row) in pixels.iter().enumerate() {
for (x, p) in row.iter().enumerate() {
if *p == 1 {
data[y * stride + x / 8] &= !(0x80 >> (x % 8));
}
}
}
data
}
/// A row the same as the one above is typical (6.2.5.5): one decision, not a row of pixels.
#[test]
fn a_repeated_row_is_one_decision() {
let row: Vec<u8> = (0..800u32).map(|x| u8::from((x * 7919 + x / 3) % 5 < 2)).collect();
let pixels = vec![row; 1000];
let data = samples(&pixels);
crate::mq::DECISIONS.with(|n| n.set(0));
let jbig2 = encode_jbig2_generic(&data, 800, 1000).unwrap();
// The first row's 800 pixels, and one typical-row decision for each of the 1000 rows.
assert_eq!(crate::mq::DECISIONS.with(|n| n.get()), 800 + 1000);
assert_eq!(decode(&jbig2), unpack(&data, 800, 1000));
}
/// SLTP is coded in the context Figure 8 shows, which ordinary pixels can share: an image
/// tiled with that very neighbourhood decodes only if both coders use the same context.
#[test]
fn the_typical_row_flag_shares_figure_8s_context() {
// Figure 8, around a pixel at x = 4 of an 8-column tile: two rows up x = 2..6, one row up
// x = 1..7, the pixel's own row x = 0..3.
let tile = [[0, 0, 1, 0, 0, 1, 1, 0], [0, 0, 1, 1, 0, 0, 1, 0], [0, 1, 0, 1, 1, 0, 0, 0]];
let pixels: Vec<Vec<u8>> = (0..90).map(|y| (0..400).map(|x| tile[y % 3][x % 8]).collect()).collect();
let data = samples(&pixels);
assert_eq!(decode(&encode_jbig2_generic(&data, 400, 90).unwrap()), unpack(&data, 400, 90));
}
#[test]
fn what_it_cannot_encode_is_an_error() {
assert!(matches!(encode_jbig2_generic(&[0; 8], 0, 1), Err(CodecError::Unsupported(_))));
assert!(matches!(encode_jbig2_generic(&[0; 8], 8, 0), Err(CodecError::Unsupported(_))));
assert!(matches!(encode_jbig2_generic(&[0; 3], 16, 2), Err(CodecError::Corrupt { .. })), "two rows of 16 need 4 bytes");
assert!(matches!(encode_jbig2_generic(&[], 1, 2), Err(CodecError::Corrupt { .. })));
}
/// The encoder stops where the decoder does, on each side and in total: past that it would
/// write a stream nothing here can read. 16,384 squared is exactly the bound on samples.
/// `encode_jbig2_shared` refuses what `encode_jbig2_generic` refuses: an image taller than
/// the decoder takes is `Unsupported`, one at the bound isn't refused. (Its one tall mark is
/// too large to be a symbol, so nothing is shared: `Ok(None)`.)
#[test]
fn shared_coding_takes_only_what_the_decoder_takes() {
let past = vec![0x7F; MAX_JBIG2_COLUMNS as usize + 1];
let tall = (&past[..], 1, MAX_JBIG2_COLUMNS + 1);
assert!(matches!(encode_jbig2_shared(&[tall, tall]), Err(CodecError::Unsupported(_))), "too tall");
let at = (&past[..MAX_JBIG2_COLUMNS as usize], 1, MAX_JBIG2_COLUMNS);
assert!(matches!(encode_jbig2_shared(&[at, at]), Ok(None)), "at the bound");
}
#[test]
fn it_encodes_only_what_the_decoder_takes() {
let unsupported = |columns, rows| matches!(encode_jbig2_generic(&[], columns, rows), Err(CodecError::Unsupported(_)));
assert!(unsupported(MAX_JBIG2_COLUMNS + 1, 1), "too wide");
assert!(unsupported(1, MAX_JBIG2_COLUMNS + 1), "too tall");
assert!(unsupported(16_385, 16_384), "too many samples, each side within the bound");
assert!(unsupported(u32::MAX, u32::MAX), "the product doesn't overflow");
// At the bounds what it writes reads back, through the decoder that has to read it.
let wide = vec![0x0F; (MAX_JBIG2_COLUMNS as usize).div_ceil(8) * 4];
assert_eq!(decode(&encode_jbig2_generic(&wide, MAX_JBIG2_COLUMNS, 4).unwrap()), unpack(&wide, MAX_JBIG2_COLUMNS, 4));
let tall = vec![0x7F; MAX_JBIG2_COLUMNS as usize];
assert_eq!(decode(&encode_jbig2_generic(&tall, 1, MAX_JBIG2_COLUMNS).unwrap()), unpack(&tall, 1, MAX_JBIG2_COLUMNS));
struct Rows(u32, u64);
impl hayro_jbig2::Decoder for Rows {
fn push_pixel(&mut self, black: bool) {
self.1 += u64::from(black);
}
fn push_pixel_chunk(&mut self, black: bool, chunks: u32) {
self.1 += u64::from(black) * u64::from(chunks) * 8;
}
fn next_line(&mut self) {
self.0 += 1;
}
}
// One black pixel, so a page that came back blank wouldn't pass.
let mut largest = vec![0xFF; 2_048 * 16_384];
largest[0] = 0x7F;
let largest = encode_jbig2_generic(&largest, 16_384, 16_384).unwrap();
let mut rows = Rows(0, 0);
hayro_jbig2::Image::new_embedded(&largest, None).unwrap().decode(&mut rows).unwrap();
assert_eq!(rows.0, 16_384);
assert_eq!(rows.1, 1);
}
proptest::proptest! {
/// Any bitmap, of noise or of runs, decodes to its own bits through an independent decoder.
#[test]
fn bitmaps_round_trip(
columns in 1u32..300,
rows in 1u32..24,
runs in proptest::collection::vec(0u8..=255, 1..48),
noise in proptest::bool::ANY,
) {
let data = bitmap(columns, rows, &runs, noise);
let jbig2 = encode_jbig2_generic(&data, columns, rows).unwrap();
proptest::prop_assert_eq!(decode(&jbig2), unpack(&data, columns, rows));
}
/// Whatever it is given, it returns.
#[test]
fn any_input_returns(data in proptest::collection::vec(0u8..=255, 0..600), columns in 0u32..5000, rows in 0u32..80) {
let _ = encode_jbig2_generic(&data, columns, rows);
}
}
}
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
//! Byte-stream and bi-level image codecs that PDF, PostScript and TIFF share.
//!
//! - [`lzw`]: variable-width LZW, 9 to 12 bits, most significant bit first (PDF `LZWDecode`,
//! TIFF compression 5), with or without the early code-width change.
//! - [`ascii85`], [`asciihex`], [`runlength`]: the PostScript and PDF text and run-length
//! encodings (`ASCII85Decode`, `ASCIIHexDecode`, `RunLengthDecode`; PackBits).
//! - [`predictor`]: TIFF horizontal differencing and the PNG row filters.
//! - [`ccitt`]: CCITT Group 3 and Group 4 fax decoding, bounded, and Group 4 encoding.
//! - [`jbig2`]: lossless JBIG2 encoding (generic regions, symbol dictionaries, a dictionary
//! shared by a group of images). Decoding is `hayro-jbig2`'s.
//!
//! The crate knows no container: a caller hands it bytes and parameters. A PDF `/Filter` chain
//! (order, limits across stages, Flate) is `craft_pdf::filters`, built on this crate.
//!
//! Every decoder is bounded by a caller-supplied output limit and never panics on malformed
//! input. A decoder that fails returns what it decoded before the damage ([`Failure`]), so a
//! caller can choose to keep it. CCITT decoding spends from an allowance the caller shares
//! across a run of images ([`pay`]); it does not decode without one:
//!
//! ```compile_fail
//! let _ = craft_stream_codecs::decode_ccitt(&[], &craft_stream_codecs::CcittParams::default(), 1);
//! ```
//!
//! ```
//! let mut work = 1 << 20;
//! let p = craft_stream_codecs::CcittParams { columns: 8, ..Default::default() };
//! assert!(craft_stream_codecs::decode_ccitt_within(&[], &p, 1, &mut work).is_ok());
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod ascii85;
pub mod asciihex;
pub mod ccitt;
pub mod jbig2;
pub mod lzw;
mod mq;
pub mod predictor;
pub mod runlength;
pub use ccitt::{CcittParams, MAX_CCITT_COLUMNS, MAX_G4_COLUMNS, decode_ccitt_within, encode_g4};
pub use jbig2::{MAX_JBIG2_COLUMNS, MAX_JBIG2_PIXELS, SharedJbig2, encode_jbig2, encode_jbig2_generic, encode_jbig2_shared};
/// Parameters of the predictors and of LZW, as a PDF `/DecodeParms` dictionary gives them
/// (ISO 32000-2 Tables 8–9); TIFF's `Predictor`, `SamplesPerPixel`, `BitsPerSample` and
/// `ImageWidth` map onto the same four.
#[derive(Clone, Debug, PartialEq)]
pub struct Params {
/// 1 = none, 2 = TIFF predictor 2, 10–15 = PNG predictors.
pub predictor: i64,
pub colors: i64,
pub bits_per_component: i64,
pub columns: i64,
/// LZW only: 1 (default) switches code width one code early, 0 does not.
pub early_change: i64,
}
impl Default for Params {
fn default() -> Self {
Params { predictor: 1, colors: 1, bits_per_component: 8, columns: 1, early_change: 1 }
}
}
/// Errors from decoding or encoding.
#[derive(Debug, thiserror::Error)]
pub enum CodecError {
/// `filter` names the codec, as PDF names its filter (`LZWDecode`, `CCITTFaxDecode`, …).
#[error("{filter}: corrupt data: {detail}")]
Corrupt { filter: &'static str, detail: String },
#[error("decoded output exceeds the limit of {0} bytes")]
LimitExceeded(usize),
#[error("unsupported filter: {0}")]
Unsupported(String),
/// A run of images has nothing left to spend ([`pay`]).
#[error("more image decoding than one operation may do")]
Spent,
}
/// A decoder failure carrying whatever was decoded before the error.
pub struct Failure {
pub error: CodecError,
pub partial: Vec<u8>,
/// Bytes decoded on the way that `partial` doesn't hold: they took as long.
pub discarded: usize,
}
impl Failure {
pub fn corrupt(filter: &'static str, detail: impl Into<String>, partial: Vec<u8>) -> Self {
Failure { error: CodecError::Corrupt { filter, detail: detail.into() }, partial, discarded: 0 }
}
pub fn limit(max: usize) -> Self {
Failure { error: CodecError::LimitExceeded(max), partial: Vec::new(), discarded: 0 }
}
}
/// What a byte-stream decoder returns: the decoded bytes, or the failure with its partial output.
pub type Step = Result<Vec<u8>, Failure>;
/// Takes `steps` from `work`, what a run of images may still cost. What it can't pay for spends
/// all of it.
pub fn pay(work: &mut u64, steps: u64) -> Result<(), CodecError> {
match work.checked_sub(steps) {
Some(left) => {
*work = left;
Ok(())
}
None => {
*work = 0;
Err(CodecError::Spent)
}
}
}
/// PDF and PostScript white-space characters (ISO 32000-2 Table 1).
pub fn is_pdf_whitespace(b: u8) -> bool {
matches!(b, 0 | 9 | 10 | 12 | 13 | 32)
}
+314
View File
@@ -0,0 +1,314 @@
//! LZWDecode (ISO 32000-2 §7.4.4.2): variable-width codes of 9–12 bits, MSB first,
//! 256 = clear-table, 257 = EOD, first free code 258. With `EarlyChange` 1 (the
//! default) the code width grows one code early.
//!
//! Tolerated: data ending without EOD, a missing initial clear-table code, a table that
//! fills up without a clear-table code (entries are then no longer added).
use std::collections::HashMap;
use crate::{Failure, Step};
const NAME: &str = "LZWDecode";
const CLEAR: u16 = 256;
const EOD: u16 = 257;
const FIRST: usize = 258;
const TABLE: usize = 4096;
/// The encoder emits clear-table when its next free code reaches this value.
const ENCODER_RESET_AT: usize = TABLE;
/// The code width a decoder uses when its next free code is `next`.
fn width(next: usize, early: bool) -> u32 {
match next + usize::from(early) {
0..512 => 9,
512..1024 => 10,
1024..2048 => 11,
_ => 12,
}
}
struct BitReader<'a> {
data: &'a [u8],
pos: usize,
acc: u32,
bits: u32,
}
impl BitReader<'_> {
fn read(&mut self, n: u32) -> Option<u16> {
while self.bits < n {
let &b = self.data.get(self.pos)?;
self.pos += 1;
self.acc = (self.acc << 8) | u32::from(b);
self.bits += 8;
}
self.bits -= n;
let v = (self.acc >> self.bits) & ((1 << n) - 1);
self.acc &= (1 << self.bits) - 1;
Some(v as u16)
}
}
pub fn decode(data: &[u8], early: bool, max: usize) -> Step {
// Table entry = (prefix code, last byte, first byte, length).
let mut prefix = vec![0u16; TABLE];
let mut last = vec![0u8; TABLE];
let mut first = vec![0u8; TABLE];
let mut len = vec![0u16; TABLE];
for i in 0..256 {
last[i] = i as u8;
first[i] = i as u8;
len[i] = 1;
}
let mut next = FIRST;
let mut w = 9;
let mut prev: Option<usize> = None;
let mut r = BitReader { data, pos: 0, acc: 0, bits: 0 };
let mut out = Vec::with_capacity(data.len().saturating_mul(2).min(max));
while let Some(code) = r.read(w) {
match code {
CLEAR => {
next = FIRST;
w = 9;
prev = None;
continue;
}
EOD => break,
_ => {}
}
let code = usize::from(code);
if let Some(p) = prev {
let head = if code < next {
first[code]
} else if code == next && next < TABLE {
first[p]
} else {
return Err(Failure::corrupt(NAME, format!("code {code} not in table (next {next})"), out));
};
if next < TABLE {
prefix[next] = p as u16;
last[next] = head;
first[next] = first[p];
len[next] = len[p] + 1;
next += 1;
}
} else if code >= 256 {
return Err(Failure::corrupt(NAME, format!("code {code} before any table entry"), out));
}
// Emit the string for `code`, walking the prefix chain backwards.
let n = usize::from(len[code]);
if out.len() + n > max {
return Err(Failure::limit(max));
}
let start = out.len();
out.resize(start + n, 0);
let mut c = code;
for slot in out[start..].iter_mut().rev() {
*slot = last[c];
c = usize::from(prefix[c]);
}
prev = Some(code);
w = width(next, early);
}
Ok(out)
}
struct BitWriter {
out: Vec<u8>,
acc: u32,
bits: u32,
}
impl BitWriter {
fn write(&mut self, code: u16, n: u32) {
self.acc = (self.acc << n) | u32::from(code);
self.bits += n;
while self.bits >= 8 {
self.bits -= 8;
self.out.push((self.acc >> self.bits) as u8);
}
self.acc &= (1 << self.bits) - 1;
}
fn finish(mut self) -> Vec<u8> {
if self.bits > 0 {
self.out.push((self.acc << (8 - self.bits)) as u8);
}
self.out
}
}
/// Greedy LZW with a leading clear-table code and a trailing EOD.
pub fn encode(data: &[u8], early: bool) -> Vec<u8> {
let mut wr = BitWriter { out: Vec::with_capacity(data.len() / 2 + 8), acc: 0, bits: 0 };
let mut dict: HashMap<(u16, u8), u16> = HashMap::new();
// `next` is the encoder's next free code; `dnext` mirrors the decoder's, which lags
// one entry behind (the decoder adds an entry only once it sees the following code).
let mut next = FIRST;
let mut dnext = FIRST;
let mut codes_since_clear = 0usize;
wr.write(CLEAR, 9);
let emit = |wr: &mut BitWriter, code: u16, dnext: &mut usize, since: &mut usize| {
wr.write(code, width(*dnext, early));
if *since > 0 && *dnext < TABLE {
*dnext += 1;
}
*since += 1;
};
let Some((&b0, rest)) = data.split_first() else {
wr.write(EOD, 9);
return wr.finish();
};
let mut cur = u16::from(b0);
for &b in rest {
if let Some(&c) = dict.get(&(cur, b)) {
cur = c;
continue;
}
emit(&mut wr, cur, &mut dnext, &mut codes_since_clear);
dict.insert((cur, b), next as u16);
next += 1;
if next == ENCODER_RESET_AT {
wr.write(CLEAR, width(dnext, early));
dict.clear();
next = FIRST;
dnext = FIRST;
codes_since_clear = 0;
}
cur = u16::from(b);
}
emit(&mut wr, cur, &mut dnext, &mut codes_since_clear);
wr.write(EOD, width(dnext, early));
wr.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodecError;
fn dec(d: &[u8], early: bool) -> Result<Vec<u8>, CodecError> {
decode(d, early, 1 << 26).map_err(|f| f.error)
}
/// ISO 32000-2 §7.4.4.2 example: codes 256 45 258 258 65 259 66 257.
const SPEC_IN: [u8; 10] = [45, 45, 45, 45, 45, 65, 45, 45, 45, 66];
const SPEC_OUT: [u8; 9] = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
#[test]
fn spec_example() {
assert_eq!(dec(&SPEC_OUT, true).unwrap(), SPEC_IN);
assert_eq!(encode(&SPEC_IN, true), SPEC_OUT);
// Below 511 codes the early-change setting makes no difference.
assert_eq!(dec(&SPEC_OUT, false).unwrap(), SPEC_IN);
}
#[test]
fn missing_eod_and_clear() {
// Drop the EOD (last 9 bits span the final 2 bytes; keep bits for 66 only).
let mut w = BitWriter { out: vec![], acc: 0, bits: 0 };
for c in [45u16, 258, 258, 65, 259, 66] {
w.write(c, 9);
}
assert_eq!(dec(&w.finish(), true).unwrap(), SPEC_IN);
}
#[test]
fn invalid_codes() {
let mut w = BitWriter { out: vec![], acc: 0, bits: 0 };
for c in [256u16, 65, 300] {
w.write(c, 9);
}
let f = decode(&w.finish(), true, 1000).err().unwrap();
assert!(matches!(f.error, CodecError::Corrupt { .. }));
assert_eq!(f.partial, b"A");
let mut w = BitWriter { out: vec![], acc: 0, bits: 0 };
w.write(258, 9);
assert!(matches!(dec(&w.finish(), true), Err(CodecError::Corrupt { .. })));
}
#[test]
fn widths() {
assert_eq!((width(510, true), width(511, true), width(511, false), width(512, false)), (9, 10, 9, 10));
assert_eq!((width(1022, true), width(1023, true), width(2047, true), width(4095, false)), (10, 11, 12, 12));
}
fn noisy(n: usize, seed: u32) -> Vec<u8> {
let mut s = seed;
(0..n)
.map(|_| {
s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345);
((s >> 16) % 7) as u8 * 31
})
.collect()
}
#[test]
fn all_widths_and_clears_round_trip() {
for early in [false, true] {
for data in [noisy(200_000, 1), (0..=255u8).cycle().take(70_000).collect(), vec![9u8; 100_000]] {
let e = encode(&data, early);
assert_eq!(dec(&e, early).unwrap(), data);
}
}
}
#[test]
fn cross_check_with_weezl() {
use weezl::BitOrder::Msb;
for data in [noisy(150_000, 7), SPEC_IN.to_vec(), vec![]] {
// early change 1 is the TIFF size switch; 0 is the plain variant.
let tiff = weezl::encode::Encoder::with_tiff_size_switch(Msb, 8).encode(&data).unwrap();
assert_eq!(dec(&tiff, true).unwrap(), data);
let plain = weezl::encode::Encoder::new(Msb, 8).encode(&data).unwrap();
assert_eq!(dec(&plain, false).unwrap(), data);
let ours = encode(&data, true);
assert_eq!(weezl::decode::Decoder::with_tiff_size_switch(Msb, 8).decode(&ours).unwrap(), data);
let ours = encode(&data, false);
assert_eq!(weezl::decode::Decoder::new(Msb, 8).decode(&ours).unwrap(), data);
}
}
#[test]
fn full_table_without_clear() {
// Hand-roll a stream that fills the table and keeps going at 12 bits.
let data = noisy(40_000, 3);
let mut w = BitWriter { out: vec![], acc: 0, bits: 0 };
let mut dict: HashMap<Vec<u8>, u16> = (0..=255u8).map(|b| (vec![b], u16::from(b))).collect();
let (mut next, mut dnext, mut since) = (FIRST, FIRST, 0usize);
let mut cur: Vec<u8> = vec![];
let mut put = |w: &mut BitWriter, code: u16, dnext: &mut usize| {
w.write(code, width(*dnext, true));
if since > 0 && *dnext < TABLE {
*dnext += 1;
}
since += 1;
};
w.write(CLEAR, 9);
for &b in &data {
let mut ext = cur.clone();
ext.push(b);
if dict.contains_key(&ext) {
cur = ext;
continue;
}
put(&mut w, dict[&cur], &mut dnext);
if next < TABLE {
dict.insert(ext, next as u16);
next += 1;
}
cur = vec![b];
}
put(&mut w, dict[&cur], &mut dnext);
w.write(EOD, width(dnext, true));
assert_eq!(dec(&w.finish(), true).unwrap(), data);
}
#[test]
fn limit() {
let e = encode(&vec![0u8; 100_000], true);
assert!(matches!(decode(&e, true, 50_000).map_err(|f| f.error), Err(CodecError::LimitExceeded(50_000))));
assert_eq!(decode(&e, true, 100_000).ok().unwrap().len(), 100_000);
}
}
+209
View File
@@ -0,0 +1,209 @@
//! The MQ arithmetic encoder of JBIG2 (ITU-T T.88 | ISO/IEC 14492, Annex E), written from the
//! procedures in its Figures E.2–E.12 and the probability estimates of Table E.1.
/// Table E.1: for each index, the LPS probability estimate Qe, the next index after an MPS and
/// after an LPS renormalisation, and whether an LPS switches the MPS sense.
const STATES: [(u32, u8, u8, bool); 47] = [
(0x5601, 1, 1, true),
(0x3401, 2, 6, false),
(0x1801, 3, 9, false),
(0x0AC1, 4, 12, false),
(0x0521, 5, 29, false),
(0x0221, 38, 33, false),
(0x5601, 7, 6, true),
(0x5401, 8, 14, false),
(0x4801, 9, 14, false),
(0x3801, 10, 14, false),
(0x3001, 11, 17, false),
(0x2401, 12, 18, false),
(0x1C01, 13, 20, false),
(0x1601, 29, 21, false),
(0x5601, 15, 14, true),
(0x5401, 16, 14, false),
(0x5101, 17, 15, false),
(0x4801, 18, 16, false),
(0x3801, 19, 17, false),
(0x3401, 20, 18, false),
(0x3001, 21, 19, false),
(0x2801, 22, 19, false),
(0x2401, 23, 20, false),
(0x2201, 24, 21, false),
(0x1C01, 25, 22, false),
(0x1801, 26, 23, false),
(0x1601, 27, 24, false),
(0x1401, 28, 25, false),
(0x1201, 29, 26, false),
(0x1101, 30, 27, false),
(0x0AC1, 31, 28, false),
(0x09C1, 32, 29, false),
(0x08A1, 33, 30, false),
(0x0521, 34, 31, false),
(0x0441, 35, 32, false),
(0x02A1, 36, 33, false),
(0x0221, 37, 34, false),
(0x0141, 38, 35, false),
(0x0111, 39, 36, false),
(0x0085, 40, 37, false),
(0x0049, 41, 38, false),
(0x0025, 42, 39, false),
(0x0015, 43, 40, false),
(0x0009, 44, 41, false),
(0x0005, 45, 42, false),
(0x0001, 45, 43, false),
(0x5601, 46, 46, false),
];
#[cfg(test)]
thread_local! {
/// Decisions coded on this thread.
pub(crate) static DECISIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// A context's adaptive state: the index into [`STATES`] (low 7 bits) and the MPS sense (bit 7).
/// 0, the initial value, is index 0 with MPS 0 (E.2.8).
pub(crate) type Context = u8;
/// The encoder's registers (E.2.1) and the bytes written so far.
pub(crate) struct MqEncoder {
a: u32,
c: u32,
ct: u32,
/// The output, after the byte before the first one (B at INITENC), which is 0x00 and is
/// dropped by [`MqEncoder::finish`]: a carry can't reach it before the first byte is out.
out: Vec<u8>,
}
impl MqEncoder {
/// INITENC (Figure E.10). The byte before the first is 0x00, not 0xFF, so CT starts at 12.
pub(crate) fn new() -> Self {
MqEncoder { a: 0x8000, c: 0, ct: 12, out: vec![0] }
}
/// The byte B, the last one written (or the one before the first).
fn b(&self) -> u8 {
self.out.last().copied().unwrap_or(0)
}
/// ENCODE (Figures E.3–E.7): code decision `d` in context `cx`, updating its state.
pub(crate) fn encode(&mut self, cx: &mut Context, d: bool) {
#[cfg(test)]
DECISIONS.with(|n| n.set(n.get() + 1));
let (index, mps) = (usize::from(*cx & 0x7F), *cx >> 7 == 1);
let Some(&(qe, nmps, nlps, switch)) = STATES.get(index) else { return };
self.a -= qe;
if d == mps {
// CODEMPS.
if self.a & 0x8000 != 0 {
self.c += qe;
return;
}
if self.a < qe {
self.a = qe;
} else {
self.c += qe;
}
*cx = nmps | (*cx & 0x80);
} else {
// CODELPS.
if self.a < qe {
self.c += qe;
} else {
self.a = qe;
}
let mps = mps != switch;
*cx = nlps | (u8::from(mps) << 7);
}
self.renorm();
}
/// RENORME (Figure E.8).
fn renorm(&mut self) {
loop {
self.a <<= 1;
self.c <<= 1;
self.ct -= 1;
if self.ct == 0 {
self.byte_out();
}
if self.a & 0x8000 != 0 {
break;
}
}
}
/// BYTEOUT (Figure E.9), with bit stuffing after 0xFF and carry into the byte before.
fn byte_out(&mut self) {
if self.b() == 0xFF {
self.stuffed();
} else if self.c < 0x800_0000 {
self.plain();
} else {
let b = self.b().wrapping_add(1);
if let Some(last) = self.out.last_mut() {
*last = b;
}
if b == 0xFF {
self.c &= 0x7FF_FFFF;
self.stuffed();
} else {
self.plain();
}
}
}
fn plain(&mut self) {
self.out.push(((self.c >> 19) & 0xFF) as u8);
self.c &= 0x7_FFFF;
self.ct = 8;
}
fn stuffed(&mut self) {
self.out.push(((self.c >> 20) & 0xFF) as u8);
self.c &= 0xF_FFFF;
self.ct = 7;
}
/// FLUSH (Figures E.11 and E.12): the final bytes, then the 0xFF 0xAC marker.
pub(crate) fn finish(mut self) -> Vec<u8> {
let top = self.c + self.a;
self.c |= 0xFFFF;
if self.c >= top {
self.c -= 0x8000;
}
self.c <<= self.ct;
self.byte_out();
self.c <<= self.ct;
self.byte_out();
if self.b() != 0xFF {
self.out.push(0xFF);
}
self.out.push(0xAC);
self.out.split_off(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Annex H.2's test sequence: 256 decisions in one context give these 30 bytes.
#[test]
fn the_annex_h_test_sequence_encodes_to_its_bytes() {
let decisions = [
0x00, 0x02, 0x00, 0x51, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x52, 0x87, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x82, 0xC0, 0x20, 0x00, 0xFC, 0xD7, 0x9E,
0xF6, 0xBF, 0x7F, 0xED, 0x90, 0x4F, 0x46, 0xA3, 0xBF,
];
let mut mq = MqEncoder::new();
let mut cx = 0;
for byte in decisions {
for bit in (0..8).rev() {
mq.encode(&mut cx, byte >> bit & 1 == 1);
}
}
let expected = [
0x84, 0xC7, 0x3B, 0xFC, 0xE1, 0xA1, 0x43, 0x04, 0x02, 0x20, 0x00, 0x00, 0x41, 0x0D, 0xBB, 0x86, 0xF4, 0x31, 0x7F, 0xFF, 0x88, 0xFF, 0x37,
0x47, 0x1A, 0xDB, 0x6A, 0xDF, 0xFF, 0xAC,
];
assert_eq!(mq.finish(), expected);
}
}
+356
View File
@@ -0,0 +1,356 @@
//! Predictor functions for FlateDecode and LZWDecode (ISO 32000-2 §7.4.4.4).
//!
//! - Predictor 2: TIFF 6.0 horizontal differencing (Section 14), per component, for
//! 1, 2, 4, 8 and 16 bits per component.
//! - Predictors 10–15: PNG filters (RFC 2083 §6): each row carries its own filter-type
//! byte (0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth), whatever the predictor value is.
//!
//! A final partial row is decoded as far as its data goes.
//!
//! The samples of an image whose rows carry no filter-type byte, although a PNG predictor is
//! declared, are taken as they are and reported as damage ([`decode`]).
use crate::{CodecError, Failure, Params, Step};
struct Layout {
bpc: usize,
colors: usize,
/// Bytes per complete pixel, at least 1 (the PNG `bpp`).
bpp: usize,
/// Bytes per row (without the PNG tag byte).
row: usize,
/// Samples per row (colors × columns).
samples: usize,
}
fn layout(p: &Params) -> Result<Layout, String> {
let bpc = match p.bits_per_component {
b @ (1 | 2 | 4 | 8 | 16) => b as usize,
b => return Err(format!("invalid BitsPerComponent {b}")),
};
if !(1..=255).contains(&p.colors) {
return Err(format!("invalid Colors {}", p.colors));
}
if p.columns < 1 {
return Err(format!("invalid Columns {}", p.columns));
}
let colors = p.colors as usize;
let samples = usize::try_from(p.columns).ok().and_then(|c| c.checked_mul(colors)).ok_or_else(|| format!("Columns {} too large", p.columns))?;
let bits = samples.checked_mul(bpc).ok_or_else(|| format!("Columns {} too large", p.columns))?;
Ok(Layout { bpc, colors, bpp: (colors * bpc).div_ceil(8), row: bits.div_ceil(8), samples })
}
/// Undoes the predictor named by `p.predictor` (1 = identity). `rows` is the height of the image
/// `data` is the samples of, if it is: with a PNG predictor, data of exactly `rows` rows without
/// their filter-type bytes ([`untagged`]) is given back as it is, as a [`Failure`] whose partial
/// output is all of it, so that tolerant decoding takes the samples and reports the repair, and
/// strict decoding refuses them.
pub fn decode(p: &Params, data: Vec<u8>, name: &'static str, rows: Option<usize>) -> Step {
match p.predictor {
1 => Ok(data),
2 | 10..=15 => {
let l = layout(p).map_err(|e| Failure::corrupt(name, e, Vec::new()))?;
if p.predictor == 2 {
return Ok(tiff_decode(&l, data));
}
if let Some(rows) = rows.filter(|&rows| untagged(&l, data.len(), rows)) {
let detail =
format!("PNG predictor {} declared, but its {rows} rows carry no filter-type bytes: read as unpredicted samples", p.predictor);
return Err(Failure::corrupt(name, detail, data));
}
png_decode(&l, &data, name)
}
other => Err(Failure::corrupt(name, format!("invalid Predictor {other}"), Vec::new())),
}
}
/// Whether `len` bytes are exactly `rows` rows of `l` without the filter-type byte that starts each
/// PNG-predicted row (ISO 32000-2 §7.4.4.4), where predicted data would be `rows` × (row + 1): a
/// writer that declared the predictor and didn't apply it. Read with the predictor, every row's
/// first sample was taken for a filter type and the image came out a row short and shifted.
/// PDFium, MuPDF and Poppler read such data as the samples it is.
fn untagged(l: &Layout, len: usize, rows: usize) -> bool {
rows > 0 && l.row > 0 && rows.checked_mul(l.row) == Some(len)
}
/// Applies the predictor named by `p.predictor` (1 = identity; 15 = per-row optimum).
pub fn encode(p: &Params, data: &[u8]) -> Result<Vec<u8>, CodecError> {
match p.predictor {
1 => Ok(data.to_vec()),
2 | 10..=15 => {
let l = layout(p).map_err(CodecError::Unsupported)?;
Ok(if p.predictor == 2 { tiff_encode(&l, data) } else { png_encode(&l, data, p.predictor) })
}
other => Err(CodecError::Unsupported(format!("Predictor {other}"))),
}
}
// ---- TIFF predictor 2 ----
fn get(row: &[u8], i: usize, bpc: usize) -> u16 {
match bpc {
16 => u16::from_be_bytes([row[2 * i], row[2 * i + 1]]),
8 => u16::from(row[i]),
_ => {
let bit = i * bpc;
let shift = 8 - bpc - bit % 8;
u16::from(row[bit / 8] >> shift) & ((1 << bpc) - 1)
}
}
}
fn set(row: &mut [u8], i: usize, bpc: usize, v: u16) {
match bpc {
16 => row[2 * i..2 * i + 2].copy_from_slice(&v.to_be_bytes()),
8 => row[i] = v as u8,
_ => {
let bit = i * bpc;
let shift = 8 - bpc - bit % 8;
let mask = (((1u16 << bpc) - 1) << shift) as u8;
row[bit / 8] = (row[bit / 8] & !mask) | (((v << shift) as u8) & mask);
}
}
}
/// Complete samples available in a (possibly partial) row.
fn samples_in(l: &Layout, row_len: usize) -> usize {
(row_len * 8 / l.bpc).min(l.samples)
}
fn tiff_decode(l: &Layout, mut data: Vec<u8>) -> Vec<u8> {
let mask = if l.bpc == 16 { u16::MAX } else { (1u16 << l.bpc) - 1 };
for row in data.chunks_mut(l.row) {
for i in l.colors..samples_in(l, row.len()) {
let v = get(row, i, l.bpc).wrapping_add(get(row, i - l.colors, l.bpc)) & mask;
set(row, i, l.bpc, v);
}
}
data
}
fn tiff_encode(l: &Layout, data: &[u8]) -> Vec<u8> {
let mask = if l.bpc == 16 { u16::MAX } else { (1u16 << l.bpc) - 1 };
let mut out = data.to_vec();
for row in out.chunks_mut(l.row) {
// Descending, so the left neighbour still holds its original value.
for i in (l.colors..samples_in(l, row.len())).rev() {
let v = get(row, i, l.bpc).wrapping_sub(get(row, i - l.colors, l.bpc)) & mask;
set(row, i, l.bpc, v);
}
}
out
}
// ---- PNG predictors ----
fn paeth(a: u8, b: u8, c: u8) -> u8 {
let p = i16::from(a) + i16::from(b) - i16::from(c);
let (pa, pb, pc) = ((p - i16::from(a)).abs(), (p - i16::from(b)).abs(), (p - i16::from(c)).abs());
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
/// The PNG predictor value for byte `i` given the reconstructed current row `cur`
/// (bytes before `i`) and the prior row `up`.
fn predict(tag: u8, cur: &[u8], up: &[u8], i: usize, bpp: usize) -> u8 {
let a = if i >= bpp { cur[i - bpp] } else { 0 };
let b = up[i];
let c = if i >= bpp { up[i - bpp] } else { 0 };
match tag {
1 => a,
2 => b,
3 => ((u16::from(a) + u16::from(b)) / 2) as u8,
4 => paeth(a, b, c),
_ => 0,
}
}
fn png_decode(l: &Layout, data: &[u8], name: &'static str) -> Step {
let mut out = Vec::with_capacity(data.len());
// The prior row: never longer than the input, even for absurd Columns values.
let mut up = vec![0u8; l.row.min(data.len())];
for chunk in data.chunks(l.row.saturating_add(1)) {
let (tag, src) = (chunk[0], &chunk[1..]);
if tag > 4 {
return Err(Failure::corrupt(name, format!("invalid PNG filter type {tag}"), out));
}
let start = out.len();
for (i, &x) in src.iter().enumerate() {
let pred = predict(tag, &out[start..], &up, i, l.bpp);
out.push(x.wrapping_add(pred));
}
up[..src.len()].copy_from_slice(&out[start..]);
}
Ok(out)
}
fn png_filter(tag: u8, cur: &[u8], up: &[u8], bpp: usize, dst: &mut Vec<u8>) {
dst.clear();
dst.push(tag);
dst.extend(cur.iter().enumerate().map(|(i, &x)| x.wrapping_sub(predict(tag, cur, up, i, bpp))));
}
/// Sum of residuals as signed bytes: the usual heuristic for choosing a PNG filter.
fn cost(filtered: &[u8]) -> u64 {
filtered[1..].iter().map(|&b| u64::from((b as i8).unsigned_abs())).sum()
}
fn png_encode(l: &Layout, data: &[u8], predictor: i64) -> Vec<u8> {
let rows = data.len().div_ceil(l.row.max(1));
let mut out = Vec::with_capacity(data.len() + rows);
let zeros = vec![0u8; l.row.min(data.len())];
let (mut best, mut trial) = (Vec::new(), Vec::new());
let mut up: &[u8] = &zeros;
for cur in data.chunks(l.row) {
let up_row = &up[..cur.len()];
if predictor == 15 {
png_filter(0, cur, up_row, l.bpp, &mut best);
let mut best_cost = cost(&best);
for tag in 1..=4 {
png_filter(tag, cur, up_row, l.bpp, &mut trial);
let c = cost(&trial);
if c < best_cost {
best_cost = c;
std::mem::swap(&mut best, &mut trial);
}
}
} else {
png_filter((predictor - 10) as u8, cur, up_row, l.bpp, &mut best);
}
out.extend_from_slice(&best);
up = cur;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn p(predictor: i64, colors: i64, bpc: i64, columns: i64) -> Params {
Params { predictor, colors, bits_per_component: bpc, columns, early_change: 1 }
}
fn dec(params: &Params, data: &[u8]) -> Result<Vec<u8>, CodecError> {
decode(params, data.to_vec(), "FlateDecode", None).map_err(|f| f.error)
}
#[test]
fn png_hand_vectors() {
// 3 columns, 1 color, 8 bpc. Rows: None, Sub, Up, Average, Paeth.
let enc = [
0, 10, 20, 30, // None -> 10 20 30
1, 5, 1, 1, // Sub -> 5 6 7
2, 1, 1, 1, // Up -> 6 7 8
3, 10, 10, 10, // Avg -> 10+3=13, 10+(13+7)/2=20, 10+(20+8)/2=24
4, 1, 1,
1, // Paeth: a=0,b=13,c=0 -> p=13 -> b: 14; a=14,b=20,c=13: p=21 -> pa=7,pb=1,pc=8 -> b: 21;
// a=21,b=24,c=20: p=25 -> pa=4,pb=1,pc=5 -> b: 25
];
let out = dec(&p(15, 1, 8, 3), &enc).unwrap();
assert_eq!(out, [10, 20, 30, 5, 6, 7, 6, 7, 8, 13, 20, 24, 14, 21, 25]);
}
#[test]
fn paeth_tie_breaks() {
// RFC 2083 order: a, then b, then c.
assert_eq!(paeth(1, 1, 1), 1);
assert_eq!(paeth(10, 20, 15), 15); // p=15: pa=5 pb=5 pc=0 -> c
assert_eq!(paeth(20, 10, 10), 20); // p=20: pa=0 -> a
assert_eq!(paeth(5, 9, 7), 7); // p=7: pa=2 pb=2 pc=0 -> c
}
#[test]
fn png_multibyte_pixels() {
// colors=3 bpc=16 -> bpp=6; 1 column; Sub on the first pixel uses a=0 throughout.
let enc = [1, 1, 2, 3, 4, 5, 6, 2, 1, 1, 1, 1, 1, 1];
let out = dec(&p(11, 3, 16, 1), &enc).unwrap();
assert_eq!(out, [1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 7]);
// colors=3 bpc=8, 2 columns: Sub looks 3 bytes back.
let enc = [1, 1, 2, 3, 1, 1, 1];
assert_eq!(dec(&p(11, 3, 8, 2), &enc).unwrap(), [1, 2, 3, 2, 3, 4]);
}
#[test]
fn png_partial_final_row() {
let enc = [0, 1, 2, 3, 4, 2, 1, 1];
assert_eq!(dec(&p(12, 1, 8, 4), &enc).unwrap(), [1, 2, 3, 4, 2, 3]);
// A lone tag byte at the end yields nothing.
assert_eq!(dec(&p(12, 1, 8, 4), &[0, 1, 2, 3, 4, 2]).unwrap(), [1, 2, 3, 4]);
}
#[test]
fn png_invalid_tag() {
let r = decode(&p(10, 1, 8, 2), vec![0, 1, 2, 5, 1, 1], "FlateDecode", None);
let f = r.err().unwrap();
assert!(matches!(f.error, CodecError::Corrupt { .. }));
assert_eq!(f.partial, [1, 2]);
}
#[test]
fn tiff_8bit() {
// 2 colors, 3 columns: differences per component.
let enc = [10, 20, 1, 2, 3, 4, /* row 2 */ 5, 5, 250, 10, 1, 1];
assert_eq!(dec(&p(2, 2, 8, 3), &enc).unwrap(), [10, 20, 11, 22, 14, 26, 5, 5, 255, 15, 0, 16]);
}
#[test]
fn tiff_16bit() {
let enc = [0x01, 0x00, 0xff, 0xff, 0x00, 0x02];
assert_eq!(dec(&p(2, 1, 16, 3), &enc).unwrap(), [0x01, 0x00, 0x00, 0xff, 0x01, 0x01]);
}
#[test]
fn tiff_sub_byte() {
// 1 bpc, 1 color, 10 columns: bits 1 0 0 1 1 0 0 0 | 1 1 (pad 000000)
// Cumulative XOR: 1 1 1 0 1 1 1 1 | 0 1
let enc = [0b1001_1000, 0b1100_0000];
assert_eq!(dec(&p(2, 1, 1, 10), &enc).unwrap(), [0b1110_1111, 0b0100_0000]);
// 4 bpc, 1 color, 3 columns: samples 5, 15, 3 -> 5, 4, 7 (mod 16).
assert_eq!(dec(&p(2, 1, 4, 3), &[0x5f, 0x30]).unwrap(), [0x54, 0x70]);
// 2 bpc, 2 colors, 2 columns: samples 1 2 3 3 -> 1 2 0 1.
assert_eq!(dec(&p(2, 2, 2, 2), &[0b0110_1111]).unwrap(), [0b0110_0001]);
// Padding bits are left alone.
assert_eq!(dec(&p(2, 1, 4, 1), &[0x5f]).unwrap(), [0x5f]);
}
#[test]
fn tiff_partial_rows() {
// 16-bit with an odd trailing byte: the lone byte is left as is.
assert_eq!(dec(&p(2, 1, 16, 4), &[0, 1, 0, 1, 9]).unwrap(), [0, 1, 0, 2, 9]);
assert_eq!(dec(&p(2, 1, 8, 4), &[1, 1, 1, 1, 2, 2]).unwrap(), [1, 2, 3, 4, 2, 4]);
}
#[test]
fn invalid_params() {
for bad in [p(2, 1, 3, 1), p(12, 0, 8, 1), p(12, 1, 8, 0), p(3, 1, 8, 1), p(12, 1, 8, i64::MAX), p(2, 300, 8, 1)] {
assert!(matches!(dec(&bad, &[0, 1, 2]), Err(CodecError::Corrupt { .. })), "{bad:?}");
assert!(encode(&bad, &[1, 2]).is_err());
}
// Huge but representable Columns does not allocate a huge row.
assert_eq!(dec(&p(12, 1, 8, 1 << 40), &[2, 7, 8]).unwrap(), [7, 8]);
}
#[test]
fn encode_choices() {
let data = [1u8, 2, 3, 4, 5, 6];
let up = encode(&p(12, 1, 8, 3), &data).unwrap();
assert_eq!(up, [2, 1, 2, 3, 2, 3, 3, 3]);
let paeth = encode(&p(14, 1, 8, 3), &data).unwrap();
assert_eq!(paeth[0], 4);
assert_eq!(paeth[4], 4);
// Per-row optimum picks Sub for a ramp.
let ramp: Vec<u8> = (0..64).collect();
let opt = encode(&p(15, 1, 8, 64), &ramp).unwrap();
assert_eq!(opt[0], 1);
for pred in [10, 11, 12, 13, 14, 15] {
let e = encode(&p(pred, 1, 8, 3), &data).unwrap();
assert_eq!(dec(&p(pred, 1, 8, 3), &e).unwrap(), data);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
//! RunLengthDecode (ISO 32000-2 §7.4.5): a length byte L of 0–127 copies the next
//! L + 1 bytes, 129–255 repeats the next byte 257 − L times, 128 is EOD.
//! A missing EOD is tolerated; a truncated run is corrupt.
use crate::{Failure, Step};
const NAME: &str = "RunLengthDecode";
pub fn decode(data: &[u8], max: usize) -> Step {
let mut out = Vec::with_capacity(data.len().saturating_mul(2).min(max));
let mut i = 0;
while let Some(&l) = data.get(i) {
i += 1;
match l {
128 => break,
0..=127 => {
let n = usize::from(l) + 1;
let avail = n.min(data.len() - i);
if out.len() + avail > max {
return Err(Failure::limit(max));
}
out.extend_from_slice(&data[i..i + avail]);
i += avail;
if avail < n {
return Err(Failure::corrupt(NAME, "truncated literal run", out));
}
}
_ => {
let Some(&b) = data.get(i) else {
return Err(Failure::corrupt(NAME, "truncated repeat run", out));
};
i += 1;
let n = 257 - usize::from(l);
if out.len() + n > max {
return Err(Failure::limit(max));
}
out.resize(out.len() + n, b);
}
}
}
Ok(out)
}
pub fn encode(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len() + data.len() / 128 + 2);
let n = data.len();
let mut i = 0;
while i < n {
let mut run = 1;
while i + run < n && run < 128 && data[i + run] == data[i] {
run += 1;
}
if run >= 2 {
out.push((257 - run) as u8);
out.push(data[i]);
i += run;
continue;
}
// Literal: extend until a repeat begins or 128 bytes.
let start = i;
let mut j = i + 1;
while j < n && j - start < 128 && !(j + 1 < n && data[j] == data[j + 1]) {
j += 1;
}
out.push((j - start - 1) as u8);
out.extend_from_slice(&data[start..j]);
i = j;
}
out.push(128);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodecError;
fn dec(d: &[u8]) -> Result<Vec<u8>, CodecError> {
decode(d, 10_000).map_err(|f| f.error)
}
#[test]
fn vectors() {
assert_eq!(dec(&[2, b'a', b'b', b'c', 254, b'x', 128]).unwrap(), b"abcxxx");
assert_eq!(dec(&[129, 7, 128]).unwrap(), [7; 128]);
assert!(dec(&[127]).is_err());
assert_eq!(dec(&[0, b'q']).unwrap(), b"q"); // missing EOD
assert_eq!(dec(&[128, 0, b'q']).unwrap(), b""); // data after EOD ignored
assert_eq!(dec(&[]).unwrap(), b"");
}
#[test]
fn truncated() {
let f = decode(&[4, b'a', b'b'], 100).err().unwrap();
assert!(matches!(f.error, CodecError::Corrupt { .. }));
assert_eq!(f.partial, b"ab");
assert!(matches!(dec(&[0, b'a', 200]), Err(CodecError::Corrupt { .. })));
}
#[test]
fn limit() {
assert!(matches!(decode(&[129, 0, 129, 0], 200).map_err(|f| f.error), Err(CodecError::LimitExceeded(200))));
assert!(matches!(decode(&[3, 1, 2, 3, 4], 3).map_err(|f| f.error), Err(CodecError::LimitExceeded(3))));
assert_eq!(decode(&[129, 0, 129, 0], 256).ok().unwrap().len(), 256);
}
#[test]
fn encoding() {
assert_eq!(encode(b"abcxxx"), [2, b'a', b'b', b'c', 254, b'x', 128]);
assert_eq!(encode(b""), [128]);
assert_eq!(encode(&[5; 300]), [129, 5, 129, 5, 213, 5, 128]);
let lit: Vec<u8> = (0..200u8).collect();
let e = encode(&lit);
assert_eq!((e[0], e[129]), (127, 71));
assert_eq!(dec(&e).unwrap(), lit);
}
}