init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "hcie-kra"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
||||
quick-xml = "0.36"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
bincode = "1.3"
|
||||
rand = "0.8"
|
||||
usvg = { workspace = true }
|
||||
resvg = { workspace = true }
|
||||
tiny-skia = { workspace = true }
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
hcie-fx = { path = "../hcie-fx" }
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
@@ -0,0 +1,411 @@
|
||||
use hcie_protocol::{Layer, LayerData, LayerType, VectorShape};
|
||||
use hcie_protocol::BlendMode;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::path::Path;
|
||||
|
||||
fn blend_mode_to_krita_compositeop(mode: BlendMode) -> &'static str {
|
||||
match mode {
|
||||
BlendMode::Normal => "normal",
|
||||
BlendMode::Dissolve => "dissolve",
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::ColorBurn => "color_burn",
|
||||
BlendMode::LinearBurn => "linear_burn",
|
||||
BlendMode::DarkerColor => "darker_color",
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::ColorDodge => "color_dodge",
|
||||
BlendMode::LinearDodge => "linear_dodge",
|
||||
BlendMode::LighterColor => "lighter_color",
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft_light",
|
||||
BlendMode::HardLight => "hard_light",
|
||||
BlendMode::VividLight => "vivid_light",
|
||||
BlendMode::LinearLight => "linear_light",
|
||||
BlendMode::PinLight => "pin_light",
|
||||
BlendMode::HardMix => "hard_mix",
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
BlendMode::Subtract => "subtract",
|
||||
BlendMode::Divide => "divide",
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
BlendMode::PassThrough => "normal",
|
||||
}
|
||||
}
|
||||
|
||||
const ICC_BYTES: &[u8] = include_bytes!("srgb_profile.icc");
|
||||
|
||||
pub fn save_kra(
|
||||
layers: &[Layer],
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
composited: &[u8],
|
||||
path: &Path,
|
||||
) -> Result<(), String> {
|
||||
let file = std::fs::File::create(path).map_err(|e| e.to_string())?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
let internal_name = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let options: zip::write::FileOptions<'_, ()> =
|
||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
zip.start_file("mimetype", options)
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(b"application/x-krita")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let maindoc = generate_maindoc(layers, canvas_w, canvas_h, &internal_name);
|
||||
zip.start_file("maindoc.xml", zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(maindoc.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let docinfo = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE document-info PUBLIC '-//KDE//DTD document-info 1.1//EN' 'http://www.calligra.org/DTD/document-info-1.1.dtd'>
|
||||
<document-info xmlns="http://www.calligra.org/DTD/document-info">
|
||||
<about>
|
||||
<title>{}</title>
|
||||
<initial-creator>hcie-rust</initial-creator>
|
||||
<editing-cycles>1</editing-cycles>
|
||||
<creation-date>2026-01-01T00:00:00</creation-date>
|
||||
</about>
|
||||
</document-info>
|
||||
"#,
|
||||
internal_name
|
||||
);
|
||||
zip.start_file("documentinfo.xml", zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(docinfo.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut png_buf = Vec::new();
|
||||
let img = image::RgbaImage::from_raw(canvas_w, canvas_h, composited.to_vec())
|
||||
.ok_or("Failed to create merged image for KRA")?;
|
||||
img.write_to(&mut Cursor::new(&mut png_buf), image::ImageFormat::Png)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
zip.start_file("mergedimage.png", zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&png_buf)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
zip.start_file("preview.png", zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&png_buf)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
for (i, layer) in layers.iter().enumerate() {
|
||||
let base_filename = format!("layer{}", i + 1);
|
||||
|
||||
match layer.layer_type {
|
||||
LayerType::Raster | LayerType::Mask => {
|
||||
let encoded = encode_layer_vers(layer);
|
||||
let layer_path = format!("{}/layers/{}", internal_name, base_filename);
|
||||
zip.start_file(&layer_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&encoded)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let icc_path = format!("{}/layers/{}.icc", internal_name, base_filename);
|
||||
zip.start_file(&icc_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(ICC_BYTES)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let dp_path = format!("{}/layers/{}.defaultpixel", internal_name, base_filename);
|
||||
zip.start_file(&dp_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(&[0u8; 4])
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
LayerType::Vector | LayerType::Text => {
|
||||
let layer_path = format!("{}/layers/{}", internal_name, base_filename);
|
||||
if let LayerData::Vector { ref shapes } = layer.data {
|
||||
let svg = shapes_to_svg(shapes, canvas_w, canvas_h);
|
||||
let shape_dir = format!("{}.shapelayer", layer_path);
|
||||
zip.add_directory(&shape_dir, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.start_file(
|
||||
format!("{}/content.svg", shape_dir),
|
||||
zip::write::FileOptions::<'_, ()>::default(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(svg.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else if let LayerData::Text {
|
||||
ref text,
|
||||
size,
|
||||
ref font,
|
||||
color,
|
||||
x,
|
||||
y,
|
||||
angle,
|
||||
..
|
||||
} = layer.data
|
||||
{
|
||||
let hex_color =
|
||||
format!("#{:02x}{:02x}{:02x}", color[0], color[1], color[2]);
|
||||
let transform = if angle != 0.0 {
|
||||
format!(" transform='rotate({} {} {})'", angle, x, y)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let svg = format!(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='{}' height='{}'\
|
||||
><text x='{}' y='{}' font-family='{}' font-size='{}' fill='{}'{}>{}</text></svg>",
|
||||
canvas_w, canvas_h, x, y, font, size, hex_color, transform, text
|
||||
);
|
||||
let shape_dir = format!("{}.shapelayer", layer_path);
|
||||
zip.add_directory(&shape_dir, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.start_file(
|
||||
format!("{}/content.svg", shape_dir),
|
||||
zip::write::FileOptions::<'_, ()>::default(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(svg.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
LayerType::Group => {
|
||||
let group_json = serde_json::json!({
|
||||
"collapsed": layer.collapsed,
|
||||
"parent_id": layer.parent_id,
|
||||
});
|
||||
let group_path =
|
||||
format!("{}/layers/group/{}.json", internal_name, base_filename);
|
||||
zip.start_file(&group_path, zip::write::FileOptions::<'_, ()>::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
zip.write_all(
|
||||
serde_json::to_string_pretty(&group_json)
|
||||
.unwrap_or_default()
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zip.finish().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_maindoc(layers: &[Layer], w: u32, h: u32, name: &str) -> String {
|
||||
let mut xml = String::new();
|
||||
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
xml.push_str("<!DOCTYPE DOC PUBLIC '-//KDE//DTD krita 2.0//EN' 'http://www.calligra.org/DTD/krita-2.0.dtd'>\n");
|
||||
xml.push_str("<DOC xmlns=\"http://www.calligra.org/DTD/krita\" editor=\"Krita\" kritaVersion=\"6.0.1\" syntaxVersion=\"2.0\">\n");
|
||||
xml.push_str(&format!(
|
||||
" <IMAGE width=\"{}\" height=\"{}\" name=\"{}\" mime=\"application/x-kra\" colorspacename=\"RGBA\" channeldepth=\"U8\" profile=\"sRGB-elle-V2-g10.icc\" x-res=\"100\" y-res=\"100\">\n",
|
||||
w, h, name
|
||||
));
|
||||
xml.push_str(" <layers>\n");
|
||||
|
||||
for (i, layer) in layers.iter().enumerate().rev() {
|
||||
let node_type = match layer.layer_type {
|
||||
LayerType::Raster | LayerType::Mask => "paintlayer",
|
||||
LayerType::Vector | LayerType::Text => "shapelayer",
|
||||
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 compositeop = blend_mode_to_krita_compositeop(layer.blend_mode);
|
||||
|
||||
xml.push_str(&format!(
|
||||
" <layer name=\"{}\" filename=\"{}\" nodetype=\"{}\" visible=\"{}\" opacity=\"{}\" uuid=\"{}\" compositeop=\"{}\" colorspacename=\"RGBA\" channeldepth=\"U8\" profile=\"sRGB-elle-V2-g10.icc\" x=\"0\" y=\"0\" />\n",
|
||||
layer.name, filename, node_type, visible, opacity, uuid, compositeop
|
||||
));
|
||||
}
|
||||
|
||||
xml.push_str(" </layers>\n");
|
||||
xml.push_str(" </IMAGE>\n");
|
||||
xml.push_str("</DOC>\n");
|
||||
xml
|
||||
}
|
||||
|
||||
fn encode_layer_vers(layer: &Layer) -> Vec<u8> {
|
||||
let tw: i32 = 64;
|
||||
let th: i32 = 64;
|
||||
let plane_size = (tw * th) as usize;
|
||||
let w = layer.width as i32;
|
||||
let h = layer.height as i32;
|
||||
|
||||
let mut tiles: Vec<(i32, i32, Vec<u8>)> = Vec::new();
|
||||
let mut ty = 0;
|
||||
while ty < h {
|
||||
let mut tx = 0;
|
||||
while tx < w {
|
||||
let mut tile_raw = vec![0u8; plane_size * 4];
|
||||
let mut has_content = false;
|
||||
|
||||
for y in 0..th {
|
||||
for x in 0..tw {
|
||||
let lx = tx + x;
|
||||
let ly = ty + y;
|
||||
if lx < w && ly < h {
|
||||
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];
|
||||
|
||||
if layer.pixels[idx + 3] > 0 {
|
||||
has_content = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
tx += tw;
|
||||
}
|
||||
ty += th;
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
writeln!(buf, "VERSION 2").unwrap();
|
||||
writeln!(buf, "TILEWIDTH 64").unwrap();
|
||||
writeln!(buf, "TILEHEIGHT 64").unwrap();
|
||||
writeln!(buf, "PIXELSIZE 4").unwrap();
|
||||
writeln!(buf, "DATA {}", tiles.len()).unwrap();
|
||||
|
||||
for (tx, ty, data) in tiles {
|
||||
writeln!(buf, "{},{},LZF,{}", tx, ty, data.len()).unwrap();
|
||||
buf.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn shapes_to_svg(shapes: &[VectorShape], w: u32, h: u32) -> String {
|
||||
fn rgba_to_hex(c: &[u8; 4]) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", c[0], c[1], c[2])
|
||||
}
|
||||
|
||||
let mut svg = format!(
|
||||
"<svg width='{}' height='{}' xmlns='http://www.w3.org/2000/svg'>\n",
|
||||
w, h
|
||||
);
|
||||
for shape in shapes {
|
||||
match shape {
|
||||
VectorShape::FreePath {
|
||||
pts,
|
||||
closed,
|
||||
stroke,
|
||||
color,
|
||||
fill_color,
|
||||
fill,
|
||||
..
|
||||
} => {
|
||||
let mut d = String::new();
|
||||
if let Some(first) = pts.first() {
|
||||
d.push_str(&format!("M {} {} ", first[0], first[1]));
|
||||
}
|
||||
for pt in pts.iter().skip(1) {
|
||||
d.push_str(&format!("L {} {} ", pt[0], pt[1]));
|
||||
}
|
||||
if *closed {
|
||||
d.push_str("Z ");
|
||||
}
|
||||
let fill_str = if *fill {
|
||||
format!("fill='{}'", rgba_to_hex(fill_color))
|
||||
} else {
|
||||
"fill='none'".to_string()
|
||||
};
|
||||
svg.push_str(&format!(
|
||||
" <path d='{}' {} stroke='{}' stroke-width='{}' />\n",
|
||||
d, fill_str, rgba_to_hex(color), stroke
|
||||
));
|
||||
}
|
||||
VectorShape::Rect {
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
stroke,
|
||||
color,
|
||||
fill_color,
|
||||
fill,
|
||||
..
|
||||
} => {
|
||||
let x = x1.min(*x2);
|
||||
let y = y1.min(*y2);
|
||||
let rw = (x2 - x1).abs();
|
||||
let rh = (y2 - y1).abs();
|
||||
let fill_str = if *fill {
|
||||
format!("fill='{}'", rgba_to_hex(fill_color))
|
||||
} else {
|
||||
"fill='none'".to_string()
|
||||
};
|
||||
svg.push_str(&format!(
|
||||
" <rect x='{}' y='{}' width='{}' height='{}' {} stroke='{}' stroke-width='{}' />\n",
|
||||
x, y, rw, rh, fill_str, rgba_to_hex(color), stroke
|
||||
));
|
||||
}
|
||||
VectorShape::Circle {
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
stroke,
|
||||
color,
|
||||
fill_color,
|
||||
fill,
|
||||
..
|
||||
} => {
|
||||
let cx = (x1 + x2) / 2.0;
|
||||
let cy = (y1 + y2) / 2.0;
|
||||
let r = ((x2 - x1).abs().min((y2 - y1).abs())) / 2.0;
|
||||
let fill_str = if *fill {
|
||||
format!("fill='{}'", rgba_to_hex(fill_color))
|
||||
} else {
|
||||
"fill='none'".to_string()
|
||||
};
|
||||
svg.push_str(&format!(
|
||||
" <ellipse cx='{}' cy='{}' rx='{}' ry='{}' {} stroke='{}' stroke-width='{}' />\n",
|
||||
cx, cy, r, r, fill_str, rgba_to_hex(color), stroke
|
||||
));
|
||||
}
|
||||
VectorShape::Line {
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
stroke,
|
||||
color,
|
||||
..
|
||||
} => {
|
||||
svg.push_str(&format!(
|
||||
" <line x1='{}' y1='{}' x2='{}' y2='{}' stroke='{}' stroke-width='{}' />\n",
|
||||
x1, y1, x2, y2, rgba_to_hex(color), stroke
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
svg.push_str("</svg>");
|
||||
svg
|
||||
}
|
||||
@@ -0,0 +1,981 @@
|
||||
mod kra_saver;
|
||||
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use hcie_protocol::{Layer, BlendMode, LayerType, LayerData, VectorShape, LineCap, LayerStyle};
|
||||
|
||||
/// Purpose: Import a Krita KRA file, decoding both raster layers and SVG vector shapes.
|
||||
/// Logic: Opens the ZIP archive, parses the maindoc XML configuration, resolves layer files and layer styles,
|
||||
/// and returns a list of Layer instances.
|
||||
/// Arguments: path - Path to the KRA file on disk.
|
||||
/// Returns: A vector of Layer structs, or an error string.
|
||||
pub fn import_kra(path: &Path) -> Result<Vec<Layer>, String> {
|
||||
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
|
||||
let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
|
||||
|
||||
match try_import_krita_maindoc(&mut archive) {
|
||||
Ok(layers) if !layers.is_empty() => {
|
||||
return Ok(layers);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut entry = archive.by_index(i).map_err(|e| e.to_string())?;
|
||||
if entry.name() == "mergedimage.png" {
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf).map_err(|e| e.to_string())?;
|
||||
let img = image::load_from_memory_with_format(&buf, image::ImageFormat::Png).map_err(|e| e.to_string())?;
|
||||
let rgba = img.to_rgba8();
|
||||
let w = rgba.width();
|
||||
let h = rgba.height();
|
||||
let pixels = rgba.into_raw();
|
||||
let mut layer = Layer::from_rgba("Krita Import", w, h, pixels);
|
||||
layer.id = rand::random::<u64>();
|
||||
return Ok(vec![layer]);
|
||||
}
|
||||
}
|
||||
Err("No maindoc.xml or mergedimage.png found in KRA file".to_string())
|
||||
}
|
||||
|
||||
/// Purpose: Intermediate metadata structure representing Krita layer elements.
|
||||
struct KritaLayerInfo {
|
||||
name: String,
|
||||
filename: String,
|
||||
visible: bool,
|
||||
opacity: f32,
|
||||
x: i32,
|
||||
y: i32,
|
||||
nodetype: String,
|
||||
compositeop: String,
|
||||
layerstyle_uuid: String,
|
||||
}
|
||||
|
||||
|
||||
/// Purpose: Read Krita defaultpixel companion files for solid backgrounds.
|
||||
fn read_defaultpixel(archive: &mut zip::ZipArchive<std::fs::File>, layer_path: &str) -> Option<[u8; 4]> {
|
||||
let suffix = format!("{}.defaultpixel", layer_path);
|
||||
let mut buf = Vec::new();
|
||||
if let Ok(mut entry) = archive.by_name(&suffix) {
|
||||
if entry.read_to_end(&mut buf).is_ok() && buf.len() >= 4 {
|
||||
return Some([buf[0], buf[1], buf[2], buf[3]]);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Purpose: Parse Krita VERS tile layout structure.
|
||||
/// Logic: Iterates over tile positions, decompresses payload using LZF or RAW, and blits into destination buffer.
|
||||
/// Arguments: buffer, doc_w, doc_h, lx, ly, default_pixel.
|
||||
/// Returns: Decoded RGBA vector.
|
||||
fn parse_vers_tiles(
|
||||
buffer: &[u8],
|
||||
doc_w: u32,
|
||||
doc_h: u32,
|
||||
lx: i32,
|
||||
ly: i32,
|
||||
default_pixel: Option<[u8; 4]>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let header_limit = buffer.len().min(2048);
|
||||
let header_str = String::from_utf8_lossy(&buffer[..header_limit]);
|
||||
|
||||
let mut tile_w = 64usize;
|
||||
let mut tile_h = 64usize;
|
||||
let mut pixel_size = 4usize;
|
||||
let mut data_offset = 0usize;
|
||||
|
||||
for line in header_str.lines() {
|
||||
let parts: Vec<&str> = line.trim().split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
match parts[0] {
|
||||
"TILEWIDTH" => tile_w = parts[1].parse().unwrap_or(64),
|
||||
"TILEHEIGHT" => tile_h = parts[1].parse().unwrap_or(64),
|
||||
"PIXELSIZE" => pixel_size = parts[1].parse().unwrap_or(4),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if line.contains("DATA") {
|
||||
if let Some(pos) = buffer[..header_limit].windows(4).position(|w| w == b"DATA") {
|
||||
let mut k = pos + 4;
|
||||
while k < header_limit && buffer[k] != 0x0A {
|
||||
k += 1;
|
||||
}
|
||||
data_offset = k + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if data_offset == 0 {
|
||||
return Err("DATA marker not found".into());
|
||||
}
|
||||
|
||||
let mut pixels = vec![0u8; (doc_w * doc_h * 4) as usize];
|
||||
let plane_size = tile_w * tile_h;
|
||||
let mut offset = data_offset;
|
||||
let mut tiles_decoded = 0usize;
|
||||
|
||||
while offset < buffer.len() {
|
||||
let mut line_end = offset;
|
||||
while line_end < buffer.len() && buffer[line_end] != 0x0A {
|
||||
line_end += 1;
|
||||
}
|
||||
if line_end >= buffer.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let tile_header = String::from_utf8_lossy(&buffer[offset..line_end]).trim().to_string();
|
||||
offset = line_end + 1;
|
||||
if tile_header.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = tile_header.split(',').collect();
|
||||
if parts.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tx: i32 = parts[0].parse().unwrap_or(0);
|
||||
let ty: i32 = parts[1].parse().unwrap_or(0);
|
||||
let compression = parts[2].trim();
|
||||
let length: usize = parts[3].parse().unwrap_or(0);
|
||||
|
||||
if offset + length > buffer.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let payload = &buffer[offset..offset + length];
|
||||
let mut tile_data = vec![0u8; plane_size * pixel_size];
|
||||
|
||||
if compression == "LZF" {
|
||||
if payload.is_empty() { offset += length; continue; }
|
||||
let version = payload[0];
|
||||
let _decoded = lzf_decompress(&payload[1..], &mut tile_data);
|
||||
if version >= 2 {
|
||||
for c in 0..pixel_size {
|
||||
let p_start = c * plane_size;
|
||||
for i in 1..plane_size {
|
||||
tile_data[p_start + i] = tile_data[p_start + i].wrapping_add(tile_data[p_start + i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let copy_len = payload.len().min(tile_data.len());
|
||||
tile_data[..copy_len].copy_from_slice(&payload[..copy_len]);
|
||||
}
|
||||
|
||||
for y in 0..tile_h {
|
||||
for x in 0..tile_w {
|
||||
let target_x = lx + tx + x as i32;
|
||||
let target_y = ly + ty + y as i32;
|
||||
|
||||
if target_x >= 0 && target_x < doc_w as i32
|
||||
&& target_y >= 0 && target_y < doc_h as i32
|
||||
{
|
||||
let target_idx = (target_y as usize * doc_w as usize + target_x as usize) * 4;
|
||||
let src_idx = y * tile_w + x;
|
||||
|
||||
if pixel_size == 4 {
|
||||
pixels[target_idx] = tile_data[src_idx + 2 * plane_size];
|
||||
pixels[target_idx + 1] = tile_data[src_idx + 1 * plane_size];
|
||||
pixels[target_idx + 2] = tile_data[src_idx];
|
||||
pixels[target_idx + 3] = tile_data[src_idx + 3 * plane_size];
|
||||
} else if pixel_size == 8 {
|
||||
pixels[target_idx] = tile_data[src_idx + 5 * plane_size];
|
||||
pixels[target_idx + 1] = tile_data[src_idx + 3 * plane_size];
|
||||
pixels[target_idx + 2] = tile_data[src_idx + 1 * plane_size];
|
||||
pixels[target_idx + 3] = tile_data[src_idx + 7 * plane_size];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += length;
|
||||
tiles_decoded += 1;
|
||||
}
|
||||
|
||||
if tiles_decoded == 0 {
|
||||
if let Some(dp) = default_pixel {
|
||||
for chunk in pixels.chunks_exact_mut(4) {
|
||||
chunk.copy_from_slice(&dp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pixels)
|
||||
}
|
||||
|
||||
/// Purpose: Decompress an LZF compressed tile data buffer.
|
||||
fn lzf_decompress(input: &[u8], output: &mut [u8]) -> usize {
|
||||
let mut ip = 0usize;
|
||||
let mut op = 0usize;
|
||||
let input_len = input.len();
|
||||
let output_len = output.len();
|
||||
|
||||
while ip < input_len {
|
||||
let ctrl = input[ip] as usize;
|
||||
ip += 1;
|
||||
|
||||
if ctrl < 32 {
|
||||
let count = ctrl + 1;
|
||||
if op + count > output_len || ip + count > input_len {
|
||||
break;
|
||||
}
|
||||
output[op..op + count].copy_from_slice(&input[ip..ip + count]);
|
||||
op += count;
|
||||
ip += count;
|
||||
} else {
|
||||
let mut len = ctrl >> 5;
|
||||
let mut ofs = (ctrl & 31) << 8;
|
||||
|
||||
if len == 7 {
|
||||
if ip >= input_len { break; }
|
||||
len += input[ip] as usize;
|
||||
ip += 1;
|
||||
}
|
||||
|
||||
if ip >= input_len { break; }
|
||||
ofs += input[ip] as usize;
|
||||
ip += 1;
|
||||
|
||||
let ref_ptr = match op.checked_sub(ofs + 1) {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
let count = len + 2;
|
||||
|
||||
if op + count > output_len { break; }
|
||||
|
||||
for i in 0..count {
|
||||
output[op + i] = output[ref_ptr + i];
|
||||
}
|
||||
op += count;
|
||||
}
|
||||
}
|
||||
op
|
||||
}
|
||||
|
||||
/// Purpose: Map Krita compositeop names to HCIE BlendModes.
|
||||
fn krita_compositeop_to_blend_mode(op: &str) -> BlendMode {
|
||||
match op.to_lowercase().as_str() {
|
||||
"normal" | "src-over" | "svg:src-over" | "composite-over" => BlendMode::Normal,
|
||||
"dissolve" => BlendMode::Dissolve,
|
||||
"darken" | "svg:darken" | "composite-darken" => BlendMode::Darken,
|
||||
"multiply" | "svg:multiply" | "composite-multiply" => BlendMode::Multiply,
|
||||
"colorburn" | "color_burn" | "color-burn" | "svg:color-burn" => BlendMode::ColorBurn,
|
||||
"linearburn" | "linear_burn" | "linear-burn" | "svg:linear-burn" => BlendMode::LinearBurn,
|
||||
"darkercolor" | "darker_color" | "darker-color" => BlendMode::DarkerColor,
|
||||
"lighten" | "svg:lighten" => BlendMode::Lighten,
|
||||
"screen" | "svg:screen" => BlendMode::Screen,
|
||||
"colordodge" | "color_dodge" | "color-dodge" | "svg:color-dodge" => BlendMode::ColorDodge,
|
||||
"lineardodge" | "linear_dodge" | "linear-dodge" | "svg:linear-dodge" => BlendMode::LinearDodge,
|
||||
"lightercolor" | "lighter_color" | "lighter-color" => BlendMode::LighterColor,
|
||||
"overlay" | "svg:overlay" => BlendMode::Overlay,
|
||||
"softlight" | "soft_light" | "soft-light" | "svg:soft-light" => BlendMode::SoftLight,
|
||||
"hardlight" | "hard_light" | "hard-light" | "svg:hard-light" => BlendMode::HardLight,
|
||||
"vividlight" | "vivid_light" | "vivid-light" => BlendMode::VividLight,
|
||||
"linearlight" | "linear_light" | "linear-light" | "svg:linear-light" => BlendMode::LinearLight,
|
||||
"pinlight" | "pin_light" | "pin-light" => BlendMode::PinLight,
|
||||
"hardmix" | "hard_mix" | "hard-mix" => BlendMode::HardMix,
|
||||
"difference" | "svg:difference" => BlendMode::Difference,
|
||||
"exclusion" | "svg:exclusion" => BlendMode::Exclusion,
|
||||
"subtract" => BlendMode::Subtract,
|
||||
"divide" => BlendMode::Divide,
|
||||
"hue" | "svg:hue" => BlendMode::Hue,
|
||||
"saturation" | "svg:saturation" => BlendMode::Saturation,
|
||||
"color" | "svg:color" => BlendMode::Color,
|
||||
"luminosity" | "luminize" | "svg:luminosity" | "lum" => BlendMode::Luminosity,
|
||||
"passthrough" | "pass_through" => BlendMode::PassThrough,
|
||||
_ => BlendMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
/// Purpose: Parse SVG viewBox tag dimensions.
|
||||
fn parse_svg_viewbox(svg_bytes: &[u8]) -> (f32, f32) {
|
||||
let mut reader = quick_xml::Reader::from_reader(svg_bytes);
|
||||
reader.config_mut().trim_text(true);
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(quick_xml::events::Event::Empty(e)) | Ok(quick_xml::events::Event::Start(e)) => {
|
||||
let tag = std::str::from_utf8(e.name().as_ref()).unwrap_or("").to_lowercase();
|
||||
if tag == "svg" {
|
||||
let mut vb = None;
|
||||
let mut w_val = None;
|
||||
let mut h_val = None;
|
||||
for attr in e.attributes().flatten() {
|
||||
let key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
let val = std::str::from_utf8(&attr.value).unwrap_or("");
|
||||
match key {
|
||||
"width" => w_val = Some(val.to_string()),
|
||||
"height" => h_val = Some(val.to_string()),
|
||||
"viewBox" => vb = Some(val.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(vb_str) = vb {
|
||||
let parts: Vec<&str> = vb_str.split_whitespace().collect();
|
||||
if parts.len() >= 4 {
|
||||
let vb_w = parts[2].parse::<f32>().unwrap_or(0.0);
|
||||
let vb_h = parts[3].parse::<f32>().unwrap_or(0.0);
|
||||
if vb_w > 0.0 && vb_h > 0.0 {
|
||||
return (vb_w, vb_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let parse_dim = |s: &str| -> f32 {
|
||||
let s = s.trim();
|
||||
let num_end = s.find(|c: char| !c.is_digit(10) && c != '.' && c != '-')
|
||||
.unwrap_or(s.len());
|
||||
s[..num_end].parse::<f32>().unwrap_or(0.0)
|
||||
};
|
||||
if let (Some(w), Some(h)) = (w_val.as_deref(), h_val.as_deref()) {
|
||||
let ww = parse_dim(w);
|
||||
let hh = parse_dim(h);
|
||||
if ww > 0.0 && hh > 0.0 {
|
||||
return (ww, hh);
|
||||
}
|
||||
}
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::Eof) => break,
|
||||
Err(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
(0.0, 0.0)
|
||||
}
|
||||
|
||||
/// Purpose: Parse HTML/SVG hex color format into RGBA8.
|
||||
fn parse_svg_color(attr: &str) -> [u8; 4] {
|
||||
let attr = attr.trim();
|
||||
if attr.is_empty() || attr == "none" {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
if let Some(hex) = attr.strip_prefix('#') {
|
||||
if hex.len() >= 6 {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
||||
let a = if hex.len() >= 8 { u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) } else { 255 };
|
||||
return [r, g, b, a];
|
||||
}
|
||||
}
|
||||
[0, 0, 0, 255]
|
||||
}
|
||||
|
||||
/// Purpose: Parse SVG float dimensions.
|
||||
fn parse_svg_float(attr: &str, default: f32) -> f32 {
|
||||
attr.trim().parse::<f32>().unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Purpose: Extract translation offsets from SVG transform matrices.
|
||||
fn parse_transform_translate(transform: &str) -> (f32, f32) {
|
||||
if let Some(start) = transform.find("translate(") {
|
||||
let s = &transform[start + 10..];
|
||||
if let Some(end) = s.find(')') {
|
||||
let inner = &s[..end];
|
||||
let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
|
||||
let tx: f32 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(0.0);
|
||||
let ty: f32 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0.0);
|
||||
return (tx, ty);
|
||||
}
|
||||
}
|
||||
(0.0, 0.0)
|
||||
}
|
||||
|
||||
/// Purpose: Parse SVG shape elements into VectorShape structs for interactive editor support.
|
||||
fn parse_svg_shapes(svg_bytes: &[u8], scale_x: f32, scale_y: f32) -> Vec<VectorShape> {
|
||||
let mut shapes = Vec::new();
|
||||
let mut reader = quick_xml::Reader::from_reader(svg_bytes);
|
||||
reader.config_mut().trim_text(true);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(quick_xml::events::Event::Empty(e)) | Ok(quick_xml::events::Event::Start(e)) => {
|
||||
let tag = std::str::from_utf8(e.name().as_ref()).unwrap_or("").to_lowercase();
|
||||
let mut attrs = std::collections::HashMap::new();
|
||||
for attr in e.attributes().flatten() {
|
||||
let key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("").to_lowercase();
|
||||
let val = std::str::from_utf8(&attr.value).unwrap_or("").to_string();
|
||||
attrs.insert(key, val);
|
||||
}
|
||||
|
||||
let (tx, ty) = attrs.get("transform")
|
||||
.map(|t| parse_transform_translate(t))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
|
||||
let stroke_color = attrs.get("stroke")
|
||||
.map(|v| parse_svg_color(v))
|
||||
.unwrap_or([0, 0, 0, 255]);
|
||||
let stroke_width = attrs.get("stroke-width")
|
||||
.map(|v| parse_svg_float(v, 1.0) * scale_x)
|
||||
.unwrap_or(scale_x);
|
||||
let fill_attr = attrs.get("fill")
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("none");
|
||||
let fill = fill_attr != "none";
|
||||
let fill_color = attrs.get("fill")
|
||||
.map(|v| parse_svg_color(v))
|
||||
.unwrap_or([255, 255, 255, 255]);
|
||||
|
||||
match tag.as_str() {
|
||||
"rect" => {
|
||||
let x = (attrs.get("x").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + tx) * scale_x;
|
||||
let y = (attrs.get("y").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + ty) * scale_y;
|
||||
let w = attrs.get("width").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) * scale_x;
|
||||
let h = attrs.get("height").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) * scale_y;
|
||||
if w > 0.0 && h > 0.0 {
|
||||
shapes.push(VectorShape::Rect {
|
||||
x1: x, y1: y,
|
||||
x2: x + w, y2: y + h,
|
||||
stroke: stroke_width,
|
||||
color: stroke_color,
|
||||
fill_color, fill,
|
||||
radius: 0.0,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
"circle" => {
|
||||
let cx = (attrs.get("cx").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + tx) * scale_x;
|
||||
let cy = (attrs.get("cy").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + ty) * scale_y;
|
||||
let r = attrs.get("r").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) * scale_x;
|
||||
if r > 0.0 {
|
||||
shapes.push(VectorShape::Circle {
|
||||
x1: cx - r, y1: cy - r,
|
||||
x2: cx + r, y2: cy + r,
|
||||
stroke: stroke_width,
|
||||
color: stroke_color,
|
||||
fill_color, fill,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
"ellipse" => {
|
||||
let cx = (attrs.get("cx").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + tx) * scale_x;
|
||||
let cy = (attrs.get("cy").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + ty) * scale_y;
|
||||
let rx = attrs.get("rx").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) * scale_x;
|
||||
let ry = attrs.get("ry").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) * scale_y;
|
||||
if rx > 0.0 && ry > 0.0 {
|
||||
shapes.push(VectorShape::Circle {
|
||||
x1: cx - rx, y1: cy - ry,
|
||||
x2: cx + rx, y2: cy + ry,
|
||||
stroke: stroke_width,
|
||||
color: stroke_color,
|
||||
fill_color, fill,
|
||||
angle: 0.0,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
"line" => {
|
||||
let x1 = (attrs.get("x1").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + tx) * scale_x;
|
||||
let y1 = (attrs.get("y1").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + ty) * scale_y;
|
||||
let x2 = (attrs.get("x2").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + tx) * scale_x;
|
||||
let y2 = (attrs.get("y2").map(|v| parse_svg_float(v, 0.0)).unwrap_or(0.0) + ty) * scale_y;
|
||||
shapes.push(VectorShape::Line {
|
||||
x1, y1, x2, y2,
|
||||
stroke: stroke_width,
|
||||
color: stroke_color,
|
||||
angle: 0.0,
|
||||
cap_start: LineCap::Round,
|
||||
cap_end: LineCap::Round,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
});
|
||||
}
|
||||
"polygon" | "polyline" => {
|
||||
if let Some(pts_str) = attrs.get("points") {
|
||||
let mut pts: Vec<[f32; 2]> = Vec::new();
|
||||
for pair in pts_str.split_whitespace() {
|
||||
let coords: Vec<&str> = pair.split(',').collect();
|
||||
if coords.len() >= 2 {
|
||||
let px = (coords[0].trim().parse::<f32>().unwrap_or(0.0) + tx) * scale_x;
|
||||
let py = (coords[1].trim().parse::<f32>().unwrap_or(0.0) + ty) * scale_y;
|
||||
pts.push([px, py]);
|
||||
}
|
||||
}
|
||||
if pts.len() >= 2 {
|
||||
shapes.push(VectorShape::FreePath {
|
||||
pts,
|
||||
closed: tag == "polygon",
|
||||
stroke: stroke_width,
|
||||
color: stroke_color,
|
||||
fill_color, fill,
|
||||
opacity: 1.0,
|
||||
hardness: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::Eof) => break,
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
shapes
|
||||
}
|
||||
|
||||
/// Purpose: Rasterize SVG XML content to raw straight RGBA8 pixels using resvg and tiny-skia.
|
||||
fn rasterize_svg_to_rgba(
|
||||
svg_bytes: &[u8],
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let tree = usvg::Tree::from_data(svg_bytes, &usvg::Options::default())
|
||||
.map_err(|e| format!("usvg parse error: {}", e))?;
|
||||
|
||||
let mut pixmap = tiny_skia::Pixmap::new(canvas_w, canvas_h)
|
||||
.ok_or_else(|| "Failed to create tiny_skia pixmap".to_string())?;
|
||||
pixmap.fill(tiny_skia::Color::from_rgba8(0, 0, 0, 0));
|
||||
|
||||
resvg::render(
|
||||
&tree,
|
||||
tiny_skia::Transform::default(),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
|
||||
let mut rgba = vec![0u8; (canvas_w * canvas_h * 4) as usize];
|
||||
let src = pixmap.data();
|
||||
for (i, pixel) in src.chunks_exact(4).enumerate() {
|
||||
let b_premul = pixel[0] as u16;
|
||||
let g_premul = pixel[1] as u16;
|
||||
let r_premul = pixel[2] as u16;
|
||||
let a = pixel[3];
|
||||
|
||||
let idx = i * 4;
|
||||
if a == 0 {
|
||||
rgba[idx] = 0;
|
||||
rgba[idx + 1] = 0;
|
||||
rgba[idx + 2] = 0;
|
||||
rgba[idx + 3] = 0;
|
||||
} else {
|
||||
let a16 = a as u16;
|
||||
rgba[idx] = ((r_premul * 255 + a16 / 2) / a16).min(255) as u8;
|
||||
rgba[idx + 1] = ((g_premul * 255 + a16 / 2) / a16).min(255) as u8;
|
||||
rgba[idx + 2] = ((b_premul * 255 + a16 / 2) / a16).min(255) as u8;
|
||||
rgba[idx + 3] = a;
|
||||
}
|
||||
}
|
||||
Ok(rgba)
|
||||
}
|
||||
|
||||
/// Purpose: Find a layer source file within a KRA file by trying several naming patterns.
|
||||
fn find_krita_layer_file(
|
||||
file_names: &[String],
|
||||
filename: &str,
|
||||
nodetype: &str,
|
||||
) -> Option<String> {
|
||||
if let Some(p) = file_names.iter().find(|n| {
|
||||
**n == filename
|
||||
|| n.ends_with(&format!("/layers/{}", filename))
|
||||
|| n.ends_with(&format!("{}.png", filename))
|
||||
|| n.ends_with(&format!("{}.tiff", filename))
|
||||
|| n.ends_with(&format!("{}.tif", filename))
|
||||
}) {
|
||||
return Some(p.clone());
|
||||
}
|
||||
|
||||
if nodetype == "shapelayer" || nodetype == "vectorlayer" {
|
||||
let shape_path = format!("{}.shapelayer/content.svg", filename);
|
||||
if let Some(p) = file_names.iter().find(|n| n.ends_with(&shape_path)) {
|
||||
return Some(p.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Purpose: Parse the main Krita document file (`maindoc.xml`), resolving ASL layer styles.
|
||||
/// Logic: First loads `layerstyles.asl` from the ZIP, parses it into an effects map, then parses the XML
|
||||
/// tags recursively, loading either vector or raster pixels and mapping ASL styles back to the Layers.
|
||||
fn try_import_krita_maindoc(archive: &mut zip::ZipArchive<std::fs::File>) -> Result<Vec<Layer>, String> {
|
||||
let mut xml_content = String::new();
|
||||
let mut found = false;
|
||||
if let Ok(mut f) = archive.by_name("maindoc.xml") {
|
||||
f.read_to_string(&mut xml_content).map_err(|e| e.to_string())?;
|
||||
found = true;
|
||||
}
|
||||
if !found {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
use quick_xml::events::Event;
|
||||
let mut reader = quick_xml::Reader::from_str(&xml_content);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut width = 800u32;
|
||||
let mut height = 600u32;
|
||||
let mut layers_info = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
|
||||
let name = e.name();
|
||||
let tag_name = std::str::from_utf8(name.as_ref()).unwrap_or("").to_lowercase();
|
||||
if tag_name == "image" {
|
||||
for attr in e.attributes() {
|
||||
let a = attr.map_err(|e| format!("XML attr error: {}", e))?;
|
||||
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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else if tag_name == "layer"
|
||||
|| tag_name == "paintlayer"
|
||||
|| tag_name == "vectorlayer"
|
||||
|| tag_name == "filllayer"
|
||||
|| tag_name == "shapelayer"
|
||||
|| tag_name == "adjustmentlayer"
|
||||
|| tag_name == "filterlayer"
|
||||
|| tag_name == "grouplayer"
|
||||
{
|
||||
let mut name = String::new();
|
||||
let mut filename = String::new();
|
||||
let mut visible = true;
|
||||
let mut opacity = 1.0_f32;
|
||||
let mut lx = 0i32;
|
||||
let mut ly = 0i32;
|
||||
let mut has_filename = false;
|
||||
let mut compositeop = "normal".to_string();
|
||||
let mut layerstyle_uuid = String::new();
|
||||
let mut nodetype = tag_name.clone();
|
||||
|
||||
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"name" => name = val.to_string(),
|
||||
b"filename" => {
|
||||
filename = val.to_string();
|
||||
has_filename = true;
|
||||
}
|
||||
b"visible" => visible = val != "0" && val.to_lowercase() != "false",
|
||||
b"opacity" => opacity = val.parse::<f32>().unwrap_or(255.0) / 255.0,
|
||||
b"x" => lx = val.parse().unwrap_or(0),
|
||||
b"y" => ly = val.parse().unwrap_or(0),
|
||||
b"compositeop" => compositeop = val.to_string(),
|
||||
b"nodetype" => nodetype = val.to_string(),
|
||||
b"layerstyle" => layerstyle_uuid = val.trim_matches(|c| c == '{' || c == '}').to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if has_filename && !filename.is_empty() {
|
||||
layers_info.push(KritaLayerInfo {
|
||||
name,
|
||||
filename,
|
||||
visible,
|
||||
opacity,
|
||||
x: lx,
|
||||
y: ly,
|
||||
nodetype,
|
||||
compositeop,
|
||||
layerstyle_uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(format!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
let all_files: Vec<String> = archive.file_names().map(|n| n.to_string()).collect();
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let asl_styles: std::collections::HashMap<String, Vec<hcie_protocol::effects::LayerEffect>> = {
|
||||
let mut styles_map = std::collections::HashMap::new();
|
||||
if let Some(asl_path) = all_files.iter().find(|n| n.ends_with("layerstyles.asl")) {
|
||||
if let Ok(mut asl_file) = archive.by_name(&asl_path.clone()) {
|
||||
let mut asl_data = Vec::new();
|
||||
if asl_file.read_to_end(&mut asl_data).is_ok() {
|
||||
if let Ok(parsed) = hcie_fx::parse_asl_styles(&asl_data) {
|
||||
for (uuid, hcie_fx_effects) in parsed {
|
||||
let protocol_effects: Vec<hcie_protocol::effects::LayerEffect> =
|
||||
hcie_fx_effects.iter()
|
||||
.map(|fx| hcie_fx::hcie_fx_effect_to_protocol(fx))
|
||||
.collect();
|
||||
styles_map.insert(uuid, protocol_effects);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
styles_map
|
||||
};
|
||||
|
||||
for info in layers_info.into_iter().rev() {
|
||||
if info.nodetype == "adjustmentlayer" || info.nodetype == "filterlayer" {
|
||||
let pixels = vec![0u8; (width * height * 4) as usize];
|
||||
let mut layer = Layer::from_rgba(&info.name, width, height, pixels);
|
||||
layer.id = rand::random::<u64>();
|
||||
layer.visible = info.visible;
|
||||
layer.opacity = info.opacity;
|
||||
layer.blend_mode = krita_compositeop_to_blend_mode(&info.compositeop);
|
||||
|
||||
if !info.layerstyle_uuid.is_empty() {
|
||||
if let Some(effects) = asl_styles.get(&info.layerstyle_uuid) {
|
||||
layer.effects = effects.clone();
|
||||
layer.styles = effects.iter().map(kra_effect_to_layer_style).collect();
|
||||
}
|
||||
}
|
||||
layers.push(layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
let mut entry = match archive.by_name(zip_path) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if entry.read_to_end(&mut buf).is_err() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let is_vector = info.nodetype == "shapelayer" || info.nodetype == "vectorlayer";
|
||||
let pixels = if is_vector {
|
||||
match rasterize_svg_to_rgba(&buf, width, height) {
|
||||
Ok(p) => p,
|
||||
Err(_) => 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],
|
||||
}
|
||||
} else {
|
||||
match image::load_from_memory(&buf) {
|
||||
Ok(img) => {
|
||||
let rgba = img.to_rgba8();
|
||||
let iw = rgba.width();
|
||||
let ih = rgba.height();
|
||||
let src = rgba.as_raw();
|
||||
let mut pixels = vec![0u8; (width * height * 4) as usize];
|
||||
for y in 0..ih {
|
||||
let dy = info.y + y as i32;
|
||||
if dy < 0 || dy >= height as i32 { continue; }
|
||||
for x in 0..iw {
|
||||
let dx = info.x + x as i32;
|
||||
if dx < 0 || dx >= width as i32 { continue; }
|
||||
let src_i = ((y * iw + x) * 4) as usize;
|
||||
let dst_i = ((dy as u32 * width + dx as u32) * 4) as usize;
|
||||
pixels[dst_i..dst_i + 4].copy_from_slice(&src[src_i..src_i + 4]);
|
||||
}
|
||||
}
|
||||
pixels
|
||||
}
|
||||
Err(_) => vec![0u8; (width * height * 4) as usize],
|
||||
}
|
||||
};
|
||||
|
||||
let mut layer = Layer::from_rgba(&info.name, width, height, pixels);
|
||||
layer.id = rand::random::<u64>();
|
||||
layer.visible = info.visible;
|
||||
layer.opacity = info.opacity;
|
||||
layer.blend_mode = krita_compositeop_to_blend_mode(&info.compositeop);
|
||||
|
||||
if is_vector {
|
||||
let mut is_text_layer = false;
|
||||
if let Ok(svg_str) = std::str::from_utf8(&buf) {
|
||||
if svg_str.contains("<text") {
|
||||
if let Some(text_data) = parse_krita_svg_text(svg_str) {
|
||||
layer.layer_type = LayerType::Text;
|
||||
layer.data = text_data;
|
||||
is_text_layer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_text_layer {
|
||||
layer.layer_type = LayerType::Vector;
|
||||
let (svg_w, svg_h) = parse_svg_viewbox(&buf);
|
||||
let (scale_x, scale_y) = if svg_w > 0.0 && svg_h > 0.0 {
|
||||
(width as f32 / svg_w, height as f32 / svg_h)
|
||||
} else {
|
||||
(1.0, 1.0)
|
||||
};
|
||||
let shapes = parse_svg_shapes(&buf, scale_x, scale_y);
|
||||
layer.data = LayerData::Vector { shapes };
|
||||
layer.adjustment_raw = Some((*b"svg ", buf.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !info.layerstyle_uuid.is_empty() {
|
||||
if let Some(effects) = asl_styles.get(&info.layerstyle_uuid) {
|
||||
layer.effects = effects.clone();
|
||||
layer.styles = effects.iter().map(kra_effect_to_layer_style).collect();
|
||||
}
|
||||
}
|
||||
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
if layers.is_empty() {
|
||||
Err("No valid layers found in maindoc.xml".to_string())
|
||||
} else {
|
||||
Ok(layers)
|
||||
}
|
||||
}
|
||||
|
||||
/// Purpose: Convert a LayerEffect enum to a LayerStyle configuration.
|
||||
fn kra_effect_to_layer_style(effect: &hcie_protocol::effects::LayerEffect) -> LayerStyle {
|
||||
match effect {
|
||||
hcie_protocol::effects::LayerEffect::DropShadow { enabled, blend_mode, color, opacity, angle, distance, spread, size, .. } => {
|
||||
LayerStyle::DropShadow {
|
||||
enabled: *enabled, opacity: *opacity, angle: *angle,
|
||||
distance: *distance, spread: *spread, size: *size,
|
||||
color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::InnerShadow { enabled, blend_mode, color, opacity, angle, distance, choke, size, .. } => {
|
||||
LayerStyle::InnerShadow {
|
||||
enabled: *enabled, opacity: *opacity, angle: *angle,
|
||||
distance: *distance, spread: *choke, size: *size,
|
||||
color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::OuterGlow { enabled, blend_mode, color, opacity, spread, size, .. } => {
|
||||
LayerStyle::OuterGlow {
|
||||
enabled: *enabled, opacity: *opacity, spread: *spread, size: *size,
|
||||
color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::InnerGlow { enabled, blend_mode, color, opacity, choke, size, .. } => {
|
||||
LayerStyle::InnerGlow {
|
||||
enabled: *enabled, opacity: *opacity, spread: *choke, size: *size,
|
||||
color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::BevelEmboss { enabled, style, depth, direction, size, soften, angle, altitude, highlight_opacity, shadow_opacity, highlight_blend, highlight_color, shadow_blend, shadow_color, technique, .. } => {
|
||||
LayerStyle::BevelEmboss {
|
||||
enabled: *enabled, depth: *depth, size: *size, angle: *angle,
|
||||
altitude: *altitude, highlight_opacity: *highlight_opacity,
|
||||
shadow_opacity: *shadow_opacity,
|
||||
direction: format!("{:?}", direction),
|
||||
style: format!("{:?}", style),
|
||||
technique: format!("{:?}", technique),
|
||||
soften: *soften,
|
||||
highlight_blend_mode: highlight_blend.clone(),
|
||||
highlight_color: *highlight_color,
|
||||
shadow_blend_mode: shadow_blend.clone(),
|
||||
shadow_color: *shadow_color,
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::Satin { enabled, color, opacity, angle, distance, size, invert, .. } => {
|
||||
LayerStyle::Satin {
|
||||
enabled: *enabled, opacity: *opacity, angle: *angle,
|
||||
distance: *distance, size: *size, color: *color, invert: *invert,
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::ColorOverlay { enabled, blend_mode, color, opacity } => {
|
||||
LayerStyle::ColorOverlay {
|
||||
enabled: *enabled, opacity: *opacity, color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::GradientOverlay { enabled, blend_mode, opacity, angle, scale, .. } => {
|
||||
LayerStyle::GradientOverlay {
|
||||
enabled: *enabled, opacity: *opacity, blend_mode: blend_mode.clone(),
|
||||
angle: *angle, scale: *scale, gradient_type: 0,
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::PatternOverlay { enabled, blend_mode, opacity, scale, .. } => {
|
||||
LayerStyle::PatternOverlay {
|
||||
enabled: *enabled, opacity: *opacity, blend_mode: blend_mode.clone(),
|
||||
scale: *scale, pattern_name: String::new(),
|
||||
}
|
||||
}
|
||||
hcie_protocol::effects::LayerEffect::Stroke { enabled, position, blend_mode, opacity, size, color, .. } => {
|
||||
LayerStyle::Stroke {
|
||||
enabled: *enabled, size: *size,
|
||||
position: format!("{:?}", position),
|
||||
opacity: *opacity, color: *color, blend_mode: blend_mode.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Purpose: Export layers as a KRA (Krita) file with VERS tile format and maindoc.xml.
|
||||
pub fn export_kra(layers: &[Layer], canvas_w: u32, canvas_h: u32, composited: &[u8], path: &Path) -> Result<(), String> {
|
||||
kra_saver::save_kra(layers, canvas_w, canvas_h, composited, path)
|
||||
}
|
||||
|
||||
fn parse_krita_svg_text(svg_str: &str) -> Option<LayerData> {
|
||||
let text_start = svg_str.find("<text")?;
|
||||
let text_end_tag = svg_str.find("</text>")?;
|
||||
let text_tag_content = &svg_str[text_start..text_end_tag];
|
||||
let tag_close = text_tag_content.find('>')?;
|
||||
let text_content = &text_tag_content[tag_close + 1..];
|
||||
|
||||
let extract_attr = |name: &str| -> Option<String> {
|
||||
let pattern = format!("{}=\"", name);
|
||||
let start = svg_str.find(&pattern)?;
|
||||
let val_start = start + pattern.len();
|
||||
let val_end = svg_str[val_start..].find('"')?;
|
||||
Some(svg_str[val_start..val_start + val_end].to_string())
|
||||
};
|
||||
|
||||
let x: f32 = extract_attr("x")?.parse().unwrap_or(0.0);
|
||||
let y: f32 = extract_attr("y")?.parse().unwrap_or(0.0);
|
||||
let font = extract_attr("font-family").unwrap_or_else(|| "Arial".to_string());
|
||||
let font_size: f32 = extract_attr("font-size")?.parse().unwrap_or(12.0);
|
||||
let fill_color = extract_attr("fill").unwrap_or_else(|| "#000000".to_string());
|
||||
|
||||
let color = if fill_color.starts_with('#') && fill_color.len() == 7 {
|
||||
let r = u8::from_str_radix(&fill_color[1..3], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&fill_color[3..5], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&fill_color[5..7], 16).unwrap_or(0);
|
||||
[r, g, b, 255]
|
||||
} else {
|
||||
[0, 0, 0, 255]
|
||||
};
|
||||
|
||||
let mut angle = 0.0f32;
|
||||
if let Some(transform) = extract_attr("transform") {
|
||||
if transform.starts_with("rotate(") {
|
||||
let parts: Vec<&str> = transform["rotate(".len()..transform.len() - 1].split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
angle = parts[0].parse().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(LayerData::Text {
|
||||
text: text_content.to_string(),
|
||||
font,
|
||||
size: font_size,
|
||||
color,
|
||||
x,
|
||||
y,
|
||||
angle,
|
||||
alignment: hcie_protocol::tools::TextAlignment::Left,
|
||||
orientation: hcie_protocol::tools::TextOrientation::Horizontal,
|
||||
effects: vec![],
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
unrotated_w: 0.0,
|
||||
unrotated_h: 0.0,
|
||||
})
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user