Commit Graph
9 Commits
Author SHA1 Message Date
336619e3e4 craft-psd: fixes ported from open PhotoCraft pull requests (#1024)
* craft-psd: raise the 64-bit .abr sample budget to 2 GiB

Photoshop writes brush packs up to its 2 GB .abr limit; a 1 GiB budget
refused valid packs over it. 32-bit targets keep 512 MiB.

Ported from storytold/photocraft#13282 (photocraft#5949).

* craft-psd: FELS is checked and transcoded as filter effects

FELS holds the smart-filter masks of linked smart objects in the FEid
layout. check_structure now re-parses it, and a layered TIFF transcodes
it field by field: a little-endian save dropped the block as unknown.

Ported from storytold/photocraft#8275 (photocraft#8260); the test is new.

* craft-psd: a layered TIFF keeps external smart-object links

An external linked file (liFE) holds the descriptor of its link, and
from version 3 its bytes. The TIFF transcoder skipped both, so the record
failed to convert and a little-endian save dropped the lnkE block.

Ported from storytold/photocraft#6072; the test is new.

* craft-psd: the example and the save test write into fresh private directories

The big_psb example and the save_and_open test used a predictable temp
directory name, which a planted directory or symlink could take over.
They now create a new owner-only directory, and the example removes a
half-written fixture from its own default folder.

Ported from storytold/photocraft#13266 (its crates/psd part).

* craft-psd: the pixel preflight sees the merged image's compression and size

from_bytes_and_merged_preflight_info and
from_source_and_merged_preflight_info hand the preflight the merged
image's compression and encoded length before the image is read, so a
caller can estimate the memory a decode will need (an opaque RGB image
stored raw needs no inflate buffer). A missing compression field is
None, and a refusal takes precedence over it.

Ported from storytold/photocraft#11288 (its crates/psd part); the test is
new.

* craft-psd: typed layer selection ids (resource 1069)

ResourceData::LayerSelectionIds: the selected layers' ids in stored
order, duplicates and zero kept. The count is checked against the whole
payload before anything is allocated; truncated or trailing data is
malformed, not a partial selection.

Ported from storytold/photocraft#10268 (its crates/psd part).

* craft-psd: README matches the code

The typed blocks tsly, lmgm and vmgm, vector masks and slices typed by
path and slices, the resources ImageResource::parsed types, the ATN
round-trip caveats and version-12 reading, and the corpus test's
known-bad file.

Ported from storytold/photocraft#11600 (its crates/psd part), with the
resource list brought up to date.

* craft-psd: atn_dump escapes control characters from the file

A set's names, warnings, paths and errors are printed with control
characters escaped, so a file cannot drive the terminal.

Ported from storytold/photocraft#11218 (merged into PhotoCraft's copy
after craft-psd was moved).

* craft-psd: the .abr budget test checks the 64-bit bound at compile time

clippy refuses an assertion whose value is constant.

---------

Co-authored-by: rgoza1996 <97354241+rgoza1996@users.noreply.github.com>
Co-authored-by: Korg1ch <57859048+Korg1ch@users.noreply.github.com>
Co-authored-by: PJaYOfficial <1597623+PJaYOfficial@users.noreply.github.com>
Co-authored-by: acilione <antoninocilione96@gmail.com>
Co-authored-by: kirubhakaran12 <27127513+kirubhakaran12@users.noreply.github.com>
Co-authored-by: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com>
2026-11-11 20:39:59 -05:00
Dylan M. Taylor 4586109726 craft-psd: Photoshop's layered TIFF conventions, from PhotoCraft (#1014)
The format parts of PhotoCraft's layered TIFF (crates/io/src/tiff_layers.rs at abeb16c96) that
sit beside craft-psd's tiff module: the two tag numbers; the layer data Photoshop writes
(empty Patt, default FMsk, MTrn, a full LMsk record, flag bit 3 and the per-layer blocks) and
its removal on open; the channel resources (1006, 1022, 1045, 1053, 1077) matched to the
samples the IFD stores; foreign-order block stripping; and the interleaved to planar sample
conversions with CMYK inversion.
2026-11-11 20:31:26 -05:00
Dylan M. TaylorandOmar Saidahmed 417680b824 craft-psd: float pixels, Lab composites and salvaging a damaged layer section (#1006)
* 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: float pixels, Lab composites and salvaging a damaged layer section

From EffectCraft's effectcraft-psd (storytold/effectcraft f334bcf3,
crates/psd/src/pixels.rs, limits.rs and placed.rs):

- `float`: `PsdFile::composite_f32` and `layer_f32` decode to straight
  RGBA f32 in every colour mode and depth: CMYK naively, indexed through
  the palette, Lab through D50 to sRGB, 32-bit encoded to sRGB, a layer's
  raster mask applied and a solid colour fill layer filled. One image is at
  most 2^28 pixels and a decode refuses before allocating when what it
  holds at once is over its budget (4 GiB; `_within` takes a limit).
  `SmartObject::render_placed` bakes a warped or perspective placement,
  bounded in pixels and in rasterising work.
- `composite_rgba8` and `Layer::rgba8` read Lab.
- `PsdFile::from_bytes_salvaging`: a file whose layer and mask section
  cannot be read opens as its merged image, with the error it skipped.

* 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.

---------

Co-authored-by: Omar Saidahmed <osaidahm@uwaterloo.ca>
2026-11-11 20:27:10 -05:00
Dylan M. TaylorandOmar Saidahmed 430a908f68 craft-psd: typed views of type layers, smart objects and adjustments (#957)
* 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.

---------

Co-authored-by: Omar Saidahmed <osaidahm@uwaterloo.ca>
2026-11-11 20:05:54 -05:00
Dylan M. Taylor be7fafd14f craft-psd: bound the builder's default composite (#985)
Without a composite, PsdBuilder::build made a white placeholder of width x height x depth x
channels bytes with vec!, before checking the geometry: a 300000 x 300000 canvas asked for
270 GB, and an overflowing size wrapped. The header is validated first, the size is checked
against MAX_DECODED_BYTES (the encoder's own limit), and the buffer is reserved fallibly.

Ported from storytold/photocraft#13309.
2026-11-11 19:45:46 -05:00
Dylan M. Taylor 45de2ed928 craft-psd: the reading rules for .ase swatch files (#984)
The rules VectorCraft's swatch reader applies on top of the format
(crates/color/src/palette_io.rs at storytold/vectorcraft 7f3a046b), as
separate functions next to `ase::parse`, which still keeps what the file
says:

- `AseColor::lab_lightness`: Lab lightness is stored as L*/100, but some
  writers store L* itself, which shows as a value above 1;
- `AseColor::value_name`: names for unnamed colours, as new swatches are
  named ("C=10 M=20 Y=30 K=0", "R=255 G=128 B=0", "Gray K=40",
  "L=52 a=70 b=-30");
- `UniqueNames` and `AseFile::apply_names`: trimmed names, "Color Group"
  for an unnamed group, and a number for a name already taken ("Black 2"),
  shared by colours and groups. `UniqueNames::reserve` lets an application
  number a file's names against its own library;
- `ase::sniff`: a swatch exchange file, not an Aseprite sprite with the
  same extension.

These are the rules craft-libs#140 asks for.
2026-11-11 19:45:18 -05:00
Dylan M. Taylor 046a927171 craft-psd: engine_data, the text engine document of type layers (#895)
* 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.
2026-11-11 18:51:10 -05:00
Dylan M. Taylor c972d5fc29 craft-psd: Head reads the header and image resources without layers or pixels (#885)
* 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: Head reads the header and image resources without layers or pixels

`Head::from_bytes` takes the file or a prefix that reaches the end of the
image resources; `Head::from_source` makes three reads and nothing after
the resources. `Header::from_bytes` parses the 26-byte header alone, and
`head::sniff` checks the signature.

The readers in LightCraft (header probe, EXIF/XMP scan), DesignCraft and
FilmCraft skip the layer section and work from the start of the file.
`PsdFile` parses every layer record, and `Header::read` was private, so
they had no equivalent here.

Section lengths past the data are errors before anything is allocated.
2026-11-11 18:38:18 -05:00
Dylan M. Taylor ee78a9bd46 craft-psd: Photoshop PSD/PSB reader and writer, moved from PhotoCraft (#875)
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.
2026-11-11 17:00:33 -05:00