Files
hcie-rust-v3.05/.kilo/plans/fix-psd-writer-structure.md
T
2026-07-09 02:59:53 +03:00

197 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PSD Writer Structural Fix Plan — Example3-mini round-trip
## Problem
`/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini-hcie.psd` is produced from the original `Example3-mini.psd` via `hcie-psd-saver/src/lib.rs`. Side-by-side binary analysis shows several structural problems that prevent Photoshop from opening the generated file correctly and corrupt the layer/folder structure:
1. **Group / section-divider layers are lost** — original has 19 layers (including a section divider for the `Dodge and Burn` group); HCIE output had 18. The divider layer with `lsct` section type 2 was dropped, so Photoshop could not reconstruct the folder.
2. **Layer blend mode string mismatch for group pass-through** — original group divider uses `pass`; HCIE wrote `norm` for the group layer. `BlendMode::PassThrough` exists in `hcie_protocol` but the PSD writer mapped it to `b"pass"` only in the record. However the imported group layer came in as `BlendMode::Normal` because `hcie-psd/src/psd_import.rs` converted "PassThrough" to `Normal`.
3. **Layer flags were always 0** — original uses flags `8` (transparency protected / group flags), `24` (group/adjustment), `56` (adjustment with vector mask).
4. **Group layers were emitted at full canvas size** — imported group layer had `width=1920 height=1080` and was written as a normal raster layer with transparent pixels. Original group divider layers have `bounds=(0,0,0,0)` and only an `lsct` block.
5. **Adjustment layers lost their extra blocks** — original `curv`, `grdm`, `hue2` layers have 40+ byte adjustment descriptors. HCIE imported them as `Raster` with no adjustment data, then wrote them as 1×1 transparent raster layers.
6. **Layer extra block order/format differences** — original uses `luni` immediately after the Pascal name. Extra block ordering in general did not match Photoshop's observed sequence.
7. **`lfx2` / effects data was missing for layers that had effects** — original `Rear Light` / `Jaw Light` / `Cheek Redness` contain `lfx2` blocks; HCIE output had none, so all layer effects were lost on export.
8. **Adjustment blocks (`curv`, `grdm`, `hue2`) were not written** — even if imported, the writer had no code to emit these.
9. **`iOpa` / `brst` ordering inside extra data was non-standard** — writer placed `iOpa` and `brst` before `lfx2`.
10. **Merged image data differs** — expected because compositing in HCIE is not pixel-identical to Photoshop, but the layer/image structure must be valid first.
## Root Causes
- `hcie-psd/src/psd_import.rs` collapsed group section-divider layers and `PassThrough` blend mode into `LayerType::Group` + `BlendMode::Normal`, so the writer could not emit the correct divider record.
- `hcie-psd/src/psd_import.rs` did not preserve adjustment data (`curv`, `grdm`, `hue2`) into `layer.adjustment`; it only built curve-based `Adjustment` for curves, but the writer never read `layer.adjustment`.
- `hcie-psd-saver/src/lib.rs` treated every layer as a 4-channel raster layer with full canvas or content bounds, and emitted the same extra block sequence regardless of layer type.
- The writer did not emit section-divider records (`lsct` type 2) for group end markers, and did not write pass-through blend mode for group dividers.
- Layer flags were hard-coded to `0`; the writer did not compute protection/group/adjustment flags.
- The writer's image resources section is a minimal stub; the original PSD contains ~12 KB of resources (thumbnail, guides, print info, paths, etc.).
- `hcie-fx::parser::parse_lfx2` cannot yet decode the newer `lfx2` descriptor format used by `Example3-mini.psd`, so effects are lost during import and therefore cannot be re-emitted.
## Files to Modify
| File | Layer | Why |
|------|-------|-----|
| `hcie-psd-saver/src/lib.rs` | GUI/open (crate not in locked list) | Main PSD writer; group/adjustment/flag/effect handling and image resources |
| `hcie-psd/src/psd_import.rs` | editable | Preserve section-divider type, pass-through blend, adjustment raw data |
| `hcie-protocol/src/lib.rs` | locked (unlocked only for these fields) | `Layer.adjustment_raw`, `Layer.is_section_divider` storage fields |
| `hcie-psd-saver/src/fx_descriptors.rs` | GUI/open | Effect descriptor serialization; needs to stay in sync with `hcie-fx` parser |
| `hcie-psd-saver/src/helpers.rs` | GUI/open | `write_8bim_block` / `write_luni_block` helpers |
| `hcie-fx/src/parser.rs` | engine (locked via AGENTS.md) | `parse_lfx2` / `parse_descriptor_body` need to decode `numM`/`ChFX`/modern descriptors |
## Implementation Status
### ✅ Done
- Added `adjustment_raw: Option<([u8; 4], Vec<u8>)>` and `is_section_divider: bool` to `hcie_protocol::Layer`.
- `psd_import` now preserves `curv`/`grdm`/`hue2` raw bytes and section-divider type/PassThrough blend.
- `psd_import` emits a `</Layer group>` section-divider marker in the correct position.
- `psd_saver` emits section dividers with `bounds=(0,0,0,0)`, empty channels, `lsct` type 2, and `pass` blend mode.
- `psd_saver` computes layer flags (group/adjustment).
- `psd_saver` passes adjustment raw blocks through before `luni`/`lfx2`.
- `psd_saver` orders extra blocks as: mask → blending ranges → Pascal name → adjustment → `lfx2`/`iOpa`/`brst``luni``lyid``lsct``clbl`/`infx`/`knko`/`lspf`/`lclr``shmd``fxrp``lyvr`.
- `roundtrip_psd` example now reports structural comparison.
### Current Test Result
```
Original layer records: 19
Saved layer records: 19
Layer structure matches (names, group/divider flags, adjustment flags).
```
Group divider blend is now `pass` and section-divider marker is present.
## Remaining Work for Full Photoshop Compatibility
### 1. Effect Import (`hcie-fx` parser)
`Example3-mini.psd` stores effects in `lfx2` descriptor format. The current `parse_lfx2` only sees `Scl`, `masterFXSwitch`, `numM` entries and fails to parse the `numM` list of `ChFX`/`DrSh`/etc. objects. Until this is fixed, `layer.effects` remains empty on import and cannot be re-emitted.
**Approach:**
- Extend `parse_descriptor_body` to handle `VlLs`/`Objc` recursion with correct skip offsets for non-ASL descriptors.
- Add explicit handling for `numM` (effect list) and common modern effect keys.
- Add fixture tests using the 100-byte `lfx2` blocks from `Rear Light`, `Jaw Light`, `Cheek Redness`.
### 2. Image Resources Passthrough
The original PSD has 24 resources (~12 KB); the writer only writes 7 minimal ones. Missing resources include:
- `0x040C` thumbnail
- `0x03F3`, `0x03F5`, `0x03F8`, `0x0400`, `0x0404`, `0x0411`, `0x041A`, `0x041E`, `0x0421`, `0x0422`, `0x0424`, `0x0425`, `0x0426`, `0x0428`, `0x0429`, `0x043A`, `0x043B`, `0x2710`
**Approach:**
- During import, store the raw image resources section bytes (or a parsed map of resource ID → data).
- In `save_psd`, if the input came from a PSD, write the original resources back unchanged, replacing only resources we must generate (resolution, global angle, layer selection if needed).
- This avoids reverse-engineering every resource format and keeps Photoshop metadata intact.
### 3. Layer Channel Data for Adjustment Layers
Original adjustment layers have 5 channels (`-1, 0, 1, 2, -2` mask) with zero-length channel data. Our writer currently writes 4 channels for adjustment layers. Photoshop may expect 5 if a mask is declared.
**Approach:**
- Preserve original channel layout during import and re-emit exactly the same channel IDs/lengths for adjustment/group layers.
- For raster layers, keep the 4-channel auto-crop logic.
### 4. `luni` Block Format
Original PSD's `luni` block does **not** have a leading Pascal string; it starts directly with 4-byte UTF-16 length. `write_8bim_block` adds a Pascal string length byte of 0 before the data, which may confuse strict parsers. In practice it parses, but for parity we should support writing `luni` without the Pascal prefix.
**Approach:**
- Add a helper that writes `luni` either with or without the leading Pascal byte, controlled by a compatibility flag.
- After verifying with Photoshop, choose the format that matches the original.
### 5. Merged Image Layout
Historical versions of the writer used interleaved row lengths (`R0 G0 B0 A0 R1 G1 B1 A1 ...`) but per-channel compressed data. Current code is internally consistent (per-channel lengths + per-channel data). The plan file `fix-psd-writer-for-photoshop.md` flagged this as a possible regression.
**Approach:**
- Add a toggle in `write_merged_image_data` for interleaved vs. per-channel row lengths.
- Generate a minimal 256×256 single-layer PSD with both layouts and test which one Photoshop/Photopea opens.
- Lock in the working layout and add a regression test.
### 6. Clipping Mask / Vector Mask Support
Original layers with effects (e.g. `Rear Light`) use an explicit `-2` mask channel (`mask_len=28`). Our writer currently writes an empty mask section. If effects rely on the mask, they will look wrong.
**Approach:**
- Preserve `mask_pixels`, `mask_bounds`, and `mask_default_color` on import (already partially done).
- Re-emit layer masks in the saved PSD when present.
### 7. Group Bounds / Section Divider Channel Count
Original section-divider record has 4 channels with 2-byte `0001` RLE markers. Our writer currently emits 4 channels with a single `0x81` PackBits run marker. Both represent empty data, but byte-for-byte parity requires matching the exact marker format.
**Approach:**
- Match the original exactly: 4 channels, each `00 01` compression type followed by nothing (or minimal empty RLE row).
## Test & Tune System
To reach full compatibility we need automated, repeatable tests. Proposed system:
### A. Structural Regression Suite
Extend `hcie-io/examples/roundtrip_psd.rs` to:
1. Compare layer count, names, order.
2. Compare per-layer bounds, channel count/IDs, channel lengths.
3. Compare blend mode keys, opacity, clipping, flags.
4. Compare extra block keys and order (mask → blending → name → adjustment → lfx2 → iOpa/brst → luni → lyid → lsct → clbl/infx/knko/lspf/lclr → shmd → fxrp → lyvr).
5. Compare image resource IDs.
6. Compare merged image compression type.
Fail on first divergence and print both sides.
### B. Photoshop Batch Validation Harness
Create `_tools/photoshop/validate_psd.jsx` that:
1. Opens a PSD in Photoshop.
2. Reports whether it opened without error.
3. Exports layer count and layer names to a JSON/text file.
4. Saves a flattened PNG for visual comparison.
Wrap it with a Rust/Node runner in `hcie-io/examples/photoshop_validate.rs` so CI/dev can call it locally if Photoshop is installed.
### C. Golden File Regression
Commit `Example3-mini-hcie.psd` and a `tests/psd_roundtrip.rs` integration test that:
1. Imports `Example3-mini.psd`.
2. Saves to a temp file.
3. Asserts structural parity with the original.
4. (Later) asserts pixel MAE under a threshold once effects are preserved.
### D. MAE-Based Effect Tuning
Reuse the existing `tune_mae_*_grid.rs` pattern:
1. Generate a grid of single-effect PSDs with `save_psd`.
2. Batch-export ground-truth PNGs from the same PSDs in Photoshop.
3. Run grid search over effect parameters (blur factor, spread scale, etc.) to minimize MAE.
4. Apply best defaults to `hcie-fx` or `hcie-psd-saver/fx_descriptors.rs`.
This is currently done for drop shadow, outer glow, inner glow, bevel, stroke; inner shadow and satin need similar treatment.
### E. Minimal Photoshop Open Test
Create `hcie-io/examples/test_psd_simple.rs` that writes a 256×256 single-layer PSD. This is the fastest way to verify whether the core PSD container opens in Photoshop before adding complex features.
## Success Criteria (Updated)
1.`Example3-mini-hcie.psd` has 19 layers and matching structural roles.
2.`roundtrip_psd` reports structural parity.
3. ⬜ File opens in Photoshop without "not compatible" error.
4. ⬜ Folder/adjustment structure is visually correct.
5. ⬜ Layer effects round-trip and render correctly (requires `hcie-fx` parser fix).
6. ⬜ Merged image MAE is within acceptable threshold vs. Photoshop ground-truth PNG.
## Next Steps (Priority Order)
1. **Image resources passthrough** — lowest risk, biggest Photoshop-compatibility win.
2. **Minimal PSD open test** — verify base container with Photoshop/Photopea.
3. **lfx2 effect parser fix** — unlock full effect round-trip.
4. **Layer mask / adjustment channel layout parity** — fix structural edge cases.
5. **Merged image row-length layout test** — confirm Photoshop-preferred order.
6. **Golden regression test** — lock in progress and prevent regressions.
## Notes
- `hcie-protocol` changes must remain minimal and backward-compatible; new fields have `#[serde(default)]`.
- `hcie-fx` is a locked engine crate; any changes there must go through `unlock.sh` / `lock.sh` and be justified by a failing regression test.
- Do not chase pixel-perfect compositing in this plan; focus on structural validity and Photoshop opening first, then MAE-based tuning.
(Updated: 2026-06-18)