Commit Graph
16 Commits
Author SHA1 Message Date
baa854341b Integration: Indexed PSD keeps transparency, ICC and channels (#4816)
Saving an Indexed Color document in its own PSD mode lost data the RGB
file used to keep (the five open CodeRabbit findings, all reproduced):

- an opaque pixel whose colour matched the transparent entry (opaque
  black next to a black transparent entry, common in PNG-8 files) was
  written as that entry, so it turned transparent; a table whose only
  entry is the transparent one now keeps the RGB file;
- PhotoCraft reopened the file with resource 1047 on the table only, so
  transparent pixels came back opaque (Photoshop stores transparent
  pixels of an Indexed file as the 1047 entry, checked on a GIF it
  opened and saved as PSD);
- the ICC profile was dropped (Photoshop writes 1039 for Indexed); only
  an RGB profile is written, as it describes the table;
- alpha channels and the Quick Mask were dropped without a warning; the
  importer doesn't read extra channels of these modes back, so such
  documents keep the RGB / Grayscale file;
- without a transparent entry, empty areas became the nearest colour to
  transparent black instead of white;
- a PSB-sourced 8B64 global block stayed 8B64 in a PSD (Multichannel
  too), which Photoshop refuses.

Tests: one per point in psd_structure.rs; each fails without this commit.

Co-authored-by: Gert Pellin <8284204+switch87@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:14:08 +02:00
Gert Pellin bfffc365c6 Merge branch 'main' into feat/4814-psd-palette-modes 2026-10-19 16:33:16 +02:00
Zer0C00L01andCursor Agent a954194277 PSD import: ResolutionInfo stays pixels per inch when the display unit is cm (#5280)
hRes and vRes are stored as pixels per inch. The unit that follows only
controls how that number is shown, so a 300 ppi file with unit cm opened
at 762 ppi. Export already writes the document ppi and does not divide
by 2.54.

Fixes #5201

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-14 01:58:27 -04:00
Gert Pellin 2a10524d53 PSD: save Indexed Color, Bitmap and Duotone documents in their own mode
Indexed Color (mode 2): the flattened document as palette indices, the
Color Table as Color Mode Data (256 reds, greens, blues), its length in
resource 1046 and the transparent entry in 1047; the importer now reads
both back. Bitmap (mode 0): 1 bit per pixel, 1 = black. Neither mode has
layers in Photoshop, so both are written flat, like Multichannel; the
layerless file building is shared with multichannel_map.

A Duotone keeps the ink data it was imported with (undocumented, kept
verbatim in Duotone::psd_raw) over its grey plate. Duotones whose inks
were set in PhotoCraft have no such data and stay Grayscale with the
warning, as before.

Fixes #4814
2026-10-13 09:00:20 +02:00
cf83e05821 fix(psd): Improve interoperability and decoding (#1955)
* fix(psd): Align global tagged blocks to four bytes

Use four-byte defaults for global blocks and streamed layer information.
Read global payloads without full copies while preserving block order,
explicit padding and the first layer candidate. Keep two-byte layer
alignment; test canonical padding and lossless PSD/PSB parsing.

* fix(psd): Preserve transparent composite colours and precision

* fix(psd): Check ZIP integrity and bound layer previews

* fix(io): Use document CMYK profiles for PSD fill pixels

Generated fill pixels used the default CMYK separation even when the PSD
embedded a different profile. Scope layer export to the document colour
space so cached pixels and the composite use the same profile.

Check native cached samples against an explicit ICC transform at 8 and
16 bits.

* io: refuse an empty RGB composite before splitting it into planes

chunks_exact(0) panics; an empty decoded composite now returns an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:46:52 -04:00
Ian Swope dc256b8b39 A PSD layer with bounds but no channel data no longer aborts on a 360 GB allocation (#755) (#885)
record_surface sized the interleave buffer from the layer rectangle alone,
so a few-hundred-byte file declaring a 300000x300000 layer with no
channels made import allocate ~360 GB and abort. The channel decoders
already bound their output by the bytes present, so a buffer is now built
only when at least one channel decodes; otherwise the layer imports empty
with a warning.
2026-10-08 15:04:40 -04:00
Elvis33LEandBrandon Thomas fdd7141788 PSD: decode the merged composite only when something consumes it (#846)
Every PSD/PSB open decoded the merged image unconditionally, but a normal
layered Photoshop save carries a composite nobody reads: with layers
present and nothing behind the colour channels, the decoded planes went
straight to the drop. That composite is typically about half the file's
bytes, and for ZIP-compressed files the parse-time validation had already
inflated the same stream once - so layered ZIP opens paid the composite
twice for nothing (P18: opening a 20 MP layered PSD measures 1753 ms p50
against a 1000 ms budget).

The composite is now decoded only when a consumer exists: a flattened or
multichannel file (the composite IS the image), or extra alpha/spot
channels behind the colour ones. Layered files without extra channels
skip the decode entirely; everything a layered import shows comes from
the layer records either way.

The parse-time ZIP validation stays: deferring it would break the
corrupt-stream rejection the fuzz tests pin down, and with the decode
skipped, layered ZIP opens now inflate the composite once (validation)
instead of twice.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 21:10:38 -04:00
57452bfd22 Bound layer-group nesting at import and Group Layers (stack-overflow guard) (#796)
* Bound layer-group nesting at import and Group Layers (stack-overflow guard)

Everything that walks the layer tree (Document::walk behind nearly every
command, the layers panel, PSD export, .pcraft save/load) recurses once per
level, but nothing bounded how deep a document could get: a PSD whose records
nest groups a few thousand deep (each level costs only a divider + folder
record pair) overflowed the stack in the importer's build recursion, in
apply_link_groups, and again in every consumer - an abort no panic hook or
fuzz run catches. The one cap that existed lived in the .pcraft save path
(MAX_GROUP_DEPTH = 100), so such a file even imported 'fine' and then bricked
saving.

- photocraft-doc now owns the limit (MAX_GROUP_DEPTH = 100, the value the
  format crate already enforced) plus Document::max_group_depth(), an
  iterative depth measure that cannot itself overflow.
- PSD/PSB import rejects files nested deeper than the cap with an actionable
  Unsupported error, mirroring what the .pcraft loader already does; the
  never-fail psd_to_document keeps working on such files by capping the
  recursion and warning about the unimported remainder (this was the overflow).
- apply_link_groups (resource 1026) recurses over the same tree and is
  bounded the same way.
- layer.groupLayers refuses to nest past the cap (checked inside the edit, so
  a rejected call records no history step and leaves the document untouched).

* Enforce the group nesting cap in layer.moveTo and Artboard from Layers

Moving a group beside or into a deeply nested layer, or wrapping a deep
group in an artboard, could still build a tree deeper than MAX_GROUP_DEPTH.
A shared check_group_depth helper now rejects those edits with an error and
records no history step, like layer.groupLayers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:46:45 -04:00
d07eecf66f PSD: read/write the artboard lock at Photoshop's lspf bit 3 (#395) (#406)
* PSD: read/write the artboard lock at Photoshop's lspf bit 3 (#395)

Photoshop writes layer locks with the artboard bit at 0x08 (a Background
layer carries 0x0D). PhotoCraft read it at 0x10, so 0x0D came back as
0x05 and the lock bytes drifted on every re-save. Map artboard to bit 3
on both read and write; still accept 0x10 on read so older PhotoCraft
files keep their lock, rewriting it at 0x08.

Co-Authored-By: Qwen 3.8 Flash <noreply@qwen.ai>

* style(io): fix PSD artboard lock review comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc

---------

Co-authored-by: Qwen 3.8 Flash <noreply@qwen.ai>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
2026-10-07 07:08:00 -04:00
MikeeI f9f1efea6e fix(io): preserve selected real PSD masks (#130) 2026-10-05 01:24:54 -04:00
MikeeI 7100b0d617 fix(io): preserve HDR samples when unmatting PSD composites (#106) 2026-10-05 01:00:19 -04:00
Brandon ThomasandClaude Opus 5.5 f7eb6ea9e7 Format: adopt the craft-family rustfmt.toml and format the workspace
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:31:17 -04:00
Brandon ThomasandClaude Opus 5.5 aa87012aab Analysis, notes, proof views, workspaces (+ in-progress work of other agents)
Image > Analysis (ruler, count, measurement scale/log/record, scale
marker), notes (PSD Anno round trip, Notes panel, import), Modifier Keys
panel, Proof Setup plates / Legacy Mac RGB / colour blindness (Vienot),
32-bit preview options, custom pixel aspect, extras options, New/Delete/
Lock Workspace. Parity 600/625, FLOOR 595.

Checkpoint: also contains compiling in-progress work from the parallel
panels, type styles, File menu, photo, filters, UI-parity and fidelity
agents, plus photocraft-cli serve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:49:07 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas ae92ac20bf continued wip 2026-09-30 09:37:18 -04:00
Brandon Thomas 16941f7198 one-shot 2026-09-30 05:02:08 -04:00