use alloc::vec::Vec;
use std::collections::HashMap;
pub(crate) struct ImageAnalysis {
pub is_grayscale: bool,
pub is_opaque: bool,
pub min_gray_bit_depth: u8,
pub is_binary_alpha: bool,
pub transparent_color: Option<[u8; 3]>,
#[allow(dead_code)]
pub unique_color_count: usize,
pub exact_palette: Option<ExactPaletteData>,
}
pub(crate) struct ExactPaletteData {
pub palette_rgba: Vec<[u8; 4]>,
pub indices: Vec<u8>,
pub has_transparency: bool,
}
pub(crate) fn analyze_rgba8(bytes: &[u8], width: usize, height: usize) -> ImageAnalysis {
let npixels = width * height;
let mut is_grayscale = true;
let mut is_opaque = true;
let mut is_binary_alpha = true;
let mut color_map: HashMap<[u8; 4], u8> = HashMap::with_capacity(257);
let mut palette: Vec<[u8; 4]> = Vec::with_capacity(256);
let mut palette_overflow = false;
let mut has_transparency = false;
let mut can_1bit = true; let mut can_2bit = true; let mut can_4bit = true;
let mut transparent_color: Option<[u8; 3]> = None;
let mut multi_transparent = false;
for i in 0..npixels {
let off = i * 4;
let r = bytes[off];
let g = bytes[off + 1];
let b = bytes[off + 2];
let a = bytes[off + 3];
if is_grayscale && (r != g || r != b) {
is_grayscale = false;
}
if a != 255 {
is_opaque = false;
has_transparency = true;
if a != 0 {
is_binary_alpha = false;
} else if !multi_transparent {
let tc = [r, g, b];
match transparent_color {
None => transparent_color = Some(tc),
Some(prev) if prev != tc => {
multi_transparent = true;
transparent_color = None;
}
_ => {} }
}
}
if is_grayscale && can_4bit {
if !r.is_multiple_of(17) {
can_4bit = false;
can_2bit = false;
can_1bit = false;
} else if can_2bit && !r.is_multiple_of(85) {
can_2bit = false;
can_1bit = false;
} else if can_1bit && r != 0 && r != 255 {
can_1bit = false;
}
}
if !palette_overflow {
let color = [r, g, b, a];
if let std::collections::hash_map::Entry::Vacant(e) = color_map.entry(color) {
if palette.len() >= 256 {
palette_overflow = true;
} else {
e.insert(palette.len() as u8);
palette.push(color);
}
}
}
}
let min_gray_bit_depth = if can_1bit {
1
} else if can_2bit {
2
} else if can_4bit {
4
} else {
8
};
if is_opaque {
is_binary_alpha = true;
}
let unique_color_count = if palette_overflow { 257 } else { palette.len() };
let exact_palette = if !palette_overflow {
let mut indices = Vec::with_capacity(npixels);
for i in 0..npixels {
let off = i * 4;
let color = [bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]];
indices.push(color_map[&color]);
}
Some(ExactPaletteData {
palette_rgba: palette,
indices,
has_transparency,
})
} else {
None
};
ImageAnalysis {
is_grayscale,
is_opaque,
min_gray_bit_depth,
is_binary_alpha,
transparent_color,
unique_color_count,
exact_palette,
}
}
pub(crate) fn analyze_rgb8(bytes: &[u8], width: usize, height: usize) -> ImageAnalysis {
let npixels = width * height;
let mut is_grayscale = true;
let mut color_map: HashMap<[u8; 4], u8> = HashMap::with_capacity(257);
let mut palette: Vec<[u8; 4]> = Vec::with_capacity(256);
let mut palette_overflow = false;
let mut can_1bit = true;
let mut can_2bit = true;
let mut can_4bit = true;
for i in 0..npixels {
let off = i * 3;
let r = bytes[off];
let g = bytes[off + 1];
let b = bytes[off + 2];
if is_grayscale && (r != g || r != b) {
is_grayscale = false;
}
if is_grayscale && can_4bit {
if !r.is_multiple_of(17) {
can_4bit = false;
can_2bit = false;
can_1bit = false;
} else if can_2bit && !r.is_multiple_of(85) {
can_2bit = false;
can_1bit = false;
} else if can_1bit && r != 0 && r != 255 {
can_1bit = false;
}
}
if !palette_overflow {
let color = [r, g, b, 255];
if let std::collections::hash_map::Entry::Vacant(e) = color_map.entry(color) {
if palette.len() >= 256 {
palette_overflow = true;
} else {
e.insert(palette.len() as u8);
palette.push(color);
}
}
}
}
let min_gray_bit_depth = if can_1bit {
1
} else if can_2bit {
2
} else if can_4bit {
4
} else {
8
};
let unique_color_count = if palette_overflow { 257 } else { palette.len() };
let exact_palette = if !palette_overflow {
let mut indices = Vec::with_capacity(npixels);
for i in 0..npixels {
let off = i * 3;
let color = [bytes[off], bytes[off + 1], bytes[off + 2], 255];
indices.push(color_map[&color]);
}
Some(ExactPaletteData {
palette_rgba: palette,
indices,
has_transparency: false,
})
} else {
None
};
ImageAnalysis {
is_grayscale,
is_opaque: true,
min_gray_bit_depth,
is_binary_alpha: true,
transparent_color: None,
unique_color_count,
exact_palette,
}
}
pub(crate) fn can_reduce_16_to_8(be_bytes: &[u8]) -> bool {
be_bytes.chunks_exact(2).all(|pair| pair[1] == 0)
}
pub(crate) fn reduce_16_to_8(be_bytes: &[u8]) -> Vec<u8> {
be_bytes.chunks_exact(2).map(|pair| pair[0]).collect()
}
pub(crate) fn rgba8_to_rgb8(rgba: &[u8]) -> Vec<u8> {
let npixels = rgba.len() / 4;
let mut rgb = Vec::with_capacity(npixels * 3);
for i in 0..npixels {
let off = i * 4;
rgb.push(rgba[off]);
rgb.push(rgba[off + 1]);
rgb.push(rgba[off + 2]);
}
rgb
}
pub(crate) fn rgba8_to_gray_alpha8(rgba: &[u8]) -> Vec<u8> {
let npixels = rgba.len() / 4;
let mut ga = Vec::with_capacity(npixels * 2);
for i in 0..npixels {
let off = i * 4;
ga.push(rgba[off]); ga.push(rgba[off + 3]); }
ga
}
pub(crate) fn rgba8_to_gray8(rgba: &[u8]) -> Vec<u8> {
let npixels = rgba.len() / 4;
let mut gray = Vec::with_capacity(npixels);
for i in 0..npixels {
gray.push(rgba[i * 4]); }
gray
}
pub(crate) fn rgb8_to_gray8(rgb: &[u8]) -> Vec<u8> {
let npixels = rgb.len() / 3;
let mut gray = Vec::with_capacity(npixels);
for i in 0..npixels {
gray.push(rgb[i * 3]); }
gray
}
pub(crate) fn trns_color_is_exclusive(bytes: &[u8], transparent_color: [u8; 3]) -> bool {
let npixels = bytes.len() / 4;
for i in 0..npixels {
let off = i * 4;
let a = bytes[off + 3];
if a == 255
&& bytes[off] == transparent_color[0]
&& bytes[off + 1] == transparent_color[1]
&& bytes[off + 2] == transparent_color[2]
{
return false; }
}
true
}
pub(crate) fn gray8_to_subbyte(gray8: &[u8], bit_depth: u8) -> Vec<u8> {
let divisor = match bit_depth {
1 => 255u8,
2 => 85,
4 => 17,
_ => return gray8.to_vec(),
};
gray8
.iter()
.map(|&v| ((v as u16 + divisor as u16 / 2) / divisor as u16) as u8)
.collect()
}
pub(crate) fn split_palette_rgba(palette: &[[u8; 4]]) -> (Vec<u8>, Vec<u8>) {
let mut rgb = Vec::with_capacity(palette.len() * 3);
let mut alpha = Vec::with_capacity(palette.len());
for entry in palette {
rgb.push(entry[0]);
rgb.push(entry[1]);
rgb.push(entry[2]);
alpha.push(entry[3]);
}
(rgb, alpha)
}
pub(crate) fn sort_palette_luminance(palette: &mut Vec<[u8; 4]>, indices: &mut [u8]) {
let n = palette.len();
if n <= 1 {
return;
}
let mut order: Vec<(u8, u32)> = palette
.iter()
.enumerate()
.map(|(i, c)| {
let lum = 299u32 * c[0] as u32 + 587 * c[1] as u32 + 114 * c[2] as u32;
let hue = c[0] as u32 * 256 + c[2] as u32; (i as u8, lum * 256 + hue)
})
.collect();
order.sort_by_key(|&(_, key)| key);
let mut old_to_new = vec![0u8; n];
let mut new_palette = Vec::with_capacity(n);
for (new_idx, &(old_idx, _)) in order.iter().enumerate() {
old_to_new[old_idx as usize] = new_idx as u8;
new_palette.push(palette[old_idx as usize]);
}
*palette = new_palette;
for idx in indices.iter_mut() {
*idx = old_to_new[*idx as usize];
}
}
pub(crate) fn optimize_palette_order(palette: &mut Vec<[u8; 4]>, indices: &mut [u8]) {
if palette.len() <= 4 {
sort_palette_luminance(palette, indices);
return;
}
sort_palette_luminance(palette, indices);
}
pub(crate) fn near_lossless_quantize(bytes: &[u8], channels: usize, bits: u8) -> Vec<u8> {
if bits == 0 || bits > 4 {
return bytes.to_vec();
}
let step = 1u16 << bits; let half = step / 2;
let mut out = bytes.to_vec();
let has_alpha = channels == 4 || channels == 2; let color_channels = if has_alpha { channels - 1 } else { channels };
for pixel in out.chunks_exact_mut(channels) {
for c in &mut pixel[..color_channels] {
let v = *c as u16;
let rounded = ((v + half) / step) * step;
*c = rounded.min(255) as u8;
}
}
out
}
pub(crate) enum OptimalEncoding {
Indexed {
palette_rgb: Vec<u8>,
palette_alpha: Option<Vec<u8>>,
indices: Vec<u8>,
},
Truecolor {
bytes: Vec<u8>,
color_type: u8,
bit_depth: u8,
trns: Option<Vec<u8>>,
},
Original,
}
pub(crate) fn optimize_rgba8(bytes: &[u8], width: usize, height: usize) -> OptimalEncoding {
let analysis = analyze_rgba8(bytes, width, height);
let trns_usable = analysis.is_binary_alpha
&& !analysis.is_opaque
&& analysis.transparent_color.is_some()
&& trns_color_is_exclusive(bytes, analysis.transparent_color.unwrap());
let truecolor_bpp = if analysis.is_grayscale && (analysis.is_opaque || trns_usable) {
1 } else if analysis.is_grayscale {
2 } else if analysis.is_opaque || trns_usable {
3 } else {
4 };
if analysis.is_grayscale && analysis.min_gray_bit_depth < 8 {
let gray8 = rgba8_to_gray8(bytes);
let bd = analysis.min_gray_bit_depth;
if analysis.is_opaque {
let scaled = gray8_to_subbyte(&gray8, bd);
return OptimalEncoding::Truecolor {
bytes: scaled,
color_type: 0,
bit_depth: bd,
trns: None,
};
}
if trns_usable {
let tc = analysis.transparent_color.unwrap();
let gray_val = tc[0]; let scaled_val = gray_val
/ match bd {
1 => 255,
2 => 85,
4 => 17,
_ => 1,
};
let scaled = gray8_to_subbyte(&gray8, bd);
return OptimalEncoding::Truecolor {
bytes: scaled,
color_type: 0,
bit_depth: bd,
trns: Some(vec![0, scaled_val]),
};
}
}
if let Some(ref exact) = analysis.exact_palette {
let n_colors = exact.palette_rgba.len();
let n_pixels = width * height;
let indexed_raw = (1 + width) * height;
let truecolor_raw = (1 + width * truecolor_bpp) * height;
let palette_overhead = 12
+ 3 * n_colors
+ if exact.has_transparency {
12 + n_colors
} else {
0
};
let indexed_cost = palette_overhead + indexed_raw;
let use_indexed = indexed_cost < truecolor_raw || n_pixels >= 512;
if use_indexed {
let mut exact = analysis.exact_palette.unwrap();
optimize_palette_order(&mut exact.palette_rgba, &mut exact.indices);
let (rgb, alpha) = split_palette_rgba(&exact.palette_rgba);
let palette_alpha = if exact.has_transparency {
Some(alpha)
} else {
None
};
return OptimalEncoding::Indexed {
palette_rgb: rgb,
palette_alpha,
indices: exact.indices,
};
}
}
if analysis.is_grayscale && trns_usable {
let tc = analysis.transparent_color.unwrap();
let gray_val = tc[0];
let gray8 = rgba8_to_gray8(bytes);
return OptimalEncoding::Truecolor {
bytes: gray8,
color_type: 0,
bit_depth: 8,
trns: Some(vec![0, gray_val]),
};
}
if analysis.is_grayscale && analysis.is_opaque {
return OptimalEncoding::Truecolor {
bytes: rgba8_to_gray8(bytes),
color_type: 0,
bit_depth: 8,
trns: None,
};
}
if analysis.is_grayscale {
return OptimalEncoding::Truecolor {
bytes: rgba8_to_gray_alpha8(bytes),
color_type: 4,
bit_depth: 8,
trns: None,
};
}
if trns_usable {
let tc = analysis.transparent_color.unwrap();
let rgb = rgba8_to_rgb8(bytes);
return OptimalEncoding::Truecolor {
bytes: rgb,
color_type: 2,
bit_depth: 8,
trns: Some(vec![0, tc[0], 0, tc[1], 0, tc[2]]),
};
}
if analysis.is_opaque {
return OptimalEncoding::Truecolor {
bytes: rgba8_to_rgb8(bytes),
color_type: 2,
bit_depth: 8,
trns: None,
};
}
OptimalEncoding::Original
}
pub(crate) fn optimize_rgb8(bytes: &[u8], width: usize, height: usize) -> OptimalEncoding {
let analysis = analyze_rgb8(bytes, width, height);
let truecolor_bpp: usize = if analysis.is_grayscale { 1 } else { 3 };
if analysis.is_grayscale && analysis.min_gray_bit_depth < 8 {
let gray8 = rgb8_to_gray8(bytes);
let bd = analysis.min_gray_bit_depth;
let scaled = gray8_to_subbyte(&gray8, bd);
return OptimalEncoding::Truecolor {
bytes: scaled,
color_type: 0,
bit_depth: bd,
trns: None,
};
}
if let Some(ref exact) = analysis.exact_palette {
let n_colors = exact.palette_rgba.len();
let n_pixels = width * height;
let indexed_raw = (1 + width) * height;
let truecolor_raw = (1 + width * truecolor_bpp) * height;
let palette_overhead = 12 + 3 * n_colors;
let indexed_cost = palette_overhead + indexed_raw;
let use_indexed = indexed_cost < truecolor_raw || n_pixels >= 512;
if use_indexed {
let mut exact = analysis.exact_palette.unwrap();
optimize_palette_order(&mut exact.palette_rgba, &mut exact.indices);
let (rgb, _alpha) = split_palette_rgba(&exact.palette_rgba);
return OptimalEncoding::Indexed {
palette_rgb: rgb,
palette_alpha: None,
indices: exact.indices,
};
}
}
if analysis.is_grayscale {
return OptimalEncoding::Truecolor {
bytes: rgb8_to_gray8(bytes),
color_type: 0,
bit_depth: 8,
trns: None,
};
}
OptimalEncoding::Original
}
pub(crate) fn optimize_16bit(
be_bytes: &[u8],
width: usize,
height: usize,
color_type: u8,
) -> OptimalEncoding {
if !can_reduce_16_to_8(be_bytes) {
return OptimalEncoding::Original;
}
let reduced = reduce_16_to_8(be_bytes);
let channels: usize = match color_type {
0 => 1,
2 => 3,
4 => 2,
6 => 4,
_ => return OptimalEncoding::Original,
};
match channels {
4 => {
let opt = optimize_rgba8(&reduced, width, height);
match opt {
OptimalEncoding::Original => OptimalEncoding::Truecolor {
bytes: reduced,
color_type,
bit_depth: 8,
trns: None,
},
other => other,
}
}
3 => {
let opt = optimize_rgb8(&reduced, width, height);
match opt {
OptimalEncoding::Original => OptimalEncoding::Truecolor {
bytes: reduced,
color_type,
bit_depth: 8,
trns: None,
},
other => other,
}
}
_ => {
OptimalEncoding::Truecolor {
bytes: reduced,
color_type,
bit_depth: 8,
trns: None,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_grayscale_opaque_rgba() {
let bytes = [128, 128, 128, 255, 64, 64, 64, 255];
let a = analyze_rgba8(&bytes, 2, 1);
assert!(a.is_grayscale);
assert!(a.is_opaque);
assert_eq!(a.unique_color_count, 2);
}
#[test]
fn detect_non_grayscale() {
let bytes = [255, 0, 0, 255, 0, 255, 0, 255];
let a = analyze_rgba8(&bytes, 2, 1);
assert!(!a.is_grayscale);
assert!(a.is_opaque);
}
#[test]
fn detect_transparency() {
let bytes = [128, 128, 128, 128, 64, 64, 64, 0];
let a = analyze_rgba8(&bytes, 2, 1);
assert!(a.is_grayscale);
assert!(!a.is_opaque);
}
#[test]
fn exact_palette_small_image() {
let bytes = [
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 0, 0, 255,
];
let a = analyze_rgba8(&bytes, 4, 1);
assert_eq!(a.unique_color_count, 3);
assert!(a.exact_palette.is_some());
let ep = a.exact_palette.unwrap();
assert_eq!(ep.palette_rgba.len(), 3);
assert_eq!(ep.indices.len(), 4);
assert_eq!(ep.indices[0], ep.indices[3]);
}
#[test]
fn palette_overflow() {
let mut bytes = Vec::with_capacity(257 * 4);
for i in 0..257u32 {
bytes.push((i & 0xFF) as u8);
bytes.push((i >> 8) as u8);
bytes.push(0);
bytes.push(255);
}
let a = analyze_rgba8(&bytes, 257, 1);
assert_eq!(a.unique_color_count, 257);
assert!(a.exact_palette.is_none());
}
#[test]
fn reduce_16_to_8_possible() {
let be = [0x80, 0x00, 0x40, 0x00, 0xFF, 0x00];
assert!(can_reduce_16_to_8(&be));
let reduced = reduce_16_to_8(&be);
assert_eq!(reduced, [0x80, 0x40, 0xFF]);
}
#[test]
fn reduce_16_to_8_not_possible() {
let be = [0x80, 0x01, 0x40, 0x00]; assert!(!can_reduce_16_to_8(&be));
}
#[test]
fn optimize_rgba_to_gray() {
let bytes = [100, 100, 100, 255, 200, 200, 200, 255];
match optimize_rgba8(&bytes, 2, 1) {
OptimalEncoding::Indexed { .. } => {}
OptimalEncoding::Truecolor {
color_type,
bit_depth,
..
} => {
assert_eq!(color_type, 0);
assert_eq!(bit_depth, 8);
}
OptimalEncoding::Original => panic!("should optimize"),
}
}
#[test]
fn optimize_rgba_to_rgb() {
let mut bytes = Vec::new();
for r in 0..20u8 {
for g in 0..20u8 {
bytes.extend_from_slice(&[r * 13, g * 13, 128, 255]);
}
}
match optimize_rgba8(&bytes, 400, 1) {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
..
} => {
assert_eq!(color_type, 2); assert_eq!(bit_depth, 8);
}
_ => panic!("expected RGB truecolor"),
}
}
#[test]
fn palette_luminance_sort() {
let mut palette = vec![
[200, 200, 200, 255], [50, 50, 50, 255], [128, 128, 128, 255], ];
let mut indices = vec![0, 1, 2, 0, 1, 2];
sort_palette_luminance(&mut palette, &mut indices);
assert_eq!(palette[0], [50, 50, 50, 255]);
assert_eq!(palette[1], [128, 128, 128, 255]);
assert_eq!(palette[2], [200, 200, 200, 255]);
assert_eq!(indices[0], 2);
assert_eq!(indices[1], 0);
}
#[test]
fn near_lossless_1bit() {
let bytes = [127, 128, 129, 0, 1, 255];
let result = near_lossless_quantize(&bytes, 3, 1);
assert_eq!(result[0], 128); assert_eq!(result[1], 128); assert_eq!(result[2], 130); assert_eq!(result[3], 0); assert_eq!(result[4], 2); assert_eq!(result[5], 255); }
#[test]
fn near_lossless_preserves_alpha() {
let bytes = [127, 128, 129, 200]; let result = near_lossless_quantize(&bytes, 4, 2);
assert_eq!(result[0], 128); assert_eq!(result[1], 128); assert_eq!(result[2], 128); assert_eq!(result[3], 200); }
#[test]
fn rgb_grayscale_detection() {
let bytes = [50, 50, 50, 100, 100, 100, 150, 150, 150];
let a = analyze_rgb8(&bytes, 3, 1);
assert!(a.is_grayscale);
assert_eq!(a.unique_color_count, 3);
}
#[test]
fn min_gray_bit_depth_1bit() {
let bytes = [0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255];
let a = analyze_rgba8(&bytes, 3, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 1);
}
#[test]
fn min_gray_bit_depth_2bit() {
let bytes = [
0, 0, 0, 255, 85, 85, 85, 255, 170, 170, 170, 255, 255, 255, 255, 255,
];
let a = analyze_rgba8(&bytes, 4, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 2);
}
#[test]
fn min_gray_bit_depth_4bit() {
let bytes = [0, 0, 0, 255, 34, 34, 34, 255, 255, 255, 255, 255];
let a = analyze_rgba8(&bytes, 3, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 4);
}
#[test]
fn min_gray_bit_depth_8bit() {
let bytes = [100, 100, 100, 255, 200, 200, 200, 255];
let a = analyze_rgba8(&bytes, 2, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 8);
}
#[test]
fn binary_alpha_detection() {
let bytes = [255, 0, 0, 255, 0, 255, 0, 0, 0, 0, 255, 255];
let a = analyze_rgba8(&bytes, 3, 1);
assert!(a.is_binary_alpha);
assert!(!a.is_opaque);
}
#[test]
fn non_binary_alpha_detection() {
let bytes = [255, 0, 0, 128, 0, 255, 0, 255];
let a = analyze_rgba8(&bytes, 2, 1);
assert!(!a.is_binary_alpha);
}
#[test]
fn single_transparent_color_tracking() {
let bytes = [255, 0, 0, 0, 0, 255, 0, 255, 255, 0, 0, 0];
let a = analyze_rgba8(&bytes, 3, 1);
assert_eq!(a.transparent_color, Some([255, 0, 0]));
}
#[test]
fn multi_transparent_colors() {
let bytes = [255, 0, 0, 0, 0, 255, 0, 0, 128, 128, 128, 255];
let a = analyze_rgba8(&bytes, 3, 1);
assert_eq!(a.transparent_color, None);
}
#[test]
fn trns_exclusive_color() {
let bytes = [255, 0, 0, 0, 0, 255, 0, 255, 0, 0, 255, 255];
assert!(trns_color_is_exclusive(&bytes, [255, 0, 0]));
}
#[test]
fn trns_non_exclusive_color() {
let bytes = [255, 0, 0, 0, 255, 0, 0, 255];
assert!(!trns_color_is_exclusive(&bytes, [255, 0, 0]));
}
#[test]
fn gray8_to_subbyte_scaling() {
assert_eq!(gray8_to_subbyte(&[0, 255], 1), vec![0, 1]);
assert_eq!(gray8_to_subbyte(&[0, 85, 170, 255], 2), vec![0, 1, 2, 3]);
assert_eq!(gray8_to_subbyte(&[0, 17, 34, 255], 4), vec![0, 1, 2, 15]);
assert_eq!(gray8_to_subbyte(&[42, 200], 8), vec![42, 200]);
}
#[test]
fn optimize_rgba_to_subbyte_gray() {
let bytes = [
0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
];
match optimize_rgba8(&bytes, 4, 1) {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
trns,
..
} => {
assert_eq!(color_type, 0); assert!(bit_depth < 8); assert!(trns.is_none());
}
OptimalEncoding::Indexed { .. } => {
}
OptimalEncoding::Original => panic!("should optimize"),
}
}
#[test]
fn optimize_rgba_to_gray_with_trns() {
let mut bytes = Vec::new();
for i in 0..400 {
let v = ((i * 7) % 256) as u8;
let gray = if v == 0 { 100 } else { v };
bytes.extend_from_slice(&[gray, gray, gray, 255]);
}
bytes[0] = 1;
bytes[1] = 1;
bytes[2] = 1;
bytes[3] = 0;
if let OptimalEncoding::Truecolor {
color_type, trns, ..
} = optimize_rgba8(&bytes, 20, 20)
{
if color_type == 0 {
assert!(trns.is_some(), "gray with tRNS expected");
}
}
}
#[test]
fn optimize_rgba_to_rgb_with_trns() {
let mut bytes = Vec::new();
for r in 0..20u8 {
for g in 0..21u8 {
bytes.extend_from_slice(&[r * 13, g * 12, 128, 255]);
}
}
bytes[0] = 3;
bytes[1] = 7;
bytes[2] = 11;
bytes[3] = 0;
if let OptimalEncoding::Truecolor {
color_type, trns, ..
} = optimize_rgba8(&bytes, 420, 1)
&& color_type == 2
{
assert!(trns.is_some(), "RGB with tRNS expected");
let t = trns.unwrap();
assert_eq!(t.len(), 6); }
}
#[test]
fn optimize_rgba_gray_alpha() {
let mut bytes = Vec::new();
for i in 0..257u16 {
let g = (i % 256) as u8;
let a = if i < 256 { 128u8 } else { 200u8 }; bytes.extend_from_slice(&[g, g, g, a]);
}
let opt = optimize_rgba8(&bytes, 257, 1);
match opt {
OptimalEncoding::Truecolor { color_type, .. } => {
assert_eq!(color_type, 4); }
_ => panic!(
"expected GrayAlpha truecolor, got {:?}",
core::mem::discriminant(&opt)
),
}
}
#[test]
fn optimize_rgba_original() {
let mut bytes = Vec::new();
for i in 0..300u16 {
let r = (i % 256) as u8;
let g = ((i * 7) % 256) as u8;
let b = ((i * 13) % 256) as u8;
let a = if i < 100 { 128u8 } else { 200u8 };
bytes.extend_from_slice(&[r, g, b, a]);
}
let opt = optimize_rgba8(&bytes, 300, 1);
assert!(matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_rgb_grayscale_subbyte() {
let mut bytes = Vec::new();
for i in 0..16 {
let g = i * 17; bytes.extend_from_slice(&[g, g, g]);
}
let opt = optimize_rgb8(&bytes, 16, 1);
assert!(!matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_rgb_original() {
let mut bytes = Vec::new();
for i in 0..300u16 {
bytes.extend_from_slice(&[
(i % 256) as u8,
((i * 7) % 256) as u8,
((i * 13) % 256) as u8,
]);
}
let opt = optimize_rgb8(&bytes, 300, 1);
assert!(matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_16bit_reducible_rgba() {
let mut bytes = Vec::new();
for _ in 0..4 {
bytes.extend_from_slice(&[128, 0, 64, 0, 32, 0, 255, 0]);
}
let opt = optimize_16bit(&bytes, 4, 1, 6);
assert!(!matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_16bit_not_reducible() {
let bytes = [128, 1, 64, 0, 32, 0, 255, 0]; let opt = optimize_16bit(&bytes, 1, 1, 6);
assert!(matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_16bit_reducible_rgb() {
let mut bytes = Vec::new();
for _ in 0..4 {
bytes.extend_from_slice(&[100, 0, 200, 0, 50, 0]);
}
let opt = optimize_16bit(&bytes, 4, 1, 2);
assert!(!matches!(opt, OptimalEncoding::Original));
}
#[test]
fn optimize_16bit_gray_alpha() {
let bytes = [128, 0, 200, 0]; let opt = optimize_16bit(&bytes, 1, 1, 4);
match opt {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
..
} => {
assert_eq!(color_type, 4);
assert_eq!(bit_depth, 8);
}
_ => panic!("expected truecolor gray_alpha8"),
}
}
#[test]
fn optimize_16bit_unknown_color_type() {
let bytes = [128, 128];
let opt = optimize_16bit(&bytes, 1, 1, 99);
assert!(matches!(opt, OptimalEncoding::Original));
}
#[test]
fn min_gray_bit_depth_2bit_values() {
let a = analyze_rgba8(&[85, 85, 85, 255, 170, 170, 170, 255], 2, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 2);
}
#[test]
fn rgba8_to_gray_alpha8_conversion() {
let rgba = [128, 128, 128, 200, 64, 64, 64, 100];
let ga = rgba8_to_gray_alpha8(&rgba);
assert_eq!(ga.len(), 4);
assert_eq!(ga[0], 128);
assert_eq!(ga[1], 200);
assert_eq!(ga[2], 64);
assert_eq!(ga[3], 100);
}
#[test]
fn rgb8_to_gray8_conversion() {
let rgb = [128, 128, 128, 64, 64, 64];
let gray = rgb8_to_gray8(&rgb);
assert_eq!(gray.len(), 2);
assert_eq!(gray[0], 128);
assert_eq!(gray[1], 64);
}
#[test]
fn rgba8_to_rgb8_conversion() {
let rgba = [128, 64, 32, 255, 10, 20, 30, 200];
let rgb = rgba8_to_rgb8(&rgba);
assert_eq!(rgb.len(), 6);
assert_eq!(&rgb[..3], &[128, 64, 32]);
assert_eq!(&rgb[3..], &[10, 20, 30]);
}
#[test]
fn sort_palette_luminance_basic() {
let mut palette = vec![[255, 255, 255, 255], [0, 0, 0, 255], [128, 128, 128, 255]];
let mut indices = vec![0u8, 1, 2, 0, 1];
sort_palette_luminance(&mut palette, &mut indices);
assert_eq!(palette[0], [0, 0, 0, 255]);
}
#[test]
fn split_palette_rgba_basic() {
let palette = vec![[255, 0, 0, 200], [0, 255, 0, 255]];
let (rgb, alpha) = split_palette_rgba(&palette);
assert_eq!(rgb, [255, 0, 0, 0, 255, 0]);
assert_eq!(alpha, [200, 255]);
}
#[test]
fn analyze_rgb8_min_gray_2bit() {
let bytes = [85, 85, 85, 170, 170, 170];
let a = analyze_rgb8(&bytes, 2, 1);
assert!(a.is_grayscale);
assert_eq!(a.min_gray_bit_depth, 2);
}
#[test]
fn optimize_palette_order_small() {
let mut palette = vec![[255, 255, 255, 255], [0, 0, 0, 255]];
let mut indices = vec![0u8, 1, 0];
optimize_palette_order(&mut palette, &mut indices);
assert_eq!(palette[0], [0, 0, 0, 255]);
assert_eq!(palette[1], [255, 255, 255, 255]);
}
#[test]
fn near_lossless_noop_for_zero_bits() {
let bytes = [100, 200, 150];
let result = near_lossless_quantize(&bytes, 3, 0);
assert_eq!(result, bytes);
}
#[test]
fn near_lossless_noop_for_large_bits() {
let bytes = [100, 200, 150];
let result = near_lossless_quantize(&bytes, 3, 5);
assert_eq!(result, bytes);
}
#[test]
fn optimize_rgba_gray8_trns() {
let mut bytes = Vec::new();
for i in 0..256u16 {
let g = i as u8;
bytes.extend_from_slice(&[g, g, g, 255]);
}
bytes[42 * 4 + 3] = 0;
bytes.extend_from_slice(&[42, 42, 42, 255]);
let mut bytes = Vec::new();
for i in 0..256u16 {
let g = if i == 0 { 1 } else { i as u8 }; bytes.extend_from_slice(&[g, g, g, 255]);
}
bytes.extend_from_slice(&[0, 0, 0, 0]);
let opt = optimize_rgba8(&bytes, 257, 1);
match opt {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
trns,
..
} => {
assert_eq!(color_type, 0); assert_eq!(bit_depth, 8);
assert!(trns.is_some());
}
OptimalEncoding::Indexed { .. } => {
}
OptimalEncoding::Original => panic!("should optimize"),
}
}
#[test]
fn optimize_rgb_to_gray8() {
let mut bytes = Vec::new();
for i in 0..257u16 {
let g = (i % 256) as u8;
bytes.extend_from_slice(&[g, g, g]);
}
let opt = optimize_rgb8(&bytes, 257, 1);
match opt {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
..
} => {
assert_eq!(color_type, 0); assert_eq!(bit_depth, 8);
}
OptimalEncoding::Indexed { .. } => {
}
OptimalEncoding::Original => panic!("should optimize to gray"),
}
}
#[test]
fn optimize_16bit_rgba_to_rgba8_no_further() {
let mut bytes = Vec::new();
for i in 0..300u16 {
let r = (i % 256) as u8;
let g = ((i * 7) % 256) as u8;
let b = ((i * 13) % 256) as u8;
let a = if i < 100 { 128u8 } else { 200u8 };
bytes.extend_from_slice(&[r, 0, g, 0, b, 0, a, 0]);
}
let opt = optimize_16bit(&bytes, 300, 1, 6);
match opt {
OptimalEncoding::Truecolor {
color_type,
bit_depth,
..
} => {
assert_eq!(color_type, 6); assert_eq!(bit_depth, 8);
}
_ => panic!("expected RGBA8 truecolor"),
}
}
}