This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "hcie-psd-saver"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib"]
[dependencies]
hcie-protocol = { path = "../hcie-protocol" }
hcie-blend = { path = "../hcie-blend" }
hcie-fx = { path = "../hcie-fx" }
+303
View File
@@ -0,0 +1,303 @@
//! PSD effect descriptor writers.
//!
//! Each effect type (DropShadow, InnerShadow, etc.) has its own independent function.
//! To add or modify an effect, edit only the relevant function below.
//! Do NOT modify psd_saver/mod.rs — it is structurally locked.
//!
//! Shared helpers are in `super::helpers` (w_key4, w_bool, w_enum, w_untf, etc.)
use hcie_fx::types::{BevelStyle, Technique, Direction, StrokePosition, StrokeFillType, GradientDef};
use hcie_fx::LayerEffect;
use super::helpers::*;
pub(crate) fn blend_mode_to_lfx2_key(mode: hcie_blend::BlendMode) -> [u8; 4] {
match mode {
hcie_blend::BlendMode::Normal => *b"Nrml",
hcie_blend::BlendMode::Multiply => *b"Mltp",
hcie_blend::BlendMode::Screen => *b"Scrn",
hcie_blend::BlendMode::Overlay => *b"Ovrl",
hcie_blend::BlendMode::SoftLight => *b"SftL",
hcie_blend::BlendMode::HardLight => *b"HrdL",
hcie_blend::BlendMode::Darken => *b"Drkn",
hcie_blend::BlendMode::Lighten => *b"Lghn",
hcie_blend::BlendMode::ColorDodge => *b"CDdg",
hcie_blend::BlendMode::ColorBurn => *b"CBrn",
hcie_blend::BlendMode::LinearDodge => *b"LDdg",
hcie_blend::BlendMode::LinearBurn => *b"LBrn",
hcie_blend::BlendMode::Difference => *b"Dffc",
hcie_blend::BlendMode::Exclusion => *b"Xclu",
hcie_blend::BlendMode::Subtract => *b"Subt",
hcie_blend::BlendMode::Divide => *b"ClcD",
hcie_blend::BlendMode::VividLight => *b"VvdL",
hcie_blend::BlendMode::LinearLight => *b"LnrL",
hcie_blend::BlendMode::PinLight => *b"PnLt",
hcie_blend::BlendMode::HardMix => *b"HrdM",
hcie_blend::BlendMode::Hue => *b"H ",
hcie_blend::BlendMode::Saturation => *b"Strt",
hcie_blend::BlendMode::Color => *b"Clr ",
hcie_blend::BlendMode::Luminosity => *b"Lmns",
hcie_blend::BlendMode::PassThrough => *b"Psth",
_ => *b"Nrml",
}
}
pub(crate) fn write_fx_descriptor(buf: &mut Vec<u8>, effect: &LayerEffect) {
match effect {
LayerEffect::DropShadow { enabled, blend_mode, color, opacity, angle, distance, spread, size, noise, .. } => {
w_key4(buf, b"DrSh");
w_objc_start(buf);
w_objc_body_start(buf, b"DrSh", 12);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
w_key4(buf, b"uglg"); w_bool(buf, false);
w_key4(buf, b"lagl"); w_untf(buf, b"#Ang", *angle as f64);
w_key4(buf, b"Dstn"); w_untf(buf, b"#Pxl", *distance as f64);
w_key4(buf, b"Ckmt"); w_untf(buf, b"#Pxl", *spread as f64);
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"Nose"); w_untf(buf, b"#Prc", pct_to_f64(*noise));
w_key4(buf, b"AntA"); w_bool(buf, false);
w_contour_with_key(buf, b"TrnS");
}
LayerEffect::InnerShadow { enabled, blend_mode, color, opacity, angle, distance, choke, size, noise, .. } => {
w_key4(buf, b"IrSh");
w_objc_start(buf);
w_objc_body_start(buf, b"IrSh", 12);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
w_key4(buf, b"uglg"); w_bool(buf, false);
w_key4(buf, b"lagl"); w_untf(buf, b"#Ang", *angle as f64);
w_key4(buf, b"Dstn"); w_untf(buf, b"#Pxl", *distance as f64);
w_key4(buf, b"Ckmt"); w_untf(buf, b"#Pxl", *choke as f64);
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"Nose"); w_untf(buf, b"#Prc", pct_to_f64(*noise));
w_key4(buf, b"AntA"); w_bool(buf, false);
w_contour_with_key(buf, b"TrnS");
}
LayerEffect::OuterGlow { enabled, blend_mode, color, opacity, spread, size, noise, .. } => {
w_key4(buf, b"OrGl");
w_objc_start(buf);
w_objc_body_start(buf, b"OrGl", 10);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
w_key4(buf, b"GlwT"); w_enum(buf, b"GlwT", b"SftR");
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"Ckmt"); w_untf(buf, b"#Pxl", *spread as f64);
w_key4(buf, b"AntA"); w_bool(buf, false);
w_contour_with_key(buf, b"TrnS");
w_key4(buf, b"Nose"); w_untf(buf, b"#Prc", pct_to_f64(*noise));
}
LayerEffect::InnerGlow { enabled, blend_mode, color, opacity, choke, size, noise, source, .. } => {
let src_val = if *source == 1 { b"CtrE" } else { b"SrcE" };
w_key4(buf, b"IrGl");
w_objc_start(buf);
w_objc_body_start(buf, b"IrGl", 10);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
w_key4(buf, b"GlwS"); w_enum(buf, b"GlwS", src_val);
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"Ckmt"); w_untf(buf, b"#Pxl", *choke as f64);
w_key4(buf, b"AntA"); w_bool(buf, false);
w_contour_with_key(buf, b"TrnS");
w_key4(buf, b"Nose"); w_untf(buf, b"#Prc", pct_to_f64(*noise));
}
LayerEffect::BevelEmboss { enabled, style, technique, depth, direction, size, soften, angle, altitude, highlight_blend, highlight_color, highlight_opacity, shadow_blend, shadow_color, shadow_opacity, .. } => {
let bvl_s = match style {
BevelStyle::InnerBevel => b"InrB",
BevelStyle::OuterBevel => b"OtrB",
BevelStyle::Emboss => b"Embs",
BevelStyle::PillowEmboss => b"PlEb",
BevelStyle::StrokeEmboss => b"Srgb",
};
let bvl_t = match technique {
Technique::Smooth => b"SfBL",
Technique::ChiselHard => b"CisL",
Technique::ChiselSoft => b"CisS",
};
let bvl_d = if matches!(direction, Direction::Up) { b"Out " } else { b"In " };
w_key4(buf, b"ebbl");
w_objc_start(buf);
w_objc_body_start(buf, b"ebbl", 28);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"hglM"); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*highlight_blend));
w_key4(buf, b"hglC"); w_rgbc_color(buf, highlight_color);
w_key4(buf, b"hglO"); w_untf(buf, b"#Prc", opacity_pct(*highlight_opacity));
w_key4(buf, b"sdwM"); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*shadow_blend));
w_key4(buf, b"sdwC"); w_rgbc_color(buf, shadow_color);
w_key4(buf, b"sdwO"); w_untf(buf, b"#Prc", opacity_pct(*shadow_opacity));
w_key4(buf, b"bvlT"); w_enum(buf, b"bvlT", bvl_t);
w_key4(buf, b"bvlS"); w_enum(buf, b"BESl", bvl_s);
w_key4(buf, b"uglg"); w_bool(buf, false);
w_key4(buf, b"lagl"); w_untf(buf, b"#Ang", *angle as f64);
w_key4(buf, b"Lald"); w_untf(buf, b"#Ang", *altitude as f64);
w_key4(buf, b"srgR"); w_untf(buf, b"#Prc", ((*depth).clamp(0.0, 1000.0) / 10.0) as f64);
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"bvlD"); w_enum(buf, b"BESs", bvl_d);
w_contour_with_key(buf, b"TrnS");
w_key_var(buf, b"antialiasGloss"); w_bool(buf, false);
w_key4(buf, b"Sftn"); w_untf(buf, b"#Pxl", *soften as f64);
w_key_var(buf, b"useShape"); w_bool(buf, false);
w_contour_with_key(buf, b"MpgS");
w_key4(buf, b"AntA"); w_bool(buf, false);
w_key4(buf, b"Inpr"); w_untf(buf, b"#Prc", 0.0);
w_key_var(buf, b"useTexture"); w_bool(buf, false);
w_key4(buf, b"InvT"); w_bool(buf, false);
w_key4(buf, b"Algn"); w_bool(buf, true);
w_key4(buf, b"Scl "); w_untf(buf, b"#Prc", 100.0);
w_key_var(buf, b"textureDepth"); w_untf(buf, b"#Prc", 100.0);
w_key4(buf, b"Ptrn");
w_objc_start(buf);
w_objc_body_start(buf, b"Ptrn", 2);
w_key4(buf, b"Nm ");
buf.extend_from_slice(b"TEXT");
w_ustr(buf, "Default/orangeslices");
w_key4(buf, b"Idnt");
buf.extend_from_slice(b"TEXT");
w_ustr(buf, "b4d43394-d71c-11e5-b1ae-a548a96ef5f9");
w_key_var(buf, b"phase");
w_objc_start(buf);
w_objc_body_start(buf, b"Pnt ", 2);
w_key4(buf, b"Hrzn"); w_doub(buf, 0.0);
w_key4(buf, b"Vrtc"); w_doub(buf, 0.0);
}
LayerEffect::Satin { enabled, blend_mode, color, opacity, angle, distance, size, invert, .. } => {
w_key4(buf, b"lagl");
w_objc_start(buf);
w_objc_body_start(buf, b"lagl", 10);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
w_key4(buf, b"lagl"); w_untf(buf, b"#Ang", *angle as f64);
w_key4(buf, b"Dstn"); w_untf(buf, b"#Pxl", *distance as f64);
w_key4(buf, b"blur"); w_untf(buf, b"#Pxl", *size as f64);
w_contour_with_key(buf, b"MpgS");
w_key4(buf, b"Invr"); w_bool(buf, *invert);
w_key4(buf, b"AntA"); w_bool(buf, false);
}
LayerEffect::ColorOverlay { enabled, blend_mode, color, opacity } => {
w_key4(buf, b"sofi");
w_objc_start(buf);
w_objc_body_start(buf, b"sofi", 4);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
}
LayerEffect::GradientOverlay { enabled, blend_mode, opacity, angle, scale, gradient } => {
w_key4(buf, b"GrFl");
w_objc_start(buf);
let gtype: &[u8; 4] = if gradient.gradient_type == 1 { b"Rdl " } else { b"Lnr " };
w_objc_body_start(buf, b"GrFl", 10);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Angl"); w_long(buf, *angle as i32);
w_key4(buf, b"Type"); w_enum(buf, b"GrdT", gtype);
w_key4(buf, b"Rvrs"); w_bool(buf, false);
w_key4(buf, b"Algn"); w_bool(buf, true);
w_key4(buf, b"Scl "); w_untf(buf, b"#Prc", pct_to_f64(*scale));
w_key4(buf, b"Ofst"); w_untf(buf, b"#Prc", 0.0);
w_key4(buf, b"Grad");
w_gradient_objc(buf, *angle, gradient);
}
LayerEffect::PatternOverlay { enabled, blend_mode, opacity, scale, pattern_id } => {
w_key4(buf, b"PtFl");
w_objc_start(buf);
w_objc_body_start(buf, b"PtFl", 6);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Algn"); w_bool(buf, true);
w_key4(buf, b"Scl "); w_untf(buf, b"#Prc", pct_to_f64(*scale));
w_key4(buf, b"Ptrn");
w_objc_start(buf);
w_objc_body_start(buf, b"Ptrn", 2);
w_key4(buf, b"Nm ");
buf.extend_from_slice(b"TEXT");
w_ustr(buf, "Default/orangeslices");
w_key4(buf, b"Idnt");
buf.extend_from_slice(b"TEXT");
w_ustr(buf, pattern_id.as_str());
w_key_var(buf, b"phase");
w_objc_start(buf);
w_objc_body_start(buf, b"Pnt ", 2);
w_key4(buf, b"Hrzn"); w_doub(buf, 0.0);
w_key4(buf, b"Vrtc"); w_doub(buf, 0.0);
}
LayerEffect::Stroke { enabled, position, fill_type, blend_mode, opacity, size, color } => {
let styl = match position {
StrokePosition::Outside => b"OutF",
StrokePosition::Inside => b"InsF",
StrokePosition::Center => b"CtrF",
};
let pnt_t = match fill_type {
StrokeFillType::Color => b"SClr",
StrokeFillType::Gradient => b"GrFl",
StrokeFillType::Pattern => b"PtFl",
};
w_key4(buf, b"FrFX");
w_objc_start(buf);
let fc: u32 = if matches!(fill_type, StrokeFillType::Color) { 7 } else { 8 };
w_objc_body_start(buf, b"FrFX", fc);
w_key4(buf, b"enab"); w_bool(buf, *enabled);
w_key4(buf, b"Styl"); w_enum(buf, b"FStl", styl);
w_key4(buf, b"PntT"); w_enum(buf, b"FrFl", pnt_t);
w_key4(buf, b"Md "); w_enum(buf, b"BlnM", &blend_mode_to_lfx2_key(*blend_mode));
w_key4(buf, b"Opct"); w_untf(buf, b"#Prc", opacity_pct(*opacity));
w_key4(buf, b"Sz "); w_untf(buf, b"#Pxl", *size as f64);
w_key4(buf, b"Clr "); w_rgbc_color(buf, color);
if !matches!(fill_type, StrokeFillType::Color) {
w_contour_with_key(buf, b"TrnS");
}
}
}
}
pub(crate) fn w_gradient_objc(buf: &mut Vec<u8>, angle: f32, gradient: &GradientDef) {
let gtype: &[u8; 4] = if gradient.gradient_type == 1 { b"Rdl " } else { b"Lnr " };
w_objc_start(buf);
w_objc_body_start(buf, b"Grdn", 7);
w_key4(buf, b"Intr"); w_long(buf, 4096);
w_key4(buf, b"GrdF"); w_enum(buf, b"GrdF", gtype);
w_key4(buf, b"Clrs");
buf.extend_from_slice(b"VlLs");
w_u32(buf, gradient.color_stops.len() as u32);
for (loc, col) in &gradient.color_stops {
w_objc_start(buf);
w_objc_body_start(buf, b"Clrt", 4);
w_key4(buf, b"Clr ");
w_rgbc_color(buf, col);
w_key4(buf, b"Type"); w_enum(buf, b"Clry", b"UsrS");
w_key4(buf, b"Lctn"); w_long(buf, *loc as i32);
w_key4(buf, b"Mdpn"); w_long(buf, 50 * 256);
}
w_key4(buf, b"Trns");
buf.extend_from_slice(b"VlLs");
w_u32(buf, gradient.transparency_stops.len().max(1) as u32);
if gradient.transparency_stops.is_empty() {
w_objc_start(buf);
w_objc_body_start(buf, b"Trns", 3);
w_key4(buf, b"Opct"); w_doub(buf, 100.0);
w_key4(buf, b"Lctn"); w_long(buf, 0);
w_key4(buf, b"Mdpn"); w_long(buf, 50 * 256);
} else {
for (loc, val) in &gradient.transparency_stops {
w_objc_start(buf);
w_objc_body_start(buf, b"Trns", 3);
w_key4(buf, b"Opct"); w_doub(buf, *val as f64);
w_key4(buf, b"Lctn"); w_long(buf, *loc as i32);
w_key4(buf, b"Mdpn"); w_long(buf, 50 * 256);
}
}
w_key4(buf, b"Rvrs"); w_bool(buf, false);
w_key4(buf, b"Angl"); w_long(buf, angle as i32);
w_key4(buf, b"Type"); w_enum(buf, b"GrdT", gtype);
}
+148
View File
@@ -0,0 +1,148 @@
pub(super) fn w_u32(buf: &mut Vec<u8>, v: u32) { buf.extend_from_slice(&v.to_be_bytes()); }
pub(super) fn w_u16(buf: &mut Vec<u8>, v: u16) { buf.extend_from_slice(&v.to_be_bytes()); }
pub(super) fn w_f64(buf: &mut Vec<u8>, v: f64) { buf.extend_from_slice(&v.to_be_bytes()); }
pub(super) fn w_key4(buf: &mut Vec<u8>, key: &[u8; 4]) {
w_u32(buf, 0);
buf.extend_from_slice(key);
}
pub(super) fn w_key_var(buf: &mut Vec<u8>, key: &[u8]) {
w_u32(buf, key.len() as u32);
buf.extend_from_slice(key);
}
/// Write an additional layer information tagged block.
///
/// Layout: signature "8BIM" (4 bytes), key (4 bytes), length (4 bytes, rounded up to even), data.
/// The length field includes padding to even, matching the PSD spec requirement.
pub(super) fn write_8bim_block(buf: &mut Vec<u8>, key: &[u8; 4], data: &[u8]) {
buf.extend_from_slice(b"8BIM");
buf.extend_from_slice(key);
// Additional layer information blocks use a 4-byte length for the data (padded to even).
let padded_len = (data.len() + 1) & !1; // round up to even
buf.extend_from_slice(&(padded_len as u32).to_be_bytes());
buf.extend_from_slice(data);
for _ in data.len()..padded_len {
buf.push(0);
}
}
/// Write a `luni` (Unicode layer name) tagged block.
///
/// Layout: unicode string length (4 bytes), UTF-16BE string.
pub(super) fn write_luni_block(buf: &mut Vec<u8>, name: &str) {
let mut data = Vec::new();
let encoded: Vec<u16> = name.encode_utf16().collect();
data.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
for c in encoded {
data.extend_from_slice(&c.to_be_bytes());
}
write_8bim_block(buf, b"luni", &data);
}
pub(super) fn w_ustr_empty(buf: &mut Vec<u8>) {
w_u32(buf, 0);
}
pub(super) fn w_ustr_null(buf: &mut Vec<u8>) {
w_u32(buf, 1);
w_u16(buf, 0);
}
pub(super) fn w_ustr(buf: &mut Vec<u8>, s: &str) {
w_u32(buf, s.chars().count() as u32);
for c in s.encode_utf16() {
w_u16(buf, c);
}
}
pub(super) fn w_class_id(buf: &mut Vec<u8>, class_id: &[u8; 4]) {
w_u32(buf, 0);
buf.extend_from_slice(class_id);
}
pub(super) fn w_bool(buf: &mut Vec<u8>, val: bool) {
buf.extend_from_slice(b"bool");
buf.push(val as u8);
}
pub(super) fn w_enum(buf: &mut Vec<u8>, type_id: &[u8; 4], value: &[u8; 4]) {
buf.extend_from_slice(b"enum");
w_class_id(buf, type_id);
w_class_id(buf, value);
}
pub(super) fn w_untf(buf: &mut Vec<u8>, unit: &[u8; 4], value: f64) {
buf.extend_from_slice(b"UntF");
buf.extend_from_slice(unit);
w_f64(buf, value);
}
pub(super) fn w_doub(buf: &mut Vec<u8>, value: f64) {
buf.extend_from_slice(b"doub");
w_f64(buf, value);
}
pub(super) fn w_long(buf: &mut Vec<u8>, value: i32) {
buf.extend_from_slice(b"long");
buf.extend_from_slice(&value.to_be_bytes());
}
pub(super) fn w_objc_start(buf: &mut Vec<u8>) {
buf.extend_from_slice(b"Objc");
}
pub(super) fn w_objc_body_start(buf: &mut Vec<u8>, class_id: &[u8; 4], count: u32) {
w_ustr_null(buf);
w_class_id(buf, class_id);
w_u32(buf, count);
}
pub(super) fn w_rgbc_color(buf: &mut Vec<u8>, color: &[u8; 4]) {
w_objc_start(buf);
w_objc_body_start(buf, b"RGBC", 3);
w_key4(buf, b"Rd ");
w_doub(buf, color[0] as f64);
w_key4(buf, b"Grn ");
w_doub(buf, color[1] as f64);
w_key4(buf, b"Bl ");
w_doub(buf, color[2] as f64);
}
pub(super) fn w_default_contour(buf: &mut Vec<u8>) {
w_objc_start(buf);
w_objc_body_start(buf, b"ShpC", 2);
w_key4(buf, b"Nm ");
buf.extend_from_slice(b"TEXT");
w_ustr(buf, "Linear\0");
w_key4(buf, b"Crv ");
buf.extend_from_slice(b"VlLs");
w_u32(buf, 2);
w_objc_start(buf);
w_objc_body_start(buf, b"CrPt", 2);
w_key4(buf, b"Hrzn");
w_doub(buf, 0.0);
w_key4(buf, b"Vrtc");
w_doub(buf, 0.0);
w_objc_start(buf);
w_objc_body_start(buf, b"CrPt", 2);
w_key4(buf, b"Hrzn");
w_doub(buf, 255.0);
w_key4(buf, b"Vrtc");
w_doub(buf, 255.0);
}
pub(super) fn w_contour_with_key(buf: &mut Vec<u8>, key: &[u8; 4]) {
w_key4(buf, key);
w_default_contour(buf);
}
pub(super) fn pct_to_f64(pct: f32) -> f64 {
pct.clamp(0.0, 100.0) as f64
}
pub(super) fn opacity_pct(opacity: f32) -> f64 {
(opacity.clamp(0.0, 1.0) * 100.0) as f64
}
+827
View File
@@ -0,0 +1,827 @@
#![allow(dead_code)]
//! PSD file format writer.
//!
//! Purpose: Export layers as an Adobe Photoshop (PSD) file with full layer
//! records, PackBits RLE compression, blend mode mapping, and a composited
//! merged image.
//!
//! Logic & Workflow:
//! 1. Keep group layers and section-divider markers as-is; do not rasterize them.
//! 2. Write PSD binary sections: Header → Color Mode → Image Resources →
//! Layer & Mask Info → Merged Image Data
//! 3. Per-layer channel data uses PackBits RLE compression
//! 4. Merged composited image is also RLE-compressed
//! 5. Section-divider records are emitted with zero bounds, empty channels, and
//! an `lsct` type 2 block with the original pass-through blend mode.
//!
//! Arguments & Returns:
//! - `save_psd(layers, canvas_w, canvas_h, composited, path)`:
//! `layers` = slice of Layer, `composited` = pre-composited RGBA pixels
//! Returns `Result<(), String>`
//!
//! Side Effects: Writes a binary file to `path`.
use hcie_protocol::{Layer, BlendMode, LayerType};
use std::path::Path;
pub mod helpers;
pub mod fx_descriptors;
use helpers::*;
use fx_descriptors::{write_fx_descriptor, blend_mode_to_lfx2_key};
pub fn save_psd(
layers: &[Layer],
canvas_w: u32,
canvas_h: u32,
composited: &[u8],
path: &Path,
) -> Result<(), String> {
let mut buf = Vec::with_capacity(1 << 20);
let w = canvas_w;
let h = canvas_h;
// If every layer preserves the original PSD image resources, pass them through
// unchanged. Otherwise fall back to the minimal default resource set.
let image_resources_raw = layers
.iter()
.filter_map(|l| l.image_resources_raw.as_ref())
.next();
write_header(&mut buf, w, h);
write_color_mode_data(&mut buf);
let img_res = if let Some(raw) = image_resources_raw {
raw.clone()
} else {
build_image_resources(composited, w, h)
};
buf.extend_from_slice(&(img_res.len() as u32).to_be_bytes());
buf.extend_from_slice(&img_res);
let layer_info = build_layer_info(layers, w, h);
let glmi: [u8; 4] = [0, 0, 0, 0]; // Global Layer Mask Info (empty, required)
let lami_len = 4 + layer_info.len() as u32 + glmi.len() as u32;
buf.extend_from_slice(&lami_len.to_be_bytes());
buf.extend_from_slice(&(layer_info.len() as u32).to_be_bytes());
buf.extend_from_slice(&layer_info);
buf.extend_from_slice(&glmi);
write_merged_image_data(&mut buf, composited, w, h);
std::fs::write(path, &buf).map_err(|e| e.to_string())
}
fn write_header(buf: &mut Vec<u8>, w: u32, h: u32) {
buf.extend_from_slice(b"8BPS");
buf.extend_from_slice(&1u16.to_be_bytes());
buf.extend_from_slice(&[0u8; 6]);
buf.extend_from_slice(&3u16.to_be_bytes());
buf.extend_from_slice(&h.to_be_bytes());
buf.extend_from_slice(&w.to_be_bytes());
buf.extend_from_slice(&8u16.to_be_bytes());
buf.extend_from_slice(&3u16.to_be_bytes());
}
fn write_color_mode_data(buf: &mut Vec<u8>) {
buf.extend_from_slice(&0u32.to_be_bytes());
}
fn w_resource(buf: &mut Vec<u8>, res_id: u16, name: &[u8], data: &[u8]) {
buf.extend_from_slice(b"8BIM");
buf.extend_from_slice(&res_id.to_be_bytes());
let total_name = 1 + name.len();
let has_pad = if total_name % 2 == 1 { 1 } else { 0 };
buf.push(name.len() as u8);
buf.extend_from_slice(name);
if has_pad > 0 { buf.push(0); }
buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
buf.extend_from_slice(data);
if data.len() % 2 != 0 { buf.push(0); }
}
fn build_image_resources(_composited: &[u8], _w: u32, _h: u32) -> Vec<u8> {
let mut buf = Vec::new();
// 0x03ED: ResolutionInfo (16 bytes)
let mut res_data = Vec::new();
res_data.extend_from_slice(&(72u32 << 16).to_be_bytes()); // hRes fixed 16.16
res_data.extend_from_slice(&1u16.to_be_bytes()); // hResUnit: 1=PPI
res_data.extend_from_slice(&2u16.to_be_bytes()); // widthUnit: 2=cm
res_data.extend_from_slice(&(72u32 << 16).to_be_bytes()); // vRes fixed 16.16
res_data.extend_from_slice(&1u16.to_be_bytes()); // vResUnit: 1=PPI
res_data.extend_from_slice(&2u16.to_be_bytes()); // heightUnit: 2=cm
w_resource(&mut buf, 0x03ED, b"", &res_data);
// 0x0408: Print flags (16 bytes) — matches Photoshop format
let print_flags = [
0x00u8, 0x00, 0x00, 0x01,
0x00, 0x00, 0x02, 0x40,
0x00, 0x00, 0x02, 0x40,
0x00, 0x00, 0x00, 0x00,
];
w_resource(&mut buf, 0x0408, b"", &print_flags);
// 0x040D: Print scale (4 bytes)
w_resource(&mut buf, 0x040D, b"", &[0x00, 0x00, 0x00, 0x1e]);
// 0x0414: Pixel aspect ratio (4 bytes)
w_resource(&mut buf, 0x0414, b"", &[0x00, 0x00, 0x00, 0x02]);
// 0x0419: Global Angle (30)
let mut ga_data = Vec::new();
ga_data.extend_from_slice(&30u32.to_be_bytes());
w_resource(&mut buf, 0x0419, b"", &ga_data);
// 0x0435: Global Altitude (1)
let mut alt_data = Vec::new();
alt_data.extend_from_slice(&1u32.to_be_bytes());
w_resource(&mut buf, 0x0435, b"", &alt_data);
// 0x042D: Layer Selection IDs — count=1, ID=1
let mut lsi_data = Vec::new();
lsi_data.extend_from_slice(&1u16.to_be_bytes()); // count
lsi_data.extend_from_slice(&1u32.to_be_bytes()); // selected layer ID
w_resource(&mut buf, 0x042D, b"", &lsi_data);
if buf.len() % 2 != 0 { buf.push(0); }
buf
}
fn build_layer_info(layers: &[Layer], canvas_w: u32, canvas_h: u32) -> Vec<u8> {
let mut buf = Vec::new();
if layers.is_empty() {
buf.extend_from_slice(&0i16.to_be_bytes());
return buf;
}
let mut layer_records_buf = Vec::new();
let layer_count = layers.len() as i16;
let layer_count_val = layer_count;
layer_records_buf.extend_from_slice(&layer_count_val.to_be_bytes());
let mut all_ch_bufs: Vec<Vec<Vec<u8>>> = Vec::with_capacity(layers.len());
for (layer_index, layer) in layers.iter().enumerate() {
let is_section_divider = layer.is_section_divider;
let is_group = matches!(layer.layer_type, LayerType::Group) && !is_section_divider;
let (crop_top, crop_left, crop_bottom, crop_right, channel_order, ch_bufs) = if is_section_divider || is_group {
// Section divider or Group: zero bounds and four empty RLE channel records.
let bounds = (0i32, 0i32, 0i32, 0i32);
let mut empty_chs: Vec<Vec<u8>> = Vec::with_capacity(4);
let channel_order: Vec<i16> = vec![-1, 0, 1, 2];
for _ in 0..4 {
empty_chs.push(Vec::new());
}
(bounds.0, bounds.1, bounds.2, bounds.3, channel_order, empty_chs)
} else if layer.adjustment_raw.is_some() {
// Adjustment layers: zero bounds and 5 channels (the 5th is the mask channel -2)
let bounds = (0i32, 0i32, 0i32, 0i32);
let mut ch_bufs = Vec::with_capacity(5);
let channel_order: Vec<i16> = vec![-1, 0, 1, 2, -2];
for _ in 0..4 {
ch_bufs.push(Vec::new());
}
if let (Some(ref mask), Some(mb)) = (&layer.mask_pixels, layer.mask_bounds) {
let mw = (mb[3] - mb[1]) as u32;
let mh = (mb[2] - mb[0]) as u32;
if mw > 0 && mh > 0 {
ch_bufs.push(packbits_encode_channel(mask, mw, mh));
} else {
ch_bufs.push(Vec::new());
}
} else {
ch_bufs.push(Vec::new());
}
(bounds.0, bounds.1, bounds.2, bounds.3, channel_order, ch_bufs)
} else {
let lw = layer.width.min(canvas_w);
let lh = layer.height.min(canvas_h);
// Auto-crop: find bounding box of non-transparent pixels.
// Photoshop stores only the content region, not the full canvas.
let (crop_top, crop_left, crop_w, crop_h) = find_content_bounds(layer, lw, lh);
let crop_bottom = crop_top + crop_h;
let crop_right = crop_left + crop_w;
let mut ch_bufs = Vec::new();
let mut channel_order: Vec<i16> = vec![-1, 0, 1, 2];
if layer.mask_pixels.is_some() {
channel_order.push(-2);
}
for &ch_id in &channel_order {
if ch_id == -2 {
if let (Some(ref mask), Some(mb)) = (&layer.mask_pixels, layer.mask_bounds) {
let mw = (mb[3] - mb[1]) as u32;
let mh = (mb[2] - mb[0]) as u32;
if mw > 0 && mh > 0 {
ch_bufs.push(packbits_encode_channel(mask, mw, mh));
} else {
ch_bufs.push(Vec::new());
}
} else {
ch_bufs.push(Vec::new());
}
} else {
let plane: Vec<u8> = (crop_top..crop_bottom)
.flat_map(|y| {
(crop_left..crop_right).map(move |x| {
let idx = (y * layer.width + x) as usize * 4;
if idx + 3 < layer.pixels.len() {
match ch_id {
-1 => layer.pixels[idx + 3],
0 => layer.pixels[idx],
1 => layer.pixels[idx + 1],
2 => layer.pixels[idx + 2],
_ => 0,
}
} else {
0
}
})
})
.collect();
ch_bufs.push(packbits_encode_channel(&plane, crop_w, crop_h));
}
}
(crop_top as i32, crop_left as i32, crop_bottom as i32, crop_right as i32, channel_order, ch_bufs)
};
// Layer rectangle: top, left, bottom, right.
layer_records_buf.extend_from_slice(&crop_top.to_be_bytes());
layer_records_buf.extend_from_slice(&crop_left.to_be_bytes());
layer_records_buf.extend_from_slice(&crop_bottom.to_be_bytes());
layer_records_buf.extend_from_slice(&crop_right.to_be_bytes());
let ch_count = channel_order.len() as u16;
layer_records_buf.extend_from_slice(&ch_count.to_be_bytes());
for (i, &ch_id) in channel_order.iter().enumerate() {
layer_records_buf.extend_from_slice(&ch_id.to_be_bytes());
let ch_data_len = ch_bufs[i].len() as u32 + 2;
layer_records_buf.extend_from_slice(&ch_data_len.to_be_bytes());
}
layer_records_buf.extend_from_slice(b"8BIM");
// PSD convention: both group open markers AND section dividers use
// "norm" as the blend key in the layer record. The real blend mode
// (often PassThrough) is stored inside the lsct tagged block.
let blend_key = if is_group || is_section_divider {
b"norm"
} else {
blend_mode_key(layer.blend_mode)
};
layer_records_buf.extend_from_slice(blend_key);
layer_records_buf.push((layer.opacity * 255.0) as u8);
layer_records_buf.push(0); // clipping
let flags = layer_flags(layer);
layer_records_buf.push(flags);
layer_records_buf.push(0); // filler
// Extra data section: layer mask data, blending ranges, layer name, and optional
// additional tagged blocks. The layer name (Pascal string, padded to a multiple of
// 4 bytes) is part of this section, not written before it.
let mut extra_buf = Vec::new();
// Layer mask data section: 4-byte length + data.
// PSD format: when mask_len is 28, the layout is:
// 16 bytes rectangle (top/left/bottom/right)
// 1 byte default color
// 1 byte flags (bit 4 = 0x10 means "real user mask parameters" follow)
// 1 byte real flags
// 1 byte real user mask background
// 8 bytes padding (zeros)
if let Some(mb) = layer.mask_bounds {
extra_buf.extend_from_slice(&28u32.to_be_bytes());
extra_buf.extend_from_slice(&mb[0].to_be_bytes()); // top
extra_buf.extend_from_slice(&mb[1].to_be_bytes()); // left
extra_buf.extend_from_slice(&mb[2].to_be_bytes()); // bottom
extra_buf.extend_from_slice(&mb[3].to_be_bytes()); // right
extra_buf.push(layer.mask_default_color);
extra_buf.push(0x10); // flags: bit 4 = real user mask parameters present
extra_buf.push(0x03); // real flags / mask parameters flags
extra_buf.push(0xff); // user mask density (100% dense)
extra_buf.extend_from_slice(&[0u8; 8]); // user mask feather (0.0)
} else if layer.adjustment_raw.is_some() {
// Write a default empty mask for adjustment layers to satisfy Photoshop requirements
extra_buf.extend_from_slice(&28u32.to_be_bytes());
extra_buf.extend_from_slice(&[0u8; 16]); // bounds: 0, 0, 0, 0
extra_buf.push(0xff); // default color: white
extra_buf.push(0x10); // flags: parameters present
extra_buf.push(0x03); // parameters flags
extra_buf.push(0xff); // user mask density (100% dense)
extra_buf.extend_from_slice(&[0u8; 8]); // user mask feather (0.0)
} else {
extra_buf.extend_from_slice(&0u32.to_be_bytes());
}
// Layer blending ranges: 4-byte length + data. Photoshop stores 40 bytes:
// composite gray range, underlying gray range, then 3 pairs of composite
// channel ranges / underlying channel ranges.
extra_buf.extend_from_slice(&40u32.to_be_bytes());
// Composite gray: black=0 white=65535, mix=0
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
// Underlying gray: same defaults
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
// Per-channel ranges (3 channels): same defaults
for _ in 0..3 {
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
extra_buf.extend_from_slice(&0u16.to_be_bytes());
extra_buf.extend_from_slice(&65535u16.to_be_bytes());
}
// Layer name: Pascal string padded to a multiple of 4 bytes.
// The length byte + name must together consume a multiple of 4 bytes.
let name_bytes = layer.name.as_bytes();
let name_len = name_bytes.len();
extra_buf.push(name_len as u8);
extra_buf.extend_from_slice(name_bytes);
let pascal_total = 1 + name_len;
let name_padding = (4 - (pascal_total % 4)) % 4;
extra_buf.resize(extra_buf.len() + name_padding, 0);
// Adjustment block passthrough (`curv`, `grdm`, `hue2`) before luni/lfx2.
if let Some((key, data)) = &layer.adjustment_raw {
write_8bim_block(&mut extra_buf, key, data);
}
// Effects are only emitted when present.
if !layer.effects.is_empty() {
let lfx2_data = write_lfx2_block(layer);
write_8bim_block(&mut extra_buf, b"lfx2", &lfx2_data);
// iOpa: sheet opacity (4 bytes: ff 00 00 00)
write_8bim_block(&mut extra_buf, b"iOpa", &[0xff, 0x00, 0x00, 0x00]);
// brst: blending ranges flag (empty)
write_8bim_block(&mut extra_buf, b"brst", &[]);
}
// luni: Unicode layer name (always present for Photoshop compatibility)
write_luni_block(&mut extra_buf, &layer.name);
// lyid: Layer ID (4 bytes, unique per layer)
let layer_id = (layer_index as u32).wrapping_add(1);
write_8bim_block(&mut extra_buf, b"lyid", &layer_id.to_be_bytes());
// lsct: Section type
// PSD spec: 0=other, 1=open folder, 2=closed folder, 3=bounding section divider.
// hcie-psd parser treats types 1 & 2 as "open group" (push), type 3 as "close group" (pop).
// Original PSD uses type=2 for group open markers and type=3 for section dividers.
if is_section_divider {
// Section divider (end of group): type=3, only 4 bytes (no blend key).
// Photoshop writes type=3 with no blend key for section dividers.
let mut lsct_data = Vec::new();
lsct_data.extend_from_slice(&3u32.to_be_bytes());
write_8bim_block(&mut extra_buf, b"lsct", &lsct_data);
} else if is_group {
// Group open marker: type=2 + "8BIM" + blend key = 12 bytes.
// The lsct block carries the real blend mode (e.g. PassThrough).
// The layer record blend key is always "norm" for group markers.
//
// In HCIE's model, the PassThrough blend mode is stored on the
// section divider layer, not the group open marker. Search
// backward for the matching section divider to retrieve the
// real blend mode.
let group_blend = find_section_divider_blend(layers, layer_index)
.unwrap_or(layer.blend_mode);
let mut lsct_data = Vec::new();
lsct_data.extend_from_slice(&2u32.to_be_bytes());
lsct_data.extend_from_slice(b"8BIM");
lsct_data.extend_from_slice(blend_mode_key(group_blend));
write_8bim_block(&mut extra_buf, b"lsct", &lsct_data);
}
// clbl / infx / knko / lspf / lclr: standard empty tagged blocks.
write_8bim_block(&mut extra_buf, b"clbl", &[0u8; 4]);
write_8bim_block(&mut extra_buf, b"infx", &[0u8; 4]);
write_8bim_block(&mut extra_buf, b"knko", &[0u8; 4]);
write_8bim_block(&mut extra_buf, b"lspf", &[0u8; 4]);
write_8bim_block(&mut extra_buf, b"lclr", &[0u8; 8]);
// shmd: Photopea sheet metadata (always present)
let shmd_data = write_shmd_block(layer);
write_8bim_block(&mut extra_buf, b"shmd", &shmd_data);
// fxrp: Reference point (16 bytes: 2 × f64 zeros)
write_8bim_block(&mut extra_buf, b"fxrp", &[0u8; 16]);
// lyvr: Layer version (4 bytes: 1)
write_8bim_block(&mut extra_buf, b"lyvr", &1u32.to_be_bytes());
layer_records_buf.extend_from_slice(&(extra_buf.len() as u32).to_be_bytes());
layer_records_buf.extend_from_slice(&extra_buf);
all_ch_bufs.push(ch_bufs);
}
let mut layer_info_section = layer_records_buf;
for ch_bufs in all_ch_bufs.into_iter() {
for ch_data in ch_bufs {
layer_info_section.extend_from_slice(&1u16.to_be_bytes());
layer_info_section.extend_from_slice(&ch_data);
}
}
if layer_info_section.len() % 2 != 0 {
layer_info_section.push(0);
}
buf.extend_from_slice(&layer_info_section);
buf
}
fn write_merged_image_data(buf: &mut Vec<u8>, merged: &[u8], w: u32, h: u32) {
// 1 = RLE compression
buf.extend_from_slice(&1u16.to_be_bytes());
let ch_offsets: [usize; 3] = [0, 1, 2];
let mut compressed_channels = Vec::with_capacity(3);
for &offset in &ch_offsets {
let mut row_lengths = Vec::with_capacity(h as usize);
let mut compressed_rows = Vec::with_capacity(h as usize);
for y in 0..h {
let mut row = Vec::with_capacity(w as usize);
for x in 0..w {
let idx = (y * w + x) as usize * 4;
if idx + offset < merged.len() {
row.push(merged[idx + offset]);
} else {
row.push(0);
}
}
let compressed = packbits_compress_row(&row);
row_lengths.push(compressed.len() as u16);
compressed_rows.push(compressed);
}
compressed_channels.push((row_lengths, compressed_rows));
}
// Write all row lengths first
for (row_lengths, _) in &compressed_channels {
for &len in row_lengths {
buf.extend_from_slice(&len.to_be_bytes());
}
}
// Write all compressed data
for (_, compressed_rows) in compressed_channels {
for row in compressed_rows {
buf.extend_from_slice(&row);
}
}
}
/// Find the bounding box of non-transparent pixels in a layer.
///
/// Returns `(top, left, width, height)` of the content region.
/// If the layer is fully transparent, returns `(0, 0, 1, 1)` with zero-filled data.
fn find_content_bounds(layer: &Layer, lw: u32, lh: u32) -> (u32, u32, u32, u32) {
let mut min_x = lw;
let mut min_y = lh;
let mut max_x = 0u32;
let mut max_y = 0u32;
for y in 0..lh {
for x in 0..lw {
let idx = (y * layer.width + x) as usize * 4;
if idx + 3 < layer.pixels.len() && layer.pixels[idx + 3] > 0 {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
}
if min_x > max_x || min_y > max_y {
// Fully transparent: use 1×1 at origin (Photoshop convention)
(0, 0, 1, 1)
} else {
(min_y, min_x, max_x - min_x + 1, max_y - min_y + 1)
}
}
/// Find the blend mode of the section divider that corresponds to a group
/// open marker at `group_index`.
///
/// In HCIE's layer model, the section divider (is_section_divider=true)
/// carries the group's real blend mode (e.g. PassThrough), while the group
/// open marker may have been imported with a default Normal blend.
///
/// The section divider is expected to appear *before* the group open marker
/// in the layers array (PSD bottom-to-top order). We search backward from
/// the group's position, looking for the nearest section divider whose id
/// forms a pair with this group's id. As a fallback, we match any section
/// divider with the same parent_id.
fn find_section_divider_blend(layers: &[Layer], group_index: usize) -> Option<BlendMode> {
let group_layer = &layers[group_index];
// Try to find the section divider by matching parent_id.
for i in (0..group_index).rev() {
let candidate = &layers[i];
if candidate.is_section_divider {
// Check if it belongs to this group: same parent_id is a good heuristic.
if candidate.parent_id == group_layer.parent_id {
return Some(candidate.blend_mode);
}
}
}
// Forward search as fallback (in case the layer list is reversed)
for i in (group_index + 1)..layers.len() {
let candidate = &layers[i];
if candidate.is_section_divider {
if candidate.parent_id == group_layer.parent_id {
return Some(candidate.blend_mode);
}
}
}
None
}
/// Compute Photoshop layer flags from layer type and state.
///
/// PSD layer flags bit layout:
/// bit 0 (0x01): transparency protected
/// bit 1 (0x02): visible (inverted: 1 = hidden)
/// bit 2 (0x04): obsolete
/// bit 3 (0x08): set if bits 4-7 are useful info
/// bit 4 (0x10): pixel data irrelevant to document appearance
/// bit 5 (0x20): pixel data irrelevant to composite
///
/// Photoshop sets bit 3 on all layers. Adjustment/group layers additionally
/// set bit 4 (0x10). Adjustment layers that have vector masks and no
/// raster content also set bit 5 (0x20).
fn layer_flags(layer: &Layer) -> u8 {
let mut flags = 0u8;
if !layer.visible {
flags |= 0x02; // bit 1 = hidden
}
// bit 3 is always set by Photoshop for all layers
flags |= 0x08;
if matches!(layer.layer_type, LayerType::Group) {
// Groups/section dividers: bits 3+4 = 0x18
flags |= 0x10;
} else if layer.adjustment_raw.is_some() {
// Adjustment layers: bits 3+4 = 0x18
flags |= 0x10;
// Adjustment layers with vector masks but no raster content: also bit 5
if layer.mask_bounds.is_some() && layer.pixels.iter().all(|&b| b == 0) {
flags |= 0x20;
}
}
flags
}
fn packbits_encode_channel(plane: &[u8], w: u32, h: u32) -> Vec<u8> {
let mut out = Vec::new();
let mut row_lengths: Vec<u16> = Vec::with_capacity(h as usize);
let mut compressed_rows: Vec<Vec<u8>> = Vec::with_capacity(h as usize);
for y in 0..h {
let row = &plane[(y * w) as usize..((y + 1) * w) as usize];
let compressed = packbits_compress_row(row);
row_lengths.push(compressed.len() as u16);
compressed_rows.push(compressed);
}
for &len in &row_lengths {
out.extend_from_slice(&len.to_be_bytes());
}
for row in compressed_rows {
out.extend_from_slice(&row);
}
out
}
fn packbits_compress_row(row: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let n = row.len();
let mut i = 0;
while i < n {
let mut run_len = 1;
while run_len < 128 && i + run_len < n && row[i + run_len] == row[i] {
run_len += 1;
}
if run_len > 1 {
out.push((1i16 - run_len as i16) as i8 as u8);
out.push(row[i]);
i += run_len;
} else {
let start = i;
let mut lit_len = 0;
while lit_len < 128 && i + lit_len < n {
let next_run = if i + lit_len + 1 < n && row[i + lit_len] == row[i + lit_len + 1] {
let mut r = 2;
while r < 128 && i + lit_len + r < n && row[i + lit_len + r] == row[i + lit_len] {
r += 1;
}
r
} else {
0
};
if next_run >= 3 { break; }
lit_len += 1;
}
if lit_len == 0 { lit_len = 1; }
out.push((lit_len - 1) as u8);
out.extend_from_slice(&row[start..start + lit_len]);
i += lit_len;
}
}
out
}
fn blend_mode_key(mode: BlendMode) -> &'static [u8; 4] {
match mode {
BlendMode::Normal => b"norm",
BlendMode::Dissolve => b"diss",
BlendMode::Darken => b"dark",
BlendMode::Multiply => b"mul ",
BlendMode::ColorBurn => b"idiv",
BlendMode::LinearBurn => b"lbrn",
BlendMode::DarkerColor => b"dkCl",
BlendMode::Lighten => b"lite",
BlendMode::Screen => b"scrn",
BlendMode::ColorDodge => b"div ",
BlendMode::LinearDodge => b"lddg",
BlendMode::LighterColor => b"lgCl",
BlendMode::Overlay => b"over",
BlendMode::SoftLight => b"sLit",
BlendMode::HardLight => b"hLit",
BlendMode::VividLight => b"vLit",
BlendMode::LinearLight => b"lLit",
BlendMode::PinLight => b"pLit",
BlendMode::HardMix => b"hMix",
BlendMode::Difference => b"diff",
BlendMode::Exclusion => b"smud",
BlendMode::Subtract => b"fsub",
BlendMode::Divide => b"fdiv",
BlendMode::Hue => b"hue ",
BlendMode::Saturation => b"sat ",
BlendMode::Color => b"colr",
BlendMode::Luminosity => b"lum ",
BlendMode::PassThrough => b"pass",
}
}
fn write_shmd_block(layer: &Layer) -> Vec<u8> {
let mut desc = Vec::new();
// Descriptor body (same format as lfx2)
w_u32(&mut desc, 16); // descriptor version
w_ustr_null(&mut desc); // name: U+0000
w_class_id(&mut desc, b"null"); // classID: null
w_u32(&mut desc, 2); // count: 2 (LyrI, layerSettings)
// Item 0: LyrI long 1
w_key4(&mut desc, b"LyrI");
desc.extend_from_slice(b"long");
desc.extend_from_slice(&1i32.to_be_bytes());
// Item 1: layerSettings VlLs
w_key_var(&mut desc, b"layerSettings");
desc.extend_from_slice(b"VlLs");
w_u32(&mut desc, 1); // count=1
// layerSettings[0]: Objc
let ls_count = if layer.effects.is_empty() { 5 } else { 6 };
w_objc_start(&mut desc);
w_objc_body_start(&mut desc, b"null", ls_count);
// a) compList: VlLs[1] { long 0 }
w_key_var(&mut desc, b"compList");
desc.extend_from_slice(b"VlLs");
w_u32(&mut desc, 1);
desc.extend_from_slice(b"long");
desc.extend_from_slice(&0i32.to_be_bytes());
// b) enab: bool true
w_key4(&mut desc, b"enab");
w_bool(&mut desc, true);
// c) Ofst: Objc { Hrzn long 0, Vrtc long 0 }
w_key4(&mut desc, b"Ofst");
write_shmd_point_objc(&mut desc);
// d) FXRefPoint: Objc { Hrzn long 0, Vrtc long 0 }
w_key_var(&mut desc, b"FXRefPoint");
write_shmd_point_objc(&mut desc);
// e) blendOptions: Objc { Md enum BlnM, Opct UntF opacity, fillOpacity UntF 100 }
w_key_var(&mut desc, b"blendOptions");
w_objc_start(&mut desc);
w_objc_body_start(&mut desc, b"null", 3);
w_key4(&mut desc, b"Md ");
w_enum(&mut desc, b"BlnM", &blend_mode_to_lfx2_key(protocol_blend_to_blend(layer.blend_mode)));
w_key4(&mut desc, b"Opct");
w_untf(&mut desc, b"#Prc", (layer.opacity * 100.0) as f64);
w_key_var(&mut desc, b"fillOpacity");
w_untf(&mut desc, b"#Prc", 100.0);
// f) Lefx: only when effects are present
if !layer.effects.is_empty() {
w_key4(&mut desc, b"Lefx");
w_objc_start(&mut desc);
let n_fx = (2 + layer.effects.len()) as u32;
w_objc_body_start(&mut desc, b"null", n_fx);
w_key4(&mut desc, b"Scl ");
w_untf(&mut desc, b"#Prc", 100.0);
w_key_var(&mut desc, b"masterFXSwitch");
w_bool(&mut desc, true);
for effect in &layer.effects {
write_fx_descriptor(&mut desc, &hcie_fx::protocol_to_hcie_fx_effect(effect));
}
}
// Build shmd container
let mut buf = Vec::new();
w_u32(&mut buf, 1); // version
buf.extend_from_slice(b"8BIMcmls"); // signature
w_u32(&mut buf, 0); // cmls sub-size
w_u32(&mut buf, desc.len() as u32); // descriptor size
buf.extend_from_slice(&desc);
buf
}
fn write_shmd_point_objc(buf: &mut Vec<u8>) {
w_objc_start(buf);
w_objc_body_start(buf, b"null", 2);
w_key4(buf, b"Hrzn");
buf.extend_from_slice(b"long");
buf.extend_from_slice(&0i32.to_be_bytes());
w_key4(buf, b"Vrtc");
buf.extend_from_slice(b"long");
buf.extend_from_slice(&0i32.to_be_bytes());
}
fn write_lfx2_block(layer: &Layer) -> Vec<u8> {
let mut buf = Vec::new();
w_u32(&mut buf, 0);
w_u32(&mut buf, 16);
w_ustr_null(&mut buf);
w_class_id(&mut buf, b"null");
let n = (2 + layer.effects.len()) as u32;
w_u32(&mut buf, n);
w_key4(&mut buf, b"Scl ");
w_untf(&mut buf, b"#Prc", 100.0);
w_key_var(&mut buf, b"masterFXSwitch");
w_bool(&mut buf, true);
for effect in &layer.effects {
write_fx_descriptor(&mut buf, &hcie_fx::protocol_to_hcie_fx_effect(effect));
}
buf
}
fn protocol_blend_to_blend(mode: hcie_protocol::BlendMode) -> hcie_blend::BlendMode {
match mode {
hcie_protocol::BlendMode::Normal => hcie_blend::BlendMode::Normal,
hcie_protocol::BlendMode::Dissolve => hcie_blend::BlendMode::Dissolve,
hcie_protocol::BlendMode::Darken => hcie_blend::BlendMode::Darken,
hcie_protocol::BlendMode::Multiply => hcie_blend::BlendMode::Multiply,
hcie_protocol::BlendMode::ColorBurn => hcie_blend::BlendMode::ColorBurn,
hcie_protocol::BlendMode::LinearBurn => hcie_blend::BlendMode::LinearBurn,
hcie_protocol::BlendMode::DarkerColor => hcie_blend::BlendMode::DarkerColor,
hcie_protocol::BlendMode::Lighten => hcie_blend::BlendMode::Lighten,
hcie_protocol::BlendMode::Screen => hcie_blend::BlendMode::Screen,
hcie_protocol::BlendMode::ColorDodge => hcie_blend::BlendMode::ColorDodge,
hcie_protocol::BlendMode::LinearDodge => hcie_blend::BlendMode::LinearDodge,
hcie_protocol::BlendMode::LighterColor => hcie_blend::BlendMode::LighterColor,
hcie_protocol::BlendMode::Overlay => hcie_blend::BlendMode::Overlay,
hcie_protocol::BlendMode::SoftLight => hcie_blend::BlendMode::SoftLight,
hcie_protocol::BlendMode::HardLight => hcie_blend::BlendMode::HardLight,
hcie_protocol::BlendMode::VividLight => hcie_blend::BlendMode::VividLight,
hcie_protocol::BlendMode::LinearLight => hcie_blend::BlendMode::LinearLight,
hcie_protocol::BlendMode::PinLight => hcie_blend::BlendMode::PinLight,
hcie_protocol::BlendMode::HardMix => hcie_blend::BlendMode::HardMix,
hcie_protocol::BlendMode::Difference => hcie_blend::BlendMode::Difference,
hcie_protocol::BlendMode::Exclusion => hcie_blend::BlendMode::Exclusion,
hcie_protocol::BlendMode::Subtract => hcie_blend::BlendMode::Subtract,
hcie_protocol::BlendMode::Divide => hcie_blend::BlendMode::Divide,
hcie_protocol::BlendMode::Hue => hcie_blend::BlendMode::Hue,
hcie_protocol::BlendMode::Saturation => hcie_blend::BlendMode::Saturation,
hcie_protocol::BlendMode::Color => hcie_blend::BlendMode::Color,
hcie_protocol::BlendMode::Luminosity => hcie_blend::BlendMode::Luminosity,
hcie_protocol::BlendMode::PassThrough => hcie_blend::BlendMode::PassThrough,
}
}