#![cfg_attr(all(doc, ENABLE_DOC_AUTO_CFG), feature(doc_cfg))]
#[cfg(feature = "adu")]
pub mod adu;
pub mod bit;
#[cfg(feature = "bit-merge")]
pub mod bitmerge;
pub mod dither;
#[cfg(feature = "focal")]
pub mod focal;
#[cfg(feature = "k-means")]
pub mod kmeans;
#[cfg(feature = "k-medians")]
pub mod kmedians;
#[cfg(feature = "median-cut")]
pub mod median_cut;
#[cfg(feature = "octree")]
pub mod octree;
#[cfg(feature = "wu")]
pub mod wu;
use image::Rgba;
use image::RgbaImage;
use palette::Hsl;
use palette::IntoColor;
use palette::Lab;
use palette::encoding::Srgb;
use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefMutIterator;
use rayon::iter::ParallelIterator;
use rayon::slice::ParallelSlice;
#[cfg(feature = "strum")]
use strum::Display;
#[cfg(feature = "strum")]
use strum::EnumString;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "strum", derive(EnumString, Display))]
#[cfg_attr(
feature = "strum",
strum(ascii_case_insensitive, serialize_all = "kebab-case")
)]
#[non_exhaustive]
pub enum PaletteBuilder {
#[cfg(feature = "adu")]
Adu,
Bit,
#[cfg(feature = "bit-merge")]
BitMergeLow,
#[cfg(feature = "bit-merge")]
BitMerge,
#[cfg(feature = "bit-merge")]
BitMergeBetter,
#[cfg(feature = "bit-merge")]
BitMergeBest,
#[cfg(feature = "focal")]
Focal,
#[cfg(feature = "k-means")]
KMeans,
#[cfg(feature = "k-medians")]
KMedians,
#[cfg(feature = "median-cut")]
MedianCut,
#[cfg(feature = "octree")]
Octree,
#[cfg(feature = "octree")]
OctreeMinHeap,
#[cfg(feature = "wu")]
Wu,
}
impl PaletteBuilder {
#[cfg(feature = "dump-image")]
fn name(&self) -> &'static str {
match self {
#[cfg(feature = "adu")]
Self::Adu => "ADU",
Self::Bit => "Bit",
#[cfg(feature = "bit-merge")]
Self::BitMergeLow | Self::BitMerge | Self::BitMergeBetter | Self::BitMergeBest => {
"Bit-Merge"
}
#[cfg(feature = "focal")]
Self::Focal => "Focal",
#[cfg(feature = "k-means")]
Self::KMeans => "K-Means",
#[cfg(feature = "k-medians")]
Self::KMedians => "K-Medians",
#[cfg(feature = "median-cut")]
Self::MedianCut => "Median-Cut",
#[cfg(feature = "octree")]
Self::Octree | Self::OctreeMinHeap => "Octree",
#[cfg(feature = "wu")]
Self::Wu => "Wu",
}
}
fn build_palette(
&self,
image: &RgbaImage,
palette_size: usize,
) -> Vec<Lab> {
match self {
#[cfg(feature = "adu")]
Self::Adu => adu::ADUPaletteBuilder::build_palette(image, palette_size),
Self::Bit => bit::BitPaletteBuilder::build_palette(image, palette_size),
#[cfg(feature = "bit-merge")]
Self::BitMergeLow => {
bitmerge::BitMergePaletteBuilder::<{ 1 << 14 }>::build_palette(image, palette_size)
}
#[cfg(feature = "bit-merge")]
Self::BitMerge => {
<bitmerge::BitMergePaletteBuilder>::build_palette(image, palette_size)
}
#[cfg(feature = "bit-merge")]
Self::BitMergeBetter => {
bitmerge::BitMergePaletteBuilder::<{ 1 << 20 }>::build_palette(image, palette_size)
}
#[cfg(feature = "bit-merge")]
Self::BitMergeBest => {
bitmerge::BitMergePaletteBuilder::<{ 1 << 21 }>::build_palette(image, palette_size)
}
#[cfg(feature = "focal")]
Self::Focal => focal::FocalPaletteBuilder::build_palette(image, palette_size),
#[cfg(feature = "k-means")]
Self::KMeans => kmeans::KMeansPaletteBuilder::build_palette(image, palette_size),
#[cfg(feature = "k-medians")]
Self::KMedians => kmedians::KMediansPaletteBuilder::build_palette(image, palette_size),
#[cfg(feature = "median-cut")]
Self::MedianCut => {
median_cut::MedianCutPaletteBuilder::build_palette(image, palette_size)
}
#[cfg(feature = "octree")]
Self::Octree => {
octree::OctreePaletteBuilder::<false>::build_palette(image, palette_size)
}
#[cfg(feature = "octree")]
Self::OctreeMinHeap => {
octree::OctreePaletteBuilder::<true>::build_palette(image, palette_size)
}
#[cfg(feature = "wu")]
Self::Wu => wu::WuPaletteBuilder::build_palette(image, palette_size),
}
}
fn build_bucketer(
&self,
palette: &[Lab],
palette_size: usize,
) -> dither::PaletteBucketer {
match self {
Self::Bit => dither::PaletteBucketer::Bit(bit::BitPaletteBuilder::build_bucketer(
palette,
palette_size,
)),
_ => dither::PaletteBucketer::KdTree(dither::KdTreeBucketer::new(palette)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SixelEncoder {
pub algorithm: PaletteBuilder,
pub dither: dither::Dither,
}
impl Default for SixelEncoder {
fn default() -> Self {
Self {
algorithm: PaletteBuilder::Bit,
dither: dither::Dither::Sierra,
}
}
}
impl SixelEncoder {
pub fn new(
algorithm: PaletteBuilder,
dither: dither::Dither,
) -> Self {
Self { algorithm, dither }
}
pub fn encode(
&self,
rgba: RgbaImage,
) -> String {
self.encode_with_palette_size(rgba, 256)
}
pub fn encode_with_palette_size(
&self,
#[allow(unused_mut)] mut image: RgbaImage,
palette_size: usize,
) -> String {
#[cfg(feature = "partial-transparency")]
{
use std::sync::LazyLock;
use std::time::Duration;
static BG_COLOR: LazyLock<Rgba<u8>> = LazyLock::new(|| {
termbg::rgb(Duration::from_millis(100))
.map(|rgb| {
Rgba([
(rgb.r as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
(rgb.g as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
(rgb.b as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
u8::MAX,
])
})
.unwrap_or(Rgba([0, 0, 0, u8::MAX]))
});
image.par_pixels_mut().for_each(|pixel| {
use image::Pixel;
use image::Rgba;
let mut color = Rgba([BG_COLOR[0], BG_COLOR[1], BG_COLOR[2], pixel[3]]);
color.blend(pixel);
*pixel = color;
});
}
let palette = if image.width().saturating_mul(image.height()) < palette_size as u32 {
image.pixels().copied().map(rgba_to_lab).collect::<Vec<_>>()
} else {
self.algorithm.build_palette(&image, palette_size)
};
let mut sixel_string = r#"P9;1q"1;1;"#.to_string();
push_usize(&mut sixel_string, image.width() as usize);
sixel_string.push(';');
push_usize(&mut sixel_string, image.height() as usize);
if image.width() > 0 && image.height() > 0 {
for (i, lab) in palette.iter().copied().enumerate() {
let hsl: Hsl = lab.into_color();
let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
sixel_string.push('#');
push_usize(&mut sixel_string, i);
sixel_string.push_str(";1;");
push_usize(&mut sixel_string, deg as usize);
sixel_string.push(';');
push_usize(&mut sixel_string, (hsl.lightness * 100.0).round() as usize);
sixel_string.push(';');
push_usize(&mut sixel_string, (hsl.saturation * 100.0).round() as usize);
}
let bucketer = self.algorithm.build_bucketer(&palette, palette_size);
let paletted_pixels = self
.dither
.dither_and_palettize(&image, &palette, &bucketer);
#[cfg(feature = "dump-mse")]
{
use rayon::iter::IntoParallelRefIterator;
let dequant = paletted_pixels
.iter()
.map(|&idx| palette[idx])
.collect::<Vec<_>>();
let mse = dequant
.par_iter()
.zip(image.par_pixels())
.map(|(l, rgb)| {
use palette::color_difference::EuclideanDistance;
let lab = rgba_to_lab(*rgb);
lab.distance_squared(*l)
})
.sum::<f32>()
/ (image.width() * image.height()) as f32;
println!("MSE: {:.2} ({} colors)", mse, palette_size);
}
#[cfg(feature = "dump-delta-e")]
{
use rayon::iter::IntoParallelRefIterator;
let dequant = paletted_pixels
.iter()
.map(|&idx| palette[idx])
.collect::<Vec<_>>();
let differences = image
.par_pixels()
.copied()
.zip(dequant.par_iter())
.map(|(rgb, lab)| {
use palette::color_difference::ImprovedCiede2000;
let lab_rgb = rgba_to_lab(rgb);
lab_rgb.improved_difference(*lab)
})
.collect::<Vec<_>>();
let mean_diff =
differences.iter().sum::<f32>() / (image.width() * image.height()) as f32;
let max_diff = differences.iter().copied().fold(0.0, f32::max);
let two_three_threshold = differences.iter().copied().filter(|d| *d > 2.3).count()
as f32
/ (image.width() * image.height()) as f32;
let five_threshold = differences.iter().copied().filter(|d| *d > 5.0).count()
as f32
/ (image.width() * image.height()) as f32;
println!("Mean DeltaE: {:.2} ({} colors)", mean_diff, palette_size);
println!("Max DeltaE: {:.2} ({} colors)", max_diff, palette_size);
println!(
"DeltaE > 2.3: {:.2} ({} colors)",
two_three_threshold, palette_size
);
println!(
"DeltaE > 5.0: {:.2} ({} colors)",
five_threshold, palette_size
);
}
#[cfg(feature = "dump-dssim")]
{
use dssim_core::Dssim;
let dssim = Dssim::new();
let image_pixels = image
.pixels()
.copied()
.map(|Rgba([r, g, b, _])| rgb::RGB::new(r, g, b))
.collect::<Vec<_>>();
let orig = dssim
.create_image_rgb(
&image_pixels,
image.width() as usize,
image.height() as usize,
)
.unwrap();
let palette_pixels = paletted_pixels
.iter()
.map(|&idx| {
let lab = palette[idx];
let rgb: palette::Srgb = lab.into_color();
let rgb = rgb.into_format::<u8>();
rgb::RGB::new(rgb.red, rgb.green, rgb.blue)
})
.collect::<Vec<_>>();
let new = dssim
.create_image_rgb(
&palette_pixels,
image.width() as usize,
image.height() as usize,
)
.unwrap();
let (dssim, _) = dssim.compare(&orig, &new);
println!("DSSIM: {:.4} ({} colors)", dssim, palette_size);
}
#[cfg(feature = "dump-phash")]
{
use image_hasher::FilterType;
use image_hasher::HashAlg;
use image_hasher::HasherConfig;
let mut output_image = image::ImageBuffer::new(image.width(), image.height());
for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
let lab = palette[idx];
let rgb: palette::Srgb = lab.into_color();
let rgb = rgb.into_format::<u8>();
*pixel = Rgba([rgb.red, rgb.green, rgb.blue, u8::MAX]);
}
let hasher = HasherConfig::new()
.hash_alg(HashAlg::DoubleGradient)
.resize_filter(FilterType::Lanczos3)
.hash_size(32, 32)
.to_hasher();
let hash_in = hasher.hash_image(&image);
let hash_out = hasher.hash_image(&output_image);
println!(
"Hash Distance: {} ({} colors)",
hash_in.dist(&hash_out),
palette_size
);
}
#[cfg(feature = "dump-image")]
{
use std::hash::BuildHasher;
use std::hash::Hasher;
use std::hash::RandomState;
let mut output_image = image::ImageBuffer::new(image.width(), image.height());
for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
let lab = palette[idx];
let rgb: palette::Srgb = lab.into_color();
let rgb = rgb.into_format::<u8>();
*pixel = Rgba([rgb.red, rgb.green, rgb.blue, u8::MAX]);
}
let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
output_image
.save(format!("{}-{rand}.png", self.algorithm.name()))
.expect("Failed to save output image");
}
let width = image.width() as usize;
let num_chunks = (paletted_pixels.len() / width).div_ceil(6);
let chunk_capacity = width * 7;
let mut strings =
Vec::from_iter((0..num_chunks).map(|_| String::with_capacity(chunk_capacity)));
paletted_pixels
.par_chunks(width * 6)
.zip(image.into_raw().par_chunks(width * 6 * 4))
.zip(&mut strings)
.for_each(|((palette_chunk, rgba_chunk), sixel_string)| {
let mut color_bits = vec![0u8; palette_size * width];
let mut color_used = vec![false; palette_size];
let chunk_height = palette_chunk.len() / width;
for row in 0..chunk_height {
let bit = 1u8 << row;
let row_offset = row * width;
for col in 0..width {
let pixel_idx = row_offset + col;
if rgba_chunk[pixel_idx * 4 + 3] != 0 {
let color = palette_chunk[pixel_idx];
color_bits[color * width + col] |= bit;
color_used[color] = true;
}
}
}
color_bits.par_iter_mut().for_each(|d| {
*d += 0x3f;
});
let color_bits = unsafe { String::from_utf8_unchecked(color_bits) };
for (color, _) in color_used.iter().enumerate().filter(|(_, u)| **u) {
sixel_string.push('#');
push_usize(sixel_string, color);
let base = color * width;
sixel_string.push_str(&color_bits[base..base + width]);
sixel_string.push('$');
}
sixel_string.push('-');
});
sixel_string.extend(strings);
}
sixel_string.push_str(r#"\"#);
sixel_string
}
}
fn rgba_to_lab(Rgba([r, g, b, _]): Rgba<u8>) -> Lab {
palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
.into_format::<f32>()
.into_color()
}
fn push_usize(
s: &mut String,
n: usize,
) {
s.push_str(itoa::Buffer::new().format(n));
}