feat(iris-psd): implement Phase 1 PSD read support#10
Merged
Conversation
Implements the Photoshop PSD importer (SPEC.md §5.1, crates/iris-psd/BRIEF.md
Phase 1): read flat RGB/Grayscale pixel layers and produce an AifDocument.
iris-psd:
- PsdReader::{read, from_reader, from_bytes} returning iris_aif::AifDocument
- Typed PsdError enum (BadSignature, Truncated, UnsupportedVersion,
UnsupportedColorMode, Corrupt, Io); malformed input never panics out of the
crate — the underlying psd parser is wrapped in catch_unwind
- Per-layer conversion: name, visibility, opacity, blend mode (all 27 modes
mapped) and clipping flag; empty-layer-section files fall back to the merged
composite as a single Background layer (COMPAT(adobe))
- RGB/Grayscale supported; CMYK/Lab/Indexed rejected with a typed error
(deferred to Phase 4)
- Unit + integration tests build PSD fixtures in-memory (single-layer RGBA,
no-layer composite, bad signature, PSB version, colour-mode rejection)
iris-aif:
- Extract shared layer_from_rgba8 helper (single source of truth for the
sRGB->linear f16 tile encoding) reused by the raster importer and iris-psd
iris-app:
- Import button opens .psd files as a new multi-layer document via PsdReader;
other raster formats still import as a single layer
Note: this adds iris-app -> iris-psd, a format-adapter dependency not in the
CLAUDE.md dependency graph (which predates the adapters). Natural direction;
flagging for an ADR/graph update.
https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk
Builds on Phase 1 PSD read with two fidelity/efficiency improvements:
- Group hierarchy: PSD group folders are reconstructed as nested
LayerContent::Group nodes. Stacking order (including interleaved groups and
layers at the same level) is recovered from each group's minimum flat layer
index, so Photoshop's top-first panel order is preserved.
- Layer-bounds cropping: each layer is cropped to its on-canvas bounding box
and given a canvas offset, so tile storage is proportional to the layer
rather than the whole document. The compositor already honours
canvas_offset_x/y, so positioning is unchanged.
Tree construction moved into a dedicated `layers` module to keep files under
the line ceiling; `convert` now covers document-level concerns only.
Tests: added a grouped-PSD fixture (bounding-section + content + open-folder
divider records) asserting the nested Group { Layer } structure.
Group-level opacity/blend/visibility are not yet applied to children by the
compositor (TODO flagged) — group nodes are structural for now.
https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk
Adds PsdWriter, a hand-rolled Photoshop PSD serialiser (the `psd` crate is read-only). Completes the PSD read/write round-trip for Phase 2. - 8-bit RGBA pixel layers with correct rectangles, channel data, blend mode (full inverse key map), opacity, visibility, and clipping flag - Group hierarchy emitted as bounding-section + open-folder divider records in Photoshop's bottom-to-top file order - Flattened merged image-data section (Porter-Duff "over") so readers that don't parse layers still show the composite - DimensionsTooLarge error guards the 30,000px PSD limit (PSB not yet written) iris-aif: - Add layer_to_rgba8 / linear_to_srgb (reverse of layer_from_rgba8) so adapters can read pixel tiles back as 8-bit sRGB; single source of truth for the tile<->8-bit encoding Tests: round-trip via our own reader (single pixel layer incl. blend/opacity/ pixel values, nested group, oversized rejection). Verified structurally and at the pixel level; not yet validated against Photoshop itself (no Photoshop in CI) — composite is sRGB-space and group opacity/blend aren't applied to children yet (TODOs flagged). https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk
Implements iris-ora (SPEC.md §5.2, BRIEF Phase 1): full ORA read and write of pixel layers and nested groups. - OraReader: parses the ZIP container + stack.xml, decodes each layer PNG into a tiled f16 pixel layer, preserving position (x/y), opacity, visibility, and blend mode (composite-op). Group <stack> nesting and top-first order are kept. - OraWriter: emits mimetype (stored, first), stack.xml, per-layer data/N.png, and a flattened mergedimage.png. - composite-op <-> BlendMode mapping covering the SVG core set plus Krita's vendor-prefixed modes (COMPAT(krita)); unknown ops degrade to Normal. iris-aif (shared raster helpers, also used by iris-psd): - flatten_to_rgba8 (merged composite) and encode_png_rgba8; iris-psd's writer now reuses flatten_to_rgba8 instead of its own copy. iris-app: - Import opens .ora (ZIP) and .psd (8BPS) as new documents via a shared open_as_document helper; other formats still import as a single layer. Tests: composite-op round-trip + unknown handling; full AIF->ORA->AIF round-trip (group hierarchy, layer position/opacity/blend, pixel values); a Krita-prefixed composite-op round-trip; and bad-mimetype rejection. Validated through our own reader (no Krita/GIMP in CI). Thumbnail part and linear-light compositing are deferred (TODOs flagged). https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk
Implements the iris-vector document model and the SVG adapter on top of it, and wires vector content into the unified layer tree. iris-vector (was a stub): - PathObject (kurbo BezPath + fill/stroke/fill-rule/transform), Paint (solid + linear/radial gradients), StrokePaint, Color, and VectorLayer. iris-pixel (core): - LayerContent::Vector now carries an iris_vector::VectorLayer, so the unified tree holds both raster and vector layers (ADR 001 / SPEC §3.3). Re-exports VectorLayer. iris-aif's layer-type mapping updated for the payload; all other match sites use let-else/catch-all and are unaffected. iris-svg (was a stub, Phase 1): - SvgReader: paths + basic shapes (rect/circle/ellipse/line/poly), <g> with composed transforms, viewBox mapping, inherited presentation attributes + inline style, solid fills/strokes, and embedded <image> data URIs -> raster layers. Built on kurbo::BezPath::from_svg (no heavy SVG dependency); local RFC 4648 base64 codec. - SvgWriter: vector objects -> <path>, raster layers -> base64 <image>, groups -> <g>. iris-app: - Import opens .svg as a new document (alongside PSD/ORA). Tests: iris-vector unit tests; SVG shape reading, group-transform composition, vector round-trip (fill/stroke/fill-rule/geometry), and embedded-raster round-trip. Gradients, text, filters, clips, masks, and patterns are deferred with TODOs; group structure is flattened on read and raster sits below vector (z-order heuristic) — noted in code. https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the Photoshop PSD importer (SPEC.md §5.1, crates/iris-psd/BRIEF.md
Phase 1): read flat RGB/Grayscale pixel layers and produce an AifDocument.
iris-psd:
UnsupportedColorMode, Corrupt, Io); malformed input never panics out of the
crate — the underlying psd parser is wrapped in catch_unwind
mapped) and clipping flag; empty-layer-section files fall back to the merged
composite as a single Background layer (COMPAT(adobe))
(deferred to Phase 4)
no-layer composite, bad signature, PSB version, colour-mode rejection)
iris-aif:
sRGB->linear f16 tile encoding) reused by the raster importer and iris-psd
iris-app:
other raster formats still import as a single layer
Note: this adds iris-app -> iris-psd, a format-adapter dependency not in the
CLAUDE.md dependency graph (which predates the adapters). Natural direction;
flagging for an ADR/graph update.
https://claude.ai/code/session_0178WAYd6KzMLnHb6xKbjgyk