Skip to content

feat(iris-psd): implement Phase 1 PSD read support#10

Merged
kevincarlson merged 5 commits into
mainfrom
claude/design-app-feature-audit-qoz91y
Jun 16, 2026
Merged

feat(iris-psd): implement Phase 1 PSD read support#10
kevincarlson merged 5 commits into
mainfrom
claude/design-app-feature-audit-qoz91y

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

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

claude added 5 commits June 15, 2026 02:44
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
@kevincarlson kevincarlson merged commit 75af63c into main Jun 16, 2026
1 check passed
@AppThere AppThere locked as resolved and limited conversation to collaborators Jun 19, 2026
@kevincarlson kevincarlson deleted the claude/design-app-feature-audit-qoz91y branch June 19, 2026 23:11
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants