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,
}
struct BitMask {
words: Vec<u64>,
}
impl BitMask {
fn zeros(len: usize) -> Self {
Self {
words: vec![0; len.div_ceil(64)],
}
}
#[inline]
fn set(&mut self, index: usize) {
self.words[index >> 6] |= 1u64 << (index & 63);
}
#[inline]
fn get(&self, index: usize) -> bool {
(self.words[index >> 6] >> (index & 63)) & 1 != 0
}
}
#[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 pixel_count = width * height;
let mut opacity_mask = BitMask::zeros(pixel_count);
let mut rgb_pixels: Vec<Srgb<u8>> = Vec::with_capacity(pixel_count);
for (i, c) in rgba.chunks_exact(4).enumerate() {
if c[3] >= 128 {
opacity_mask.set(i);
}
rgb_pixels.push(Srgb::new(c[0], c[1], c[2]));
}
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: &BitMask,
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");
out.push('"');
write_number(&mut out, aspect_ratio.pad() as usize);
out.push(';');
write_number(&mut out, aspect_ratio.pan() as usize);
out.push(';');
write_number(&mut out, width);
out.push(';');
write_number(&mut out, height);
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);
let palette_len = palette.len();
let mut sixels = vec![0u8; palette_len * width];
let mut colors_used = vec![false; palette_len];
for band in 0..bands {
let y0 = band * 6;
let y_max = usize::min(y0 + 6, height);
for (color_index, used) in colors_used.iter_mut().enumerate() {
if *used {
sixels[color_index * width..(color_index + 1) * width].fill(0);
*used = false;
}
}
for y in y0..y_max {
let bit = 1u8 << (y - y0);
let row = y * width;
for x in 0..width {
let pixel_idx = row + x;
if opacity_mask.get(pixel_idx) {
let color_index = indices[pixel_idx] as usize;
sixels[color_index * width + x] |= bit;
colors_used[color_index] = true;
}
}
}
for color_index in 0..palette_len {
if !colors_used[color_index] {
continue; }
out.push('#');
write_number(&mut out, color_index);
let row = &sixels[color_index * width..(color_index + 1) * width];
let mut x = 0;
while x < width {
let bits = row[x];
let mut run_len = 1usize;
while x + run_len < width && row[x + run_len] == bits {
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\"1;1;1;1"));
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;"));
assert!(sixel.contains("\"1;1;2;1"));
}
#[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());
}
}