init
This commit is contained in:
@@ -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" }
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user