1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! FGbz foreground-palette parser.
//!
//! Decodes the `FGbz` chunk payload into a color palette and an optional
//! per-blit color-index table. The render module consumes the already-decoded
//! [`FgbzPalette`]; keeping the parser here means [`crate::djvu_render`] never
//! touches BZZ directly — the only `bzz_decode` call for the index table lives
//! in this module.
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::error::BzzError;
/// An RGB color from the FGbz palette.
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct PaletteColor {
pub(crate) r: u8,
pub(crate) g: u8,
pub(crate) b: u8,
}
/// Parsed FGbz data: palette colors and optional per-blit color indices.
pub(crate) struct FgbzPalette {
pub(crate) colors: Vec<PaletteColor>,
/// Per-blit color index: `indices[blit_idx]` → index into `colors`.
/// Empty when the FGbz chunk has no index table (version bit 7 unset).
pub(crate) indices: Vec<i16>,
}
/// Parse the FGbz chunk into palette colors and per-blit index table.
///
/// FGbz format:
/// - byte 0: version (bit 7 = has index table, bits 6-0 must be 0)
/// - byte 1-2: big-endian u16 palette size (number of colors)
/// - next `palette_size * 3` bytes: BGR triples (raw if version=0, BZZ if version has bit 0 set)
/// - if bit 7 set: 3-byte big-endian count + BZZ-compressed i16be index table
pub(crate) fn parse_fgbz(data: &[u8]) -> Result<FgbzPalette, BzzError> {
if data.len() < 3 {
return Ok(FgbzPalette {
colors: Vec::new(),
indices: Vec::new(),
});
}
let version = data[0];
let has_indices = (version & 0x80) != 0;
let n_colors =
u16::from_be_bytes([*data.get(1).unwrap_or(&0), *data.get(2).unwrap_or(&0)]) as usize;
if n_colors == 0 {
return Ok(FgbzPalette {
colors: Vec::new(),
indices: Vec::new(),
});
}
// Colors: raw BGR triples starting at byte 3
let color_bytes = n_colors * 3;
let color_data = data.get(3..).unwrap_or(&[]);
let mut colors = Vec::with_capacity(n_colors);
for i in 0..n_colors {
let base = i * 3;
if base + 2 < color_data.len().min(color_bytes) {
colors.push(PaletteColor {
r: color_data[base + 2],
g: color_data[base + 1],
b: color_data[base],
});
} else {
colors.push(PaletteColor { r: 0, g: 0, b: 0 });
}
}
// Per-blit index table
let mut indices = Vec::new();
if has_indices {
let idx_start = 3 + color_bytes;
if idx_start + 3 <= data.len() {
let num_indices = ((data[idx_start] as u32) << 16)
| ((data[idx_start + 1] as u32) << 8)
| (data[idx_start + 2] as u32);
let bzz_data = data.get(idx_start + 3..).unwrap_or(&[]);
let decoded = crate::bzz_new::bzz_decode(bzz_data)?;
let n = num_indices as usize;
indices.reserve(n);
for i in 0..n {
if i * 2 + 1 < decoded.len() {
indices.push(i16::from_be_bytes([decoded[i * 2], decoded[i * 2 + 1]]));
}
}
}
}
Ok(FgbzPalette { colors, indices })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_fgbz_short_data_returns_empty() {
// data.len() < 3 → empty palette
let result = parse_fgbz(&[]).unwrap();
assert!(result.colors.is_empty());
assert!(result.indices.is_empty());
let result = parse_fgbz(&[0x00, 0x00]).unwrap();
assert!(result.colors.is_empty());
}
#[test]
fn parse_fgbz_zero_colors_returns_empty() {
// version=0, n_colors=0 → empty palette
let data = [0x00u8, 0x00, 0x00]; // version, n_colors (u16be) = 0
let result = parse_fgbz(&data).unwrap();
assert!(result.colors.is_empty());
}
#[test]
fn parse_fgbz_truncated_color_data_fills_black() {
// n_colors=2 but only 3 bytes of color data (1 full color, 1 truncated)
let mut data = Vec::new();
data.push(0x00); // version: no index table
data.extend_from_slice(&2u16.to_be_bytes()); // n_colors = 2
// Only 3 bytes for first color (BGR): b=10, g=20, r=30
data.extend_from_slice(&[10, 20, 30]);
// Second color is truncated (not enough bytes)
let result = parse_fgbz(&data).unwrap();
assert_eq!(result.colors.len(), 2);
// First color parsed: r=30, g=20, b=10
assert_eq!(result.colors[0].r, 30);
// Second color falls back to black (0,0,0)
assert_eq!(result.colors[1].r, 0);
assert_eq!(result.colors[1].g, 0);
assert_eq!(result.colors[1].b, 0);
}
}