use crate::bzz_encode::bzz_encode;
use crate::bzz_new::bzz_decode;
use crate::error::DjVuError;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FgbzColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
pub fn encode_fgbz(palette: &[FgbzColor], indices: Option<&[i16]>) -> Vec<u8> {
assert!(
palette.len() <= u16::MAX as usize,
"FGbz palette size {} exceeds wire-format limit 65535",
palette.len()
);
if let Some(idx) = indices {
assert!(
idx.len() < (1usize << 24),
"FGbz index count {} exceeds wire-format limit 2^24 - 1",
idx.len()
);
}
let cap = 3 + palette.len() * 3 + indices.map_or(0, |i| 3 + i.len() * 2);
let mut out = Vec::with_capacity(cap);
out.push(if indices.is_some() { 0x80 } else { 0 });
out.extend_from_slice(&(palette.len() as u16).to_be_bytes());
for c in palette {
out.push(c.b);
out.push(c.g);
out.push(c.r);
}
if let Some(idx) = indices {
let n = idx.len() as u32;
out.push(((n >> 16) & 0xff) as u8);
out.push(((n >> 8) & 0xff) as u8);
out.push((n & 0xff) as u8);
let mut raw = Vec::with_capacity(idx.len() * 2);
for &v in idx {
raw.extend_from_slice(&v.to_be_bytes());
}
out.extend_from_slice(&bzz_encode(&raw));
}
out
}
pub fn decode_fgbz(data: &[u8]) -> Result<(Vec<FgbzColor>, Vec<i16>), DjVuError> {
if data.len() < 3 {
return Ok((Vec::new(), Vec::new()));
}
let version = data[0];
let has_indices = (version & 0x80) != 0;
if version & 0x7f != 0 {
return Err(DjVuError::InvalidStructure(
"FGbz: reserved version bits set",
));
}
let n_colors = u16::from_be_bytes([data[1], data[2]]) as usize;
let color_bytes = n_colors * 3;
if data.len() < 3 + color_bytes {
return Err(DjVuError::InvalidStructure("FGbz: palette truncated"));
}
let mut colors = Vec::with_capacity(n_colors);
for i in 0..n_colors {
let base = 3 + i * 3;
colors.push(FgbzColor {
b: data[base],
g: data[base + 1],
r: data[base + 2],
});
}
let mut indices = Vec::new();
if has_indices {
let idx_start = 3 + color_bytes;
if idx_start + 3 > data.len() {
return Err(DjVuError::InvalidStructure("FGbz: index header truncated"));
}
let m = ((data[idx_start] as u32) << 16)
| ((data[idx_start + 1] as u32) << 8)
| (data[idx_start + 2] as u32);
let bzz = &data[idx_start + 3..];
let raw = bzz_decode(bzz)?;
if raw.len() < (m as usize) * 2 {
return Err(DjVuError::InvalidStructure(
"FGbz: index payload shorter than declared count",
));
}
indices.reserve(m as usize);
for i in 0..(m as usize) {
indices.push(i16::from_be_bytes([raw[i * 2], raw[i * 2 + 1]]));
}
}
Ok((colors, indices))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_palette_no_indices() {
let bytes = encode_fgbz(&[], None);
assert_eq!(bytes, [0x00, 0x00, 0x00]);
let (colors, idx) = decode_fgbz(&bytes).unwrap();
assert!(colors.is_empty());
assert!(idx.is_empty());
}
#[test]
fn palette_only_byte_layout() {
let palette = [
FgbzColor {
r: 0x11,
g: 0x22,
b: 0x33,
},
FgbzColor {
r: 0xaa,
g: 0xbb,
b: 0xcc,
},
];
let bytes = encode_fgbz(&palette, None);
assert_eq!(
bytes,
[0x00, 0x00, 0x02, 0x33, 0x22, 0x11, 0xcc, 0xbb, 0xaa]
);
}
#[test]
fn version_byte_signals_index_presence() {
let palette = [FgbzColor { r: 1, g: 2, b: 3 }];
let with_idx = encode_fgbz(&palette, Some(&[0]));
let without_idx = encode_fgbz(&palette, None);
assert_eq!(with_idx[0] & 0x80, 0x80);
assert_eq!(without_idx[0] & 0x80, 0x00);
}
#[test]
fn roundtrip_palette_only() {
let palette: Vec<FgbzColor> = (0..256)
.map(|i| FgbzColor {
r: i as u8,
g: (i * 3) as u8,
b: (i * 7) as u8,
})
.collect();
let bytes = encode_fgbz(&palette, None);
let (decoded, idx) = decode_fgbz(&bytes).unwrap();
assert_eq!(decoded, palette);
assert!(idx.is_empty());
}
#[test]
fn roundtrip_with_indices() {
let palette = [
FgbzColor { r: 255, g: 0, b: 0 },
FgbzColor { r: 0, g: 255, b: 0 },
FgbzColor { r: 0, g: 0, b: 255 },
];
let indices: Vec<i16> = (0..1000).map(|i| (i % 3) as i16).collect();
let bytes = encode_fgbz(&palette, Some(&indices));
let (dp, di) = decode_fgbz(&bytes).unwrap();
assert_eq!(dp.as_slice(), palette.as_slice());
assert_eq!(di, indices);
}
#[test]
fn roundtrip_negative_indices_preserved() {
let palette = [FgbzColor { r: 1, g: 2, b: 3 }];
let indices: Vec<i16> = vec![0, -1, 0, -1, 0];
let bytes = encode_fgbz(&palette, Some(&indices));
let (_, di) = decode_fgbz(&bytes).unwrap();
assert_eq!(di, indices);
}
#[test]
fn roundtrip_via_renderer_parse_fgbz() {
let palette = [
FgbzColor {
r: 10,
g: 20,
b: 30,
},
FgbzColor {
r: 40,
g: 50,
b: 60,
},
FgbzColor {
r: 70,
g: 80,
b: 90,
},
];
let indices: Vec<i16> = vec![0, 1, 2, 1, 0, 2];
let bytes = encode_fgbz(&palette, Some(&indices));
let parsed = crate::djvu_render::parse_fgbz(&bytes).expect("parse_fgbz");
assert_eq!(parsed.colors.len(), palette.len());
for (a, b) in parsed.colors.iter().zip(palette.iter()) {
assert_eq!((a.r, a.g, a.b), (b.r, b.g, b.b));
}
assert_eq!(parsed.indices, indices);
}
#[test]
fn decode_rejects_truncated_palette() {
let bytes = [0x00, 0x00, 0x05, 0x01, 0x02, 0x03];
assert!(decode_fgbz(&bytes).is_err());
}
#[test]
fn decode_rejects_truncated_index_header() {
let bytes = [0x80, 0x00, 0x01, 0x01, 0x02, 0x03];
assert!(decode_fgbz(&bytes).is_err());
}
}