use crate::aspect::{decode_output_size, derive_grid};
use crate::bitpack::read_bits;
use crate::color::{oklab_to_linear_srgb, soft_gamut_clamp};
use crate::constants::*;
use crate::dct::{dct_decode_pixel_separable, precompute_cos_table, scan_order};
use crate::math_utils::{clamp01, round_half_away_from_zero};
use crate::mulaw::mu_law_dequantize;
use crate::transfer::srgb_gamma;
fn build_gamma_lut() -> [u8; 4096] {
let mut lut = [0u8; 4096];
for (i, entry) in lut.iter_mut().enumerate() {
let x = i as f64 / 4095.0;
let srgb = srgb_gamma(x);
*entry = round_half_away_from_zero(srgb.clamp(0.0, 1.0) * 255.0) as u8;
}
lut
}
fn linear_to_srgb8(x: f64, lut: &[u8; 4096]) -> u8 {
let idx = (round_half_away_from_zero(x * 4095.0) as i64).clamp(0, 4095) as usize;
lut[idx]
}
fn read_aspect(hash: &[u8; 32]) -> u8 {
let header: u64 = hash[..6]
.iter()
.enumerate()
.fold(0u64, |acc, (i, &b)| acc | ((b as u64) << (i * 8)));
((header >> 38) & 0xFF) as u8
}
fn render_at_size(hash: &[u8; 32], w: usize, h: usize) -> Vec<u8> {
let header: u64 = hash[..6]
.iter()
.enumerate()
.fold(0u64, |acc, (i, &b)| acc | ((b as u64) << (i * 8)));
let l_dc_q = (header & 0x7F) as u32;
let a_dc_q = ((header >> 7) & 0x7F) as u32;
let b_dc_q = ((header >> 14) & 0x7F) as u32;
let l_scl_q = ((header >> 21) & 0x3F) as u32;
let a_scl_q = ((header >> 27) & 0x3F) as u32;
let b_scl_q = ((header >> 33) & 0x1F) as u32;
let aspect = ((header >> 38) & 0xFF) as u8;
let has_alpha = ((header >> 46) & 1) == 1;
let l_dc = l_dc_q as f64 / 127.0;
let a_dc = (a_dc_q as f64 - 64.0) / 63.0 * MAX_CHROMA_A;
let b_dc = (b_dc_q as f64 - 64.0) / 63.0 * MAX_CHROMA_B;
let l_scale = l_scl_q as f64 / 63.0 * MAX_L_SCALE;
let a_scale = a_scl_q as f64 / 63.0 * MAX_A_SCALE;
let b_scale = b_scl_q as f64 / 31.0 * MAX_B_SCALE;
let (l_nx, l_ny) = if has_alpha {
derive_grid(aspect, 6)
} else {
derive_grid(aspect, 7)
};
let (c_nx, c_ny) = derive_grid(aspect, 4);
let l_scan = scan_order(l_nx, l_ny, aspect);
let chroma_scan = scan_order(c_nx, c_ny, aspect);
let l_cap = if has_alpha { 20usize } else { 27 };
let c_cap = 9usize;
let l_usable = l_cap.min(l_scan.len());
let c_usable = c_cap.min(chroma_scan.len());
let mut bitpos = 48usize;
let (alpha_dc_val, alpha_scale_val) = if has_alpha {
let adc = read_bits(hash, bitpos, 5) as f64 / 31.0;
bitpos += 5;
let ascl = read_bits(hash, bitpos, 4) as f64 / 15.0 * MAX_A_ALPHA_SCALE;
bitpos += 4;
(adc, ascl)
} else {
(1.0, 0.0)
};
let l_ac = if has_alpha {
let mut lac = Vec::with_capacity(20);
for _ in 0..7 {
let q = read_bits(hash, bitpos, 6);
bitpos += 6;
lac.push(mu_law_dequantize(q, 6) * l_scale);
}
for _ in 7..20 {
let q = read_bits(hash, bitpos, 5);
bitpos += 5;
lac.push(mu_law_dequantize(q, 5) * l_scale);
}
lac
} else {
let mut lac = Vec::with_capacity(27);
for _ in 0..27 {
let q = read_bits(hash, bitpos, 5);
bitpos += 5;
lac.push(mu_law_dequantize(q, 5) * l_scale);
}
lac
};
let mut a_ac = Vec::with_capacity(9);
for _ in 0..9 {
let q = read_bits(hash, bitpos, 4);
bitpos += 4;
a_ac.push(mu_law_dequantize(q, 4) * a_scale);
}
let mut b_ac = Vec::with_capacity(9);
for _ in 0..9 {
let q = read_bits(hash, bitpos, 4);
bitpos += 4;
b_ac.push(mu_law_dequantize(q, 4) * b_scale);
}
let (alpha_ac, alpha_scan, alpha_usable) = if has_alpha {
let (a_nx, a_ny) = derive_grid(aspect, 3);
let alpha_scan_inner = scan_order(a_nx, a_ny, aspect);
let a_usable = 5usize.min(alpha_scan_inner.len());
let mut aac = Vec::with_capacity(5);
for _ in 0..5 {
let q = read_bits(hash, bitpos, 4);
bitpos += 4;
aac.push(mu_law_dequantize(q, 4) * alpha_scale_val);
}
(aac, alpha_scan_inner, a_usable)
} else {
(vec![], vec![], 0)
};
let gamma_lut = build_gamma_lut();
let max_cx = l_nx.max(c_nx);
let max_cy = l_ny.max(c_ny);
let cos_x = precompute_cos_table(w, max_cx);
let cos_y = precompute_cos_table(h, max_cy);
let mut rgba_out = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let l = dct_decode_pixel_separable(
l_dc,
&l_ac[..l_usable],
&l_scan[..l_usable],
x,
y,
&cos_x,
&cos_y,
);
let a = dct_decode_pixel_separable(
a_dc,
&a_ac[..c_usable],
&chroma_scan[..c_usable],
x,
y,
&cos_x,
&cos_y,
);
let b = dct_decode_pixel_separable(
b_dc,
&b_ac[..c_usable],
&chroma_scan[..c_usable],
x,
y,
&cos_x,
&cos_y,
);
let alpha = if has_alpha {
dct_decode_pixel_separable(
alpha_dc_val,
&alpha_ac[..alpha_usable],
&alpha_scan[..alpha_usable],
x,
y,
&cos_x,
&cos_y,
)
} else {
1.0
};
let l_clamped = clamp01(l);
let [l_out, a_out, b_out] = soft_gamut_clamp(l_clamped, a, b);
let rgb_linear = oklab_to_linear_srgb([l_out, a_out, b_out]);
let idx = (y * w + x) * 4;
rgba_out[idx] = linear_to_srgb8(clamp01(rgb_linear[0]), &gamma_lut);
rgba_out[idx + 1] = linear_to_srgb8(clamp01(rgb_linear[1]), &gamma_lut);
rgba_out[idx + 2] = linear_to_srgb8(clamp01(rgb_linear[2]), &gamma_lut);
rgba_out[idx + 3] = round_half_away_from_zero(255.0 * clamp01(alpha)) as u8;
}
}
rgba_out
}
pub fn decode(hash: &[u8; 32]) -> (u32, u32, Vec<u8>) {
let aspect = read_aspect(hash);
let (w, h) = decode_output_size(aspect);
let rgba = render_at_size(hash, w as usize, h as usize);
(w, h, rgba)
}
pub fn decode_capped(hash: &[u8; 32], max_w: u32, max_h: u32) -> (u32, u32, Vec<u8>) {
let aspect = read_aspect(hash);
let (nat_w, nat_h) = decode_output_size(aspect);
let w = nat_w.min(max_w);
let h = nat_h.min(max_h);
let rgba = render_at_size(hash, w as usize, h as usize);
(w, h, rgba)
}
pub fn average_color(hash: &[u8; 32]) -> [u8; 4] {
let header: u64 = hash[..6]
.iter()
.enumerate()
.fold(0u64, |acc, (i, &b)| acc | ((b as u64) << (i * 8)));
let l_dc_q = (header & 0x7F) as u32;
let a_dc_q = ((header >> 7) & 0x7F) as u32;
let b_dc_q = ((header >> 14) & 0x7F) as u32;
let has_alpha = ((header >> 46) & 1) == 1;
let l_dc = l_dc_q as f64 / 127.0;
let a_dc = (a_dc_q as f64 - 64.0) / 63.0 * MAX_CHROMA_A;
let b_dc = (b_dc_q as f64 - 64.0) / 63.0 * MAX_CHROMA_B;
let l_clamped = clamp01(l_dc);
let [l_out, a_out, b_out] = soft_gamut_clamp(l_clamped, a_dc, b_dc);
let rgb_linear = oklab_to_linear_srgb([l_out, a_out, b_out]);
let gamma_lut = build_gamma_lut();
let alpha = if has_alpha {
read_bits(hash, 48, 5) as f64 / 31.0
} else {
1.0
};
[
linear_to_srgb8(clamp01(rgb_linear[0]), &gamma_lut),
linear_to_srgb8(clamp01(rgb_linear[1]), &gamma_lut),
linear_to_srgb8(clamp01(rgb_linear[2]), &gamma_lut),
round_half_away_from_zero(255.0 * clamp01(alpha)) as u8,
]
}