132 lines
4.7 KiB
Markdown
132 lines
4.7 KiB
Markdown
# PSD Writer: Regression Fix for Photoshop Compatibility
|
||
|
||
## Problem
|
||
|
||
The PSD files generated by `hcie-psd/src/psd_saver.rs` can be parsed by `Psd::from_bytes` (our internal parser) but **cannot be opened in Adobe Photoshop**. The user says PSD writing was working earlier in the project's git history and has regressed.
|
||
|
||
## Git History
|
||
|
||
There are 3 commits touching `hcie-psd/src/psd_saver.rs`:
|
||
|
||
1. `cd0b6b1` — Initial PSD writer (997 lines)
|
||
2. `0a821ea` — Added examples, 1-line change to saver
|
||
3. `6616b31` — "Fix layer extra data and merged image layout" (our recent changes)
|
||
|
||
## Key Differences: `cd0b6b1` vs `6616b31` (HEAD)
|
||
|
||
### Change 1: Merged Image Row Length Order (SUSPECT)
|
||
|
||
**Original (cd0b6b1):** Interleaved — `[R0, G0, B0, A0, R1, G1, B1, A1, ...]`
|
||
```rust
|
||
for y in 0..h {
|
||
for ch_idx in 0..4 {
|
||
buf.extend_from_slice(&row_lengths[ch_idx][y]);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Current (6616b31):** Per-channel — `[R0, R1, ..., Rn, G0, G1, ..., Gn, ...]`
|
||
```rust
|
||
for ch_idx in 0..4 {
|
||
for y in 0..h {
|
||
buf.extend_from_slice(&row_lengths[ch_idx][y]);
|
||
}
|
||
}
|
||
```
|
||
|
||
**PSD Spec says:** "byte counts for all the scan lines (rows × channels)" — ambiguous ordering, but data is planar (`RRR GGG BBB`). Both orderings produce the same total count. The question is which order Photoshop expects.
|
||
|
||
**CRITICAL:** The original code has a **mismatch** — byte counts are interleaved but compressed data is per-channel. This would cause Photoshop to read wrong row lengths → corrupted data. The current code is internally consistent (both per-channel). But maybe Photoshop expects interleaved for both.
|
||
|
||
### Change 2: Layer Count Sign (SUSPECT)
|
||
|
||
Both versions always write **negative** layer count:
|
||
```rust
|
||
layer_records_buf.extend_from_slice(&(-layer_count.abs()).to_be_bytes());
|
||
```
|
||
|
||
Per PSD spec: "If the count is negative, the first layer in the following record is a group." For a simple document with no groups, Photoshop may reject negative count.
|
||
|
||
### Change 3: Extra Data Blocks (6616b31 additions)
|
||
|
||
- Added `luni` (Unicode layer name) block
|
||
- Added `lyid` (layer ID) block
|
||
- Moved `lfx2` odd-length padding from external to internal
|
||
- Used `write_8bim_block` helper for `iOpa`, `brst`, `shmd`
|
||
|
||
### Change 4: `PassThrough` blend mode added
|
||
|
||
```rust
|
||
BlendMode::PassThrough => b"pass",
|
||
```
|
||
|
||
## Debugging Plan
|
||
|
||
### Step 1: Create Minimal Test PSD (no effects)
|
||
|
||
Write a Rust example `examples/test_psd_simple.rs` that:
|
||
1. Creates a 256×256 canvas
|
||
2. Adds one layer with a red rectangle (no effects)
|
||
3. Calls `save_psd()` to write `_tmp/simple_test.psd`
|
||
4. The user opens this in Photoshop
|
||
|
||
This isolates the PSD structural issues from effects serialization.
|
||
|
||
### Step 2: Binary Comparison with Reference PSD
|
||
|
||
If Step 1 PSD doesn't open:
|
||
1. User creates the same image in Photopea/Photoshop and saves as PSD
|
||
2. Write a Python script to hex-dump both files and diff the binary structure
|
||
3. Identify exact byte offset where the format diverges
|
||
|
||
### Step 3: Fix Identified Issues
|
||
|
||
Based on the debugging results, fix the PSD writer. Most likely fixes:
|
||
|
||
**Fix A — Merged Image Row Lengths:** Try interleaved order (matching original) for BOTH byte counts AND data. This is the most common PSD layout in practice.
|
||
|
||
**Fix B — Layer Count:** Write positive count when there are no groups:
|
||
```rust
|
||
let count = if layers.iter().any(|l| l.layer_type == LayerType::Group) {
|
||
-(layers.len() as i16)
|
||
} else {
|
||
layers.len() as i16
|
||
};
|
||
```
|
||
|
||
**Fix C — lfx2/shmd descriptor correctness:** Verify the ActionDescriptor binary format matches what Photoshop writes. Cross-reference with the spec's descriptor format section.
|
||
|
||
### Step 4: Test with Effects
|
||
|
||
Once the basic PSD opens, add inner shadow effects and test again.
|
||
|
||
### Step 5: Batch Generate 192 Inner Shadow PSDs
|
||
|
||
Write `examples/generate_inner_shadow_psd_set.rs` using the fixed `save_psd()`.
|
||
|
||
### Step 6: Batch Export PNGs in Photoshop
|
||
|
||
User runs `_tools/photoshop/export_psd_folder_to_png.jsx` in Photoshop to batch-export all 192 PSDs as transparent PNGs.
|
||
|
||
### Step 7: Run MAE Grid Search
|
||
|
||
```bash
|
||
cargo run --package hcie-io --example tune_mae_inner_shadow_grid
|
||
```
|
||
|
||
## Files to Modify/Create
|
||
|
||
| File | Action |
|
||
|---|---|
|
||
| `hcie-psd/src/psd_saver.rs` | Fix merged image row order, layer count, possibly effects format |
|
||
| `hcie-io/examples/test_psd_simple.rs` | **Create** — minimal PSD for Photoshop validation |
|
||
| `hcie-io/examples/generate_inner_shadow_psd_set.rs` | **Create** — batch inner shadow PSD generator |
|
||
| `hcie-io/examples/tune_mae_inner_shadow_grid.rs` | Already created, ready to use |
|
||
|
||
## Verification
|
||
|
||
1. `cargo run --package hcie-io --example test_psd_simple` → user opens in Photoshop
|
||
2. If opens → proceed to effects test
|
||
3. If fails → hex dump comparison with Photoshop-generated PSD
|
||
4. Final: 192 PSDs → Photoshop batch export → MAE tuning
|