This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+23
View File
@@ -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"]
+411
View File
@@ -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
}
+981
View File
@@ -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.