mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +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>
121 lines
4.4 KiB
Rust
121 lines
4.4 KiB
Rust
//! # craft-psd
|
|
//!
|
|
//! Faithful, lossless reader and writer for Adobe Photoshop PSD (version 1)
|
|
//! and PSB (version 2, "large document format") files.
|
|
//!
|
|
//! * [`PsdFile::from_bytes`] parses a file into a format-level model;
|
|
//! [`PsdFile::to_bytes`] writes it back. Unmodified files round-trip byte
|
|
//! for byte: channel data is kept in its original encoding, unknown tagged
|
|
//! blocks and resources are kept raw, and padding is preserved.
|
|
//! * Channel data is decoded lazily ([`ChannelData::decode`],
|
|
//! [`ImageData::decode`]) with all four compression methods supported for
|
|
//! both decoding and encoding.
|
|
//! * [`PsdBuilder`] constructs new files from RGBA/gray/CMYK buffers; the
|
|
//! caller supplies the merged composite (this crate never composites).
|
|
//! * [`Head`] reads only the header, colour mode data and image resources (size, ICC profile,
|
|
//! EXIF, XMP) from the start of a file, without the layers or pixels.
|
|
//! * [`descriptor`] parses and writes ActionDescriptors.
|
|
//! * [`engine_data`] parses and writes the text engine document of type layers.
|
|
//! * [`text`], [`smart`] and [`adjust`] are read-only views of the blocks a viewer needs: a type
|
|
//! layer's text and style, a smart object's placement, warp and embedded file, an adjustment
|
|
//! layer's settings, a fill layer's colour and the layer effects.
|
|
//! * [`PsdFile::composite_f32`] and [`PsdFile::layer_f32`] decode to straight RGBA `f32` in
|
|
//! every colour mode and depth, within a memory budget; [`smart::SmartObject::render_placed`]
|
|
//! bakes a warped or perspective placement.
|
|
//!
|
|
//! Implemented clean-room from Adobe's public "Photoshop File Formats
|
|
//! Specification". Undocumented details that follow MIT-licensed psd-tools
|
|
//! or ag-psd behavior are noted in comments.
|
|
//!
|
|
//! The parser never panics on malformed input and enforces limits
|
|
//! (dimensions ≤ 300000, bounded allocations, bounded descriptor nesting).
|
|
//!
|
|
//! ```
|
|
//! use craft_psd::{LayerSpec, PixelData, PsdBuilder, PsdFile};
|
|
//!
|
|
//! let mut b = PsdBuilder::new(2, 1);
|
|
//! b.push_layer(LayerSpec::new("Red", 0, 0, 2, 1, PixelData::Rgba8(vec![255, 0, 0, 255, 255, 0, 0, 128])));
|
|
//! b.composite(PixelData::Rgba8(vec![255, 0, 0, 255, 255, 0, 0, 128]));
|
|
//! let bytes = b.to_bytes()?;
|
|
//!
|
|
//! let file = PsdFile::from_bytes(&bytes)?;
|
|
//! assert_eq!(file.to_bytes()?, bytes); // byte-stable
|
|
//! let layer = file.layer(0).unwrap();
|
|
//! assert_eq!(layer.name(), "Red");
|
|
//! assert_eq!(layer.rgba8()?.data[7], 128);
|
|
//! # Ok::<(), craft_psd::PsdError>(())
|
|
//! ```
|
|
#![forbid(unsafe_code)]
|
|
#![warn(missing_docs)]
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
|
|
pub mod abr;
|
|
pub mod aco;
|
|
pub mod act;
|
|
pub mod adjust;
|
|
pub mod animation;
|
|
pub mod ase;
|
|
pub mod asl;
|
|
pub mod atn;
|
|
pub mod blend;
|
|
pub mod builder;
|
|
mod codepage;
|
|
pub mod compression;
|
|
pub mod csh;
|
|
pub mod descriptor;
|
|
pub mod engine_data;
|
|
pub mod error;
|
|
pub mod file;
|
|
pub mod filter_effects;
|
|
#[cfg(any(test, feature = "testgen"))]
|
|
pub mod fixtures;
|
|
pub mod float;
|
|
pub mod grd;
|
|
pub mod hdr;
|
|
pub mod head;
|
|
pub mod header;
|
|
pub mod image_data;
|
|
mod io;
|
|
pub mod layer;
|
|
mod lazy;
|
|
pub mod metadata;
|
|
pub mod path;
|
|
pub mod patterns;
|
|
pub mod pixels;
|
|
pub mod preview;
|
|
pub mod resources;
|
|
pub mod rows;
|
|
pub mod shc;
|
|
pub mod slices;
|
|
pub mod smart;
|
|
pub mod source;
|
|
pub mod tagged;
|
|
#[cfg(any(test, feature = "testgen"))]
|
|
// Test-data generator (tests and fuzz seeds only): failing loudly is the point.
|
|
#[allow(clippy::expect_used)]
|
|
pub mod testgen;
|
|
pub mod text;
|
|
pub mod tiff;
|
|
pub mod tree;
|
|
|
|
pub use blend::BlendMode;
|
|
pub use builder::{GroupSpec, LayerSpec, MaskSpec, PixelData, PsdBuilder};
|
|
pub use compression::{Compression, PlaneLayout};
|
|
pub use descriptor::{Descriptor, VersionedDescriptor};
|
|
pub use error::{PsdError, Result};
|
|
pub use file::{GlobalLayerMask, LayerInfoPlacement, PsdFile};
|
|
pub use float::FloatImage;
|
|
pub use head::Head;
|
|
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))]
|
|
pub use source::FileSource;
|
|
pub use source::{ReadAt, Source, Stored};
|
|
pub use tagged::{BlockData, SectionDivider, SectionType, TaggedBlock};
|
|
pub use tree::LayerNode;
|