Merge pull request #540 from storytold/fix/eps-sources-restore
Fix #498, part of #505: EPS image sources read in turn, restore undoes dictionary changes (PLRM)
This commit is contained in:
commit
8c4c665ee2
File diff suppressed because one or more lines are too long
|
|
@ -953,6 +953,12 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
|
|||
"/T3 << /FontType 3 /FontMatrix [0.1 0 0 0.1 0 0] /FontBBox [0 0 9 9] /Encoding [/a /b] /BuildChar { pop pop 9 0 setcharwidth 0 0 5 5 rectfill } >> definefont setfont",
|
||||
"/T4 << /FontType 3 /FontMatrix [1 0 0 1 0 0] /BuildGlyph { pop pop (x) show } /Encoding [/a] >> definefont setfont",
|
||||
"glyphshow", "xshow", "xyshow", "awidthshow", "kshow", "/a", "[1 2 3]",
|
||||
// Several data sources reading one file in turn, and sources of uneven lengths.
|
||||
"/f currentfile /ASCIIHexDecode filter def", "{f 3 string readstring pop}", "{f 0 string readstring pop}", "true 3 colorimage",
|
||||
"3 2 8 [3 0 0 2 0 0] {f 5 string readstring pop} {(ab)} {f 1 string readstring pop} true 3 colorimage",
|
||||
"<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 4 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 2 0 0] /MultipleDataSources true /DataSource [(a) (bc) {f 2 string readstring pop} (d)] >>",
|
||||
// Local VM restored: dictionary changes undone, saves restored twice.
|
||||
"/a 1 def save /a 2 def", "restore", "save dup restore", "1 dict save exch /k 1 put", "cachestatus", "/n 0 def save /n n 1 add store",
|
||||
// Executable strings and keys other than names (PLRM 3rd ed., `cvx`, `load`).
|
||||
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
|
||||
])
|
||||
|
|
|
|||
|
|
@ -635,33 +635,51 @@ impl Interp<'_> {
|
|||
|
||||
/// The bytes a data procedure gives, called until it gives an empty string or `n` bytes.
|
||||
fn gather(&mut self, p: &Obj, n: usize) -> Res<Vec<u8>> {
|
||||
let mut out = vec![];
|
||||
while out.len() < n {
|
||||
self.call(p.clone())?;
|
||||
let s = self.pop_str()?;
|
||||
let s = s.borrow();
|
||||
if s.is_empty() {
|
||||
break;
|
||||
}
|
||||
self.alloc(s.len())?;
|
||||
out.extend_from_slice(&s);
|
||||
}
|
||||
out.truncate(n);
|
||||
Ok(out)
|
||||
Ok(self.read_sources(&[Source::Proc(p.clone())], n, n)?.pop().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// `n` bytes of image data from `src` (zeros past its end).
|
||||
fn source_bytes(&mut self, src: &Source, n: usize) -> Res<Vec<u8>> {
|
||||
let mut v = match src {
|
||||
Source::File(f) => self.read(f, n)?,
|
||||
Source::Str(s) if !s.is_empty() => s.iter().copied().cycle().take(n).collect(),
|
||||
Source::Str(_) => vec![],
|
||||
Source::Proc(p) => self.gather(p, n)?,
|
||||
};
|
||||
let mut v = self.read_sources(std::slice::from_ref(src), n, n)?.pop().unwrap_or_default();
|
||||
v.resize(n, 0);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Up to `n` bytes from each of `sources`, read in turn as an image reads several data sources
|
||||
/// (PLRM 3rd ed., §4.10 "Images" and `image`/`colorimage` in chapter 8: the sources are read
|
||||
/// in turn, the same amount from each): each procedure called once a round (its string,
|
||||
/// whatever its length), each file and string giving `step` bytes a round, until each has
|
||||
/// given `n` bytes or ended. Procedures that read one file get their own rows that way.
|
||||
fn read_sources(&mut self, sources: &[Source], n: usize, step: usize) -> Res<Vec<Vec<u8>>> {
|
||||
let mut out: Vec<Vec<u8>> = vec![vec![]; sources.len()];
|
||||
let mut ended = vec![false; sources.len()];
|
||||
while out.iter().zip(&ended).any(|(o, e)| o.len() < n && !e) {
|
||||
for ((src, o), e) in sources.iter().zip(out.iter_mut()).zip(ended.iter_mut()) {
|
||||
if *e || o.len() >= n {
|
||||
continue;
|
||||
}
|
||||
let want = step.min(n - o.len());
|
||||
let chunk = match src {
|
||||
Source::File(f) => self.read(f, want)?,
|
||||
// A string gives its bytes over and over.
|
||||
Source::Str(s) => {
|
||||
let at = o.len() % s.len().max(1);
|
||||
s.get(at..).unwrap_or_default().iter().chain(s.iter().cycle()).take(want).copied().collect()
|
||||
}
|
||||
Source::Proc(p) => {
|
||||
self.call(p.clone())?;
|
||||
let s = self.pop_str()?.to_vec();
|
||||
self.alloc(s.len())?;
|
||||
s
|
||||
}
|
||||
};
|
||||
*e = chunk.is_empty();
|
||||
o.extend_from_slice(chunk.get(..chunk.len().min(n - o.len())).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `eexec`: an encrypted font program follows. It is skipped (type is drawn in the app's
|
||||
/// fonts, by name): the font dictionary it would complete is defined as it is.
|
||||
fn eexec(&mut self) -> Res {
|
||||
|
|
@ -849,16 +867,12 @@ impl Interp<'_> {
|
|||
let multi = s.sources.len() > 1;
|
||||
let row_bits = if multi { s.w * bpc } else { s.w * n * bpc };
|
||||
let row = row_bits.div_ceil(8);
|
||||
let planes: Vec<Vec<u8>> = if multi {
|
||||
let mut v = vec![];
|
||||
for src in &s.sources {
|
||||
v.push(self.source_bytes(src, row * s.h)?);
|
||||
}
|
||||
v
|
||||
} else {
|
||||
let src = s.sources.first().ok_or(PsError::Ps("undefined", "DataSource".into()))?;
|
||||
vec![self.source_bytes(src, row * s.h)?]
|
||||
};
|
||||
// Several sources are read in turn, a row at a time each (zeros past their ends).
|
||||
let sources = if multi { &s.sources[..] } else { s.sources.get(..1).ok_or(PsError::Ps("undefined", "DataSource".into()))? };
|
||||
let mut planes = self.read_sources(sources, row * s.h, if multi { row } else { row * s.h })?;
|
||||
for p in &mut planes {
|
||||
p.resize(row * s.h, 0);
|
||||
}
|
||||
let max = ((1u32 << bpc.min(16)) - 1) as f64;
|
||||
let sample = |plane: &[u8], y: usize, i: usize| -> u32 {
|
||||
let bit = y * row * 8 + i * bpc;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ pub(crate) struct Interp<'a> {
|
|||
/// Bytes asked for so far (see [`MAX_MEMORY`]).
|
||||
allocated: usize,
|
||||
seed: u32,
|
||||
/// The `save`s not restored yet, oldest first.
|
||||
saves: Vec<SaveMark>,
|
||||
/// What dictionary entries were before they changed since the oldest `save` (dictionary,
|
||||
/// key, old value): `restore` puts them back, as restoring local VM does (PLRM 3rd ed.,
|
||||
/// §3.7.3 "Save and Restore": composite objects get the values they had at the `save`).
|
||||
journal: Vec<(DictRef, Key, Option<Obj>)>,
|
||||
/// Where the error that is unwinding was raised (see [`Fault`]).
|
||||
pub fault: Option<Fault>,
|
||||
/// The width a Type 3 glyph procedure gave (`setcachedevice`, `setcharwidth`).
|
||||
|
|
@ -72,6 +78,13 @@ pub(crate) struct Fault {
|
|||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
|
||||
/// Where a `save` was made: the graphics states saved and the journal's length then.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct SaveMark {
|
||||
gstates: usize,
|
||||
journal: usize,
|
||||
}
|
||||
|
||||
fn new_dict() -> DictRef {
|
||||
Rc::new(RefCell::new(Dict::new()))
|
||||
}
|
||||
|
|
@ -143,6 +156,8 @@ impl<'a> Interp<'a> {
|
|||
ops: 0,
|
||||
allocated: 0,
|
||||
seed: 1,
|
||||
saves: vec![],
|
||||
journal: vec![],
|
||||
fault: None,
|
||||
glyph_width: None,
|
||||
g,
|
||||
|
|
@ -190,6 +205,7 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
self.dicts.clear();
|
||||
self.stack.clear();
|
||||
self.journal.clear();
|
||||
}
|
||||
|
||||
// ---------- the operand stack ----------
|
||||
|
|
@ -879,7 +895,7 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Obj::Dict(d) => {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
insert(&d, key, v)?;
|
||||
self.insert(&d, key, Some(v))?;
|
||||
true
|
||||
}
|
||||
_ => return ps_err("typecheck", ""),
|
||||
|
|
@ -968,7 +984,7 @@ impl<'a> Interp<'a> {
|
|||
let v = self.pop()?;
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Load => {
|
||||
let k = self.pop()?;
|
||||
|
|
@ -981,7 +997,7 @@ impl<'a> Interp<'a> {
|
|||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).or(self.dicts.last()).cloned();
|
||||
let d = d.ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Known => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
|
|
@ -1001,7 +1017,8 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Undef => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
self.pop_dict()?.borrow_mut().remove(&k);
|
||||
let d = self.pop_dict()?;
|
||||
self.insert(&d, k, None)?;
|
||||
}
|
||||
CurrentDict => {
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
|
|
@ -1034,11 +1051,24 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Save => {
|
||||
self.gsave()?;
|
||||
self.push(Obj::Save(self.saved.len()))?;
|
||||
self.push(Obj::Save(self.saves.len()))?;
|
||||
self.saves.push(SaveMark { gstates: self.saved.len(), journal: self.journal.len() });
|
||||
}
|
||||
Restore => {
|
||||
let Obj::Save(level) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
while self.saved.len() >= level && !self.saved.is_empty() {
|
||||
let Obj::Save(i) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
// A save restored already (or through an outer one) is no longer valid: §3.7.3,
|
||||
// `invalidrestore`.
|
||||
let mark = self.saves.get(i).copied().ok_or(PsError::Ps("invalidrestore", String::new()))?;
|
||||
self.saves.truncate(i);
|
||||
// The dictionaries as they were at the save, newest change undone first.
|
||||
for (d, k, old) in self.journal.drain(mark.journal..).rev() {
|
||||
let mut d = d.borrow_mut();
|
||||
match old {
|
||||
Some(v) => d.insert(k, v),
|
||||
None => d.remove(&k),
|
||||
};
|
||||
}
|
||||
while self.saved.len() >= mark.gstates && !self.saved.is_empty() {
|
||||
self.grestore();
|
||||
}
|
||||
}
|
||||
|
|
@ -1083,9 +1113,12 @@ impl<'a> Interp<'a> {
|
|||
self.push(Obj::Int(1 << 20))?;
|
||||
}
|
||||
}
|
||||
// An empty font cache of a device's sizes, in `cachestatus`'s order (PLRM 3rd ed.,
|
||||
// chapter 8): bytes used and most, fonts used and most, glyphs used and most, and the
|
||||
// most bytes one cached glyph may take (positive on a real device: programs divide by it).
|
||||
CacheStatus => {
|
||||
for _ in 0..7 {
|
||||
self.push(Obj::Int(0))?;
|
||||
for v in [0, 1 << 20, 0, 1000, 0, 10_000, 25_000] {
|
||||
self.push(Obj::Int(v))?;
|
||||
}
|
||||
}
|
||||
StartJob => {
|
||||
|
|
@ -1143,7 +1176,9 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Obj::Dict(d) => {
|
||||
let from = self.pop_dict()?.borrow().clone();
|
||||
d.borrow_mut().extend(from);
|
||||
for (k, v) in from {
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
self.push(Obj::Dict(d))?;
|
||||
}
|
||||
_ => return ps_err("typecheck", "copy"),
|
||||
|
|
@ -1219,6 +1254,26 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d` (`None`: remove it), within [`MAX_ALLOC`] entries; while a `save`
|
||||
/// is open the old value is kept for `restore`.
|
||||
fn insert(&mut self, d: &DictRef, k: Key, v: Option<Obj>) -> Res {
|
||||
let old = {
|
||||
let mut dm = d.borrow_mut();
|
||||
if v.is_some() && dm.len() >= MAX_ALLOC && !dm.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
match v {
|
||||
Some(v) => dm.insert(k.clone(), v),
|
||||
None => dm.remove(&k),
|
||||
}
|
||||
};
|
||||
if !self.saves.is_empty() {
|
||||
self.alloc(2 * std::mem::size_of::<Obj>())?;
|
||||
self.journal.push((d.clone(), k, old));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------- resources ----------
|
||||
|
||||
/// The resources of `category`.
|
||||
|
|
@ -1293,16 +1348,6 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
||||
fn insert(d: &DictRef, k: Key, v: Obj) -> Res {
|
||||
let mut d = d.borrow_mut();
|
||||
if d.len() >= MAX_ALLOC && !d.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
d.insert(k, v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An array or string index.
|
||||
fn index(k: &Obj) -> Res<usize> {
|
||||
match *k {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ mod tests_fontnames;
|
|||
mod tests_generators;
|
||||
#[cfg(test)]
|
||||
mod tests_illustrator;
|
||||
#[cfg(test)]
|
||||
mod tests_images;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ fn a85_flate(data: &[u8]) -> String {
|
|||
}
|
||||
|
||||
/// Pixel `(x, y)` (straight RGBA) of the first image in `d`.
|
||||
fn pixel(d: &Document, x: u32, y: u32) -> [u8; 4] {
|
||||
pub(super) fn pixel(d: &Document, x: u32, y: u32) -> [u8; 4] {
|
||||
let im = all(d)
|
||||
.into_iter()
|
||||
.find_map(|n| match &n.kind {
|
||||
|
|
@ -932,3 +932,30 @@ showpage"##;
|
|||
assert!(all(d).iter().any(|n| fill(n).and_then(Paint::color) == Some(Color::rgb(0.0, 1.0, 0.0))));
|
||||
assert_eq!(objects(d).len(), 3);
|
||||
}
|
||||
|
||||
/// `restore` puts local VM back as it was at the `save` (PLRM 3rd ed., §3.7.3 "Save and
|
||||
/// Restore"): dictionaries get back the entries they had, whatever changed them. A program that
|
||||
/// keeps a count of the saves it has open in a dictionary, and refuses to go past a depth, reads
|
||||
/// to its end only if each `restore` puts the count back (part of #505: files with many images,
|
||||
/// each drawn between `save` and `restore`).
|
||||
#[test]
|
||||
fn restore_puts_dictionaries_back() {
|
||||
let r = read(
|
||||
"/Depth 2 dict def Depth /open 0 put \
|
||||
/enter { save Depth /open get 1 add dup 8 gt { rangecheck } if Depth exch /open exch put } def \
|
||||
1 1 100 { pop enter 0 0 1 1 rectfill restore } for Depth /open get 0 eq { 10 10 5 5 rectfill } if",
|
||||
);
|
||||
clean(&r);
|
||||
assert_eq!(objects(&r.document).len(), 101);
|
||||
// `def`, `store`, `put`, `undef` and dictionary `copy` are undone, newest first.
|
||||
super::tests::check(
|
||||
"/a 1 def /d 2 dict def d /k 1 put save /a 2 def /b 3 def d /k 2 put d /k undef 1 dict dup /z 9 put d copy pop \
|
||||
/a 4 store restore a 1 eq /b where not and d /k get 1 eq and d /z known not and",
|
||||
);
|
||||
// A save restored (itself, or through an outer one) is no longer valid: `invalidrestore`.
|
||||
super::tests::check("save dup restore { restore } stopped");
|
||||
super::tests::check("save save exch restore { restore } stopped");
|
||||
// `cachestatus` gives a device's cache sizes; its last, the most bytes one cached glyph may
|
||||
// take, is positive (chapter 8), and programs divide by it.
|
||||
super::tests::check("cachestatus 7 1 roll 6 { pop } repeat 0 gt");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
//! Image data read as the PostScript Language Reference (3rd ed.) defines it: several data sources
|
||||
//! read in turn (§4.10 "Images", `image` and `colorimage` in chapter 8), rows padded to a whole
|
||||
//! byte at every bit depth and odd widths (§4.10), and Flate predictors undone row by row (§3.13,
|
||||
//! `FlateDecode` parameters) (#498).
|
||||
|
||||
use super::tests::read;
|
||||
use super::tests_generators::pixel;
|
||||
|
||||
/// Each pixel of the first image, row by row.
|
||||
fn pixels(r: &crate::Imported, w: u32, h: u32) -> Vec<[u8; 4]> {
|
||||
assert!(!r.preview && r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
(0..h).flat_map(|y| (0..w).map(move |x| (x, y))).map(|(x, y)| pixel(&r.document, x, y)).collect()
|
||||
}
|
||||
|
||||
/// A CMYK image whose four data sources are procedures reading one file, a row of one ink each:
|
||||
/// the image operator calls them in turn, so each gets its own rows. Read one after the other,
|
||||
/// the cyan source took the first four rows of all four inks and the image came out in
|
||||
/// horizontal bands (#498).
|
||||
#[test]
|
||||
fn procedures_reading_one_file_are_read_in_turn() {
|
||||
// 3 × 2 pixels; rows: C M Y K of row 0, then of row 1.
|
||||
let rows = ["ff0000", "00ff00", "000000", "000000", "000000", "000000", "ffff00", "0000ff"];
|
||||
let r = read(&format!(
|
||||
"/DeviceCMYK setcolorspace 30 20 scale /f currentfile /ASCIIHexDecode filter def \
|
||||
<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 8 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 -2 0 2] \
|
||||
/MultipleDataSources true /DataSource [ {{f 3 string readstring pop}} {{f 3 string readstring pop}} \
|
||||
{{f 3 string readstring pop}} {{f 3 string readstring pop}} ] >> image\n{}>",
|
||||
rows.concat()
|
||||
));
|
||||
let cyan = [0, 174, 239, 255];
|
||||
let magenta = [236, 0, 140, 255];
|
||||
let yellow = [255, 242, 0, 255];
|
||||
let black = [35, 31, 32, 255];
|
||||
let white = [255, 255, 255, 255];
|
||||
let px = pixels(&r, 3, 2);
|
||||
let near = |a: [u8; 4], b: [u8; 4]| a.iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 40);
|
||||
let want = [cyan, magenta, white, yellow, yellow, black];
|
||||
assert!(px.iter().zip(want).all(|(a, b)| near(*a, b)), "{px:?}");
|
||||
|
||||
// The operand form: `{r} {g} {b} true 3 colorimage`, procedures reading one file.
|
||||
let r = read(
|
||||
"30 20 scale /f currentfile /ASCIIHexDecode filter def \
|
||||
3 2 8 [3 0 0 -2 0 2] {f 3 string readstring pop} {f 3 string readstring pop} {f 3 string readstring pop} true 3 colorimage\n\
|
||||
ff0000 00ff00 0000ff 102030 405060 708090>",
|
||||
);
|
||||
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 64, 112, 255], [32, 80, 128, 255], [48, 96, 144, 255]]);
|
||||
}
|
||||
|
||||
/// Separate strings and files as the sources: each is read for its own plane, whatever the row.
|
||||
#[test]
|
||||
fn separate_sources_keep_their_planes() {
|
||||
let r = read("30 20 scale 3 2 8 [3 0 0 -2 0 2] <ff0000102030> <00ff00405060> <0000ff708090> true 3 colorimage");
|
||||
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 64, 112, 255], [32, 80, 128, 255], [48, 96, 144, 255]]);
|
||||
}
|
||||
|
||||
/// Rows of an odd width at 1, 2, 4, 12 and 16 bits a sample start at a byte (their last byte
|
||||
/// padded), in one source and in several.
|
||||
#[test]
|
||||
fn rows_of_every_depth_start_at_a_byte() {
|
||||
// 3 pixels a row, two rows: 0, max, mid / max, 0, 0 (grey), each row padded.
|
||||
let cases = [
|
||||
(1, "a0 80"), // 101(00000) 100(00000)
|
||||
(2, "cc c0"), // 11 00 11 (00), 11 00 00 (00)
|
||||
(4, "f0 80 f0 00"), // f 0 8 (0), f 0 0 (0)
|
||||
(12, "fff0008000 fff0000000"), // fff 000 800 (0), fff 000 000 (0)
|
||||
(16, "ffff00008000 ffff00000000"),
|
||||
];
|
||||
for (bpc, data) in cases {
|
||||
let data = data.replace(' ', "");
|
||||
let r = read(&format!("30 20 scale 3 2 {bpc} [3 0 0 -2 0 2] <{data}> image"));
|
||||
let px: Vec<u8> = pixels(&r, 3, 2).iter().map(|p| p[0]).collect();
|
||||
let want: &[u8] = match bpc {
|
||||
1 => &[255, 0, 255, 255, 0, 0],
|
||||
2 => &[255, 0, 255, 255, 0, 0],
|
||||
4 => &[255, 0, 136, 255, 0, 0],
|
||||
12 => &[255, 0, 128, 255, 0, 0],
|
||||
_ => &[255, 0, 128, 255, 0, 0],
|
||||
};
|
||||
assert_eq!(px, want, "{bpc} bits");
|
||||
}
|
||||
// Several 4-bit sources, odd width: each plane's rows padded on their own.
|
||||
let r = read("30 20 scale 3 2 4 [3 0 0 -2 0 2] <f0f000f0> <0f00f000> <00f00ff0> true 3 colorimage");
|
||||
let (on, off) = (255, 0);
|
||||
let want = [[on, off, off], [off, on, off], [on, off, on], [off, on, off], [off, off, on], [on, off, on]];
|
||||
assert_eq!(pixels(&r, 3, 2), want.map(|[r, g, b]| [r, g, b, 255]));
|
||||
}
|
||||
|
||||
/// A PNG-predicted Flate image of an odd width: every row filter (None, Sub, Up, Average, Paeth)
|
||||
/// undone on its own row.
|
||||
#[test]
|
||||
fn predicted_rows_of_an_odd_width() {
|
||||
// 3 RGB pixels a row, five rows; each row predicted with its own filter.
|
||||
let raw: Vec<[u8; 9]> = (0..5u8).map(|y| std::array::from_fn(|i| (i as u8).wrapping_mul(29).wrapping_add(y.wrapping_mul(53)))).collect();
|
||||
let mut encoded = vec![];
|
||||
let mut prev = [0u8; 9];
|
||||
for (kind, row) in raw.iter().enumerate() {
|
||||
encoded.push(kind as u8);
|
||||
for i in 0..9 {
|
||||
let a = if i >= 3 { row[i - 3] } else { 0 };
|
||||
let b = prev[i];
|
||||
let c = if i >= 3 { prev[i - 3] } else { 0 };
|
||||
let pred = match kind {
|
||||
1 => a,
|
||||
2 => b,
|
||||
3 => ((u16::from(a) + u16::from(b)) / 2) as u8,
|
||||
4 => {
|
||||
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
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
encoded.push(row[i].wrapping_sub(pred));
|
||||
}
|
||||
prev = *row;
|
||||
}
|
||||
let r = read(&format!(
|
||||
"/DeviceRGB setcolorspace 30 50 scale << /ImageType 1 /Width 3 /Height 5 /BitsPerComponent 8 /Decode [0 1 0 1 0 1] /ImageMatrix [3 0 0 -5 0 5] \
|
||||
/DataSource currentfile /ASCII85Decode filter << /Predictor 15 /Colors 3 /Columns 3 >> /FlateDecode filter >> image\n{}",
|
||||
crate::ps::ascii85(&crate::ps::deflate(&encoded))
|
||||
));
|
||||
let want: Vec<[u8; 4]> = raw.iter().flat_map(|row| (0..3).map(move |x| [row[3 * x], row[3 * x + 1], row[3 * x + 2], 255])).collect();
|
||||
assert_eq!(pixels(&r, 3, 5), want);
|
||||
}
|
||||
Loading…
Reference in New Issue