kra_saver_fixed
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user