164 lines
6.8 KiB
Markdown
164 lines
6.8 KiB
Markdown
# KRA Format Fix Plan
|
||
|
||
## Root Causes (confirmed by analysis)
|
||
|
||
### 1. VERSION Header Mismatch — Krita Crash + Self-Import Failures
|
||
**File:** `hcie-kra/src/kra_saver.rs` lines 324–329, 409–415
|
||
|
||
The saver writes `VERSION 2\n` in the VERS tile header, but the compressed payload uses
|
||
version byte `1` (meaning **no** delta decoding). Krita's VERS parser sees VERSION 2 and
|
||
applies delta decoding after decompression, producing garbage or a crash.
|
||
|
||
Our own importer (`hcie-kra/src/lib.rs` line 158) also checks `if version >= 2` and
|
||
would apply delta decoding, producing wrong pixel data.
|
||
|
||
**Fix:** Change both `encode_layer_vers` and `encode_mask_vers` to write `VERSION 1`
|
||
instead of `VERSION 2`.
|
||
|
||
### 2. Fake "LZF" Compression — Bloated Files + Non-Standard
|
||
**File:** `hcie-kra/src/kra_saver.rs` lines 308–316, 394–403
|
||
|
||
The "LZF" compression is a custom RLE scheme that adds 513 bytes of overhead per tile
|
||
(1 version byte + 512 control bytes), making files **larger** than uncompressed. A full
|
||
1920×1080 layer (510 tiles) goes from 8.29 MB to 8.62 MB (+4%). The format happens to
|
||
look like real LZF literal runs, so our `lzf_decompress` function processes it correctly,
|
||
but a real LZF decoder would fail on tiles requiring back-references.
|
||
|
||
**Fix:** Store tiles as RAW (uncompressed). Remove the fake compression logic and write
|
||
raw planar pixel data. Label tiles as `RAW` instead of `LZF` in the tile header. The
|
||
importer already handles RAW tiles correctly (lib.rs line 166–168).
|
||
|
||
### 3. Invalid UUID Format — Krita Rejects Our XML
|
||
**File:** `hcie-kra/src/kra_saver.rs` line 240
|
||
|
||
UUIDs are generated from a single `u64` (8 bytes) and formatted as 32 hex digits with
|
||
leading zeros: `{00000000000000003aeba3f7cf423ad9}`. Proper UUIDs are 128 bits (16 bytes)
|
||
with dashed formatting: `{3aeba3f7-cf42-3ad9-…}`. Krita may validate UUID format and
|
||
reject our non-standard forms.
|
||
|
||
**Fix:** Generate proper 128-bit random UUIDs using two `rand::random::<u64>()` calls
|
||
and format with dashes per standard UUID layout.
|
||
|
||
### 4. Color Space / Bit Depth Ignored — Wrong Colors on Import
|
||
**File:** `hcie-kra/src/lib.rs` lines 647–698
|
||
|
||
The importer reads `colorspacename`, `channeldepth`, and `profile` attributes from the
|
||
`<IMAGE>` and `<layer>` tags but ignores them. If the source file uses a non-RGBA color
|
||
space (e.g., CMYK, Grayscale) or 16-bit channel depth, the raw pixel bytes are
|
||
misinterpreted, producing wrong colors.
|
||
|
||
**Fix:** At minimum, validate on import that:
|
||
- `colorspacename` is `"RGBA"`
|
||
- `channeldepth` is `"U8"` or `"U16"` (for U16, convert to U8 via shifting)
|
||
- Return an error if the format is unsupported
|
||
|
||
### 5. Silent Layer Failures — Data Loss Without Notice
|
||
**File:** `hcie-kra/src/lib.rs` lines 790, 808, 814, 837
|
||
|
||
When a layer file can't be found, VERS parsing fails, or image decoding fails, the
|
||
layer is silently skipped or turned into a transparent layer. The user has no indication
|
||
that data was lost.
|
||
|
||
**Fix:** Add `log::warn!` calls at each failure point, including the layer name and
|
||
reason for failure.
|
||
|
||
### 6. Missing `layers.xml` Fallback
|
||
**File:** `hcie-kra/src/lib.rs` line 626
|
||
|
||
The main importer only tries `maindoc.xml`. Some Krita variants write `layers.xml`
|
||
instead. The v2 loader (`custom_loaders_kra_v2.rs`) has this fallback but it's separate.
|
||
|
||
**Fix:** Add `layers.xml` as a secondary attempt in `try_import_krita_maindoc`.
|
||
|
||
### 7. Inconsistent `DATA` Line Format
|
||
**File:** `hcie-kra/src/kra_saver.rs` lines 329 vs 415
|
||
|
||
`encode_layer_vers` writes `DATA {tiles.len()}\n` but `encode_mask_vers` writes `DATA\n`
|
||
(no count). The parser handles both, but it's inconsistent.
|
||
|
||
**Fix:** Make both use `DATA\n` (no count) — the parser ignores the count anyway, and
|
||
removing it avoids an extra format requirement.
|
||
|
||
---
|
||
|
||
## Implementation Tasks
|
||
|
||
### Task A: Fix VERS header and compression (`kra_saver.rs`)
|
||
1. Change `writeln!(output, "VERSION 2")` → `writeln!(output, "VERSION 1")` in `encode_layer_vers`
|
||
2. Change `b"VERSION 2\n"` → `b"VERSION 1\n"` in `encode_mask_vers`
|
||
3. Remove the fake LZF compression loop in `encode_layer_vers` (lines 308–316)
|
||
4. Replace with writing raw planar pixel data directly (no version byte, no control bytes)
|
||
5. Update tile header tag from `"LZF"` to `"RAW"` (line 333)
|
||
6. Repeat steps 3–5 for `encode_mask_vers` (lines 394–403)
|
||
|
||
### Task B: Fix UUID format (`kra_saver.rs`)
|
||
7. Replace `let uuid = format!("{{{:032x}}}", layer.id)` with proper 128-bit UUID:
|
||
```rust
|
||
let high = rand::random::<u64>();
|
||
let low = rand::random::<u64>();
|
||
let uuid = format!(
|
||
"{{{:08x}-{:04x}-{:04x}-{:04x}-{:012x}}}",
|
||
(high >> 32) as u32,
|
||
(high >> 16) as u16 & 0xFFFF,
|
||
high as u16,
|
||
(low >> 48) as u16,
|
||
low & 0x0000_FFFF_FFFF_FFFF
|
||
);
|
||
```
|
||
8. Apply same fix to mask UUID generation on line 257
|
||
|
||
### Task C: Add color space validation (`lib.rs`)
|
||
9. In `try_import_krita_maindoc`, after reading width/height from `<IMAGE>`:
|
||
- Read `colorspacename` and `channeldepth` attributes
|
||
- If `colorspacename != "RGBA"`, return error: "Unsupported color space: {name}"
|
||
- If `channeldepth != "U8"`, return error: "Unsupported channel depth: {depth}"
|
||
- (Future: add U16→U8 conversion)
|
||
|
||
### Task D: Add error logging (`lib.rs`)
|
||
10. Add `log::warn!` before each `continue` / silent fallback:
|
||
- Line 790 (layer file not found): warn with layer name
|
||
- Line 808 (SVG rasterization failed): warn with layer name
|
||
- Line 814 (VERS parse failed): warn with layer name
|
||
- Line 837 (image load failed): warn with layer name
|
||
|
||
### Task E: Add `layers.xml` fallback (`lib.rs`)
|
||
11. In `try_import_krita_maindoc`, after the `maindoc.xml` attempt fails, try
|
||
`archive.by_name("layers.xml")` before returning `Ok(vec![])`.
|
||
|
||
### Task F: Normalize `DATA` line format (`kra_saver.rs`)
|
||
12. Change line 329: `writeln!(output, "DATA {}", tiles.len())` → `writeln!(output, "DATA")`
|
||
13. The mask encoder already uses `b"DATA\n"` — leave unchanged.
|
||
|
||
---
|
||
|
||
## Files to Modify
|
||
|
||
| File | Changes |
|
||
|------|---------|
|
||
| `hcie-kra/src/kra_saver.rs` | Tasks A, B, F |
|
||
| `hcie-kra/src/lib.rs` | Tasks C, D, E |
|
||
|
||
---
|
||
|
||
## Validation
|
||
|
||
After implementing, verify with:
|
||
|
||
1. **Roundtrip test:** Save a multi-layer document as KRA, then import it back. Compare
|
||
pixels and layer names against the original.
|
||
|
||
2. **Krita compatibility:** Open the exported KRA in Krita ≥ 5.x. Verify it opens without
|
||
crash and displays correct colors. Also open the PSD→KRA exported file and verify it
|
||
matches the mergedimage.png preview.
|
||
|
||
3. **Existing test suite:** Run `cargo test -p hcie-kra` and
|
||
`cargo test -p hcie-engine-api --test visual_regression` to ensure no regressions.
|
||
|
||
4. **Edge cases:**
|
||
- Empty layers (all-transparent pixels)
|
||
- Layers with masks
|
||
- Group layers
|
||
- Layer names containing special characters (`&`, `<`, `>`, Unicode)
|
||
- Error on unsupported color space (e.g., CMYK, Grayscale)
|
||
- Error on unsupported bit depth (e.g., U16, F16)
|