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, 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 { 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, mask_info: Option, adjustment: Option, adjustment_raw: Option<([u8; 4], Vec)>, is_group: bool, is_section_divider: bool, section_divider_blend: Option, curve_points: Vec<(u16, u16)>, effects: Vec, fill_opacity: f32, is_text: bool, text_data: Option, } pub struct ParsedLayerData { pub name: String, pub is_group: bool, pub is_section_divider: bool, pub section_divider_blend: Option, pub adjustment: Option, pub adjustment_raw: Option<([u8; 4], Vec)>, pub mask_pixels: Option>, pub mask_bounds: Option<[i32; 4]>, pub mask_default_color: u8, pub curve_points: Vec<(u16, u16)>, pub effects: Vec, pub fill_opacity: f32, pub is_text: bool, pub text_data: Option, } fn build_gradient_map_luts( color_stops: &[(u32, u32, [u8; 3])], ) -> (Vec, Vec, Vec) { 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, pub image_resources_raw: Vec, } pub fn parse_psd_sequential(bytes: &[u8]) -> Result, String> { parse_psd_sequential_full(bytes).map(|parsed| parsed.layers) } pub fn parse_psd_sequential_full(bytes: &[u8]) -> Result { 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 = None; let mut adjustment = None; let mut adjustment_raw: Option<([u8; 4], Vec)> = None; let mut curve_points: Vec<(u16, u16)> = vec![]; let mut effects: Vec = 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 { if (channel.id == -2 || channel.id == -3) && decompressed_mask.is_none() { 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 { 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, groups: &HashMap, ) -> 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, 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> = 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::()); } 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::(); 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 = 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 = "".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::(); 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) } #[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); } } } }