diff --git a/crates/psd/src/file.rs b/crates/psd/src/file.rs index 13c7a6a14..8404e38f5 100644 --- a/crates/psd/src/file.rs +++ b/crates/psd/src/file.rs @@ -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 { 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}"); } diff --git a/crates/psd/src/layer/tests.rs b/crates/psd/src/layer/tests.rs index 53cc409ed..423b017ae 100644 --- a/crates/psd/src/layer/tests.rs +++ b/crates/psd/src/layer/tests.rs @@ -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); } diff --git a/crates/psd/src/lazy.rs b/crates/psd/src/lazy.rs index d05474ab9..271d22088 100644 --- a/crates/psd/src/lazy.rs +++ b/crates/psd/src/lazy.rs @@ -154,6 +154,31 @@ impl PsdFile { source: Source, want: impl FnOnce(&PsdFile, Option, u64) -> Result, recover: impl FnOnce(&PsdFile) -> bool, + ) -> Result<(Self, Option>>)> { + 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, + recover: impl FnOnce(&PsdFile) -> bool, + ) -> Result<(Self, Option>>)> { + 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, u64) -> Result, + recover: impl FnOnce(&PsdFile) -> bool, ) -> Result<(Self, Option>>)> { 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>) -> Result>, + defer: &dyn Fn(&[u8; 4], u64) -> bool, blocks: &mut Vec, ) -> Result> { 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() { diff --git a/crates/psd/src/lib.rs b/crates/psd/src/lib.rs index 42dc0b61a..2ad36b035 100644 --- a/crates/psd/src/lib.rs +++ b/crates/psd/src/lib.rs @@ -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))] diff --git a/crates/psd/src/preview.rs b/crates/psd/src/preview.rs new file mode 100644 index 000000000..04087b32c --- /dev/null +++ b/crates/psd/src/preview.rs @@ -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, +} + +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> { + 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| { + 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 = (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 = [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]); + } +} diff --git a/crates/psd/src/rows.rs b/crates/psd/src/rows.rs index 9fc95d36a..483785190 100644 --- a/crates/psd/src/rows.rs +++ b/crates/psd/src/rows.rs @@ -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> { 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> { + 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, + bytes: Bytes<'a>, + layout: &PlaneLayout, + encoded: impl FnOnce() -> Result>, + ) -> Result { + // 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 } => { diff --git a/crates/psd/src/smart.rs b/crates/psd/src/smart.rs index 64f5a23ca..7f05010f6 100644 --- a/crates/psd/src/smart.rs +++ b/crates/psd/src/smart.rs @@ -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 { } 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> { + 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>> { + 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) -> 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::>(), + [(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] diff --git a/crates/psd/src/source.rs b/crates/psd/src/source.rs index 84aac1e60..8ecb8bb45 100644 --- a/crates/psd/src/source.rs +++ b/crates/psd/src/source.rs @@ -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, } #[cfg(any(unix, windows))] @@ -95,8 +99,8 @@ impl FileSource { /// Wraps an open file. pub fn new(file: std::fs::File) -> std::io::Result { - 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> { 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(); } } diff --git a/crates/psd/src/tagged.rs b/crates/psd/src/tagged.rs index 99ff3f91f..cc8b429ce 100644 --- a/crates/psd/src/tagged.rs +++ b/crates/psd/src/tagged.rs @@ -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>, + /// 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, } +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) -> 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>> { + // 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, 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> { + 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( 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(()) diff --git a/crates/psd/src/tiff.rs b/crates/psd/src/tiff.rs index f18b29043..73a7caed3 100644 --- a/crates/psd/src/tiff.rs +++ b/crates/psd/src/tiff.rs @@ -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) -> 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; diff --git a/crates/psd/src/tiff/tests.rs b/crates/psd/src/tiff/tests.rs index 6d8b5cdb4..1582da824 100644 --- a/crates/psd/src/tiff/tests.rs +++ b/crates/psd/src/tiff/tests.rs @@ -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();