Files
craft-libs/crates/psd/src/rows.rs
T
f462bb8128 craft-psd: deferred global blocks and a merged-image preview (ported from PhotoCraft) (#1037)
* 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>
2026-11-11 20:56:55 -05:00

266 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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());
}
}