kra_saver_fixed
This commit is contained in:
+131
-37
@@ -1,5 +1,6 @@
|
||||
use hcie_protocol::{Layer, LayerData, LayerType, VectorShape};
|
||||
use hcie_protocol::BlendMode;
|
||||
use rand;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -231,13 +232,22 @@ fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
let node_type = match layer.layer_type {
|
||||
LayerType::Raster | LayerType::Mask => "paintlayer",
|
||||
LayerType::Vector | LayerType::Text => "shapelayer",
|
||||
LayerType::Group => "paintlayer",
|
||||
LayerType::Group => "grouplayer",
|
||||
};
|
||||
|
||||
let visible = if layer.visible { "1" } else { "0" };
|
||||
let opacity = (layer.opacity * 255.0) as u8;
|
||||
let filename = format!("layer{}", i + 1);
|
||||
let uuid = format!("{{{:032x}}}", layer.id);
|
||||
let uuid_high = rand::random::<u64>();
|
||||
let uuid_low = rand::random::<u64>();
|
||||
let uuid = format!(
|
||||
"{{{:08x}-{:04x}-{:04x}-{:04x}-{:012x}}}",
|
||||
(uuid_high >> 32) as u32,
|
||||
(uuid_high >> 16) as u16 & 0xFFFF,
|
||||
uuid_high as u16,
|
||||
(uuid_low >> 48) as u16,
|
||||
uuid_low & 0x0000_FFFF_FFFF_FFFF
|
||||
);
|
||||
let compositeop = blend_mode_to_krita_compositeop(layer.blend_mode);
|
||||
|
||||
let escaped_name = layer.name
|
||||
@@ -254,9 +264,18 @@ fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
if layer.mask_pixels.is_some() {
|
||||
xml.push_str(" <layers>\n");
|
||||
let mask_filename = format!("{}_mask", filename);
|
||||
let mask_uuid = format!("{{{:032x}}}", layer.id.wrapping_add(1)); // pseudo uuid
|
||||
let mask_high = rand::random::<u64>();
|
||||
let mask_low = rand::random::<u64>();
|
||||
let mask_uuid = format!(
|
||||
"{{{:08x}-{:04x}-{:04x}-{:04x}-{:012x}}}",
|
||||
(mask_high >> 32) as u32,
|
||||
(mask_high >> 16) as u16 & 0xFFFF,
|
||||
mask_high as u16,
|
||||
(mask_low >> 48) as u16,
|
||||
mask_low & 0x0000_FFFF_FFFF_FFFF
|
||||
);
|
||||
xml.push_str(&format!(
|
||||
" <layer name=\"Transparency Mask\" filename=\"{}\" nodetype=\"transparencymask\" visible=\"1\" opacity=\"255\" uuid=\"{}\" compositeop=\"normal\" colorspacename=\"Alpha\" channeldepth=\"U8\" profile=\"\" x=\"0\" y=\"0\" />\n",
|
||||
" <layer name=\"Transparency Mask\" filename=\"{}\" nodetype=\"transparencymask\" visible=\"1\" opacity=\"255\" uuid=\"{}\" compositeop=\"normal\" colorspacename=\"GRAYA\" channeldepth=\"U8\" profile=\"Gray-D50-elle-V2-g10.icc\" x=\"0\" y=\"0\" />\n",
|
||||
mask_filename, mask_uuid
|
||||
));
|
||||
xml.push_str(" </layers>\n");
|
||||
@@ -293,10 +312,11 @@ fn encode_layer_vers(layer: &Layer) -> Vec<u8> {
|
||||
let idx = (ly as usize * layer.width as usize + lx as usize) * 4;
|
||||
let tidx = y as usize * tw as usize + x as usize;
|
||||
|
||||
tile_raw[tidx] = layer.pixels[idx + 2];
|
||||
tile_raw[tidx + 1 * plane_size] = layer.pixels[idx + 1];
|
||||
tile_raw[tidx + 2 * plane_size] = layer.pixels[idx];
|
||||
tile_raw[tidx + 3 * plane_size] = layer.pixels[idx + 3];
|
||||
// Krita planar order: B, G, R, A
|
||||
tile_raw[tidx] = layer.pixels[idx + 2]; // B
|
||||
tile_raw[tidx + 1 * plane_size] = layer.pixels[idx + 1]; // G
|
||||
tile_raw[tidx + 2 * plane_size] = layer.pixels[idx]; // R
|
||||
tile_raw[tidx + 3 * plane_size] = layer.pixels[idx + 3]; // A
|
||||
|
||||
if layer.pixels[idx + 3] > 0 {
|
||||
has_content = true;
|
||||
@@ -306,15 +326,14 @@ fn encode_layer_vers(layer: &Layer) -> Vec<u8> {
|
||||
}
|
||||
|
||||
if has_content {
|
||||
let mut compressed = vec![1u8];
|
||||
let mut i = 0;
|
||||
while i < tile_raw.len() {
|
||||
let chunk = (tile_raw.len() - i).min(32);
|
||||
compressed.push((chunk - 1) as u8);
|
||||
compressed.extend_from_slice(&tile_raw[i..i + chunk]);
|
||||
i += chunk;
|
||||
}
|
||||
tiles.push((tx, ty, compressed));
|
||||
// Krita VERSION 2 tile format:
|
||||
// payload = [version_byte = 1][lzf_compressed_planar_data]
|
||||
// version_byte < 2 → no delta-decode on read (straightforward LZF).
|
||||
let compressed = lzf_compress(&tile_raw);
|
||||
let mut payload = Vec::with_capacity(1 + compressed.len());
|
||||
payload.push(1u8); // LZF version byte
|
||||
payload.extend_from_slice(&compressed);
|
||||
tiles.push((tx, ty, payload));
|
||||
}
|
||||
tx += tw;
|
||||
}
|
||||
@@ -329,15 +348,93 @@ fn encode_layer_vers(layer: &Layer) -> Vec<u8> {
|
||||
writeln!(output, "DATA {}", tiles.len()).unwrap();
|
||||
|
||||
tiles.sort_by_key(|(x, y, _)| (*y, *x));
|
||||
for (tx, ty, tile_data) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, tile_data.len());
|
||||
for (tx, ty, payload) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, payload.len());
|
||||
output.extend_from_slice(header.as_bytes());
|
||||
output.extend_from_slice(&tile_data);
|
||||
output.extend_from_slice(&payload);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Purpose: LZF compress a byte slice for use as Krita tile payload.
|
||||
/// Logic: Simple LZF implementation — hash-table based back-reference search.
|
||||
/// Each control byte describes either a literal run (ctrl < 32) or a
|
||||
/// back-reference (ctrl >= 32). This matches the format expected by the
|
||||
/// lzf_decompress reader in lib.rs.
|
||||
fn lzf_compress(input: &[u8]) -> Vec<u8> {
|
||||
// Worst-case output: all literals → 1 control byte per 32 data bytes.
|
||||
let mut output = Vec::with_capacity(input.len() + input.len() / 20 + 32);
|
||||
let mut hash_table: Vec<usize> = vec![usize::MAX; 1 << 16];
|
||||
|
||||
let mut ip = 0usize;
|
||||
let mut lit_start = 0usize;
|
||||
|
||||
// Flush pending literal bytes [lit_start .. lit_end) into output.
|
||||
let flush_literals = |out: &mut Vec<u8>, data: &[u8], from: usize, to: usize| {
|
||||
let mut i = from;
|
||||
while i < to {
|
||||
let run = (to - i).min(32);
|
||||
out.push((run - 1) as u8); // ctrl: literal run length − 1
|
||||
out.extend_from_slice(&data[i..i + run]);
|
||||
i += run;
|
||||
}
|
||||
};
|
||||
|
||||
while ip + 2 < input.len() {
|
||||
// 16-bit hash of three bytes at ip.
|
||||
let h = ((input[ip] as u32)
|
||||
.wrapping_mul(2654435761)
|
||||
^ (input[ip + 1] as u32)
|
||||
.wrapping_mul(2246822519)
|
||||
^ (input[ip + 2] as u32))
|
||||
as usize
|
||||
& 0xFFFF;
|
||||
|
||||
let ref_pos = hash_table[h];
|
||||
hash_table[h] = ip;
|
||||
|
||||
// Check if back-reference is valid (within last 8191 bytes, matches >= 3 bytes).
|
||||
let max_len = (input.len() - ip).min(264);
|
||||
if ref_pos != usize::MAX
|
||||
&& ip > ref_pos
|
||||
&& (ip - ref_pos) <= 8191
|
||||
&& ref_pos + 2 < input.len()
|
||||
&& input[ref_pos] == input[ip]
|
||||
&& input[ref_pos + 1] == input[ip + 1]
|
||||
&& input[ref_pos + 2] == input[ip + 2]
|
||||
{
|
||||
// Count match length (capped at 264).
|
||||
let ofs = ip - ref_pos - 1;
|
||||
let mut mlen = 3usize;
|
||||
while mlen < max_len && input[ref_pos + mlen] == input[ip + mlen] {
|
||||
mlen += 1;
|
||||
}
|
||||
|
||||
// Flush literals accumulated before this back-reference.
|
||||
flush_literals(&mut output, input, lit_start, ip);
|
||||
lit_start = ip + mlen;
|
||||
ip += mlen;
|
||||
|
||||
// Encode back-reference.
|
||||
let len_code = mlen - 2; // stored as (len − 2)
|
||||
if len_code < 7 {
|
||||
output.push(((len_code << 5) | (ofs >> 8)) as u8);
|
||||
} else {
|
||||
output.push(((7 << 5) | (ofs >> 8)) as u8);
|
||||
output.push((len_code - 7) as u8);
|
||||
}
|
||||
output.push((ofs & 0xFF) as u8);
|
||||
} else {
|
||||
ip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining literal bytes.
|
||||
flush_literals(&mut output, input, lit_start, input.len());
|
||||
output
|
||||
}
|
||||
|
||||
fn encode_mask_vers(layer: &Layer, mask: &[u8]) -> Vec<u8> {
|
||||
let tw: i32 = 64;
|
||||
let th: i32 = 64;
|
||||
@@ -366,12 +463,12 @@ fn encode_mask_vers(layer: &Layer, mask: &[u8]) -> Vec<u8> {
|
||||
let tidx = (y as usize) * (tw as usize) + (x as usize);
|
||||
let mut v = layer.mask_default_color;
|
||||
if let Some(mb) = layer.mask_bounds {
|
||||
let m_top = mb[0] as i32;
|
||||
let m_left = mb[1] as i32;
|
||||
let m_top = mb[0] as i32;
|
||||
let m_left = mb[1] as i32;
|
||||
let m_bottom = mb[2] as i32;
|
||||
let m_right = mb[3] as i32;
|
||||
let m_right = mb[3] as i32;
|
||||
if ly >= m_top && ly < m_bottom && lx >= m_left && lx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((ly - m_top) * mask_w + (lx - m_left)) as usize;
|
||||
if mask_idx < mask.len() {
|
||||
v = mask[mask_idx];
|
||||
@@ -392,15 +489,12 @@ fn encode_mask_vers(layer: &Layer, mask: &[u8]) -> Vec<u8> {
|
||||
}
|
||||
|
||||
if has_content {
|
||||
let mut compressed = vec![1u8];
|
||||
let mut i = 0;
|
||||
while i < tile_raw.len() {
|
||||
let chunk = (tile_raw.len() - i).min(32);
|
||||
compressed.push((chunk - 1) as u8);
|
||||
compressed.extend_from_slice(&tile_raw[i..i + chunk]);
|
||||
i += chunk;
|
||||
}
|
||||
tiles.push((tx, ty, compressed));
|
||||
// Same LZF format as raster tiles but PIXELSIZE=1.
|
||||
let compressed = lzf_compress(&tile_raw);
|
||||
let mut payload = Vec::with_capacity(1 + compressed.len());
|
||||
payload.push(1u8); // version byte (no delta-encode)
|
||||
payload.extend_from_slice(&compressed);
|
||||
tiles.push((tx, ty, payload));
|
||||
}
|
||||
tx += tw;
|
||||
}
|
||||
@@ -412,13 +506,13 @@ fn encode_mask_vers(layer: &Layer, mask: &[u8]) -> Vec<u8> {
|
||||
output.extend_from_slice(b"TILEWIDTH 64\n");
|
||||
output.extend_from_slice(b"TILEHEIGHT 64\n");
|
||||
output.extend_from_slice(b"PIXELSIZE 1\n");
|
||||
output.extend_from_slice(b"DATA\n");
|
||||
output.extend_from_slice(format!("DATA {}\n", tiles.len()).as_bytes());
|
||||
|
||||
tiles.sort_by_key(|(x, y, _)| (*y, *x));
|
||||
for (tx, ty, tile_data) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, tile_data.len());
|
||||
for (tx, ty, payload) in tiles {
|
||||
let header = format!("{},{},LZF,{}\n", tx, ty, payload.len());
|
||||
output.extend_from_slice(header.as_bytes());
|
||||
output.extend_from_slice(&tile_data);
|
||||
output.extend_from_slice(&payload);
|
||||
}
|
||||
|
||||
output
|
||||
|
||||
+35
-6
@@ -627,6 +627,12 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
f.read_to_string(&mut xml_content).map_err(|e| e.to_string())?;
|
||||
found = true;
|
||||
}
|
||||
if !found {
|
||||
if let Ok(mut f) = archive.by_name("layers.xml") {
|
||||
f.read_to_string(&mut xml_content).map_err(|e| e.to_string())?;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -649,14 +655,25 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
let name = e.name();
|
||||
let tag_name = std::str::from_utf8(name.as_ref()).unwrap_or("").to_lowercase();
|
||||
if tag_name == "image" {
|
||||
let mut colorspace = String::new();
|
||||
let mut depth = String::new();
|
||||
for attr in e.attributes() {
|
||||
let a = attr.map_err(|e| format!("XML attr error: {}", e))?;
|
||||
let val = std::str::from_utf8(&a.value).unwrap_or("");
|
||||
match a.key.as_ref() {
|
||||
b"width" => width = std::str::from_utf8(&a.value).unwrap_or("800").parse().unwrap_or(800),
|
||||
b"height" => height = std::str::from_utf8(&a.value).unwrap_or("600").parse().unwrap_or(600),
|
||||
b"width" => width = val.parse().unwrap_or(800),
|
||||
b"height" => height = val.parse().unwrap_or(600),
|
||||
b"colorspacename" => colorspace = val.to_string(),
|
||||
b"channeldepth" => depth = val.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !colorspace.is_empty() && colorspace != "RGBA" && colorspace != "RGB" {
|
||||
return Err(format!("Unsupported KRA color space: '{}' (only RGBA/RGB is supported)", colorspace));
|
||||
}
|
||||
if !depth.is_empty() && depth != "U8" {
|
||||
return Err(format!("Unsupported KRA channel depth: '{}' (only U8 is supported)", depth));
|
||||
}
|
||||
} else if tag_name == "layer"
|
||||
|| tag_name == "paintlayer"
|
||||
|| tag_name == "vectorlayer"
|
||||
@@ -787,7 +804,10 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
let layer_path = find_krita_layer_file(&all_files, &info.filename, &info.nodetype);
|
||||
let zip_path = match layer_path {
|
||||
Some(ref path) => path,
|
||||
None => continue,
|
||||
None => {
|
||||
log::warn!("KRA import: layer file not found for '{}' (filename={}, type={})", info.name, info.filename, info.nodetype);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
@@ -805,13 +825,19 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
let pixels = if is_vector {
|
||||
match rasterize_svg_to_rgba(&buf, width, height) {
|
||||
Ok(p) => p,
|
||||
Err(_) => vec![0u8; (width * height * 4) as usize],
|
||||
Err(e) => {
|
||||
log::warn!("KRA import: SVG rasterization failed for layer '{}': {}", info.name, e);
|
||||
vec![0u8; (width * height * 4) as usize]
|
||||
}
|
||||
}
|
||||
} else if buf.starts_with(b"VERS") {
|
||||
let default_pixel = read_defaultpixel(archive, &info.filename);
|
||||
match parse_vers_tiles(&buf, width, height, info.x, info.y, default_pixel) {
|
||||
Ok(p) => p,
|
||||
Err(_) => vec![0u8; (width * height * 4) as usize],
|
||||
Err(e) => {
|
||||
log::warn!("KRA import: VERS tile parse failed for layer '{}': {}", info.name, e);
|
||||
vec![0u8; (width * height * 4) as usize]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match image::load_from_memory(&buf) {
|
||||
@@ -834,7 +860,10 @@ fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Res
|
||||
}
|
||||
pixels
|
||||
}
|
||||
Err(_) => vec![0u8; (width * height * 4) as usize],
|
||||
Err(e) => {
|
||||
log::warn!("KRA import: image decode failed for layer '{}': {}", info.name, e);
|
||||
vec![0u8; (width * height * 4) as usize]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user