use crate::{BackgroundMode, PixelAspectRatio, Result, SixelError};
use quantette::{deps::palette::Srgb, dither::FloydSteinberg, ImageRef, PaletteSize, Pipeline};
pub use quantette::QuantizeMethod;
#[derive(Clone, Copy, Debug)]
struct Rgb {
r: u8,
g: u8,
b: u8,
}
#[derive(Clone, Debug)]
pub struct EncodeOptions {
pub max_colors: u16,
pub diffusion: f32,
pub quantize_method: QuantizeMethod,
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
max_colors: 256,
diffusion: FloydSteinberg::DEFAULT_ERROR_DIFFUSION,
quantize_method: QuantizeMethod::Wu,
}
}
}
#[must_use = "this returns the encoded SIXEL string"]
pub fn sixel_encode(
rgba: &[u8],
width: usize,
height: usize,
opts: &EncodeOptions,
) -> Result<String> {
sixel_encode_impl(
rgba,
width,
height,
opts,
PixelAspectRatio::default(),
BackgroundMode::default(),
)
}
pub(crate) fn sixel_encode_impl(
rgba: &[u8],
width: usize,
height: usize,
opts: &EncodeOptions,
pixel_aspect_ratio: PixelAspectRatio,
background_mode: BackgroundMode,
) -> Result<String> {
if width == 0 || height == 0 {
return Err(SixelError::InvalidDimensions { width, height });
}
let expected = width * height * 4;
if rgba.len() != expected {
return Err(SixelError::BufferSizeMismatch {
expected,
actual: rgba.len(),
});
}
let opacity_mask: Vec<bool> = rgba.chunks_exact(4).map(|c| c[3] >= 128).collect();
let rgb_pixels: Vec<Srgb<u8>> = rgba
.chunks_exact(4)
.map(|c| Srgb::new(c[0], c[1], c[2]))
.collect();
let max_colors = opts.max_colors.clamp(2, 256) as u8;
let palette_size = PaletteSize::try_from(max_colors).unwrap_or(PaletteSize::MAX);
let image = ImageRef::new(width as u32, height as u32, &rgb_pixels)
.map_err(|e| SixelError::Quantization(e.to_string()))?;
let diffusion = opts.diffusion.clamp(0.0, 1.0);
let pipeline = Pipeline::new()
.palette_size(palette_size)
.quantize_method(opts.quantize_method.clone());
let indexed_image = if diffusion <= 0.0 {
pipeline
.ditherer(None)
.input_image(image)
.output_srgb8_indexed_image()
} else {
let ditherer = FloydSteinberg::with_error_diffusion(diffusion).unwrap_or_default();
pipeline
.ditherer(ditherer)
.input_image(image)
.output_srgb8_indexed_image()
};
let palette: Vec<Rgb> = indexed_image
.palette()
.iter()
.map(|c| Rgb {
r: c.red,
g: c.green,
b: c.blue,
})
.collect();
let indices: Vec<u8> = indexed_image.indices().to_vec();
encode_indexed_to_sixel(
&palette,
&indices,
&opacity_mask,
width,
height,
pixel_aspect_ratio,
background_mode,
)
}
#[inline]
#[deprecated(since = "0.5.0", note = "use SixelImage::from_rgba().encode() instead")]
#[must_use = "this returns the encoded SIXEL string"]
pub fn sixel_encode_default(rgba: &[u8], width: usize, height: usize) -> Result<String> {
#[allow(deprecated)]
sixel_encode(rgba, width, height, &EncodeOptions::default())
}
fn encode_indexed_to_sixel(
palette: &[Rgb],
indices: &[u8],
opacity_mask: &[bool],
width: usize,
height: usize,
aspect_ratio: PixelAspectRatio,
background_mode: BackgroundMode,
) -> Result<String> {
let mut out = String::new();
out.push_str("\x1bP");
write_number(&mut out, aspect_ratio.to_p1_value() as usize);
out.push(';');
write_number(&mut out, background_mode.to_p2_value() as usize);
out.push_str(";0q");
for (i, c) in palette.iter().enumerate() {
let r = (c.r as u32 * 100) / 255;
let g = (c.g as u32 * 100) / 255;
let b = (c.b as u32 * 100) / 255;
out.push('#');
write_number(&mut out, i);
out.push(';');
out.push('2');
out.push(';');
write_number(&mut out, r as usize);
out.push(';');
write_number(&mut out, g as usize);
out.push(';');
write_number(&mut out, b as usize);
}
let bands = height.div_ceil(6);
for band in 0..bands {
let y0 = band * 6;
let y_max = usize::min(y0 + 6, height);
let mut colors_used = [false; 256];
for y in y0..y_max {
for x in 0..width {
let pixel_idx = y * width + x;
if opacity_mask[pixel_idx] {
let idx = indices[pixel_idx] as usize;
colors_used[idx] = true;
}
}
}
for (color_index, &is_used) in colors_used.iter().enumerate().take(palette.len()) {
if !is_used {
continue; }
out.push('#');
write_number(&mut out, color_index);
let mut x = 0;
while x < width {
let mut bits: u8 = 0;
for bit in 0..6 {
let y = y0 + bit;
if y >= y_max {
break;
}
let pixel_idx = y * width + x;
if opacity_mask[pixel_idx] && indices[pixel_idx] as usize == color_index {
bits |= 1 << bit;
}
}
let mut run_len = 1usize;
while x + run_len < width {
let mut bits_next: u8 = 0;
for bit in 0..6 {
let y = y0 + bit;
if y >= y_max {
break;
}
let pixel_idx = y * width + (x + run_len);
if opacity_mask[pixel_idx] && indices[pixel_idx] as usize == color_index {
bits_next |= 1 << bit;
}
}
if bits_next != bits {
break;
}
run_len += 1;
}
if run_len > 3 {
out.push('!');
write_number(&mut out, run_len);
out.push((63 + bits) as char);
} else {
let ch = (63 + bits) as char;
for _ in 0..run_len {
out.push(ch);
}
}
x += run_len;
}
out.push('$');
}
out.push('-');
}
out.push('\x1b');
out.push('\\');
Ok(out)
}
#[inline]
fn write_number(out: &mut String, mut n: usize) {
if n == 0 {
out.push('0');
return;
}
let mut buf = [0u8; 20];
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
out.push_str(unsafe { std::str::from_utf8_unchecked(&buf[i..]) });
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(deprecated)]
fn test_encode_simple() {
let rgba = vec![255u8, 0, 0, 255]; let result = sixel_encode(&rgba, 1, 1, &EncodeOptions::default());
assert!(result.is_ok());
let sixel = result.unwrap();
assert!(sixel.starts_with("\x1bP9;1;0q"));
assert!(sixel.ends_with("\x1b\\"));
}
#[test]
#[allow(deprecated)]
fn test_encode_2x2() {
let rgba = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ];
let result = sixel_encode(&rgba, 2, 2, &EncodeOptions::default());
assert!(result.is_ok());
}
#[test]
#[allow(deprecated)]
fn test_encode_is_set_to_square_pixels() {
let rgba = vec![
255, 0, 0, 255, 0, 0, 255, 255, ];
let sixel = sixel_encode(&rgba, 2, 1, &EncodeOptions::default()).unwrap();
assert!(sixel.contains("\x1bP9;"));
}
#[test]
#[allow(deprecated)]
fn test_invalid_dimensions() {
let rgba = vec![0u8; 16];
assert!(sixel_encode(&rgba, 0, 4, &EncodeOptions::default()).is_err());
assert!(sixel_encode(&rgba, 4, 0, &EncodeOptions::default()).is_err());
assert!(sixel_encode(&rgba, 10, 10, &EncodeOptions::default()).is_err());
}
}