init
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "hcie-composite"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hcie-blend = { path = "../hcie-blend" }
|
||||
hcie-fx = { path = "../hcie-fx" }
|
||||
hcie-tile = { path = "../hcie-tile" }
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
rayon = "1.10"
|
||||
log = "0.4"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
env_logger = "0.11"
|
||||
@@ -0,0 +1,22 @@
|
||||
#![allow(dead_code)]
|
||||
pub mod parallel;
|
||||
pub mod tiled;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_composite;
|
||||
|
||||
pub use hcie_protocol::Layer;
|
||||
|
||||
pub fn composite_layers(layers: &[Layer], canvas_width: u32, canvas_height: u32) -> Vec<u8> {
|
||||
parallel::composite_layers_parallel(layers, canvas_width, canvas_height)
|
||||
}
|
||||
|
||||
pub fn composite_layers_region(
|
||||
layers: &[Layer],
|
||||
canvas_width: u32,
|
||||
canvas_height: u32,
|
||||
x0: u32, y0: u32, x1: u32, y1: u32,
|
||||
output: &mut [u8],
|
||||
) {
|
||||
parallel::composite_layers_region_parallel(layers, canvas_width, canvas_height, x0, y0, x1, y1, output)
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
use hcie_protocol::Layer;
|
||||
use hcie_blend::blend_pixels;
|
||||
use hcie_fx::protocol_to_hcie_fx_effect;
|
||||
use rayon::prelude::*;
|
||||
|
||||
pub(crate) fn apply_curves(r: u8, g: u8, b: u8, lut_r: &[u8], lut_g: &[u8], lut_b: &[u8]) -> [u8; 3] {
|
||||
[
|
||||
lut_r[r as usize],
|
||||
lut_g[g as usize],
|
||||
lut_b[b as usize],
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn apply_gradient_map(r: u8, g: u8, b: u8, lut_r: &[u8], lut_g: &[u8], lut_b: &[u8]) -> [u8; 3] {
|
||||
let dr = r as f32;
|
||||
let dg = g as f32;
|
||||
let db = b as f32;
|
||||
let l = (0.30 * dr + 0.59 * dg + 0.11 * db).clamp(0.0, 255.0).round() as usize;
|
||||
[
|
||||
lut_r[l],
|
||||
lut_g[l],
|
||||
lut_b[l],
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
|
||||
let c_max = r.max(g).max(b);
|
||||
let c_min = r.min(g).min(b);
|
||||
let delta = c_max - c_min;
|
||||
|
||||
let mut h = 0.0;
|
||||
let mut s = 0.0;
|
||||
let l = (c_max + c_min) / 2.0;
|
||||
|
||||
if delta > 1e-5 {
|
||||
s = if l < 0.5 { delta / (c_max + c_min) } else { delta / (2.0 - c_max - c_min) };
|
||||
if c_max == r {
|
||||
h = (g - b) / delta + (if g < b { 6.0 } else { 0.0 });
|
||||
} else if c_max == g {
|
||||
h = (b - r) / delta + 2.0;
|
||||
} else {
|
||||
h = (r - g) / delta + 4.0;
|
||||
}
|
||||
h *= 60.0;
|
||||
}
|
||||
(h, s, l)
|
||||
}
|
||||
|
||||
pub(crate) fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
|
||||
if s < 1e-5 {
|
||||
return (l, l, l);
|
||||
}
|
||||
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||
let m = l - c / 2.0;
|
||||
let (r_prime, g_prime, b_prime) = if h < 60.0 {
|
||||
(c, x, 0.0)
|
||||
} else if h < 120.0 {
|
||||
(x, c, 0.0)
|
||||
} else if h < 180.0 {
|
||||
(0.0, c, x)
|
||||
} else if h < 240.0 {
|
||||
(0.0, x, c)
|
||||
} else if h < 300.0 {
|
||||
(x, 0.0, c)
|
||||
} else {
|
||||
(c, 0.0, x)
|
||||
};
|
||||
(r_prime + m, g_prime + m, b_prime + m)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_hue_saturation(r: u8, g: u8, b: u8, dh: i32, ds: i32, dl: i32) -> [u8; 3] {
|
||||
let fr = r as f32 / 255.0;
|
||||
let fg = g as f32 / 255.0;
|
||||
let fb = b as f32 / 255.0;
|
||||
|
||||
let (h, s, l) = rgb_to_hsl(fr, fg, fb);
|
||||
|
||||
// 1. Hue shift
|
||||
let mut new_h = h + dh as f32;
|
||||
while new_h < 0.0 { new_h += 360.0; }
|
||||
while new_h >= 360.0 { new_h -= 360.0; }
|
||||
|
||||
// 2. Saturation relative/clamped shift
|
||||
let s_factor = ds as f32 / 100.0;
|
||||
let new_s = if s_factor >= 0.0 {
|
||||
s + s_factor * (1.0 - s)
|
||||
} else {
|
||||
s + s_factor * s
|
||||
};
|
||||
let new_s = new_s.clamp(0.0, 1.0);
|
||||
|
||||
// 3. Lightness relative/clamped shift
|
||||
let l_factor = dl as f32 / 100.0;
|
||||
let new_l = if l_factor >= 0.0 {
|
||||
l + l_factor * (1.0 - l)
|
||||
} else {
|
||||
l + l_factor * l
|
||||
};
|
||||
let new_l = new_l.clamp(0.0, 1.0);
|
||||
|
||||
let (out_r, out_g, out_b) = hsl_to_rgb(new_h, new_s, new_l);
|
||||
[
|
||||
(out_r * 255.0).round().clamp(0.0, 255.0) as u8,
|
||||
(out_g * 255.0).round().clamp(0.0, 255.0) as u8,
|
||||
(out_b * 255.0).round().clamp(0.0, 255.0) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn composite_layers_parallel(layers: &[Layer], canvas_width: u32, canvas_height: u32) -> Vec<u8> {
|
||||
let px_count = (canvas_width * canvas_height) as usize;
|
||||
let mut output = vec![0u8; px_count * 4];
|
||||
|
||||
log::debug!("[composite_layers_parallel] START: {} layers, canvas={}x{}, px_count={}",
|
||||
layers.len(), canvas_width, canvas_height, px_count);
|
||||
for (i, l) in layers.iter().enumerate() {
|
||||
log::debug!("[composite_layers_parallel] input layer[{}]: name='{}' visible={} w={} h={} opacity={} blend={:?} clip={} pixels_len={}",
|
||||
i, l.name, l.visible, l.width, l.height, l.opacity, l.blend_mode, l.clipping_mask, l.pixels.len());
|
||||
}
|
||||
|
||||
// Build a lookup for base layers of clipping masks
|
||||
let mut base_indices = Vec::with_capacity(layers.len());
|
||||
for i in 0..layers.len() {
|
||||
if layers[i].clipping_mask {
|
||||
let mut base = None;
|
||||
for j in (0..i).rev() {
|
||||
if !layers[j].clipping_mask {
|
||||
base = Some(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
base_indices.push(base);
|
||||
} else {
|
||||
base_indices.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
for (rev_i, layer) in layers.iter().enumerate() {
|
||||
if !layer.visible { continue; }
|
||||
if layer.layer_type == hcie_protocol::LayerType::Group {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_adj = layer.adjustment.is_some();
|
||||
if !is_adj && (layer.width == 0 || layer.height == 0) { continue; }
|
||||
|
||||
let i = rev_i;
|
||||
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
|
||||
let opacity = layer.opacity.clamp(0.0, 1.0);
|
||||
let is_clipping = layer.clipping_mask;
|
||||
let base_idx = base_indices[i];
|
||||
|
||||
// Apply layer effects if present
|
||||
let fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(protocol_to_hcie_fx_effect).collect();
|
||||
let effect_pixels = if (!fx_effects.is_empty() || layer.fill_opacity < 1.0) && !is_adj {
|
||||
Some(hcie_fx::apply_layer_effects(&layer.pixels, layer.width, layer.height, &fx_effects, layer.fill_opacity))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pixels = effect_pixels.as_deref().unwrap_or(&layer.pixels);
|
||||
|
||||
if is_adj {
|
||||
output.par_chunks_exact_mut(canvas_width as usize * 4)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
for x in 0..canvas_width {
|
||||
let out_idx = x as usize * 4;
|
||||
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
|
||||
if dst[3] == 0 { continue; }
|
||||
|
||||
let mask_val = layer.get_mask_value(x, gy);
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
|
||||
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
|
||||
apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
|
||||
apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
|
||||
apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
|
||||
}
|
||||
};
|
||||
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
|
||||
let out_r = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
|
||||
let out_g = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
|
||||
let out_b = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
|
||||
row[out_idx] = out_r;
|
||||
row[out_idx + 1] = out_g;
|
||||
row[out_idx + 2] = out_b;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let (offset_x, offset_y) = match &layer.data {
|
||||
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
let cx0 = (0).max(offset_x).max(0) as u32;
|
||||
let cx1 = (canvas_width as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
|
||||
let cy0 = (0).max(offset_y).max(0) as u32;
|
||||
let cy1 = (canvas_height as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
|
||||
if cx0 >= cx1 || cy0 >= cy1 { continue; }
|
||||
|
||||
output.par_chunks_exact_mut(canvas_width as usize * 4)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < cy0 || gy >= cy1 { return; }
|
||||
let ly = (y as i32 - offset_y) as u32;
|
||||
for x in cx0 as usize..cx1 as usize {
|
||||
let lx = (x as i32 - offset_x) as u32;
|
||||
let out_idx = x * 4;
|
||||
let pi = (ly * layer.width + lx) as usize * 4;
|
||||
let mut src = if pi + 3 < pixels.len() {
|
||||
[pixels[pi], pixels[pi + 1], pixels[pi + 2], pixels[pi + 3]]
|
||||
} else {
|
||||
[0, 0, 0, 0]
|
||||
};
|
||||
|
||||
let mask_val = layer.get_mask_value(x as u32, gy);
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
let base = layers[bi].get_pixel(x as u32, gy);
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0).round() as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { continue; }
|
||||
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[out_idx..out_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let non_zero_alpha = output.iter().skip(3).step_by(4).filter(|&&a| a > 0).count();
|
||||
log::debug!("[composite_layers_parallel] DONE: output non_zero_alpha={}/{} ({}%)",
|
||||
non_zero_alpha, px_count, if px_count > 0 { non_zero_alpha * 100 / px_count } else { 0 });
|
||||
output
|
||||
}
|
||||
|
||||
pub fn composite_layers_region_parallel(
|
||||
layers: &[Layer],
|
||||
canvas_width: u32,
|
||||
canvas_height: u32,
|
||||
x0: u32, y0: u32, x1: u32, y1: u32,
|
||||
output: &mut [u8],
|
||||
) {
|
||||
let w = x1.saturating_sub(x0);
|
||||
let h = y1.saturating_sub(y0);
|
||||
if w == 0 || h == 0 { return; }
|
||||
|
||||
let mut base_indices = Vec::with_capacity(layers.len());
|
||||
for i in 0..layers.len() {
|
||||
if layers[i].clipping_mask {
|
||||
let mut base = None;
|
||||
for j in (0..i).rev() {
|
||||
if !layers[j].clipping_mask {
|
||||
base = Some(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
base_indices.push(base);
|
||||
} else {
|
||||
base_indices.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
for (rev_i, layer) in layers.iter().enumerate() {
|
||||
if !layer.visible { continue; }
|
||||
if layer.layer_type == hcie_protocol::LayerType::Group {
|
||||
continue;
|
||||
}
|
||||
|
||||
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
|
||||
let opacity = layer.opacity.clamp(0.0, 1.0);
|
||||
let is_clipping = layer.clipping_mask;
|
||||
let i = rev_i;
|
||||
let base_idx = base_indices[i];
|
||||
|
||||
let is_adj = layer.adjustment.is_some();
|
||||
|
||||
// Apply layer effects if present
|
||||
let fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(protocol_to_hcie_fx_effect).collect();
|
||||
let effect_pixels = if (!fx_effects.is_empty() || layer.fill_opacity < 1.0) && !is_adj {
|
||||
Some(hcie_fx::apply_layer_effects(&layer.pixels, layer.width, layer.height, &fx_effects, layer.fill_opacity))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pixels = effect_pixels.as_deref().unwrap_or(&layer.pixels);
|
||||
|
||||
if is_adj {
|
||||
let region_y0 = y0;
|
||||
let region_y1 = y1.min(canvas_height);
|
||||
let region_x0 = x0;
|
||||
let region_x1 = x1.min(canvas_width);
|
||||
|
||||
let row_stride = canvas_width as usize * 4;
|
||||
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < region_y0 || gy >= region_y1 { return; }
|
||||
for x in region_x0..region_x1 {
|
||||
let out_idx = (x as usize) * 4;
|
||||
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
|
||||
if dst[3] == 0 { continue; }
|
||||
|
||||
let mask_val = layer.get_mask_value(x, gy);
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
|
||||
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
|
||||
apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
|
||||
apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
|
||||
apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
|
||||
}
|
||||
};
|
||||
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
|
||||
let out_r = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
|
||||
let out_g = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
|
||||
let out_b = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
|
||||
row[out_idx] = out_r;
|
||||
row[out_idx + 1] = out_g;
|
||||
row[out_idx + 2] = out_b;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if layer.width == 0 || layer.height == 0 { continue; }
|
||||
|
||||
let (offset_x, offset_y) = match &layer.data {
|
||||
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
|
||||
let cx1 = (x1 as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
|
||||
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
|
||||
let cy1 = (y1 as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
|
||||
if cx0 >= cx1 || cy0 >= cy1 { continue; }
|
||||
|
||||
let row_stride = canvas_width as usize * 4;
|
||||
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < cy0 || gy >= cy1 { return; }
|
||||
let ly = (gy as i32 - offset_y) as u32;
|
||||
for x in cx0..cx1 {
|
||||
let lx = (x as i32 - offset_x) as u32;
|
||||
let out_idx = (x as usize) * 4;
|
||||
let pi = ((ly * layer.width + lx) * 4) as usize;
|
||||
let mut src = if pi + 3 < pixels.len() {
|
||||
[pixels[pi], pixels[pi + 1], pixels[pi + 2], pixels[pi + 3]]
|
||||
} else {
|
||||
[0, 0, 0, 0]
|
||||
};
|
||||
|
||||
let mask_val = layer.get_mask_value(x, gy);
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
let base = layers[bi].get_pixel(x, gy);
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0).round() as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { continue; }
|
||||
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[out_idx..out_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[test]
|
||||
fn test_composite_basic() {
|
||||
use hcie_protocol::BlendMode;
|
||||
|
||||
let layer = hcie_protocol::Layer {
|
||||
name: "Red".into(),
|
||||
layer_type: hcie_protocol::LayerType::Raster,
|
||||
data: hcie_protocol::LayerData::Raster,
|
||||
pixels: vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255],
|
||||
width: 2,
|
||||
height: 2,
|
||||
visible: true,
|
||||
opacity: 1.0,
|
||||
blend_mode: BlendMode::Normal,
|
||||
locked: false,
|
||||
dirty: false,
|
||||
id: 0,
|
||||
parent_id: None,
|
||||
styles: vec![],
|
||||
clipping_mask: false,
|
||||
collapsed: false,
|
||||
adjustment: None,
|
||||
mask_pixels: None,
|
||||
mask_bounds: None,
|
||||
mask_default_color: 0,
|
||||
curve_points: vec![],
|
||||
fill_opacity: 1.0,
|
||||
effects: vec![],
|
||||
effects_dirty: AtomicBool::new(true),
|
||||
effects_cache: Mutex::new(None),
|
||||
adjustment_raw: None,
|
||||
is_section_divider: false,
|
||||
image_resources_raw: None,
|
||||
};
|
||||
let layers = vec![layer];
|
||||
let out = crate::composite_layers(&layers, 2, 2);
|
||||
assert_eq!(out, vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255]);
|
||||
|
||||
let layer0 = hcie_protocol::Layer {
|
||||
name: "Red".into(),
|
||||
layer_type: hcie_protocol::LayerType::Raster,
|
||||
data: hcie_protocol::LayerData::Raster,
|
||||
pixels: vec![255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255],
|
||||
width: 2,
|
||||
height: 2,
|
||||
visible: true,
|
||||
opacity: 1.0,
|
||||
blend_mode: BlendMode::Normal,
|
||||
locked: false,
|
||||
dirty: false,
|
||||
id: 0,
|
||||
parent_id: None,
|
||||
styles: vec![],
|
||||
clipping_mask: false,
|
||||
collapsed: false,
|
||||
adjustment: None,
|
||||
mask_pixels: None,
|
||||
mask_bounds: None,
|
||||
mask_default_color: 0,
|
||||
curve_points: vec![],
|
||||
fill_opacity: 1.0,
|
||||
effects: vec![],
|
||||
effects_dirty: AtomicBool::new(true),
|
||||
effects_cache: Mutex::new(None),
|
||||
adjustment_raw: None,
|
||||
is_section_divider: false,
|
||||
image_resources_raw: None,
|
||||
};
|
||||
let layer1 = hcie_protocol::Layer {
|
||||
name: "Blue".into(),
|
||||
layer_type: hcie_protocol::LayerType::Raster,
|
||||
data: hcie_protocol::LayerData::Raster,
|
||||
pixels: vec![0,0,255,255, 0,0,255,255, 0,0,255,255, 0,0,255,255],
|
||||
width: 2,
|
||||
height: 2,
|
||||
visible: true,
|
||||
opacity: 1.0,
|
||||
blend_mode: BlendMode::Normal,
|
||||
locked: false,
|
||||
dirty: false,
|
||||
id: 0,
|
||||
parent_id: None,
|
||||
styles: vec![],
|
||||
clipping_mask: false,
|
||||
collapsed: false,
|
||||
adjustment: None,
|
||||
mask_pixels: None,
|
||||
mask_bounds: None,
|
||||
mask_default_color: 0,
|
||||
curve_points: vec![],
|
||||
fill_opacity: 1.0,
|
||||
effects: vec![],
|
||||
effects_dirty: AtomicBool::new(true),
|
||||
effects_cache: Mutex::new(None),
|
||||
adjustment_raw: None,
|
||||
is_section_divider: false,
|
||||
image_resources_raw: None,
|
||||
};
|
||||
let layers2 = vec![layer0, layer1];
|
||||
let out2 = crate::composite_layers(&layers2, 2, 2);
|
||||
assert_eq!(out2, vec![0,0,255,255, 0,0,255,255, 0,0,255,255, 0,0,255,255]);
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
use hcie_tile::{TiledLayer, TILE_SIZE};
|
||||
use hcie_protocol::Layer;
|
||||
use hcie_blend::blend_pixels;
|
||||
use hcie_fx::apply_layer_effects;
|
||||
use hcie_fx::protocol_to_hcie_fx_effect;
|
||||
use rayon::prelude::*;
|
||||
|
||||
/// Pixel-area threshold below which the compositor uses a sequential row
|
||||
/// loop instead of Rayon parallelism. For small dirty regions (e.g. a single
|
||||
/// brush stamp) the thread-pool overhead of `par_chunks_exact_mut` dominates
|
||||
/// the actual blending work. 256x256 matches the tile size and is a safe
|
||||
/// crossover point where sequential iteration is consistently faster.
|
||||
const SEQUENTIAL_PIXEL_THRESHOLD: u32 = 256 * 256;
|
||||
|
||||
/// Composite layers using tile storage.
|
||||
/// Each layer's dense buffer is read via `layer.get_pixel()`.
|
||||
/// This is the same as the parallel version but optimized to skip
|
||||
/// transparent tiles. Uses dirty region to further cull work.
|
||||
pub fn composite_tiled(
|
||||
layers: &[Layer],
|
||||
tile_layers: &[Option<TiledLayer>],
|
||||
canvas_width: u32,
|
||||
canvas_height: u32,
|
||||
) -> Vec<u8> {
|
||||
let px_count = (canvas_width * canvas_height) as usize;
|
||||
let mut output = vec![0u8; px_count * 4];
|
||||
composite_tiled_into(layers, tile_layers, canvas_width, canvas_height, 0, 0, canvas_width, canvas_height, &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
/// Composite tile layers into an existing buffer within a region.
|
||||
pub fn composite_tiled_into(
|
||||
layers: &[Layer],
|
||||
tile_layers: &[Option<TiledLayer>],
|
||||
canvas_width: u32,
|
||||
canvas_height: u32,
|
||||
x0: u32, y0: u32, x1: u32, y1: u32,
|
||||
output: &mut [u8],
|
||||
) {
|
||||
let region_w = x1.saturating_sub(x0);
|
||||
let region_h = y1.saturating_sub(y0);
|
||||
if region_w == 0 || region_h == 0 { return; }
|
||||
|
||||
let use_sequential = region_w.saturating_mul(region_h) <= SEQUENTIAL_PIXEL_THRESHOLD;
|
||||
let row_stride = canvas_width as usize * 4;
|
||||
|
||||
// Pre-compute the base layer index for every clipping mask layer once.
|
||||
// This avoids an O(i) reverse scan per pixel during blending.
|
||||
let mut base_indices = Vec::with_capacity(layers.len());
|
||||
for i in 0..layers.len() {
|
||||
if layers[i].clipping_mask {
|
||||
let mut base = None;
|
||||
for j in (0..i).rev() {
|
||||
if !layers[j].clipping_mask {
|
||||
base = Some(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
base_indices.push(base);
|
||||
} else {
|
||||
base_indices.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
for (i, layer) in layers.iter().enumerate() {
|
||||
if !layer.visible { continue; }
|
||||
if layer.layer_type == hcie_protocol::LayerType::Group {
|
||||
continue;
|
||||
}
|
||||
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
|
||||
let opacity = layer.opacity.clamp(0.0, 1.0);
|
||||
let is_clipping = layer.clipping_mask;
|
||||
let base_idx = base_indices[i];
|
||||
|
||||
let (offset_x, offset_y) = match &layer.data {
|
||||
hcie_protocol::LayerData::Text { offset_x, offset_y, .. } => (*offset_x as i32, *offset_y as i32),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
|
||||
let cx1 = (x1 as i32).min(offset_x + layer.width as i32).min(canvas_width as i32).max(0) as u32;
|
||||
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
|
||||
let cy1 = (y1 as i32).min(offset_y + layer.height as i32).min(canvas_height as i32).max(0) as u32;
|
||||
|
||||
let layer_w = layer.width.min(canvas_width);
|
||||
let layer_h = layer.height.min(canvas_height);
|
||||
|
||||
let is_adj = layer.adjustment.is_some();
|
||||
if is_adj {
|
||||
let region_y0 = y0;
|
||||
let region_y1 = y1.min(canvas_height);
|
||||
let region_x0 = x0;
|
||||
let region_x1 = x1.min(canvas_width);
|
||||
|
||||
if use_sequential {
|
||||
for y in region_y0..region_y1 {
|
||||
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
||||
let gy = y;
|
||||
for x in region_x0..region_x1 {
|
||||
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < region_y0 || gy >= region_y1 { return; }
|
||||
for x in region_x0..region_x1 {
|
||||
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If layer has effects or non-default fill_opacity, compute effects buffer.
|
||||
// Prefer the cached `effects_cache` when available to avoid re-running the
|
||||
// expensive effects pipeline on every partial composite.
|
||||
let has_effects = !layer.effects.is_empty() || (layer.fill_opacity < 1.0);
|
||||
let fx_effects: Vec<hcie_fx::LayerEffect> = layer.effects.iter().map(protocol_to_hcie_fx_effect).collect();
|
||||
let effect_buf = if has_effects {
|
||||
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
|
||||
if cached.width == layer.width && cached.height == layer.height {
|
||||
Some(cached.rendered.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}.unwrap_or_else(|| apply_layer_effects(
|
||||
&layer.pixels, layer.width, layer.height,
|
||||
&fx_effects, layer.fill_opacity,
|
||||
))
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Try tile path first (only if no effects — effects need full dense buffer)
|
||||
if !has_effects {
|
||||
if let Some(Some(tl)) = tile_layers.get(i).filter(|_| layer.layer_type != hcie_protocol::LayerType::Text) {
|
||||
if tl.tile_count() == 0 && i > 0 {
|
||||
continue; // fully transparent layer with no tiles
|
||||
}
|
||||
let t_start_x = x0 / TILE_SIZE;
|
||||
let _t_start_y = y0 / TILE_SIZE;
|
||||
let t_end_x = div_ceil(x1, TILE_SIZE);
|
||||
let _t_end_y = div_ceil(y1, TILE_SIZE);
|
||||
|
||||
if use_sequential {
|
||||
for y in y0..y1.min(layer_h) {
|
||||
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
||||
let gy = y;
|
||||
let ty = gy / TILE_SIZE;
|
||||
let ly = (gy % TILE_SIZE) as usize;
|
||||
for tx in t_start_x..t_end_x {
|
||||
let gx_start = tx * TILE_SIZE;
|
||||
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
|
||||
if gx_end <= gx_start { continue; }
|
||||
if let Some(tile) = tl.get_tile(tx, ty) {
|
||||
let start_x = gx_start.max(x0);
|
||||
for gx in start_x..gx_end {
|
||||
blend_tile_pixel(row, gx, gy, ly, tile, layer, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < y0 || gy >= y1 || gy >= layer_h { return; }
|
||||
let ty = gy / TILE_SIZE;
|
||||
let ly = (gy % TILE_SIZE) as usize;
|
||||
for tx in t_start_x..t_end_x {
|
||||
let gx_start = tx * TILE_SIZE;
|
||||
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
|
||||
if gx_end <= gx_start { continue; }
|
||||
if let Some(tile) = tl.get_tile(tx, ty) {
|
||||
let start_x = gx_start.max(x0);
|
||||
for gx in start_x..gx_end {
|
||||
blend_tile_pixel(row, gx, gy, ly, tile, layer, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback to dense pixel path (no effects)
|
||||
if cx0 >= cx1 || cy0 >= cy1 { continue; }
|
||||
if use_sequential {
|
||||
for y in cy0..cy1 {
|
||||
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
||||
let gy = y;
|
||||
let ly = (gy as i32 - offset_y) as u32;
|
||||
for gx in cx0..cx1 {
|
||||
let lx = (gx as i32 - offset_x) as u32;
|
||||
blend_dense_pixel_offset(row, gx, gy, lx, ly, layer, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < cy0 || gy >= cy1 { return; }
|
||||
let ly = (gy as i32 - offset_y) as u32;
|
||||
for gx in cx0..cx1 {
|
||||
let lx = (gx as i32 - offset_x) as u32;
|
||||
blend_dense_pixel_offset(row, gx, gy, lx, ly, layer, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Effects path — use pre-computed effects buffer
|
||||
if cx0 >= cx1 || cy0 >= cy1 { continue; }
|
||||
let eb = effect_buf.as_slice();
|
||||
if use_sequential {
|
||||
for y in cy0..cy1 {
|
||||
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
||||
let gy = y;
|
||||
let ly = (gy as i32 - offset_y) as u32;
|
||||
for gx in cx0..cx1 {
|
||||
let lx = (gx as i32 - offset_x) as u32;
|
||||
blend_effects_pixel_offset(row, gx, gy, lx, ly, layer, eb, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.par_chunks_exact_mut(row_stride)
|
||||
.enumerate()
|
||||
.for_each(|(y, row)| {
|
||||
let gy = y as u32;
|
||||
if gy < cy0 || gy >= cy1 { return; }
|
||||
let ly = (gy as i32 - offset_y) as u32;
|
||||
for gx in cx0..cx1 {
|
||||
let lx = (gx as i32 - offset_x) as u32;
|
||||
blend_effects_pixel_offset(row, gx, gy, lx, ly, layer, eb, is_clipping, base_idx, layers, blend, opacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_adjustment_pixel(
|
||||
row: &mut [u8],
|
||||
x: u32,
|
||||
gy: u32,
|
||||
layer: &Layer,
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
let out_idx = (x as usize) * 4;
|
||||
let dst = [row[out_idx], row[out_idx + 1], row[out_idx + 2], row[out_idx + 3]];
|
||||
if dst[3] == 0 { return; }
|
||||
|
||||
let mask_val = layer.get_mask_value(x, gy);
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
|
||||
hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b } => {
|
||||
crate::parallel::apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::GradientMap { lut_r, lut_g, lut_b } => {
|
||||
crate::parallel::apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b)
|
||||
}
|
||||
hcie_blend::Adjustment::HueSaturation { hue, saturation, lightness } => {
|
||||
crate::parallel::apply_hue_saturation(dst[0], dst[1], dst[2], *hue, *saturation, *lightness)
|
||||
}
|
||||
};
|
||||
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended_opaque = blend_pixels([dst[0], dst[1], dst[2], 255], [adj_rgb[0], adj_rgb[1], adj_rgb[2], 255], blend, 1.0);
|
||||
row[out_idx] = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32).round() as u8;
|
||||
row[out_idx + 1] = ((1.0 - eff_opacity) * dst[1] as f32 + eff_opacity * blended_opaque[1] as f32).round() as u8;
|
||||
row[out_idx + 2] = ((1.0 - eff_opacity) * dst[2] as f32 + eff_opacity * blended_opaque[2] as f32).round() as u8;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_tile_pixel(
|
||||
row: &mut [u8],
|
||||
gx: u32,
|
||||
gy: u32,
|
||||
ly: usize,
|
||||
tile: &hcie_tile::Tile,
|
||||
layer: &Layer,
|
||||
is_clipping: bool,
|
||||
base_idx: Option<usize>,
|
||||
layers: &[Layer],
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
let lx = (gx % TILE_SIZE) as usize;
|
||||
let src_idx = (ly * TILE_SIZE as usize + lx) * 4;
|
||||
let mut src = [
|
||||
tile.pixels[src_idx],
|
||||
tile.pixels[src_idx + 1],
|
||||
tile.pixels[src_idx + 2],
|
||||
tile.pixels[src_idx + 3],
|
||||
];
|
||||
|
||||
// Inline get_mask_value — hot path
|
||||
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
||||
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
||||
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
||||
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
|
||||
} else {
|
||||
layer.mask_default_color
|
||||
}
|
||||
} else {
|
||||
255u8
|
||||
};
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
// Inline get_pixel for clipping base layer
|
||||
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
||||
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { return; }
|
||||
let dst_idx = (gx as usize) * 4;
|
||||
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_dense_pixel(
|
||||
row: &mut [u8],
|
||||
gx: u32,
|
||||
gy: u32,
|
||||
layer: &Layer,
|
||||
is_clipping: bool,
|
||||
base_idx: Option<usize>,
|
||||
layers: &[Layer],
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
// Inline get_pixel — hot path
|
||||
let src_i = (gy * layer.width + gx) as usize * 4;
|
||||
if src_i + 3 >= layer.pixels.len() {
|
||||
return;
|
||||
}
|
||||
let mut src = [
|
||||
layer.pixels[src_i],
|
||||
layer.pixels[src_i + 1],
|
||||
layer.pixels[src_i + 2],
|
||||
layer.pixels[src_i + 3],
|
||||
];
|
||||
|
||||
// Inline get_mask_value — hot path
|
||||
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
||||
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
||||
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
||||
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
|
||||
} else {
|
||||
layer.mask_default_color
|
||||
}
|
||||
} else {
|
||||
255u8
|
||||
};
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
// Inline get_pixel for clipping base layer
|
||||
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
||||
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { return; }
|
||||
let dst_idx = (gx as usize) * 4;
|
||||
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_effects_pixel(
|
||||
row: &mut [u8],
|
||||
gx: u32,
|
||||
gy: u32,
|
||||
layer: &Layer,
|
||||
eb: &[u8],
|
||||
is_clipping: bool,
|
||||
base_idx: Option<usize>,
|
||||
layers: &[Layer],
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
let src_idx = (gy as usize * layer.width as usize + gx as usize) * 4;
|
||||
let mut src = [
|
||||
eb[src_idx],
|
||||
eb[src_idx + 1],
|
||||
eb[src_idx + 2],
|
||||
eb[src_idx + 3],
|
||||
];
|
||||
|
||||
// Inline get_mask_value — hot path
|
||||
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
||||
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
||||
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
||||
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
|
||||
} else {
|
||||
layer.mask_default_color
|
||||
}
|
||||
} else {
|
||||
255u8
|
||||
};
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
// Inline get_pixel for clipping base layer
|
||||
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
||||
let base = [layers[bi].pixels[base_i], layers[bi].pixels[base_i + 1], layers[bi].pixels[base_i + 2], layers[bi].pixels[base_i + 3]];
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { return; }
|
||||
let dst_idx = (gx as usize) * 4;
|
||||
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_dense_pixel_offset(
|
||||
row: &mut [u8],
|
||||
gx: u32,
|
||||
gy: u32,
|
||||
lx: u32,
|
||||
ly: u32,
|
||||
layer: &Layer,
|
||||
is_clipping: bool,
|
||||
base_idx: Option<usize>,
|
||||
layers: &[Layer],
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
let src_i = (ly * layer.width + lx) as usize * 4;
|
||||
if src_i + 3 >= layer.pixels.len() {
|
||||
return;
|
||||
}
|
||||
let mut src = [
|
||||
layer.pixels[src_i],
|
||||
layer.pixels[src_i + 1],
|
||||
layer.pixels[src_i + 2],
|
||||
layer.pixels[src_i + 3],
|
||||
];
|
||||
|
||||
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
||||
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
||||
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
||||
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
|
||||
} else {
|
||||
layer.mask_default_color
|
||||
}
|
||||
} else {
|
||||
255u8
|
||||
};
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
let base = layers[bi].get_pixel(gx, gy);
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { return; }
|
||||
let dst_idx = (gx as usize) * 4;
|
||||
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn blend_effects_pixel_offset(
|
||||
row: &mut [u8],
|
||||
gx: u32,
|
||||
gy: u32,
|
||||
lx: u32,
|
||||
ly: u32,
|
||||
layer: &Layer,
|
||||
eb: &[u8],
|
||||
is_clipping: bool,
|
||||
base_idx: Option<usize>,
|
||||
layers: &[Layer],
|
||||
blend: hcie_blend::BlendMode,
|
||||
opacity: f32,
|
||||
) {
|
||||
let src_idx = (ly as usize * layer.width as usize + lx as usize) * 4;
|
||||
let mut src = [
|
||||
eb[src_idx],
|
||||
eb[src_idx + 1],
|
||||
eb[src_idx + 2],
|
||||
eb[src_idx + 3],
|
||||
];
|
||||
|
||||
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
||||
let (m_top, m_left, m_bottom, m_right) = (mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
||||
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
||||
let mask_w = m_right - m_left;
|
||||
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
||||
mask.get(mask_idx).copied().unwrap_or(layer.mask_default_color)
|
||||
} else {
|
||||
layer.mask_default_color
|
||||
}
|
||||
} else {
|
||||
255u8
|
||||
};
|
||||
if mask_val == 0 { return; }
|
||||
|
||||
if is_clipping {
|
||||
if let Some(bi) = base_idx {
|
||||
let base = layers[bi].get_pixel(gx, gy);
|
||||
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
||||
} else {
|
||||
src[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if src[3] == 0 { return; }
|
||||
let dst_idx = (gx as usize) * 4;
|
||||
let dst = [row[dst_idx], row[dst_idx + 1], row[dst_idx + 2], row[dst_idx + 3]];
|
||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
||||
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn div_ceil(a: u32, b: u32) -> u32 {
|
||||
(a + b - 1) / b
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns)]
|
||||
use hcie_composite::composite_layers;
|
||||
use hcie_protocol::Layer as ProtoLayer;
|
||||
|
||||
fn count_opaque(pixels: &[u8]) -> usize {
|
||||
pixels.iter().skip(3).step_by(4).filter(|&&a| a > 0).count()
|
||||
}
|
||||
|
||||
fn proto_to_comp(l: &ProtoLayer) -> ProtoLayer {
|
||||
l.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_visibility_from_protocol_layer() {
|
||||
let _ = env_logger::try_init();
|
||||
let w = 100u32; let h = 100u32;
|
||||
|
||||
// Build layers using hcie_protocol::Layer (like Engine does)
|
||||
let mut bg = ProtoLayer::new_transparent("bg", w, h);
|
||||
bg.id = 1;
|
||||
bg.pixels = vec![255u8; (w*h*4) as usize];
|
||||
|
||||
let mut top = ProtoLayer::new_transparent("top", w, h);
|
||||
top.id = 2;
|
||||
top.pixels = vec![0u8; (w*h*4) as usize];
|
||||
for y in 20..80 { for x in 20..80 {
|
||||
let i = (y*w+x) as usize * 4;
|
||||
top.pixels[i]=0; top.pixels[i+1]=0; top.pixels[i+2]=0; top.pixels[i+3]=255;
|
||||
}}
|
||||
|
||||
let run_composite = |layers: &[ProtoLayer]| -> Vec<u8> {
|
||||
composite_layers(layers, w, h)
|
||||
};
|
||||
|
||||
eprintln!("BOTH VISIBLE: opaque={}/10000", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
|
||||
bg.visible = false;
|
||||
eprintln!("BG HIDDEN: opaque={}/10000 (expect 3600)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
|
||||
bg.visible = true; top.visible = false;
|
||||
eprintln!("TOP HIDDEN: opaque={}/10000 (expect 10000)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
|
||||
bg.visible = false; top.visible = false;
|
||||
eprintln!("BOTH HIDDEN: opaque={}/10000 (expect 0)", count_opaque(&run_composite(&[bg.clone(), top.clone()])));
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
use hcie_composite::composite_layers;
|
||||
use hcie_protocol::{BlendMode, Layer, LayerType};
|
||||
|
||||
fn make_layer(name: &str, w: u32, h: u32, rgba: [u8; 4]) -> Layer {
|
||||
let mut l = Layer::from_rgba(name, w, h, vec![rgba; (w * h) as usize].into_iter().flatten().collect());
|
||||
l.blend_mode = BlendMode::Normal;
|
||||
l
|
||||
}
|
||||
|
||||
fn make_group(name: &str, w: u32, h: u32, id: u64) -> Layer {
|
||||
let mut l = Layer::new_transparent(name, w, h);
|
||||
l.layer_type = LayerType::Group;
|
||||
l.blend_mode = BlendMode::PassThrough;
|
||||
l.id = id;
|
||||
l
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pass_through_group_skipped_in_composite() {
|
||||
let w = 2u32;
|
||||
let h = 2u32;
|
||||
|
||||
// Background: opaque white
|
||||
let bg = make_layer("bg", w, h, [255, 255, 255, 255]);
|
||||
|
||||
// Group: PassThrough (no pixels)
|
||||
let group = make_group("group", w, h, 1);
|
||||
|
||||
// Child: Multiply on grey
|
||||
let mut child = make_layer("child", w, h, [128, 128, 128, 255]);
|
||||
child.blend_mode = BlendMode::Multiply;
|
||||
child.parent_id = Some(1);
|
||||
|
||||
// Composite order: bg, group, child
|
||||
let layers = vec![bg, group, child];
|
||||
let out = composite_layers(&layers, w, h);
|
||||
|
||||
// PassThrough: child blends directly onto bg.
|
||||
// Multiply: 255 * 128 / 255 = 128 for each channel.
|
||||
// Expected: every pixel [128, 128, 128, 255]
|
||||
let expected = vec![128u8, 128, 128, 255, 128, 128, 128, 255,
|
||||
128, 128, 128, 255, 128, 128, 128, 255];
|
||||
assert_eq!(out, expected, "PassThrough group should let child blend directly onto backdrop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pass_through_group_invisible_children() {
|
||||
let w = 2u32;
|
||||
let h = 2u32;
|
||||
|
||||
let bg = make_layer("bg", w, h, [255, 255, 255, 255]);
|
||||
let group = make_group("group", w, h, 1);
|
||||
let mut child = make_layer("child", w, h, [128, 128, 128, 255]);
|
||||
child.blend_mode = BlendMode::Multiply;
|
||||
child.parent_id = Some(1);
|
||||
child.visible = false;
|
||||
|
||||
let layers = vec![bg, group, child];
|
||||
let out = composite_layers(&layers, w, h);
|
||||
|
||||
// Child hidden → only bg visible
|
||||
let expected = vec![255u8, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255];
|
||||
assert_eq!(out, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pass_through_group_with_opacity() {
|
||||
let w = 2u32;
|
||||
let h = 2u32;
|
||||
|
||||
// Background white
|
||||
let bg = make_layer("bg", w, h, [255, 255, 255, 255]);
|
||||
|
||||
// Group with 50% opacity (PassThrough)
|
||||
let mut group = make_group("group", w, h, 1);
|
||||
group.opacity = 0.5;
|
||||
|
||||
// Child with Multiply on grey
|
||||
let mut child = make_layer("child", w, h, [128, 128, 128, 255]);
|
||||
child.blend_mode = BlendMode::Multiply;
|
||||
child.parent_id = Some(1);
|
||||
|
||||
// In a true PassThrough implementation the group opacity would multiply
|
||||
// the child's effective opacity. Our MVP skips group pixels entirely,
|
||||
// so the child blends at full opacity. We keep this test documenting
|
||||
// current MVP behaviour.
|
||||
let layers = vec![bg, group, child];
|
||||
let out = composite_layers(&layers, w, h);
|
||||
|
||||
// Multiply full-strength
|
||||
let expected = vec![128u8, 128, 128, 255, 128, 128, 128, 255,
|
||||
128, 128, 128, 255, 128, 128, 128, 255];
|
||||
assert_eq!(out, expected);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use hcie_composite::composite_layers;
|
||||
use hcie_protocol::{BlendMode, Layer};
|
||||
|
||||
fn count_opaque_pixels(pixels: &[u8]) -> usize {
|
||||
pixels.iter().skip(3).step_by(4).filter(|&&a| a > 0).count()
|
||||
}
|
||||
|
||||
fn create_test_layer(name: &str, w: u32, h: u32, pixels: Vec<u8>) -> Layer {
|
||||
let mut l = Layer::from_rgba(name, w, h, pixels);
|
||||
l.blend_mode = BlendMode::Normal;
|
||||
l.visible = true;
|
||||
l.opacity = 1.0;
|
||||
l
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_visibility_toggle() {
|
||||
let w = 100u32;
|
||||
let h = 100u32;
|
||||
let px = (w * h * 4) as usize;
|
||||
|
||||
// Layer 0: opaque white background
|
||||
let bg = create_test_layer("bg", w, h, vec![255u8; px]);
|
||||
|
||||
// Layer 1: opaque black square
|
||||
let mut top_pixels = vec![0u8; px];
|
||||
for y in 20..80 {
|
||||
for x in 20..80 {
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
top_pixels[i] = 0;
|
||||
top_pixels[i + 1] = 0;
|
||||
top_pixels[i + 2] = 0;
|
||||
top_pixels[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
let top = create_test_layer("top", w, h, top_pixels);
|
||||
|
||||
let layers = vec![bg.clone(), top.clone()];
|
||||
|
||||
// Test 1: both visible → 10000 opaque (white bg behind black square)
|
||||
let p = composite_layers(&layers, w, h);
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
eprintln!("Test 1 (both visible): opaque={}/10000", opaque);
|
||||
assert_eq!(opaque, 10000, "all pixels should be opaque");
|
||||
|
||||
// Test 2: hide background → only black square visible (3600 px)
|
||||
let mut bg_hidden = bg.clone();
|
||||
bg_hidden.visible = false;
|
||||
let layers = vec![bg_hidden, top.clone()];
|
||||
let p = composite_layers(&layers, w, h);
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
eprintln!("Test 2 (bg hidden): opaque={}/10000 (expected 3600)", opaque);
|
||||
assert_eq!(opaque, 3600, "only black square should be visible");
|
||||
|
||||
// Test 3: hide top → only white background visible
|
||||
let mut top_hidden = top.clone();
|
||||
top_hidden.visible = false;
|
||||
let layers = vec![bg.clone(), top_hidden];
|
||||
let p = composite_layers(&layers, w, h);
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
eprintln!("Test 3 (top hidden): opaque={}/10000 (expected 10000)", opaque);
|
||||
assert_eq!(opaque, 10000, "white background should be visible");
|
||||
|
||||
// Test 4: hide both → fully transparent
|
||||
let mut bg_h = bg.clone(); bg_h.visible = false;
|
||||
let mut top_h = top.clone(); top_h.visible = false;
|
||||
let layers = vec![bg_h, top_h];
|
||||
let p = composite_layers(&layers, w, h);
|
||||
let opaque = count_opaque_pixels(&p);
|
||||
eprintln!("Test 4 (both hidden): opaque={}/10000 (expected 0)", opaque);
|
||||
assert_eq!(opaque, 0, "should be fully transparent");
|
||||
}
|
||||
Reference in New Issue
Block a user