use crate::core::encode::utils::pack_rgba_to_cmyk_fast::pack_rgba_to_cmyk_fast;
use crate::core::encode::utils::pack_rgba_to_rgb_fast::pack_rgba_to_rgb_fast;
use crate::core::encode::utils::read_be_u16::read_be_u16;
use crate::core::encode::utils::rebuild_minimal_jpeg_header::rebuild_minimal_jpeg_header;
use crate::core::image::{ImageBlp, MAX_MIPS};
use crate::error::error::BlpError;
use std::ffi::CStr;
use turbojpeg::{libc, raw};
#[derive(Clone)]
pub struct Mip {
pub w: u32,
pub h: u32,
pub visible: bool,
pub encode_ms: f64,
}
pub struct Ctx {
pub bytes: Vec<u8>,
pub mips: Vec<Mip>,
pub has_alpha: bool,
pub encode_ms_total: f64,
}
impl ImageBlp {
pub fn encode_blp(&self, quality: u8, mip_visible: &[bool]) -> Result<Ctx, BlpError> {
use image::RgbaImage;
use std::{ptr, time::Instant};
struct WorkMip<'a> {
w: u32,
h: u32,
vis: bool,
img: Option<&'a RgbaImage>, encoded: Vec<u8>, encode_ms: f64,
}
let total = self.mipmaps.len().min(MAX_MIPS);
let start_idx = (0..total)
.find(|&i| {
mip_visible
.get(i)
.copied()
.unwrap_or(true)
&& self.mipmaps[i].image.is_some()
})
.ok_or_else(|| BlpError::new("no_visible_mips_after_mask"))?;
let mut work: Vec<WorkMip> = Vec::with_capacity(total - start_idx);
for i in start_idx..total {
let m = &self.mipmaps[i];
work.push(WorkMip {
w: m.width,
h: m.height,
vis: mip_visible
.get(i)
.copied()
.unwrap_or(true),
img: m.image.as_ref(), encoded: Vec::new(),
encode_ms: 0.0,
});
}
let base_img = work[0]
.img
.ok_or_else(|| BlpError::new("first_visible_slot_missing_src"))?;
if base_img.width() != work[0].w || base_img.height() != work[0].h {
return Err(BlpError::new("mip.size_mismatch")
.with_arg("want_w", work[0].w)
.with_arg("want_h", work[0].h)
.with_arg("got_w", base_img.width())
.with_arg("got_h", base_img.height()));
}
let has_alpha = base_img.pixels().any(|p| p.0[3] != 255);
let t0 = Instant::now();
for wm in &mut work {
if !(wm.vis && wm.img.is_some()) {
wm.encoded.clear();
wm.encode_ms = 0.0;
continue;
}
let rgba = wm.img.unwrap();
let wz = rgba.width() as usize;
let hz = rgba.height() as usize;
if wz != wm.w as usize || hz != wm.h as usize {
return Err(BlpError::new("mip.size_mismatch")
.with_arg("want_w", wm.w)
.with_arg("want_h", wm.h)
.with_arg("got_w", wz)
.with_arg("got_h", hz));
}
let src = rgba.as_raw();
let (packed, pitch) = if has_alpha {
pack_rgba_to_cmyk_fast(src, wz, hz) } else {
pack_rgba_to_rgb_fast(src, wz, hz) };
let t_mip = Instant::now();
let handle = unsafe { raw::tj3Init(raw::TJINIT_TJINIT_COMPRESS as libc::c_int) };
if handle.is_null() {
return Err(BlpError::new("tj3.init"));
}
let jpeg_raw = unsafe {
struct Guard(raw::tjhandle);
impl Drop for Guard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { raw::tj3Destroy(self.0) };
}
}
}
let _g = Guard(handle);
if raw::tj3Set(handle, raw::TJPARAM_TJPARAM_QUALITY as libc::c_int, quality as libc::c_int) != 0 {
return Err(tj3_err(handle, "tj3.quality"));
}
if raw::tj3Set(handle, raw::TJPARAM_TJPARAM_SUBSAMP as libc::c_int, raw::TJSAMP_TJSAMP_444 as libc::c_int) != 0 {
return Err(tj3_err(handle, "tj3.subsamp"));
}
if raw::tj3Set(handle, raw::TJPARAM_TJPARAM_OPTIMIZE as libc::c_int, 0) != 0 {
return Err(tj3_err(handle, "tj3.optimize"));
}
if raw::tj3Set(
handle, raw::TJPARAM_TJPARAM_COLORSPACE as libc::c_int,
if has_alpha { raw::TJCS_TJCS_CMYK } else { raw::TJCS_TJCS_RGB } as libc::c_int,
) != 0
{
return Err(tj3_err(handle, "tj3.colorspace"));
}
let mut out_ptr: *mut libc::c_uchar = ptr::null_mut();
let mut out_size: raw::size_t = 0;
let r = raw::tj3Compress8(
handle, packed.as_ptr(),
wz as libc::c_int,
pitch as libc::c_int,
hz as libc::c_int,
if has_alpha { raw::TJPF_TJPF_CMYK } else { raw::TJPF_TJPF_BGR } as libc::c_int,
&mut out_ptr,
&mut out_size,
);
if r != 0 {
return Err(tj3_err(handle, "tj3.compress"));
}
let slice = std::slice::from_raw_parts(out_ptr, out_size as usize);
let vec = slice.to_vec();
raw::tj3Free(out_ptr as *mut libc::c_void);
vec
};
let (head_len, _scan_len) = split_header_and_scan(&jpeg_raw)?;
let header_clean = rebuild_minimal_jpeg_header(&jpeg_raw[..head_len])?;
wm.encoded = {
let mut v = Vec::with_capacity(jpeg_raw.len());
v.extend_from_slice(&header_clean);
v.extend_from_slice(&jpeg_raw[head_len..]); v
};
wm.encode_ms = t_mip.elapsed().as_secs_f64() * 1000.0;
}
let encode_ms_total = t0.elapsed().as_secs_f64() * 1000.0;
if work
.first()
.map(|m| m.encoded.is_empty())
.unwrap_or(true)
{
return Err(BlpError::new("first_visible_slot_missing"));
}
let mut heads: Vec<&[u8]> = Vec::new();
for m in &work {
if m.encoded.is_empty() {
continue;
}
let (hlen, _) = split_header_and_scan(&m.encoded)?; heads.push(&m.encoded[..hlen]);
}
if heads.is_empty() {
return Err(BlpError::new("no_encoded_heads"));
}
let mut common_header = header_prefix(&heads);
if common_header.len() < 2 || common_header[0] != 0xFF || common_header[1] != 0xD8 {
return Err(BlpError::new("bad_common_header"));
}
for h in &heads {
while !h.starts_with(&common_header) && !common_header.is_empty() {
common_header.pop();
}
if !h.starts_with(&common_header) {
return Err(BlpError::new("head_prefix_mismatch"));
}
}
#[inline]
fn write_u32_le_at(buf: &mut [u8], pos: usize, v: u32) {
buf[pos..pos + 4].copy_from_slice(&v.to_le_bytes());
}
let visible_count = work
.iter()
.filter(|m| !m.encoded.is_empty())
.count();
let mut bytes = Vec::new();
bytes.extend_from_slice(b"BLP1");
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&(if has_alpha { 8u32 } else { 0u32 }).to_le_bytes()); bytes.extend_from_slice(&work[0].w.to_le_bytes());
bytes.extend_from_slice(&work[0].h.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&(if visible_count > 1 { 1u32 } else { 0u32 }).to_le_bytes());
let pos_offsets = bytes.len();
bytes.resize(bytes.len() + MAX_MIPS * 4, 0);
let pos_sizes = bytes.len();
bytes.resize(bytes.len() + MAX_MIPS * 4, 0);
let jpeg_header_size: u32 = common_header
.len()
.try_into()
.map_err(|_| BlpError::new("jpeg_header_too_large"))?;
bytes.extend_from_slice(&jpeg_header_size.to_le_bytes());
bytes.extend_from_slice(&common_header);
bytes.extend_from_slice(b"RAFT");
for i in 0..MAX_MIPS.min(work.len()) {
let m = &work[i];
if m.encoded.is_empty() {
continue;
}
let (head_len, _) = split_header_and_scan(&m.encoded)?;
if head_len < common_header.len() {
return Err(BlpError::new("mip.head_too_short")
.with_arg("mip", i)
.with_arg("head_len", head_len)
.with_arg("common_len", common_header.len()));
}
if &m.encoded[..common_header.len()] != &common_header[..] {
return Err(BlpError::new("mip.common_header_mismatch")
.with_arg("mip", i)
.with_arg("head_prefix", hex::encode(&m.encoded[..common_header.len()]))
.with_arg("common_prefix", hex::encode(&common_header)));
}
let payload = &m.encoded[common_header.len()..];
let off = bytes.len();
let sz = payload.len();
if off > u32::MAX as usize {
return Err(BlpError::new("offset_too_large"));
}
if sz > u32::MAX as usize {
return Err(BlpError::new("payload_too_large"));
}
write_u32_le_at(&mut bytes, pos_offsets + (i << 2), off as u32);
write_u32_le_at(&mut bytes, pos_sizes + (i << 2), sz as u32);
bytes.extend_from_slice(payload);
}
let mut out_mips: Vec<Mip> = Vec::with_capacity(work.len());
for wm in &work {
out_mips.push(Mip { w: wm.w, h: wm.h, visible: wm.vis, encode_ms: wm.encode_ms });
}
Ok(Ctx { bytes, mips: out_mips, has_alpha, encode_ms_total })
}
}
fn split_header_and_scan(jpeg: &[u8]) -> Result<(usize, usize), BlpError> {
if jpeg.len() < 4 || jpeg[0] != 0xFF || jpeg[1] != 0xD8 {
return Err(BlpError::new("jpeg.bad_soi"));
}
let mut i = 2usize;
loop {
while i < jpeg.len() && jpeg[i] == 0xFF {
i += 1;
}
if i >= jpeg.len() {
return Err(BlpError::new("jpeg.truncated"));
}
let m = jpeg[i];
i += 1;
match m {
0xD9 => return Err(BlpError::new("jpeg.eoi_before_sos")),
0xD0..=0xD7 | 0x01 => {} 0xDA => {
if i + 2 > jpeg.len() {
return Err(BlpError::new("jpeg.sos_len"));
}
let seg_len = read_be_u16(&jpeg[i..i + 2])? as usize;
let seg_end = i + seg_len;
if seg_end > jpeg.len() {
return Err(BlpError::new("jpeg.sos_trunc"));
}
let head_len = seg_end;
let mut j = head_len;
while j + 1 < jpeg.len() {
if jpeg[j] == 0xFF {
let n = jpeg[j + 1];
if n == 0x00 || (0xD0..=0xD7).contains(&n) {
j += 2;
continue;
}
if n == 0xD9 {
return Ok((head_len, j - head_len));
}
}
j += 1;
}
return Err(BlpError::new("jpeg.eoi_not_found"));
}
_ => {
if i + 2 > jpeg.len() {
return Err(BlpError::new("jpeg.seg_len"));
}
let seg_len = read_be_u16(&jpeg[i..i + 2])? as usize;
let seg_end = i + seg_len;
if seg_end > jpeg.len() {
return Err(BlpError::new("jpeg.seg_trunc"));
}
i = seg_end;
}
}
}
}
fn header_prefix(heads: &[&[u8]]) -> Vec<u8> {
if heads.is_empty() {
return Vec::new();
}
let min_len = heads
.iter()
.map(|h| h.len())
.min()
.unwrap();
let mut out = Vec::with_capacity(min_len);
for i in 0..min_len {
let b = heads[0][i];
if heads.iter().all(|h| h[i] == b) {
out.push(b);
} else {
break;
}
}
out
}
fn tj3_err(handle: raw::tjhandle, key: &'static str) -> BlpError {
let msg = unsafe {
let p = raw::tj3GetErrorStr(handle);
if p.is_null() {
"unknown".to_string()
} else {
CStr::from_ptr(p)
.to_string_lossy()
.into_owned()
}
};
BlpError::new(key).with_arg("msg", msg)
}