2026-07-09 02:59:53 +03:00
|
|
|
use std::path::Path;
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
|
|
use log::{warn, error};
|
|
|
|
|
|
|
|
|
|
use crate::psd_reader::PsdReader;
|
|
|
|
|
use crate::Psd;
|
|
|
|
|
|
|
|
|
|
fn packbits_decompress(src: &[u8], dst: &mut [u8]) -> Result<(), String> {
|
|
|
|
|
let mut src_idx = 0;
|
|
|
|
|
let mut dst_idx = 0;
|
|
|
|
|
while src_idx < src.len() && dst_idx < dst.len() {
|
|
|
|
|
let header = src[src_idx] as i8;
|
|
|
|
|
src_idx += 1;
|
|
|
|
|
if header >= 0 {
|
|
|
|
|
let len = (header as usize) + 1;
|
|
|
|
|
if src_idx + len > src.len() || dst_idx + len > dst.len() {
|
|
|
|
|
return Err("PackBits: Out of bounds literal copy".to_string());
|
|
|
|
|
}
|
|
|
|
|
dst[dst_idx..dst_idx + len].copy_from_slice(&src[src_idx..src_idx + len]);
|
|
|
|
|
src_idx += len;
|
|
|
|
|
dst_idx += len;
|
|
|
|
|
} else if header != -128 {
|
|
|
|
|
let len = (-header as usize) + 1;
|
|
|
|
|
if src_idx >= src.len() || dst_idx + len > dst.len() {
|
|
|
|
|
return Err("PackBits: Out of bounds repeat copy".to_string());
|
|
|
|
|
}
|
|
|
|
|
let val = src[src_idx];
|
|
|
|
|
src_idx += 1;
|
|
|
|
|
for _ in 0..len {
|
|
|
|
|
dst[dst_idx] = val;
|
|
|
|
|
dst_idx += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn rle_decompress_rows(src: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
|
|
|
|
|
let mut dst = vec![0u8; (width * height) as usize];
|
|
|
|
|
if src.is_empty() { return Ok(dst); }
|
|
|
|
|
|
|
|
|
|
let mut r = PsdReader::new(src);
|
|
|
|
|
let mut scanline_lens = Vec::with_capacity(height as usize);
|
|
|
|
|
for _ in 0..height {
|
|
|
|
|
scanline_lens.push(r.read_u16()? as usize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let compressed_start = r.pos();
|
|
|
|
|
let mut src_offset = compressed_start;
|
|
|
|
|
|
|
|
|
|
for row in 0..height {
|
|
|
|
|
let row_len = scanline_lens[row as usize];
|
|
|
|
|
if src_offset + row_len > src.len() {
|
|
|
|
|
return Err("PackBits: Row length exceeds source".to_string());
|
|
|
|
|
}
|
|
|
|
|
let row_data = &src[src_offset..src_offset + row_len];
|
|
|
|
|
src_offset += row_len;
|
|
|
|
|
|
|
|
|
|
let dst_offset = (row * width) as usize;
|
|
|
|
|
let dst_row = &mut dst[dst_offset..dst_offset + width as usize];
|
|
|
|
|
packbits_decompress(row_data, dst_row)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(dst)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cubic_spline_lut(points: &[(u16, u16)]) -> Vec<u8> {
|
|
|
|
|
let mut lut = vec![0u8; 256];
|
|
|
|
|
if points.is_empty() {
|
|
|
|
|
for i in 0..256 { lut[i] = i as u8; }
|
|
|
|
|
return lut;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut pts: Vec<(f32, f32)> = points.iter()
|
|
|
|
|
.map(|p| (p.1 as f32, p.0 as f32))
|
|
|
|
|
.collect();
|
|
|
|
|
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
|
|
|
|
pts.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-4);
|
|
|
|
|
|
|
|
|
|
if pts.len() < 2 {
|
|
|
|
|
let val = pts.first().map(|p| p.1).unwrap_or(0.0) as u8;
|
|
|
|
|
return vec![val; 256];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let n = pts.len();
|
|
|
|
|
let mut h = vec![0.0; n - 1];
|
|
|
|
|
for i in 0..n - 1 {
|
|
|
|
|
h[i] = pts[i + 1].0 - pts[i].0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut a = vec![0.0; n];
|
|
|
|
|
let mut b = vec![0.0; n];
|
|
|
|
|
let mut c = vec![0.0; n];
|
|
|
|
|
let mut d = vec![0.0; n];
|
|
|
|
|
|
|
|
|
|
b[0] = 1.0;
|
|
|
|
|
for i in 1..n - 1 {
|
|
|
|
|
a[i] = h[i - 1] / 6.0;
|
|
|
|
|
b[i] = (h[i - 1] + h[i]) / 3.0;
|
|
|
|
|
c[i] = h[i] / 6.0;
|
|
|
|
|
d[i] = (pts[i + 1].1 - pts[i].1) / h[i] - (pts[i].1 - pts[i - 1].1) / h[i - 1];
|
|
|
|
|
}
|
|
|
|
|
b[n - 1] = 1.0;
|
|
|
|
|
|
|
|
|
|
let mut c_prime = vec![0.0; n];
|
|
|
|
|
let mut d_prime = vec![0.0; n];
|
|
|
|
|
c_prime[0] = c[0] / b[0];
|
|
|
|
|
d_prime[0] = d[0] / b[0];
|
|
|
|
|
for i in 1..n {
|
|
|
|
|
let denom = b[i] - a[i] * c_prime[i - 1];
|
|
|
|
|
if denom.abs() > 1e-6 {
|
|
|
|
|
c_prime[i] = c[i] / denom;
|
|
|
|
|
d_prime[i] = (d[i] - a[i] * d_prime[i - 1]) / denom;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut m = vec![0.0; n];
|
|
|
|
|
m[n - 1] = d_prime[n - 1];
|
|
|
|
|
for i in (0..n - 1).rev() {
|
|
|
|
|
m[i] = d_prime[i] - c_prime[i] * m[i + 1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i in 0..256 {
|
|
|
|
|
let x = i as f32;
|
|
|
|
|
if x <= pts[0].0 {
|
|
|
|
|
lut[i] = pts[0].1.clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
} else if x >= pts[n - 1].0 {
|
|
|
|
|
lut[i] = pts[n - 1].1.clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
} else {
|
|
|
|
|
for j in 0..n - 1 {
|
|
|
|
|
let x0 = pts[j].0;
|
|
|
|
|
let x1 = pts[j + 1].0;
|
|
|
|
|
if x >= x0 && x <= x1 {
|
|
|
|
|
let hj = h[j];
|
|
|
|
|
let mj = m[j];
|
|
|
|
|
let mj1 = m[j + 1];
|
|
|
|
|
let y0 = pts[j].1;
|
|
|
|
|
let y1 = pts[j + 1].1;
|
|
|
|
|
|
|
|
|
|
let term1 = mj * (x1 - x).powi(3) / (6.0 * hj);
|
|
|
|
|
let term2 = mj1 * (x - x0).powi(3) / (6.0 * hj);
|
|
|
|
|
let term3 = (y0 / hj - hj * mj / 6.0) * (x1 - x);
|
|
|
|
|
let term4 = (y1 / hj - hj * mj1 / 6.0) * (x - x0);
|
|
|
|
|
|
|
|
|
|
let y = term1 + term2 + term3 + term4;
|
|
|
|
|
lut[i] = y.clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lut
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct MaskInfo {
|
|
|
|
|
top: i32,
|
|
|
|
|
left: i32,
|
|
|
|
|
bottom: i32,
|
|
|
|
|
right: i32,
|
|
|
|
|
default_color: u8,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ChannelInfo {
|
|
|
|
|
id: i16,
|
|
|
|
|
len: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct LayerRecord {
|
|
|
|
|
name: String,
|
|
|
|
|
channels: Vec<ChannelInfo>,
|
|
|
|
|
mask_info: Option<MaskInfo>,
|
|
|
|
|
adjustment: Option<hcie_blend::Adjustment>,
|
|
|
|
|
adjustment_raw: Option<([u8; 4], Vec<u8>)>,
|
|
|
|
|
is_group: bool,
|
|
|
|
|
is_section_divider: bool,
|
|
|
|
|
section_divider_blend: Option<String>,
|
|
|
|
|
curve_points: Vec<(u16, u16)>,
|
|
|
|
|
effects: Vec<hcie_fx::LayerEffect>,
|
|
|
|
|
fill_opacity: f32,
|
|
|
|
|
is_text: bool,
|
|
|
|
|
text_data: Option<hcie_protocol::LayerData>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ParsedLayerData {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub is_group: bool,
|
|
|
|
|
pub is_section_divider: bool,
|
|
|
|
|
pub section_divider_blend: Option<String>,
|
|
|
|
|
pub adjustment: Option<hcie_blend::Adjustment>,
|
|
|
|
|
pub adjustment_raw: Option<([u8; 4], Vec<u8>)>,
|
|
|
|
|
pub mask_pixels: Option<Vec<u8>>,
|
|
|
|
|
pub mask_bounds: Option<[i32; 4]>,
|
|
|
|
|
pub mask_default_color: u8,
|
|
|
|
|
pub curve_points: Vec<(u16, u16)>,
|
|
|
|
|
pub effects: Vec<hcie_fx::LayerEffect>,
|
|
|
|
|
pub fill_opacity: f32,
|
|
|
|
|
pub is_text: bool,
|
|
|
|
|
pub text_data: Option<hcie_protocol::LayerData>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_gradient_map_luts(
|
|
|
|
|
color_stops: &[(u32, u32, [u8; 3])],
|
|
|
|
|
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
|
|
|
|
let mut lut_r = vec![0u8; 256];
|
|
|
|
|
let mut lut_g = vec![0u8; 256];
|
|
|
|
|
let mut lut_b = vec![0u8; 256];
|
|
|
|
|
|
|
|
|
|
if color_stops.is_empty() {
|
|
|
|
|
return (lut_r, lut_g, lut_b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut stops = color_stops.to_vec();
|
|
|
|
|
stops.sort_by_key(|s| s.0);
|
|
|
|
|
|
|
|
|
|
for i in 0..256 {
|
|
|
|
|
let x = (i as f32 * 16.0627) as u32;
|
|
|
|
|
|
|
|
|
|
if x <= stops[0].0 {
|
|
|
|
|
lut_r[i] = stops[0].2[0];
|
|
|
|
|
lut_g[i] = stops[0].2[1];
|
|
|
|
|
lut_b[i] = stops[0].2[2];
|
|
|
|
|
} else if x >= stops[stops.len() - 1].0 {
|
|
|
|
|
let last = stops.len() - 1;
|
|
|
|
|
lut_r[i] = stops[last].2[0];
|
|
|
|
|
lut_g[i] = stops[last].2[1];
|
|
|
|
|
lut_b[i] = stops[last].2[2];
|
|
|
|
|
} else {
|
|
|
|
|
for j in 0..stops.len() - 1 {
|
|
|
|
|
let x0 = stops[j].0;
|
|
|
|
|
let x1 = stops[j + 1].0;
|
|
|
|
|
if x >= x0 && x <= x1 {
|
|
|
|
|
let t = (x - x0) as f32 / (x1 - x0) as f32;
|
|
|
|
|
let midpoint = (stops[j].1 as f32 / 100.0).clamp(0.01, 0.99);
|
|
|
|
|
let t_prime = if (midpoint - 0.5).abs() < 1e-4 {
|
|
|
|
|
t
|
|
|
|
|
} else {
|
|
|
|
|
t.powf(0.5_f32.ln() / midpoint.ln())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let c0 = stops[j].2;
|
|
|
|
|
let c1 = stops[j + 1].2;
|
|
|
|
|
|
|
|
|
|
lut_r[i] = (c0[0] as f32 + t_prime * (c1[0] as f32 - c0[0] as f32)).clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
lut_g[i] = (c0[1] as f32 + t_prime * (c1[1] as f32 - c0[1] as f32)).clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
lut_b[i] = (c0[2] as f32 + t_prime * (c1[2] as f32 - c0[2] as f32)).clamp(0.0, 255.0).round() as u8;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(lut_r, lut_g, lut_b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ParsedPsdData {
|
|
|
|
|
pub layers: Vec<ParsedLayerData>,
|
|
|
|
|
pub image_resources_raw: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn parse_psd_sequential(bytes: &[u8]) -> Result<Vec<ParsedLayerData>, String> {
|
|
|
|
|
parse_psd_sequential_full(bytes).map(|parsed| parsed.layers)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn parse_psd_sequential_full(bytes: &[u8]) -> Result<ParsedPsdData, String> {
|
|
|
|
|
let mut r = PsdReader::new(bytes);
|
|
|
|
|
|
|
|
|
|
r.read(26)?;
|
|
|
|
|
let color_mode_len = r.read_u32()? as usize;
|
|
|
|
|
r.read(color_mode_len)?;
|
|
|
|
|
let img_res_start = r.pos();
|
|
|
|
|
let img_res_len = r.read_u32()? as usize;
|
|
|
|
|
let image_resources_raw = if img_res_len > 0 {
|
|
|
|
|
bytes[img_res_start + 4..img_res_start + 4 + img_res_len].to_vec()
|
|
|
|
|
} else {
|
|
|
|
|
Vec::new()
|
|
|
|
|
};
|
|
|
|
|
r.read(img_res_len)?;
|
|
|
|
|
let _layer_mask_len_val = r.read_u32()? as usize;
|
|
|
|
|
let _layer_info_len = r.read_u32()? as usize;
|
|
|
|
|
let _layer_info_start = r.pos();
|
|
|
|
|
|
|
|
|
|
let layer_count_raw = r.read_i16()?;
|
|
|
|
|
let layer_count = layer_count_raw.abs() as usize;
|
|
|
|
|
let mut layers_meta = Vec::with_capacity(layer_count);
|
|
|
|
|
|
|
|
|
|
for _idx in 0..layer_count {
|
|
|
|
|
let _top = r.read_i32()?;
|
|
|
|
|
let _left = r.read_i32()?;
|
|
|
|
|
let _bottom = r.read_i32()?;
|
|
|
|
|
let _right = r.read_i32()?;
|
|
|
|
|
let channel_count = r.read_u16()? as usize;
|
|
|
|
|
|
|
|
|
|
let mut channels = Vec::new();
|
|
|
|
|
for _ in 0..channel_count {
|
|
|
|
|
let id = r.read_i16()?;
|
|
|
|
|
let len = r.read_u32()?;
|
|
|
|
|
channels.push(ChannelInfo { id, len });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.read(4)?;
|
|
|
|
|
r.read(4)?;
|
|
|
|
|
let _opacity = r.read_u8()?;
|
|
|
|
|
let _clipping = r.read_u8()?;
|
|
|
|
|
let _flags = r.read_u8()?;
|
|
|
|
|
r.read(1)?;
|
|
|
|
|
|
|
|
|
|
let extra_data_len = r.read_u32()? as usize;
|
|
|
|
|
let extra_start = r.pos();
|
|
|
|
|
|
|
|
|
|
let layer_mask_len = r.read_u32()? as usize;
|
|
|
|
|
let mask_info = if layer_mask_len >= 18 {
|
|
|
|
|
let top = r.read_i32()?;
|
|
|
|
|
let left = r.read_i32()?;
|
|
|
|
|
let bottom = r.read_i32()?;
|
|
|
|
|
let right = r.read_i32()?;
|
|
|
|
|
let default_color = r.read_u8()?;
|
|
|
|
|
let _flags = r.read_u8()?;
|
|
|
|
|
r.read(layer_mask_len - 18)?;
|
|
|
|
|
Some(MaskInfo { top, left, bottom, right, default_color })
|
|
|
|
|
} else {
|
|
|
|
|
r.read(layer_mask_len)?;
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let layer_blending_ranges_len = r.read_u32()? as usize;
|
|
|
|
|
r.read(layer_blending_ranges_len)?;
|
|
|
|
|
|
|
|
|
|
let name_len = r.read_u8()? as usize;
|
|
|
|
|
let name_bytes = r.read(name_len)?;
|
|
|
|
|
let name = String::from_utf8_lossy(name_bytes).to_string();
|
|
|
|
|
|
|
|
|
|
let bytes_mod_4 = (name_len + 1) % 4;
|
|
|
|
|
let padding = (4 - bytes_mod_4) % 4;
|
|
|
|
|
r.read(padding)?;
|
|
|
|
|
|
|
|
|
|
let mut is_group = false;
|
|
|
|
|
let mut is_section_divider = false;
|
|
|
|
|
let mut section_divider_blend: Option<String> = None;
|
|
|
|
|
let mut adjustment = None;
|
|
|
|
|
let mut adjustment_raw: Option<([u8; 4], Vec<u8>)> = None;
|
|
|
|
|
let mut curve_points: Vec<(u16, u16)> = vec![];
|
|
|
|
|
let mut effects: Vec<hcie_fx::LayerEffect> = vec![];
|
|
|
|
|
let mut fill_opacity = 1.0f32;
|
|
|
|
|
let mut is_text = false;
|
|
|
|
|
let mut text_data = None;
|
|
|
|
|
|
|
|
|
|
while r.pos() < extra_start + extra_data_len {
|
|
|
|
|
let sig = r.read(4)?;
|
|
|
|
|
if sig != b"8BIM" && sig != b"8B64" {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let key_bytes = r.read(4)?;
|
|
|
|
|
let key = String::from_utf8_lossy(key_bytes).to_string();
|
|
|
|
|
let block_len = r.read_u32()? as usize;
|
|
|
|
|
let block_start = r.pos();
|
|
|
|
|
|
|
|
|
|
if key == "lsct" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
if block_data.len() >= 4 {
|
|
|
|
|
let section_type = u32::from_be_bytes([block_data[0], block_data[1], block_data[2], block_data[3]]);
|
|
|
|
|
if section_type == 1 || section_type == 3 {
|
|
|
|
|
// Type 3 is bounding section; in HCIE it behaves as a group open marker.
|
|
|
|
|
is_group = true;
|
|
|
|
|
if section_type == 3 {
|
|
|
|
|
is_section_divider = false;
|
|
|
|
|
}
|
|
|
|
|
} else if section_type == 2 || section_type == 4 {
|
|
|
|
|
is_group = false;
|
|
|
|
|
is_section_divider = true;
|
|
|
|
|
if block_data.len() >= 16 {
|
|
|
|
|
// lsct layout: 4 bytes type + "8BIM" + 4-byte blend key
|
|
|
|
|
let bm = String::from_utf8_lossy(
|
|
|
|
|
&block_data[8..12]
|
|
|
|
|
).trim().to_string();
|
|
|
|
|
section_divider_blend = Some(bm);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if key == "curv" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
adjustment_raw = Some((*b"curv", block_data.to_vec()));
|
|
|
|
|
|
|
|
|
|
let crv_sig = b"Crv ";
|
|
|
|
|
let mut crv_offset = None;
|
|
|
|
|
for i in 0..block_data.len().saturating_sub(3) {
|
|
|
|
|
if &block_data[i..i+4] == crv_sig {
|
|
|
|
|
crv_offset = Some(i + 4);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(off) = crv_offset {
|
|
|
|
|
let src = &block_data[off..];
|
|
|
|
|
if src.len() >= 10 {
|
|
|
|
|
let version = u16::from_be_bytes([src[4], src[5]]);
|
|
|
|
|
let point_count = u16::from_be_bytes([src[8], src[9]]) as usize;
|
|
|
|
|
|
|
|
|
|
if version == 1 && src.len() >= 10 + point_count * 4 {
|
|
|
|
|
let mut points = Vec::with_capacity(point_count);
|
|
|
|
|
let mut poff = 10;
|
|
|
|
|
for _ in 0..point_count {
|
|
|
|
|
let out_val = u16::from_be_bytes([src[poff], src[poff + 1]]);
|
|
|
|
|
let in_val = u16::from_be_bytes([src[poff + 2], src[poff + 3]]);
|
|
|
|
|
points.push((out_val, in_val));
|
|
|
|
|
poff += 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lut_m = cubic_spline_lut(&points);
|
|
|
|
|
let lut_r = lut_m.clone();
|
|
|
|
|
let lut_g = lut_m.clone();
|
|
|
|
|
let lut_b = lut_m.clone();
|
|
|
|
|
curve_points = points;
|
|
|
|
|
adjustment = Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if block_data.len() >= 8 {
|
|
|
|
|
let version = u16::from_le_bytes([block_data[2], block_data[3]]);
|
|
|
|
|
let bitmap = u16::from_le_bytes([block_data[6], block_data[7]]);
|
|
|
|
|
if version == 1 {
|
|
|
|
|
let mut sub_r = PsdReader::new(&block_data[8..]);
|
|
|
|
|
let mut curves = Vec::new();
|
|
|
|
|
for bit in 0..16 {
|
|
|
|
|
if (bitmap & (1 << bit)) != 0 {
|
|
|
|
|
if let Ok(point_count) = sub_r.read_u16_le() {
|
|
|
|
|
let mut points = Vec::with_capacity(point_count as usize);
|
|
|
|
|
for _ in 0..point_count {
|
|
|
|
|
if let (Ok(out_val), Ok(in_val)) = (sub_r.read_u16_le(), sub_r.read_u16_le()) {
|
|
|
|
|
points.push((out_val, in_val));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
curves.push(points);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !curves.is_empty() {
|
|
|
|
|
let master_pts = &curves[0];
|
|
|
|
|
let lut_m = cubic_spline_lut(master_pts);
|
|
|
|
|
|
|
|
|
|
let mut lut_r = lut_m.clone();
|
|
|
|
|
let mut lut_g = lut_m.clone();
|
|
|
|
|
let mut lut_b = lut_m.clone();
|
|
|
|
|
|
|
|
|
|
if curves.len() >= 4 {
|
|
|
|
|
let c_r = cubic_spline_lut(&curves[1]);
|
|
|
|
|
let c_g = cubic_spline_lut(&curves[2]);
|
|
|
|
|
let c_b = cubic_spline_lut(&curves[3]);
|
|
|
|
|
for idx in 0..256 {
|
|
|
|
|
lut_r[idx] = c_r[lut_r[idx] as usize];
|
|
|
|
|
lut_g[idx] = c_g[lut_g[idx] as usize];
|
|
|
|
|
lut_b[idx] = c_b[lut_b[idx] as usize];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
adjustment = Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if key == "grdm" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
adjustment_raw = Some((*b"grdm", block_data.to_vec()));
|
|
|
|
|
if block_data.len() >= 26 {
|
|
|
|
|
let mut sub_r = PsdReader::new(&block_data);
|
|
|
|
|
let _version = sub_r.read_u16()?;
|
|
|
|
|
let _reversed = sub_r.read_u8()?;
|
|
|
|
|
let _dithered = sub_r.read_u8()?;
|
|
|
|
|
|
|
|
|
|
let peek = sub_r.read(4)?;
|
|
|
|
|
let grad_name_len = if peek == b"Perc" {
|
|
|
|
|
sub_r.read_u32()? as usize
|
|
|
|
|
} else {
|
|
|
|
|
u32::from_be_bytes([peek[0], peek[1], peek[2], peek[3]]) as usize
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sub_r.read(grad_name_len * 2)?;
|
|
|
|
|
|
|
|
|
|
if let Ok(stops_count) = sub_r.read_u16() {
|
|
|
|
|
let mut color_stops = Vec::new();
|
|
|
|
|
for _ in 0..stops_count {
|
|
|
|
|
if let (Ok(loc), Ok(mid), Ok(mode), Ok(c1), Ok(c2), Ok(c3), Ok(_)) = (
|
|
|
|
|
sub_r.read_u32(), sub_r.read_u32(), sub_r.read_u16(),
|
|
|
|
|
sub_r.read_u16(), sub_r.read_u16(), sub_r.read_u16(), sub_r.read_u16()
|
|
|
|
|
) {
|
|
|
|
|
let _ = sub_r.read(2);
|
|
|
|
|
let r_val = (c1 as f64 / 257.0).round() as u8;
|
|
|
|
|
let g_val = (c2 as f64 / 257.0).round() as u8;
|
|
|
|
|
let b_val = (c3 as f64 / 257.0).round() as u8;
|
|
|
|
|
if mode == 0 {
|
|
|
|
|
color_stops.push((loc, mid, [r_val, g_val, b_val]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (lut_r, lut_g, lut_b) = build_gradient_map_luts(&color_stops);
|
|
|
|
|
adjustment = Some(hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if key == "hue2" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
adjustment_raw = Some((*b"hue2", block_data.to_vec()));
|
|
|
|
|
if block_data.len() >= 16 {
|
|
|
|
|
let hue = i16::from_be_bytes([block_data[10], block_data[11]]) as i32;
|
|
|
|
|
let saturation = i16::from_be_bytes([block_data[12], block_data[13]]) as i32;
|
|
|
|
|
let lightness = i16::from_be_bytes([block_data[14], block_data[15]]) as i32;
|
|
|
|
|
adjustment = Some(hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness });
|
|
|
|
|
}
|
|
|
|
|
} else if key == "iOpa" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
if !block_data.is_empty() {
|
|
|
|
|
fill_opacity = block_data[0] as f32 / 255.0;
|
|
|
|
|
}
|
|
|
|
|
} else if key == "TySh" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
is_text = true;
|
|
|
|
|
if block_data.len() >= 50 {
|
|
|
|
|
let version = u16::from_be_bytes([block_data[0], block_data[1]]);
|
|
|
|
|
if version == 1 {
|
|
|
|
|
let xx = f64::from_be_bytes(block_data[2..10].try_into().unwrap());
|
|
|
|
|
let xy = f64::from_be_bytes(block_data[10..18].try_into().unwrap());
|
|
|
|
|
let tx = f64::from_be_bytes(block_data[34..42].try_into().unwrap());
|
|
|
|
|
let ty = f64::from_be_bytes(block_data[42..50].try_into().unwrap());
|
|
|
|
|
let angle = xy.atan2(xx).to_degrees();
|
|
|
|
|
|
|
|
|
|
let mut text = String::new();
|
|
|
|
|
for idx in 0..block_data.len().saturating_sub(12) {
|
|
|
|
|
if &block_data[idx..idx+4] == b"Txt " && &block_data[idx+4..idx+8] == b"TEXT" {
|
|
|
|
|
let char_count = u32::from_be_bytes([block_data[idx+8], block_data[idx+9], block_data[idx+10], block_data[idx+11]]) as usize;
|
|
|
|
|
if block_data.len() >= idx + 12 + char_count * 2 {
|
|
|
|
|
let mut s = String::new();
|
|
|
|
|
for j in 0..char_count {
|
|
|
|
|
let c = u16::from_be_bytes([
|
|
|
|
|
block_data[idx + 12 + j * 2],
|
|
|
|
|
block_data[idx + 12 + j * 2 + 1],
|
|
|
|
|
]);
|
|
|
|
|
if let Some(ch) = std::char::from_u32(c as u32) {
|
|
|
|
|
s.push(ch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
text = s.trim().to_string();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
text = name.clone();
|
|
|
|
|
}
|
|
|
|
|
text_data = Some(hcie_protocol::LayerData::Text {
|
|
|
|
|
text,
|
|
|
|
|
font: "Arial".to_string(),
|
|
|
|
|
size: 24.0,
|
|
|
|
|
color: [0, 0, 0, 255],
|
|
|
|
|
x: tx as f32,
|
|
|
|
|
y: ty as f32,
|
|
|
|
|
angle: angle as f32,
|
|
|
|
|
alignment: hcie_protocol::tools::TextAlignment::Left,
|
|
|
|
|
orientation: hcie_protocol::tools::TextOrientation::Horizontal,
|
|
|
|
|
effects: vec![],
|
|
|
|
|
offset_x: 0.0,
|
|
|
|
|
offset_y: 0.0,
|
|
|
|
|
unrotated_w: 0.0,
|
|
|
|
|
unrotated_h: 0.0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if key == "lfx2" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
match hcie_fx::parser::parse_lfx2(&block_data) {
|
|
|
|
|
Ok(fx) => {
|
|
|
|
|
if !fx.is_empty() {
|
|
|
|
|
effects = fx;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => { warn!("parse_lfx2 failed for layer '{}': {}", name, e); }
|
|
|
|
|
}
|
|
|
|
|
} else if key == "lrFX" {
|
|
|
|
|
let block_data = r.read(block_len)?;
|
|
|
|
|
if effects.is_empty() {
|
|
|
|
|
match hcie_fx::parser::parse_lrFX(&block_data) {
|
|
|
|
|
Ok(fx) => { effects = fx; }
|
|
|
|
|
Err(e) => { warn!("parse_lrFX failed for layer '{}': {}", name, e); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.seek(block_start + block_len)?;
|
|
|
|
|
if block_len % 2 != 0 {
|
|
|
|
|
r.read(1)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
layers_meta.push(LayerRecord {
|
|
|
|
|
name,
|
|
|
|
|
channels,
|
|
|
|
|
mask_info,
|
|
|
|
|
adjustment,
|
|
|
|
|
adjustment_raw,
|
|
|
|
|
is_group,
|
|
|
|
|
is_section_divider,
|
|
|
|
|
section_divider_blend,
|
|
|
|
|
curve_points,
|
|
|
|
|
effects,
|
|
|
|
|
fill_opacity,
|
|
|
|
|
is_text,
|
|
|
|
|
text_data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
r.seek(extra_start + extra_data_len)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut parsed_layers = Vec::with_capacity(layer_count);
|
|
|
|
|
|
|
|
|
|
for layer in layers_meta {
|
|
|
|
|
let mut decompressed_mask = None;
|
|
|
|
|
let mut mask_bounds = None;
|
|
|
|
|
let mut mask_default_color = 0;
|
|
|
|
|
|
|
|
|
|
for channel in &layer.channels {
|
2026-07-24 06:01:30 +03:00
|
|
|
if (channel.id == -2 || channel.id == -3) && decompressed_mask.is_none() {
|
2026-07-09 02:59:53 +03:00
|
|
|
if let Some(mask) = &layer.mask_info {
|
|
|
|
|
let w = (mask.right - mask.left) as u32;
|
|
|
|
|
let h = (mask.bottom - mask.top) as u32;
|
|
|
|
|
|
|
|
|
|
if w > 0 && h > 0 && channel.len > 2 {
|
|
|
|
|
let compression = r.read_u16()?;
|
|
|
|
|
let raw_channel_data = r.read(channel.len as usize - 2)?;
|
|
|
|
|
|
|
|
|
|
if compression == 1 {
|
|
|
|
|
if let Ok(decompressed) = rle_decompress_rows(raw_channel_data, w, h) {
|
|
|
|
|
decompressed_mask = Some(decompressed);
|
|
|
|
|
mask_bounds = Some([mask.top, mask.left, mask.bottom, mask.right]);
|
|
|
|
|
mask_default_color = mask.default_color;
|
|
|
|
|
}
|
|
|
|
|
} else if compression == 0 {
|
|
|
|
|
decompressed_mask = Some(raw_channel_data.to_vec());
|
|
|
|
|
mask_bounds = Some([mask.top, mask.left, mask.bottom, mask.right]);
|
|
|
|
|
mask_default_color = mask.default_color;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
r.read(channel.len as usize)?;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
r.read(channel.len as usize)?;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
r.read(channel.len as usize)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let is_group = layer.is_group;
|
|
|
|
|
let is_section_divider = layer.is_section_divider;
|
|
|
|
|
|
|
|
|
|
parsed_layers.push(ParsedLayerData {
|
|
|
|
|
name: layer.name,
|
|
|
|
|
is_group,
|
|
|
|
|
is_section_divider,
|
|
|
|
|
section_divider_blend: layer.section_divider_blend,
|
|
|
|
|
adjustment: layer.adjustment,
|
|
|
|
|
adjustment_raw: layer.adjustment_raw,
|
|
|
|
|
mask_pixels: decompressed_mask,
|
|
|
|
|
mask_bounds,
|
|
|
|
|
mask_default_color,
|
|
|
|
|
curve_points: layer.curve_points,
|
|
|
|
|
effects: layer.effects,
|
|
|
|
|
fill_opacity: layer.fill_opacity,
|
|
|
|
|
is_text: layer.is_text,
|
|
|
|
|
text_data: layer.text_data,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ParsedPsdData {
|
|
|
|
|
layers: parsed_layers,
|
|
|
|
|
image_resources_raw,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn psd_blend_mode_to_hcie(mode: &str) -> hcie_protocol::BlendMode {
|
|
|
|
|
let bm = match mode {
|
|
|
|
|
s if s.contains("VividLight") => hcie_blend::BlendMode::VividLight,
|
|
|
|
|
s if s.contains("LinearLight") => hcie_blend::BlendMode::LinearLight,
|
|
|
|
|
s if s.contains("PinLight") => hcie_blend::BlendMode::PinLight,
|
|
|
|
|
s if s.contains("HardMix") => hcie_blend::BlendMode::HardMix,
|
|
|
|
|
s if s.contains("SoftLight") => hcie_blend::BlendMode::SoftLight,
|
|
|
|
|
s if s.contains("HardLight") => hcie_blend::BlendMode::HardLight,
|
|
|
|
|
s if s.contains("LighterColor") => hcie_blend::BlendMode::LighterColor,
|
|
|
|
|
s if s.contains("DarkerColor") => hcie_blend::BlendMode::DarkerColor,
|
|
|
|
|
s if s.contains("LinearBurn") => hcie_blend::BlendMode::LinearBurn,
|
|
|
|
|
s if s.contains("LinearDodge") => hcie_blend::BlendMode::LinearDodge,
|
|
|
|
|
s if s.contains("ColorBurn") => hcie_blend::BlendMode::ColorBurn,
|
|
|
|
|
s if s.contains("ColorDodge") => hcie_blend::BlendMode::ColorDodge,
|
|
|
|
|
s if s.contains("Dissolve") => hcie_blend::BlendMode::Dissolve,
|
|
|
|
|
s if s.contains("Multiply") => hcie_blend::BlendMode::Multiply,
|
|
|
|
|
s if s.contains("Screen") => hcie_blend::BlendMode::Screen,
|
|
|
|
|
s if s.contains("Overlay") => hcie_blend::BlendMode::Overlay,
|
|
|
|
|
s if s.contains("Subtract") => hcie_blend::BlendMode::Subtract,
|
|
|
|
|
s if s.contains("Difference") => hcie_blend::BlendMode::Difference,
|
|
|
|
|
s if s.contains("Exclusion") => hcie_blend::BlendMode::Exclusion,
|
|
|
|
|
s if s.contains("Divide") => hcie_blend::BlendMode::Divide,
|
|
|
|
|
s if s.contains("Saturation") => hcie_blend::BlendMode::Saturation,
|
|
|
|
|
s if s.contains("Luminosity") => hcie_blend::BlendMode::Luminosity,
|
|
|
|
|
s if s.contains("PassThrough") => hcie_blend::BlendMode::PassThrough,
|
|
|
|
|
s if s.contains("Darken") => hcie_blend::BlendMode::Darken,
|
|
|
|
|
s if s.contains("Lighten") => hcie_blend::BlendMode::Lighten,
|
|
|
|
|
s if s.contains("Normal") => hcie_blend::BlendMode::Normal,
|
|
|
|
|
s if s.contains("Hue") => hcie_blend::BlendMode::Hue,
|
|
|
|
|
s if s.contains("Color") => hcie_blend::BlendMode::Color,
|
|
|
|
|
_ => hcie_blend::BlendMode::Normal,
|
|
|
|
|
};
|
|
|
|
|
bm.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_group_depth(group_id: u32, groups: &HashMap<u32, crate::PsdGroup>) -> u32 {
|
|
|
|
|
let mut depth = 1;
|
|
|
|
|
let mut current_id = group_id;
|
|
|
|
|
while let Some(g) = groups.get(¤t_id) {
|
|
|
|
|
if let Some(parent) = g.parent_id() {
|
|
|
|
|
depth += 1;
|
|
|
|
|
current_id = parent;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
depth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_ancestor_of_layer(
|
|
|
|
|
ancestor_group_id: u32,
|
|
|
|
|
mut current_parent_id: Option<u32>,
|
|
|
|
|
groups: &HashMap<u32, crate::PsdGroup>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
while let Some(pid) = current_parent_id {
|
|
|
|
|
if pid == ancestor_group_id {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
current_parent_id = groups.get(&pid).and_then(|g| g.parent_id());
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn import_psd(path: &Path) -> Result<Vec<hcie_protocol::Layer>, String> {
|
|
|
|
|
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
|
|
|
|
|
let psd = Psd::from_bytes(&bytes).map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
if psd.layers().is_empty() {
|
|
|
|
|
return Err("No layers found in PSD".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let canvas_w = psd.width() as u32;
|
|
|
|
|
let canvas_h = psd.height() as u32;
|
|
|
|
|
|
|
|
|
|
let parsed_result = parse_psd_sequential_full(&bytes);
|
|
|
|
|
let (parsed_layers, image_resources_raw) = match parsed_result {
|
|
|
|
|
Ok(parsed) => (parsed.layers, parsed.image_resources_raw),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("parse_psd_sequential failed: {}. Effects will not be imported.", e);
|
|
|
|
|
(Vec::new(), Vec::new())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let non_group_parsed: Vec<&ParsedLayerData> = parsed_layers.iter()
|
|
|
|
|
.filter(|p| !p.is_group && !p.is_section_divider)
|
|
|
|
|
.collect();
|
|
|
|
|
// Build a mapping from PSD layer index → non_group_parsed index
|
|
|
|
|
// Both parsed_layers and psd.layers() are in the same order
|
|
|
|
|
let mut parsed_to_non_group: Vec<Option<usize>> = Vec::with_capacity(parsed_layers.len());
|
|
|
|
|
let mut ng_idx = 0;
|
|
|
|
|
for p in &parsed_layers {
|
|
|
|
|
if !p.is_group {
|
|
|
|
|
parsed_to_non_group.push(Some(ng_idx));
|
|
|
|
|
ng_idx += 1;
|
|
|
|
|
} else {
|
|
|
|
|
parsed_to_non_group.push(None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut group_id_map = HashMap::new();
|
|
|
|
|
for (&psd_group_id, _) in psd.groups().iter() {
|
|
|
|
|
group_id_map.insert(psd_group_id, rand::random::<u64>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut flat_layers = Vec::new();
|
|
|
|
|
for (i, psd_layer) in psd.layers().iter().enumerate() {
|
|
|
|
|
let name = psd_layer.name().to_string();
|
|
|
|
|
let layer_rgba = psd_layer.rgba();
|
|
|
|
|
let layer_opacity = psd_layer.opacity() as f32 / 255.0;
|
|
|
|
|
let visible = !psd_layer.visible();
|
|
|
|
|
let blend_mode = psd_blend_mode_to_hcie(&format!("{:?}", psd_layer.blend_mode()));
|
|
|
|
|
let clipping_mask = !psd_layer.is_clipping_mask();
|
|
|
|
|
|
|
|
|
|
let mut layer = hcie_protocol::Layer::from_rgba(name, canvas_w, canvas_h, layer_rgba);
|
|
|
|
|
layer.id = rand::random::<u64>();
|
|
|
|
|
layer.visible = visible;
|
|
|
|
|
layer.opacity = layer_opacity;
|
|
|
|
|
layer.blend_mode = blend_mode;
|
|
|
|
|
layer.clipping_mask = clipping_mask;
|
|
|
|
|
if let Some(parent_u32) = psd_layer.parent_id() {
|
|
|
|
|
layer.parent_id = group_id_map.get(&parent_u32).copied();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parsed_idx = non_group_parsed.len().saturating_sub(1).saturating_sub(i);
|
|
|
|
|
if parsed_idx < non_group_parsed.len() {
|
|
|
|
|
let parsed = non_group_parsed[parsed_idx];
|
|
|
|
|
println!("MAP: i={} psd_layer.name='{}' -> parsed_idx={} parsed.name='{}'", i, psd_layer.name(), parsed_idx, parsed.name);
|
|
|
|
|
if parsed.name != psd_layer.name() {
|
|
|
|
|
log::warn!("PSD import layer name mismatch at index {}: psd.layers name='{}' vs parsed name='{}'", i, psd_layer.name(), parsed.name);
|
|
|
|
|
}
|
|
|
|
|
layer.adjustment = parsed.adjustment.clone();
|
|
|
|
|
layer.adjustment_raw = parsed.adjustment_raw.clone();
|
|
|
|
|
layer.mask_pixels = parsed.mask_pixels.clone();
|
|
|
|
|
layer.mask_bounds = parsed.mask_bounds;
|
|
|
|
|
layer.mask_default_color = parsed.mask_default_color;
|
|
|
|
|
layer.curve_points = parsed.curve_points.clone();
|
|
|
|
|
let protocol_effects: Vec<hcie_protocol::effects::LayerEffect> =
|
|
|
|
|
parsed.effects.iter().map(hcie_fx::hcie_fx_effect_to_protocol).collect();
|
|
|
|
|
layer.effects = protocol_effects;
|
|
|
|
|
layer.fill_opacity = parsed.fill_opacity;
|
|
|
|
|
if parsed.is_text {
|
|
|
|
|
layer.layer_type = hcie_protocol::LayerType::Text;
|
|
|
|
|
layer.data = parsed.text_data.clone().unwrap_or(hcie_protocol::LayerData::Raster);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
layer.image_resources_raw = Some(image_resources_raw.clone());
|
|
|
|
|
|
|
|
|
|
flat_layers.push(layer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert group open and section-divider markers into flat_layers.
|
|
|
|
|
//
|
|
|
|
|
// flat_layers is currently in Photoshop bottom-to-top order (index 0 is bottom).
|
|
|
|
|
// psd_group.contained_layers is a Range in the same bottom-to-top layer order.
|
|
|
|
|
// We place the group open marker just before the first child and the section
|
|
|
|
|
// divider just after the last child.
|
|
|
|
|
let mut group_markers: Vec<(usize, hcie_protocol::Layer)> = Vec::new();
|
|
|
|
|
for (&psd_group_id, psd_group) in psd.groups().iter() {
|
|
|
|
|
let name = psd_group.name().to_string();
|
|
|
|
|
let group_opacity = psd_group.opacity() as f32 / 255.0;
|
|
|
|
|
let visible = !psd_group.visible();
|
|
|
|
|
let blend_mode = psd_blend_mode_to_hcie(&format!("{:?}", psd_group.blend_mode()));
|
|
|
|
|
|
|
|
|
|
let group_id = *group_id_map.get(&psd_group_id).unwrap();
|
|
|
|
|
let parent_id = psd_group.parent_id().and_then(|p| group_id_map.get(&p).copied());
|
|
|
|
|
|
|
|
|
|
// Group open marker
|
|
|
|
|
let mut open_layer = hcie_protocol::Layer::new_transparent(name.clone(), canvas_w, canvas_h);
|
|
|
|
|
open_layer.layer_type = hcie_protocol::LayerType::Group;
|
|
|
|
|
open_layer.id = group_id;
|
|
|
|
|
open_layer.visible = visible;
|
|
|
|
|
open_layer.opacity = group_opacity;
|
|
|
|
|
open_layer.blend_mode = blend_mode;
|
|
|
|
|
open_layer.parent_id = parent_id;
|
|
|
|
|
|
|
|
|
|
// Section-divider (group end marker)
|
|
|
|
|
let divider_name = "</Layer group>".to_string();
|
|
|
|
|
let mut divider = hcie_protocol::Layer::new_transparent(divider_name, canvas_w, canvas_h);
|
|
|
|
|
divider.layer_type = hcie_protocol::LayerType::Group;
|
|
|
|
|
divider.id = rand::random::<u64>();
|
|
|
|
|
divider.visible = visible;
|
|
|
|
|
divider.opacity = group_opacity;
|
|
|
|
|
// Photoshop group pass-through is encoded in the original section-divider lsct block.
|
|
|
|
|
// If any PSD layer record with the same name reported pass-through, use it.
|
|
|
|
|
let has_pass_through = parsed_layers.iter().any(|p| {
|
|
|
|
|
p.name == name && p.section_divider_blend.as_deref() == Some("pass")
|
|
|
|
|
});
|
|
|
|
|
if has_pass_through {
|
|
|
|
|
divider.blend_mode = hcie_protocol::BlendMode::PassThrough;
|
|
|
|
|
} else {
|
|
|
|
|
divider.blend_mode = blend_mode;
|
|
|
|
|
}
|
|
|
|
|
divider.is_section_divider = true;
|
|
|
|
|
divider.parent_id = parent_id;
|
|
|
|
|
|
|
|
|
|
let range = &psd_group.contained_layers;
|
|
|
|
|
let open_idx = range.start;
|
|
|
|
|
let divider_idx = range.end;
|
|
|
|
|
|
|
|
|
|
group_markers.push((open_idx, open_layer));
|
|
|
|
|
group_markers.push((divider_idx, divider));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert from high indices to low so earlier insertions do not shift later ones.
|
|
|
|
|
group_markers.sort_by(|a, b| b.0.cmp(&a.0));
|
|
|
|
|
for (insert_idx, marker) in group_markers {
|
|
|
|
|
flat_layers.insert(insert_idx, marker);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flat_layers.reverse();
|
|
|
|
|
|
|
|
|
|
let is_bg_at_end = flat_layers.last().map(|l| l.name.trim().to_lowercase() == "background").unwrap_or(false);
|
|
|
|
|
let is_bg_at_start = flat_layers.first().map(|l| l.name.trim().to_lowercase() == "background").unwrap_or(false);
|
|
|
|
|
if is_bg_at_end && !is_bg_at_start {
|
|
|
|
|
flat_layers.reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(flat_layers)
|
|
|
|
|
}
|
2026-07-24 06:01:30 +03:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_example3_mask_import() {
|
|
|
|
|
let path = std::path::Path::new("/mnt/extra/00_PROJECTS/hcie-rust-v3.05/_images/_test_images/example3/Example3-mini.psd");
|
|
|
|
|
if path.exists() {
|
|
|
|
|
let layers = import_psd(path).expect("import_psd failed");
|
|
|
|
|
for l in &layers {
|
|
|
|
|
println!("TEST_LAYER: '{}' has_mask={} bounds={:?}", l.name, l.mask_pixels.is_some(), l.mask_bounds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|