mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
* craft-psd: Photoshop PSD/PSB reader and writer, moved from PhotoCraft PhotoCraft's `photocraft-psd` crate (crates/psd at storytold/photocraft 63b641b55e54bf8f5c83efd977fdb16a2c255431) as `craft-psd`: the lossless PSD/PSB model, reader, writer and builder, descriptors, the Photoshop resource files that share its primitives (.abr, .asl, .aco, .act, .ase, .grd, .pat, .atn, .csh, .shc) and the Photoshop blocks of a layered TIFF. The crate had no dependency on any other PhotoCraft crate. Code is unchanged apart from the crate name, craft-libs' rustfmt width, comments that named other PhotoCraft crates, issue references (now `photocraft#N`), and the corpus tests' directory (`CRAFT_PSD_CORPUS` or `corpus/` at the workspace root). Tests, proptests, fuzz targets and fixtures move with it. Part of #778, #786. * craft-psd: engine_data, the text engine document of type layers The EngineData reader and writer from PhotoCraft's photocraft-text (crates/text/src/engine_data.rs at storytold/photocraft 63b641b5): a tree of values that is written back in Photoshop's layout, with strings stored as plain bytes kept as plain bytes. From EffectCraft's effectcraft-psd (crates/psd/src/engine_data.rs at storytold/effectcraft 072e1af9), ported onto that tree: `first_style`, the first style run as a typed `TextStyle` with bounded sizes and spacings, and `minimal`, a one-run document for writers and fixtures. `Value::index` is new. Both apps had their own parser for the same syntax. * craft-psd: engine_data::parse_lossy reads what it can of a damaged document EffectCraft's EngineData reader was best effort: a truncated or damaged document still gave the text and style it held. `parse_lossy` does that on the same parser: a truncated dictionary, array or string keeps what it has, a byte that starts no key is skipped, a token that is no number becomes a name, and garbage gives an empty dictionary. `parse` stays strict and is unchanged in what it accepts. The parser's `value` is split into one function per kind of value, and reads with `get` instead of indexing. * craft-psd: typed views of type layers, smart objects and adjustments Read-only views EffectCraft's effectcraft-psd had over the blocks this crate keeps raw (storytold/effectcraft f334bcf3, crates/psd/src/parse.rs and placed.rs). The blocks stay raw and write back unchanged. - `text`: `TypeLayer` from `TySh`: text, transform (the identity when not finite), bounds, the first style run (read with `engine_data::parse_lossy`) and the text descriptor. `LayerRecord::type_layer`. - `smart`: `SmartObject` from `SoLd`/`SoLE` (id, quad, non-affine transform, size, warp) and `PlLd`; `Warp` with the named styles and the custom quilt mesh; the quad geometry (`map`, `placed_bounds`, `placed_size`, `bake_scale`, `placement`). `LinkedFile` and `linked_files` for `lnk2`/`lnkD`/`lnk3`/`lnkE`, `PsdFile::linked_files` and `PsdFile::linked_data`. `LayerRecord::smart_object`. - `adjust`: `Adjustment` from `nvrt`, `brit`, `CgEd`, `hue2`/`hue `, `levl`, `expA`, and the other adjustment keys by name; `LayerRecord::adjustment`, `solid_color` (`SoCo`) and `effects` (`lfx2`). - `Descriptor`: `object`, `number`, `numbers`, `boolean`, `text`, `enumerated`, `raw_data`, `color`; `Value::as_number`. Linked-file data is a range of the block's data, checked against the record, instead of a range of the whole file. * craft-psd: shared descriptor lookups Descriptor::get_bool, get_number, get_text, get_enum and get_object, used by asl, grd and slices in place of their own copies. Ported from storytold/photocraft#9552 (its crates/psd part). * craft-psd: the typed views use the shared descriptor lookups One set of Descriptor accessors: the get_* lookups from photocraft#9552. numbers, raw_data and color move next to them as get_numbers, get_raw_data and get_color. * craft-psd: global blocks can stay in the source, and a preview of the merged image - `PsdFile::from_source_deferring` leaves the data of each global block a callback picks in the source (`TaggedBlock::stored`), so an embedded smart-object file that is most of a PSD is not read on open. `TaggedBlock::encoded`, `encoded_data_len` and `load` give the data wherever it is; writing copies it from the source; equality compares the bytes, and the same range of the same source without reading it. `parsed` refuses a block still in the source. - `FileSource` fails a read after the file changed in place (length or modification time), instead of mixing old offsets with new bytes. - `PsdFile::merged_preview(max_side)` samples every k-th row and pixel of a raw or RLE RGB/gray merged image, 8/16/32-bit (32-bit encoded to sRGB), reading a few megabytes of a large file. `ImageData::row_decoder` decodes the merged image's rows on their own. Ported from storytold/photocraft#11586, which contains #6589 and #6616's crates/psd changes (photocraft#5106). * craft-psd: linked files read deferred blocks, and the preview size is capped - `PsdFile::linked_files` and `linked_data` read a linked-layer block through `TaggedBlock::encoded`, so a block left in the source is found; they return `Result`, and `linked_data` a `Cow` borrowed from a block in memory. - `merged_preview` refuses an output over `MAX_DECODED_BYTES` before it decodes or allocates (the size is the caller's), and a failed allocation is `OutOfMemory`. --------- Co-authored-by: Omar Saidahmed <osaidahm@uwaterloo.ca> Co-authored-by: Phil Trinh <ptrinh@me.com>
266 lines
11 KiB
Rust
266 lines
11 KiB
Rust
//! Decoding a channel a band of rows at a time: [`ChannelData::row_decoder`].
|
||
//!
|
||
//! [`ChannelData::decode`] returns the whole plane, so turning a layer into tiles holds every
|
||
//! channel's plane plus an interleaved copy at once: for a 20000×15000 RGBA layer that is 2.4 GB
|
||
//! on top of the tiles. A [`RowDecoder`] hands out any band of rows instead. RLE and raw channels
|
||
//! are read band by band (a channel still in its [`Stored`](crate::Stored) source reads only the
|
||
//! band's bytes: RLE's per-row byte counts give each row's offset); ZIP streams cannot be entered
|
||
//! part way, so a ZIP channel is inflated once, when the decoder is made.
|
||
|
||
use std::borrow::Cow;
|
||
|
||
use crate::compression::{Compression, MAX_DECODED_BYTES, PlaneLayout, decode_planes, packbits};
|
||
use crate::error::{PsdError, Result};
|
||
use crate::header::Version;
|
||
use crate::layer::ChannelData;
|
||
use crate::source::Stored;
|
||
|
||
/// Where a channel's encoded bytes are.
|
||
enum Bytes<'a> {
|
||
Memory(&'a [u8]),
|
||
Source(&'a Stored),
|
||
}
|
||
|
||
impl Bytes<'_> {
|
||
fn len(&self) -> u64 {
|
||
match self {
|
||
Bytes::Memory(b) => b.len() as u64,
|
||
Bytes::Source(s) => s.len,
|
||
}
|
||
}
|
||
|
||
fn get(&self, at: u64, len: u64) -> Result<Cow<'_, [u8]>> {
|
||
match self {
|
||
Bytes::Memory(b) => {
|
||
let range = usize::try_from(at).ok().zip(usize::try_from(len).ok()).and_then(|(a, n)| b.get(a..a.checked_add(n)?));
|
||
range.map(Cow::Borrowed).ok_or(PsdError::UnexpectedEof {
|
||
offset: usize::try_from(at).unwrap_or(usize::MAX),
|
||
needed: usize::try_from(len).unwrap_or(usize::MAX),
|
||
})
|
||
}
|
||
Bytes::Source(s) => {
|
||
if at.checked_add(len).is_none_or(|end| end > s.len) {
|
||
return Err(PsdError::UnexpectedEof {
|
||
offset: usize::try_from(at).unwrap_or(usize::MAX),
|
||
needed: usize::try_from(len).unwrap_or(usize::MAX),
|
||
});
|
||
}
|
||
s.read_part(at, len).map(Cow::Owned)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
enum Kind<'a> {
|
||
/// Rows at `row * row_bytes`.
|
||
Raw(Bytes<'a>),
|
||
/// `offsets[r]..offsets[r + 1]` are row `r`'s PackBits bytes (offsets from the start of the
|
||
/// data, after the count table).
|
||
Rle { bytes: Bytes<'a>, offsets: Vec<u64> },
|
||
/// The whole decoded plane.
|
||
Decoded(Vec<u8>),
|
||
/// No data and no rows.
|
||
Empty,
|
||
}
|
||
|
||
/// Decodes bands of rows of one channel: see the [module docs](self).
|
||
pub struct RowDecoder<'a> {
|
||
kind: Kind<'a>,
|
||
row_bytes: usize,
|
||
height: usize,
|
||
}
|
||
|
||
impl ChannelData {
|
||
/// A decoder of this channel's rows, for a `width × height` plane. Fails where
|
||
/// [`Self::decode`] would on everything it can check without decoding the rows (sizes, the
|
||
/// RLE count table, the whole of a ZIP stream); a damaged row fails when its band is decoded.
|
||
pub fn row_decoder(&self, width: usize, height: usize, depth: u16, version: Version) -> Result<RowDecoder<'_>> {
|
||
let layout = PlaneLayout { planes: 1, width, height, depth, version };
|
||
let bytes = match &self.stored {
|
||
Some(s) => Bytes::Source(s),
|
||
None => Bytes::Memory(&self.data),
|
||
};
|
||
RowDecoder::new(self.compression, bytes, &layout, || self.encoded())
|
||
}
|
||
}
|
||
|
||
impl crate::image_data::ImageData {
|
||
/// A decoder of the merged image's rows: its planes one after another, so plane `p`'s row
|
||
/// `r` is row `p × height + r`. As [`ChannelData::row_decoder`]; only a ZIP merged image is
|
||
/// decoded whole (and so only one within `MAX_DECODED_BYTES`).
|
||
pub fn row_decoder(&self, header: &crate::header::Header) -> Result<RowDecoder<'_>> {
|
||
let layout = Self::layout(header);
|
||
let bytes = match &self.stored {
|
||
Some(s) => Bytes::Source(s),
|
||
None => Bytes::Memory(&self.data),
|
||
};
|
||
RowDecoder::new(Some(self.compression), bytes, &layout, || self.encoded())
|
||
}
|
||
}
|
||
|
||
impl<'a> RowDecoder<'a> {
|
||
fn new(
|
||
compression: Option<Compression>,
|
||
bytes: Bytes<'a>,
|
||
layout: &PlaneLayout,
|
||
encoded: impl FnOnce() -> Result<Cow<'a, [u8]>>,
|
||
) -> Result<Self> {
|
||
// The whole-plane cap applies to a whole decode (ZIP) only: rows are decoded a band at a
|
||
// time, each band within the cap.
|
||
let total = layout.total_bytes()?;
|
||
let row_bytes = layout.row_bytes();
|
||
let height = layout.rows()?;
|
||
let kind = match compression {
|
||
None if total == 0 => Kind::Empty,
|
||
None => return Err(PsdError::invalid("channel has no data")),
|
||
Some(Compression::Raw) => {
|
||
if bytes.len() < total {
|
||
return Err(PsdError::UnexpectedEof {
|
||
offset: usize::try_from(bytes.len()).unwrap_or(usize::MAX),
|
||
needed: usize::try_from(total - bytes.len()).unwrap_or(usize::MAX),
|
||
});
|
||
}
|
||
Kind::Raw(bytes)
|
||
}
|
||
Some(Compression::Rle) => {
|
||
let size: u64 = if layout.version.is_psb() { 4 } else { 2 };
|
||
let table_len = (height as u64).saturating_mul(size);
|
||
if table_len > bytes.len() {
|
||
return Err(PsdError::UnexpectedEof { offset: 0, needed: usize::try_from(table_len - bytes.len()).unwrap_or(usize::MAX) });
|
||
}
|
||
let table = bytes.get(0, table_len)?;
|
||
let mut offsets = Vec::new();
|
||
offsets.try_reserve_exact(height + 1).map_err(|_| PsdError::OutOfMemory("not enough memory for the RLE row table"))?;
|
||
let mut at = table_len;
|
||
offsets.push(at);
|
||
for c in table.chunks_exact(size as usize) {
|
||
let n = match *c {
|
||
[a, b] => u64::from(u16::from_be_bytes([a, b])),
|
||
[a, b, c, d] => u64::from(u32::from_be_bytes([a, b, c, d])),
|
||
_ => 0,
|
||
};
|
||
at = at.saturating_add(n);
|
||
offsets.push(at);
|
||
}
|
||
let data_len = bytes.len() - table_len;
|
||
// As `decode_planes` checks: PackBits expands at most 64x.
|
||
if total > data_len.saturating_mul(64) {
|
||
return Err(PsdError::Decompress("RLE data too short for declared size".into()));
|
||
}
|
||
if at > bytes.len() {
|
||
return Err(PsdError::UnexpectedEof {
|
||
offset: usize::try_from(bytes.len()).unwrap_or(usize::MAX),
|
||
needed: usize::try_from(at - bytes.len()).unwrap_or(usize::MAX),
|
||
});
|
||
}
|
||
Kind::Rle { bytes, offsets }
|
||
}
|
||
Some(c) => Kind::Decoded(decode_planes(c, &encoded()?, layout)?),
|
||
};
|
||
Ok(RowDecoder { kind, row_bytes, height })
|
||
}
|
||
}
|
||
|
||
impl RowDecoder<'_> {
|
||
/// Bytes per decoded row.
|
||
pub fn row_bytes(&self) -> usize {
|
||
self.row_bytes
|
||
}
|
||
|
||
/// Number of rows.
|
||
pub fn height(&self) -> usize {
|
||
self.height
|
||
}
|
||
|
||
/// Rows `r0..r1` as planar big-endian samples (`(r1 - r0) × row_bytes` bytes).
|
||
pub fn rows(&self, r0: usize, r1: usize) -> Result<Vec<u8>> {
|
||
if r0 > r1 || r1 > self.height {
|
||
return Err(PsdError::invalid(format!("rows {r0}..{r1} outside 0..{}", self.height)));
|
||
}
|
||
let n = (r1 - r0).checked_mul(self.row_bytes).ok_or(PsdError::LimitExceeded("band size overflow"))?;
|
||
if n as u64 > MAX_DECODED_BYTES {
|
||
return Err(PsdError::LimitExceeded("decoded channel data exceeds MAX_DECODED_BYTES"));
|
||
}
|
||
let mut out = Vec::new();
|
||
out.try_reserve_exact(n).map_err(|_| PsdError::OutOfMemory("not enough memory for the decoded channel data"))?;
|
||
match &self.kind {
|
||
Kind::Empty => {}
|
||
Kind::Raw(bytes) => out.extend_from_slice(&bytes.get(r0 as u64 * self.row_bytes as u64, n as u64)?),
|
||
Kind::Decoded(plane) => out
|
||
.extend_from_slice(plane.get(r0 * self.row_bytes..r1 * self.row_bytes).ok_or_else(|| PsdError::invalid("decoded plane too short"))?),
|
||
Kind::Rle { bytes, offsets } => {
|
||
let (Some(&start), Some(&end)) = (offsets.get(r0), offsets.get(r1)) else {
|
||
return Err(PsdError::invalid("RLE row table too short"));
|
||
};
|
||
let band = bytes.get(start, end - start)?;
|
||
for r in r0..r1 {
|
||
let (Some(&a), Some(&b)) = (offsets.get(r), offsets.get(r + 1)) else {
|
||
return Err(PsdError::invalid("RLE row table too short"));
|
||
};
|
||
let row = band.get((a - start) as usize..(b - start) as usize).ok_or_else(|| PsdError::invalid("RLE row outside its band"))?;
|
||
packbits::decode_into(row, self.row_bytes, &mut out)?;
|
||
}
|
||
}
|
||
}
|
||
Ok(out)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use crate::source::Source;
|
||
use crate::{PsdFile, testgen};
|
||
|
||
/// Every channel of every generated file, decoded in uneven bands from memory and from a
|
||
/// source, is its whole-plane decode.
|
||
#[test]
|
||
fn bands_join_up_to_the_whole_plane() {
|
||
let mut checked = 0;
|
||
for case in testgen::all_cases() {
|
||
let bytes = case.file.to_bytes().unwrap();
|
||
let eager = PsdFile::from_bytes(&bytes).unwrap();
|
||
let lazy = PsdFile::from_source(Source::new(bytes)).unwrap();
|
||
let (d, v) = (eager.header.depth, eager.header.version);
|
||
for file in [&eager, &lazy] {
|
||
for rec in file.layers() {
|
||
for ch in &rec.channels {
|
||
let (w, h) = rec.channel_rect(ch.id).size().unwrap();
|
||
let whole = ch.decode(w, h, d, v);
|
||
let dec = ch.row_decoder(w, h, d, v);
|
||
let (Ok(whole), Ok(dec)) = (whole, dec) else { continue };
|
||
let mut joined = Vec::new();
|
||
let mut r = 0;
|
||
for step in [1, 3, 2, 7].iter().cycle() {
|
||
if r >= h {
|
||
break;
|
||
}
|
||
let r1 = (r + step).min(h);
|
||
joined.extend(dec.rows(r, r1).unwrap());
|
||
r = r1;
|
||
}
|
||
assert_eq!(joined, whole, "{}", case.name);
|
||
assert!(dec.rows(0, h + 1).is_err());
|
||
checked += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assert!(checked > 100, "{checked}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_damaged_rle_row_fails_its_band() {
|
||
use crate::Compression;
|
||
use crate::layer::ChannelData;
|
||
// Two rows of 4 bytes: a valid row, then one that ends early.
|
||
let mut data = vec![0, 2, 0, 2];
|
||
data.extend([0xFD, 7]); // header -3: 1 - (-3) = 4 copies of 7
|
||
data.extend([0x01, 9]); // a literal of 2 bytes, only 1 present
|
||
let ch = ChannelData::new(0, Some(Compression::Rle), data);
|
||
let dec = ch.row_decoder(4, 2, 8, crate::Version::Psd).unwrap();
|
||
assert_eq!(dec.rows(0, 1).unwrap(), vec![7; 4]);
|
||
assert!(dec.rows(1, 2).is_err());
|
||
assert!(ch.decode(4, 2, 8, crate::Version::Psd).is_err());
|
||
}
|
||
}
|