mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
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>
This commit is contained in:
11 files changed
+386
-53
No files matched your search
@@ -488,7 +488,7 @@ impl PsdFile {
|
||||
let (v, widen) = self.write_form();
|
||||
let psb = v.is_psb();
|
||||
let len_field = |long: bool| if long { 8u64 } else { 4 };
|
||||
let blocks_len = |blocks: &[TaggedBlock]| blocks.iter().fold(0u64, |n, b| n.saturating_add(b.encoded_len_aligned(v, 4) as u64));
|
||||
let blocks_len = |blocks: &[TaggedBlock]| blocks.iter().fold(0u64, |n, b| n.saturating_add(b.encoded_len_aligned(v, 4)));
|
||||
let body_len = |info: &LayerInfo| -> Result<u64> {
|
||||
let unpadded = info.unpadded_len_as(v, widen)?;
|
||||
let padding = info.padding.as_ref().map_or(unpadded % 2, |p| p.len() as u64);
|
||||
@@ -1051,9 +1051,10 @@ mod write_tests {
|
||||
let mut materialized = file.clone();
|
||||
let mut data = Vec::new();
|
||||
materialized.layer_info.take().unwrap().write_body(&mut data, version).unwrap();
|
||||
materialized
|
||||
.global_blocks
|
||||
.insert(index.min(materialized.global_blocks.len()), TaggedBlock { signature: *b"8B64", key, data, padding });
|
||||
materialized.global_blocks.insert(
|
||||
index.min(materialized.global_blocks.len()),
|
||||
TaggedBlock { signature: *b"8B64", key, data, padding, stored: None },
|
||||
);
|
||||
materialized.layer_info_placement = LayerInfoPlacement::Section;
|
||||
assert_eq!(actual, materialized.to_bytes().unwrap(), "{version:?} {key:?} {index}");
|
||||
}
|
||||
|
||||
@@ -322,8 +322,8 @@ fn unpadded_len_matches_the_written_body() {
|
||||
let mut rec = sample_record(version);
|
||||
rec.mask = MaskData::Mask(mask);
|
||||
rec.name = vec![b'n'; 300];
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"lsct", data: vec![1; 5], padding: Some(vec![0; 3]) });
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"Lr16", data: vec![2; 7], padding: None });
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"lsct", data: vec![1; 5], padding: Some(vec![0; 3]), stored: None });
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"Lr16", data: vec![2; 7], padding: None, stored: None });
|
||||
rec.extra_trailing = vec![9; 2];
|
||||
layers.push(rec);
|
||||
}
|
||||
|
||||
+45
-2
@@ -154,6 +154,31 @@ impl PsdFile {
|
||||
source: Source,
|
||||
want: impl FnOnce(&PsdFile, Option<Compression>, u64) -> Result<bool>,
|
||||
recover: impl FnOnce(&PsdFile) -> bool,
|
||||
) -> Result<(Self, Option<Result<Vec<u8>>>)> {
|
||||
Self::from_source_deferring_info(source, |_, _| false, want, recover)
|
||||
}
|
||||
|
||||
/// [`Self::from_source_and_merged_preflight`] that also leaves the data of each global block
|
||||
/// `defer(key, length)` accepts in the source ([`TaggedBlock::stored`]): an embedded
|
||||
/// smart-object block (`lnk2`) can be most of a file. Defer only blocks the reader loads
|
||||
/// before use (the linked-file blocks `lnk2`/`lnk3`/`lnkD`): a deferred block's `data` is
|
||||
/// empty, and [`TaggedBlock::parsed`] refuses it.
|
||||
pub fn from_source_deferring(
|
||||
source: Source,
|
||||
defer: impl Fn(&[u8; 4], u64) -> bool,
|
||||
want: impl FnOnce(&PsdFile) -> Result<bool>,
|
||||
recover: impl FnOnce(&PsdFile) -> bool,
|
||||
) -> Result<(Self, Option<Result<Vec<u8>>>)> {
|
||||
Self::from_source_deferring_info(source, defer, |file, _, _| want(file), recover)
|
||||
}
|
||||
|
||||
/// [`Self::from_source_deferring`] whose preflight also sees the merged image's compression
|
||||
/// and encoded length, as [`Self::from_source_and_merged_preflight_info`] gives them.
|
||||
pub fn from_source_deferring_info(
|
||||
source: Source,
|
||||
defer: impl Fn(&[u8; 4], u64) -> bool,
|
||||
want: impl FnOnce(&PsdFile, Option<Compression>, u64) -> Result<bool>,
|
||||
recover: impl FnOnce(&PsdFile) -> bool,
|
||||
) -> Result<(Self, Option<Result<Vec<u8>>>)> {
|
||||
let mut c = Cursor::new(&source);
|
||||
let header = Header::read(&mut Reader::new(&c.bytes(26)?))?;
|
||||
@@ -221,6 +246,7 @@ impl PsdFile {
|
||||
layer_info_placement = LayerInfoPlacement::GlobalBlock { index, signature, key, padding: padding.clone() };
|
||||
Ok(Some(()))
|
||||
},
|
||||
&defer,
|
||||
&mut global_blocks,
|
||||
)?;
|
||||
layer_mask_trailing = trailing;
|
||||
@@ -262,6 +288,7 @@ fn read_blocks<'a>(
|
||||
r: &mut Cursor<'a>,
|
||||
version: Version,
|
||||
mut consume: impl FnMut(usize, [u8; 4], [u8; 4], Cursor<'a>, &Option<Vec<u8>>) -> Result<Option<()>>,
|
||||
defer: &dyn Fn(&[u8; 4], u64) -> bool,
|
||||
blocks: &mut Vec<TaggedBlock>,
|
||||
) -> Result<Vec<u8>> {
|
||||
const ALIGNMENT: usize = 4;
|
||||
@@ -291,8 +318,13 @@ fn read_blocks<'a>(
|
||||
let padding = if pad_bytes.len() == default && pad_bytes.iter().all(|&b| b == 0) { None } else { Some(pad_bytes) };
|
||||
let (start, end) = (data.pos, data.end);
|
||||
if consume(blocks.len(), signature, key, data, &padding)?.is_none() {
|
||||
let data = Cursor { src: r.src, pos: start, end }.rest()?;
|
||||
blocks.push(TaggedBlock { signature, key, data, padding });
|
||||
if defer(&key, end - start) {
|
||||
let stored = Stored { source: r.src.clone(), offset: start, len: end - start };
|
||||
blocks.push(TaggedBlock { signature, key, data: Vec::new(), padding, stored: Some(stored) });
|
||||
} else {
|
||||
let data = Cursor { src: r.src, pos: start, end }.rest()?;
|
||||
blocks.push(TaggedBlock { signature, key, data, padding, stored: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,6 +468,17 @@ mod tests {
|
||||
assert!(lazy.to_bytes().unwrap() == written, "the lazily read PSD writes another PSB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_blocks_read_back_the_same() {
|
||||
for bytes in samples() {
|
||||
let eager = PsdFile::from_bytes(&bytes).unwrap();
|
||||
let (lazy, _) = PsdFile::from_source_deferring(Source::new(bytes.clone()), |_, _| true, |_| Ok(false), |_| false).unwrap();
|
||||
assert!(lazy.global_blocks.iter().all(|b| b.data.is_empty() && b.stored.is_some()));
|
||||
assert_eq!(lazy, eager);
|
||||
assert_eq!(lazy.to_bytes().unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy_parse_fails_where_the_eager_one_does() {
|
||||
for bytes in samples() {
|
||||
|
||||
@@ -82,6 +82,7 @@ pub mod metadata;
|
||||
pub mod path;
|
||||
pub mod patterns;
|
||||
pub mod pixels;
|
||||
pub mod preview;
|
||||
pub mod resources;
|
||||
pub mod rows;
|
||||
pub mod shc;
|
||||
@@ -109,6 +110,7 @@ pub use header::{ColorMode, Header, Version};
|
||||
pub use image_data::ImageData;
|
||||
pub use layer::{BlendingRanges, ChannelData, LayerFlags, LayerInfo, LayerMask, LayerRecord, MaskData, MaskParameters, RealMask, Rect};
|
||||
pub use pixels::{GrayImage, Layer, RgbaImage};
|
||||
pub use preview::Preview;
|
||||
pub use resources::{ImageResource, ResolutionInfo, ResourceData};
|
||||
pub use rows::RowDecoder;
|
||||
#[cfg(any(unix, windows))]
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//! A small picture of a file from its merged image, without decoding it whole:
|
||||
//! [`PsdFile::merged_preview`].
|
||||
//!
|
||||
//! Every `k`-th row of each plane is decoded (RLE and raw rows are read on their own, see
|
||||
//! [`crate::RowDecoder`]) and every `k`-th sample of it kept. For a file parsed with
|
||||
//! [`PsdFile::from_source`] that reads a few megabytes of a multi-gigabyte file, so the picture
|
||||
//! can show while the layers are still being decoded.
|
||||
|
||||
use crate::compression::Compression;
|
||||
use crate::error::Result;
|
||||
use crate::file::PsdFile;
|
||||
use crate::header::ColorMode;
|
||||
|
||||
/// A preview picture: RGBA, 8 bits per channel, row-major, opaque. A transparent document's
|
||||
/// merged image is stored matted with white, so the picture is the document over white.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Preview {
|
||||
/// Width in pixels.
|
||||
pub width: u32,
|
||||
/// Height in pixels.
|
||||
pub height: u32,
|
||||
/// `width × height × 4` bytes.
|
||||
pub rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PsdFile {
|
||||
/// The merged image reduced (by a whole factor, nearest sample) to fit `max_side`. `None` when
|
||||
/// there is no real merged image to show: an RGB or grayscale one (8/16/32-bit) stored raw
|
||||
/// or RLE (a ZIP stream would have to be inflated whole), and not flagged as a placeholder.
|
||||
pub fn merged_preview(&self, max_side: u32) -> Result<Option<Preview>> {
|
||||
let h = &self.header;
|
||||
let color = match h.color_mode {
|
||||
ColorMode::Rgb => 3usize,
|
||||
ColorMode::Grayscale => 1,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
if !matches!(h.depth, 8 | 16 | 32)
|
||||
|| !matches!(self.image_data.compression, Compression::Raw | Compression::Rle)
|
||||
|| self.has_real_merged_data() == Some(false)
|
||||
|| usize::from(h.channels) < color
|
||||
|| h.width == 0
|
||||
|| h.height == 0
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let k = h.width.max(h.height).div_ceil(max_side.max(1)).max(1);
|
||||
let (w, ht) = (h.width.div_ceil(k), h.height.div_ceil(k));
|
||||
let bps = usize::from(h.depth / 8);
|
||||
// At most `max_side`² pixels, but `max_side` is the caller's: checked and fallible.
|
||||
let n = (w as usize)
|
||||
.checked_mul(ht as usize)
|
||||
.and_then(|p| p.checked_mul(4))
|
||||
.filter(|&n| n as u64 <= crate::compression::MAX_DECODED_BYTES)
|
||||
.ok_or(crate::error::PsdError::LimitExceeded("preview exceeds MAX_DECODED_BYTES"))?;
|
||||
let dec = self.image_data.row_decoder(h)?;
|
||||
let mut rgba = Vec::new();
|
||||
rgba.try_reserve_exact(n).map_err(|_| crate::error::PsdError::OutOfMemory("not enough memory for the preview"))?;
|
||||
rgba.resize(n, 255u8);
|
||||
for y in 0..ht {
|
||||
let src_y = (y * k) as usize;
|
||||
for p in 0..color {
|
||||
let r = p * h.height as usize + src_y;
|
||||
let row = dec.rows(r, r + 1)?;
|
||||
for x in 0..w as usize {
|
||||
let at = x * k as usize * bps;
|
||||
let Some(s) = row.get(at..at + bps) else { continue };
|
||||
let v = match *s {
|
||||
[a] => a,
|
||||
[a, _] => a,
|
||||
// 32-bit samples are linear light: encoded for display (sRGB) first.
|
||||
[a, b, c, d] => (srgb_encode(f32::from_be_bytes([a, b, c, d]).clamp(0.0, 1.0)) * 255.0).round() as u8,
|
||||
_ => 0,
|
||||
};
|
||||
let px = (y as usize * w as usize + x) * 4;
|
||||
if color == 1 {
|
||||
rgba[px..px + 3].fill(v);
|
||||
} else {
|
||||
rgba[px + p] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Preview { width: w, height: ht, rgba }))
|
||||
}
|
||||
}
|
||||
|
||||
/// The sRGB transfer of a linear value in `0..=1`.
|
||||
fn srgb_encode(v: f32) -> f32 {
|
||||
if v <= 0.003_130_8 { v * 12.92 } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::source::Source;
|
||||
use crate::{Compression, PsdFile, Version, testgen};
|
||||
|
||||
/// The preview of a file read from a source is every k-th pixel of the merged image's
|
||||
/// colour planes, opaque.
|
||||
#[test]
|
||||
fn preview_samples_the_merged_image() {
|
||||
for c in [Compression::Raw, Compression::Rle] {
|
||||
let file = testgen::small(Version::Psd, c);
|
||||
let bytes = file.to_bytes().unwrap();
|
||||
let lazy = PsdFile::from_source(Source::new(bytes)).unwrap();
|
||||
let planes = file.decode_merged().unwrap();
|
||||
let (w, h) = (file.header.width, file.header.height);
|
||||
for max in [2, 3, 1000] {
|
||||
let p = lazy.merged_preview(max).unwrap().unwrap();
|
||||
let k = w.max(h).div_ceil(max);
|
||||
assert_eq!((p.width, p.height), (w.div_ceil(k), h.div_ceil(k)));
|
||||
for y in 0..p.height {
|
||||
for x in 0..p.width {
|
||||
let b = ((y * p.width + x) * 4) as usize;
|
||||
for ch in 0..3u32 {
|
||||
let a = (ch * w * h + y * k * w + x * k) as usize;
|
||||
assert_eq!(p.rgba[b + ch as usize], planes[a], "{c:?} {max} at {x},{y}");
|
||||
}
|
||||
assert_eq!(p.rgba[b + 3], 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(testgen::small(Version::Psd, Compression::Zip).merged_preview(4).unwrap().is_none());
|
||||
}
|
||||
|
||||
/// The preview size is the caller's: one over the decode limit is refused before allocating.
|
||||
#[test]
|
||||
fn a_preview_over_the_decode_limit_is_refused() {
|
||||
let mut file = testgen::small(Version::Psb, Compression::Raw);
|
||||
(file.header.width, file.header.height) = (300_000, 300_000);
|
||||
assert!(matches!(file.merged_preview(300_000), Err(crate::error::PsdError::LimitExceeded(_))));
|
||||
}
|
||||
|
||||
/// Grayscale is replicated to RGB; 16-bit keeps each sample's high byte; 32-bit linear
|
||||
/// samples are sRGB-encoded (0.5 is 188).
|
||||
#[test]
|
||||
fn preview_converts_grayscale_and_deep_samples() {
|
||||
use crate::header::{ColorMode, Header};
|
||||
use crate::image_data::ImageData;
|
||||
let make = |mode: ColorMode, depth: u16, channels: u16, plane: Vec<u8>| {
|
||||
let mut file = testgen::small(Version::Psd, Compression::Raw);
|
||||
file.layer_info = None;
|
||||
file.global_blocks.clear();
|
||||
file.header = Header { channels, width: 2, height: 1, depth, color_mode: mode, ..file.header.clone() };
|
||||
let planes: Vec<u8> = (0..channels).flat_map(|_| plane.clone()).collect();
|
||||
file.image_data = ImageData::encode(Compression::Raw, &planes, &file.header).unwrap();
|
||||
let bytes = file.to_bytes().unwrap();
|
||||
PsdFile::from_source(Source::new(bytes)).unwrap().merged_preview(16).unwrap().unwrap()
|
||||
};
|
||||
let gray = make(ColorMode::Grayscale, 8, 1, vec![10, 200]);
|
||||
assert_eq!(gray.rgba, vec![10, 10, 10, 255, 200, 200, 200, 255]);
|
||||
let deep = make(ColorMode::Rgb, 16, 3, vec![0x12, 0x34, 0xAB, 0xCD]);
|
||||
assert_eq!(deep.rgba, vec![0x12, 0x12, 0x12, 255, 0xAB, 0xAB, 0xAB, 255]);
|
||||
let float: Vec<u8> = [0.5f32, 1.0].iter().flat_map(|v| v.to_be_bytes()).collect();
|
||||
let f = make(ColorMode::Rgb, 32, 3, float);
|
||||
assert_eq!(f.rgba, vec![188, 188, 188, 255, 255, 255, 255, 255]);
|
||||
}
|
||||
}
|
||||
+37
-9
@@ -76,26 +76,54 @@ impl ChannelData {
|
||||
/// 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 total = layout.decoded_len()?;
|
||||
let row_bytes = layout.row_bytes();
|
||||
let bytes = match &self.stored {
|
||||
Some(s) => Bytes::Source(s),
|
||||
None => Bytes::Memory(&self.data),
|
||||
};
|
||||
let kind = match self.compression {
|
||||
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 as u64 {
|
||||
if bytes.len() < total {
|
||||
return Err(PsdError::UnexpectedEof {
|
||||
offset: usize::try_from(bytes.len()).unwrap_or(usize::MAX),
|
||||
needed: total - bytes.len() as usize,
|
||||
needed: usize::try_from(total - bytes.len()).unwrap_or(usize::MAX),
|
||||
});
|
||||
}
|
||||
Kind::Raw(bytes)
|
||||
}
|
||||
Some(Compression::Rle) => {
|
||||
let size: u64 = if version.is_psb() { 4 } else { 2 };
|
||||
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) });
|
||||
@@ -116,7 +144,7 @@ impl ChannelData {
|
||||
}
|
||||
let data_len = bytes.len() - table_len;
|
||||
// As `decode_planes` checks: PackBits expands at most 64x.
|
||||
if (total as u64) > data_len.saturating_mul(64) {
|
||||
if total > data_len.saturating_mul(64) {
|
||||
return Err(PsdError::Decompress("RLE data too short for declared size".into()));
|
||||
}
|
||||
if at > bytes.len() {
|
||||
@@ -127,7 +155,7 @@ impl ChannelData {
|
||||
}
|
||||
Kind::Rle { bytes, offsets }
|
||||
}
|
||||
Some(c) => Kind::Decoded(decode_planes(c, &self.encoded()?, &layout)?),
|
||||
Some(c) => Kind::Decoded(decode_planes(c, &encoded()?, layout)?),
|
||||
};
|
||||
Ok(RowDecoder { kind, row_bytes, height })
|
||||
}
|
||||
@@ -157,7 +185,7 @@ impl RowDecoder<'_> {
|
||||
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 * self.row_bytes) as u64, n as u64)?),
|
||||
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 } => {
|
||||
|
||||
+40
-17
@@ -16,6 +16,7 @@
|
||||
//! The blocks themselves stay raw in the model ([`crate::TaggedBlock`]) and write back
|
||||
//! unchanged; these are read-only views. Came from EffectCraft's `effectcraft-psd`.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::descriptor::{Descriptor, UnicodeString, Value};
|
||||
@@ -23,7 +24,6 @@ use crate::error::{PsdError, Result};
|
||||
use crate::file::PsdFile;
|
||||
use crate::io::{Reader, read_pascal};
|
||||
use crate::layer::LayerRecord;
|
||||
use crate::tagged::TaggedBlock;
|
||||
|
||||
/// The most pixels a baked smart object may have: 2^24 (256 MiB as float RGBA). The size comes
|
||||
/// from the file's transform, which is untrusted.
|
||||
@@ -458,19 +458,29 @@ fn linked_file(item: &[u8], at: usize) -> Result<LinkedFile> {
|
||||
}
|
||||
|
||||
impl PsdFile {
|
||||
/// Every linked file of the document's linked layer blocks, each with the block that holds
|
||||
/// it ([`LinkedFile::data`] is a range of that block's data).
|
||||
pub fn linked_files(&self) -> Vec<(&TaggedBlock, LinkedFile)> {
|
||||
self.global_blocks
|
||||
.iter()
|
||||
.filter(|b| is_linked_layer_key(&b.key))
|
||||
.flat_map(|b| linked_files(&b.data).into_iter().map(move |f| (b, f)))
|
||||
.collect()
|
||||
/// Every linked file of the document's linked layer blocks, each with the index of the
|
||||
/// global block that holds it ([`LinkedFile::data`] is a range of that block's data). A
|
||||
/// block left in the source ([`crate::TaggedBlock::stored`]) is read for it.
|
||||
pub fn linked_files(&self) -> Result<Vec<(usize, LinkedFile)>> {
|
||||
let mut out = Vec::new();
|
||||
for (i, b) in self.global_blocks.iter().enumerate().filter(|(_, b)| is_linked_layer_key(&b.key)) {
|
||||
out.extend(linked_files(&b.encoded()?).into_iter().map(|f| (i, f)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The embedded bytes of the linked file `uuid`, when the document embeds it.
|
||||
pub fn linked_data(&self, uuid: &str) -> Option<&[u8]> {
|
||||
self.linked_files().into_iter().find_map(|(b, f)| if f.uuid == uuid { b.data.get(f.data?) } else { None })
|
||||
/// The embedded bytes of the linked file `uuid`, when the document embeds it: borrowed from
|
||||
/// a block in memory, read from the source for a block left there.
|
||||
pub fn linked_data(&self, uuid: &str) -> Result<Option<Cow<'_, [u8]>>> {
|
||||
for b in self.global_blocks.iter().filter(|b| is_linked_layer_key(&b.key)) {
|
||||
let data = b.encoded()?;
|
||||
let Some(range) = linked_files(&data).into_iter().find(|f| f.uuid == uuid).and_then(|f| f.data) else { continue };
|
||||
return Ok(match data {
|
||||
Cow::Borrowed(d) => d.get(range).map(Cow::Borrowed),
|
||||
Cow::Owned(d) => d.get(range).map(|e| Cow::Owned(e.to_vec())),
|
||||
});
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +489,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::descriptor::{Id, ObjectArray};
|
||||
use crate::io::{WriteExt, write_pascal};
|
||||
use crate::tagged::TaggedBlock;
|
||||
|
||||
fn so(quad: [[f64; 2]; 4], warp: Option<Warp>) -> SmartObject {
|
||||
SmartObject { uuid: "x".into(), quad, size: None, warp }
|
||||
@@ -651,15 +662,27 @@ mod tests {
|
||||
#[test]
|
||||
fn a_document_finds_the_embedded_bytes_of_a_smart_object() {
|
||||
let mut file = crate::testgen::small(crate::Version::Psd, crate::Compression::Raw);
|
||||
let first = file.global_blocks.len();
|
||||
file.global_blocks.push(TaggedBlock::new(*b"lnk2", linked_record(b"liFD", "uuid-1", "a.png", b"PNGf", b"PNGDATA")));
|
||||
file.global_blocks.push(TaggedBlock::new(*b"lnkE", linked_record(b"liFE", "uuid-2", "b.psd", b"8BPS", b"")));
|
||||
assert_eq!(file.linked_files().len(), 2);
|
||||
assert_eq!(file.linked_data("uuid-1"), Some(b"PNGDATA".as_slice()));
|
||||
assert_eq!(file.linked_data("uuid-2"), None);
|
||||
assert_eq!(file.linked_data("nope"), None);
|
||||
let embedded = |f: &PsdFile, uuid: &str| f.linked_data(uuid).unwrap().map(Cow::into_owned);
|
||||
assert_eq!(
|
||||
file.linked_files().unwrap().iter().map(|(i, f)| (*i, f.uuid.as_str())).collect::<Vec<_>>(),
|
||||
[(first, "uuid-1"), (first + 1, "uuid-2")]
|
||||
);
|
||||
assert_eq!(embedded(&file, "uuid-1").as_deref(), Some(b"PNGDATA".as_slice()));
|
||||
assert_eq!(embedded(&file, "uuid-2"), None);
|
||||
assert_eq!(embedded(&file, "nope"), None);
|
||||
// The blocks are untouched: the file still writes and reads back.
|
||||
let again = PsdFile::from_bytes(&file.to_bytes().unwrap()).unwrap();
|
||||
assert_eq!(again.linked_data("uuid-1"), Some(b"PNGDATA".as_slice()));
|
||||
assert_eq!(embedded(&again, "uuid-1").as_deref(), Some(b"PNGDATA".as_slice()));
|
||||
// With the linked-file blocks left in the source, the same files and bytes are found.
|
||||
let bytes = file.to_bytes().unwrap();
|
||||
let (deferred, _) =
|
||||
PsdFile::from_source_deferring(crate::Source::new(bytes), |key, _| is_linked_layer_key(key), |_| Ok(false), |_| false).unwrap();
|
||||
assert!(deferred.global_blocks.iter().filter(|b| is_linked_layer_key(&b.key)).all(|b| b.data.is_empty() && b.stored.is_some()));
|
||||
assert_eq!(deferred.linked_files().unwrap(), file.linked_files().unwrap());
|
||||
assert_eq!(embedded(&deferred, "uuid-1").as_deref(), Some(b"PNGDATA".as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -84,6 +84,10 @@ const MAX_READ: usize = 64 << 20;
|
||||
pub struct FileSource {
|
||||
file: std::fs::File,
|
||||
len: u64,
|
||||
/// Modification time when opened: a read after the file changed in place fails rather than
|
||||
/// mixing old offsets with new bytes. (A save that replaces the file by renaming leaves this
|
||||
/// open file, the old one, unchanged.)
|
||||
modified: Option<std::time::SystemTime>,
|
||||
}
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
@@ -95,8 +99,8 @@ impl FileSource {
|
||||
|
||||
/// Wraps an open file.
|
||||
pub fn new(file: std::fs::File) -> std::io::Result<Self> {
|
||||
let len = file.metadata()?.len();
|
||||
Ok(FileSource { file, len })
|
||||
let meta = file.metadata()?;
|
||||
Ok(FileSource { file, len: meta.len(), modified: meta.modified().ok() })
|
||||
}
|
||||
|
||||
/// The file.
|
||||
@@ -117,6 +121,10 @@ impl ReadAt for FileSource {
|
||||
}
|
||||
let n = buf.len();
|
||||
let failed = |e: std::io::Error| PsdError::Read(format!("reading {n} bytes at offset {offset}: {e}"));
|
||||
let meta = self.file.metadata().map_err(failed)?;
|
||||
if meta.len() != self.len || meta.modified().ok() != self.modified {
|
||||
return Err(PsdError::Read("the file changed after it was opened".into()));
|
||||
}
|
||||
// Bounded reads, in chunks: some Windows file
|
||||
// systems reject a single read of gigabytes (photocraft#375).
|
||||
let mut done = 0usize;
|
||||
@@ -206,6 +214,11 @@ pub struct Stored {
|
||||
}
|
||||
|
||||
impl Stored {
|
||||
/// Whether `other` is the same bytes of the same source (a clone of it).
|
||||
pub fn same_range(&self, other: &Stored) -> bool {
|
||||
std::ptr::addr_eq(Arc::as_ptr(&self.source.0), Arc::as_ptr(&other.source.0)) && self.offset == other.offset && self.len == other.len
|
||||
}
|
||||
|
||||
/// Reads the bytes.
|
||||
pub fn read(&self) -> Result<Vec<u8>> {
|
||||
self.source.read_vec(self.offset, self.len)
|
||||
@@ -265,6 +278,9 @@ mod tests {
|
||||
f.read_at(2, &mut b).unwrap();
|
||||
assert_eq!(b, [7, 6, 5]);
|
||||
assert!(f.read_at(3, &mut b).is_err());
|
||||
// Changed in place after opening: reads fail instead of returning other bytes.
|
||||
std::fs::OpenOptions::new().append(true).open(&path).unwrap().write_all(&[1]).unwrap();
|
||||
assert!(f.read_at(0, &mut b).is_err());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
}
|
||||
+76
-14
@@ -28,7 +28,11 @@ pub fn uses_long_length(version: Version, key: &[u8; 4]) -> bool {
|
||||
}
|
||||
|
||||
/// A tagged block, stored raw.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
///
|
||||
/// A file parsed by [`PsdFile::from_source_deferring`](crate::PsdFile::from_source_deferring) may
|
||||
/// leave a large block's data in the source (`stored`, with `data` empty); equality compares the
|
||||
/// bytes wherever they are.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TaggedBlock {
|
||||
/// `8BIM` or `8B64`.
|
||||
@@ -42,8 +46,36 @@ pub struct TaggedBlock {
|
||||
/// layer blocks. Parsing yields `None` whenever
|
||||
/// the stored padding equals the default, so the model stays canonical.
|
||||
pub padding: Option<Vec<u8>>,
|
||||
/// Where the data is when it was left in the source. (Not serialized: load it first.)
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub stored: Option<crate::source::Stored>,
|
||||
}
|
||||
|
||||
impl PartialEq for TaggedBlock {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.signature != other.signature
|
||||
|| self.key != other.key
|
||||
|| self.padding != other.padding
|
||||
|| self.encoded_data_len() != other.encoded_data_len()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// The same bytes of the same source (a clone): equal without reading them, even when
|
||||
// the source can no longer be read.
|
||||
if let (Some(a), Some(b)) = (&self.stored, &other.stored)
|
||||
&& a.same_range(b)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match (self.encoded(), other.encoded()) {
|
||||
(Ok(a), Ok(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TaggedBlock {}
|
||||
|
||||
/// Layer section divider type (`lsct`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -138,7 +170,7 @@ pub enum BlockData<'a> {
|
||||
impl TaggedBlock {
|
||||
/// A new `8BIM` block with default padding.
|
||||
pub fn new(key: [u8; 4], data: Vec<u8>) -> Self {
|
||||
TaggedBlock { signature: *b"8BIM", key, data, padding: None }
|
||||
TaggedBlock { signature: *b"8BIM", key, data, padding: None, stored: None }
|
||||
}
|
||||
|
||||
/// `true` for `MIB8` / `46B8`: the signatures [`crate::tiff`] gives a block whose data was
|
||||
@@ -175,6 +207,11 @@ impl TaggedBlock {
|
||||
/// Typed view for known keys. `None` for keys without a typed parser,
|
||||
/// `Some(Err)` if a known key's data is malformed.
|
||||
pub fn parsed(&self) -> Option<Result<BlockData<'_>>> {
|
||||
// Its views borrow `data`: a block still in the source must be loaded first, not read
|
||||
// as empty.
|
||||
if self.stored.is_some() {
|
||||
return Some(Err(PsdError::invalid(format!("{}: block data is still in the source; load it first", self.key_str()))));
|
||||
}
|
||||
let d = &self.data[..];
|
||||
let mut r = Reader::new(d);
|
||||
Some(match &self.key {
|
||||
@@ -282,7 +319,9 @@ impl TaggedBlock {
|
||||
/// ("Additional Layer Information": Placed Layer, Placed Layer Data,
|
||||
/// Linked Layer, Object Based Effects Layer Info).
|
||||
pub fn check_structure(&self) -> Result<()> {
|
||||
let d = &self.data[..];
|
||||
// A block left in the source (an embedded smart-object file) is checked from there.
|
||||
let data = self.encoded()?;
|
||||
let d = &data[..];
|
||||
let mut r = Reader::new(d);
|
||||
match &self.key {
|
||||
b"PlLd" => {
|
||||
@@ -333,7 +372,26 @@ impl TaggedBlock {
|
||||
}
|
||||
|
||||
pub(crate) fn write(&self, out: &mut Vec<u8>, version: Version, alignment: usize) -> Result<()> {
|
||||
Self::write_parts(out, self.signature, self.key, &self.data, self.padding.as_deref(), version, alignment)
|
||||
Self::write_parts(out, self.signature, self.key, &self.encoded()?, self.padding.as_deref(), version, alignment)
|
||||
}
|
||||
|
||||
/// The data: borrowed from `data`, or read from the source when it was left there.
|
||||
pub fn encoded(&self) -> Result<std::borrow::Cow<'_, [u8]>> {
|
||||
crate::source::encoded(&self.data, self.stored.as_ref())
|
||||
}
|
||||
|
||||
/// Length of the data, wherever it is.
|
||||
pub fn encoded_data_len(&self) -> u64 {
|
||||
self.stored.as_ref().map_or(self.data.len() as u64, |s| s.len)
|
||||
}
|
||||
|
||||
/// Reads data left in the source into `data`.
|
||||
pub fn load(&mut self) -> Result<()> {
|
||||
if let Some(s) = &self.stored {
|
||||
self.data = s.read()?;
|
||||
self.stored = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes a block from its parts (no owned [`TaggedBlock`] needed). `padding: None` writes
|
||||
@@ -360,14 +418,16 @@ impl TaggedBlock {
|
||||
|
||||
/// Serialized size in bytes as a layer block (two-byte alignment).
|
||||
pub fn encoded_len(&self, version: Version) -> usize {
|
||||
self.encoded_len_aligned(version, 2)
|
||||
usize::try_from(self.encoded_len_aligned(version, 2)).unwrap_or(usize::MAX)
|
||||
}
|
||||
|
||||
/// Serialized size in bytes as [`Self::write`] writes it with `alignment`.
|
||||
pub(crate) fn encoded_len_aligned(&self, version: Version, alignment: usize) -> usize {
|
||||
let l = if uses_long_length(version, &self.key) { 8 } else { 4 };
|
||||
let p = self.padding.as_ref().map_or(Self::default_padding(self.data.len(), alignment), Vec::len);
|
||||
(8 + l + p).saturating_add(self.data.len())
|
||||
/// Serialized size in bytes as [`Self::write`] writes it with `alignment`. In `u64`: a
|
||||
/// deferred PSB block can be longer than a 32-bit `usize`.
|
||||
pub(crate) fn encoded_len_aligned(&self, version: Version, alignment: usize) -> u64 {
|
||||
let l: u64 = if uses_long_length(version, &self.key) { 8 } else { 4 };
|
||||
let n = self.encoded_data_len();
|
||||
let p = self.padding.as_ref().map_or(Self::default_padding((n % alignment as u64) as usize, alignment), Vec::len);
|
||||
(8 + l + p as u64).saturating_add(n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,7 +559,7 @@ pub(crate) fn read_blocks_with(
|
||||
Some(pad_bytes)
|
||||
};
|
||||
if !consume(blocks.len(), signature, key, data, &padding) {
|
||||
blocks.push(TaggedBlock { signature, key, data: data.to_vec(), padding });
|
||||
blocks.push(TaggedBlock { signature, key, data: data.to_vec(), padding, stored: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,12 +583,14 @@ pub(crate) fn write_blocks_to<W: std::io::Write + std::io::Seek + ?Sized>(
|
||||
out.small(|o| {
|
||||
o.put(&b.signature);
|
||||
o.put(&b.key);
|
||||
o.put_len(b.data.len() as u64, uses_long_length(version, &b.key))
|
||||
o.put_len(b.encoded_data_len(), uses_long_length(version, &b.key))
|
||||
})?;
|
||||
out.put(&b.data)?;
|
||||
// A block left in the source is copied from it piece by piece.
|
||||
out.put_encoded(&b.data, b.stored.as_ref())?;
|
||||
match &b.padding {
|
||||
Some(p) => out.put(p)?,
|
||||
None => out.zeros(TaggedBlock::default_padding(b.data.len(), alignment))?,
|
||||
// From the length's remainder: a PSB block can be longer than a 32-bit `usize`.
|
||||
None => out.zeros(TaggedBlock::default_padding((b.encoded_data_len() % alignment as u64) as usize, alignment))?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -185,7 +185,7 @@ impl ImageSourceData {
|
||||
let pad = TaggedBlock::default_padding(m.data.len(), 4);
|
||||
len = len.saturating_add(8 + len_field(b"LMsk")).saturating_add(m.data.len() as u64).saturating_add(pad as u64);
|
||||
}
|
||||
len = self.global_blocks.iter().fold(len, |n, b| n.saturating_add(b.encoded_len_aligned(version, 4) as u64));
|
||||
len = self.global_blocks.iter().fold(len, |n, b| n.saturating_add(b.encoded_len_aligned(version, 4)));
|
||||
len = len.saturating_add(self.trailing.len() as u64);
|
||||
let no_memory = || PsdError::OutOfMemory("not enough memory for the layer data");
|
||||
let mut body = Vec::new();
|
||||
@@ -290,7 +290,7 @@ impl ImageSourceData {
|
||||
#[cfg(test)]
|
||||
fn padded_block(key: [u8; 4], data: Vec<u8>) -> TaggedBlock {
|
||||
let pad = (4 - data.len() % 4) % 4;
|
||||
TaggedBlock { signature: *b"8BIM", key, data, padding: Some(vec![0; pad]) }
|
||||
TaggedBlock { signature: *b"8BIM", key, data, padding: Some(vec![0; pad]), stored: None }
|
||||
}
|
||||
|
||||
/// Parses a 34377 tag (image resources). Little-endian resource blocks (`MIB8`) are converted;
|
||||
|
||||
@@ -906,7 +906,7 @@ fn psb_foreign_order_long_key_roundtrips() {
|
||||
let d = ImageSourceData {
|
||||
version: Version::Psb,
|
||||
byte_order: ByteOrder::Little,
|
||||
global_blocks: vec![TaggedBlock { signature: *b"MIB8", key: *b"Mt16", data: vec![9, 8, 7, 6, 5, 4], padding: None }],
|
||||
global_blocks: vec![TaggedBlock { signature: *b"MIB8", key: *b"Mt16", data: vec![9, 8, 7, 6, 5, 4], padding: None, stored: None }],
|
||||
..Default::default()
|
||||
};
|
||||
let (little, w) = d.to_bytes(ByteOrder::Little).unwrap();
|
||||
|
||||
Reference in new issue
Block a user