#![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 = "adu")]
pub use crate::adu::ADUPaletteBuilder;
pub use crate::bit::BitPaletteBuilder;
#[cfg(feature = "bit-merge")]
pub use crate::bitmerge::BitMergePaletteBuilder;
use crate::dither::Dither;
use crate::dither::Sierra;
#[cfg(feature = "focal")]
pub use crate::focal::FocalPaletteBuilder;
#[cfg(feature = "k-means")]
pub use crate::kmeans::KMeansPaletteBuilder;
#[cfg(feature = "k-medians")]
pub use crate::kmedians::KMediansPaletteBuilder;
#[cfg(feature = "median-cut")]
pub use crate::median_cut::MedianCutPaletteBuilder;
#[cfg(feature = "octree")]
pub use crate::octree::OctreePaletteBuilder;
#[cfg(feature = "wu")]
pub use crate::wu::WuPaletteBuilder;
mod private {
pub trait Sealed {}
}
pub trait PaletteBuilder: private::Sealed {
const NAME: &'static str;
fn build_palette(
image: &RgbaImage,
palette_size: usize,
) -> Vec<Lab>;
fn build_bucketer(
palette: &[Lab],
_palette_size: usize,
) -> impl dither::PaletteBucketer {
dither::KdTreeBucketer::new(palette)
}
}
pub struct SixelEncoder<P: PaletteBuilder = BitPaletteBuilder, D: Dither = Sierra> {
_p: std::marker::PhantomData<P>,
_d: std::marker::PhantomData<D>,
}
#[cfg(feature = "adu")]
pub type ADUSixelEncoder<D = Sierra> = SixelEncoder<ADUPaletteBuilder, D>;
#[cfg(feature = "bit-merge")]
pub type BitMergeSixelEncoderLow<D = Sierra> = SixelEncoder<BitMergePaletteBuilder<{ 1 << 14 }>, D>;
#[cfg(feature = "bit-merge")]
pub type BitMergeSixelEncoder<D = Sierra> = SixelEncoder<BitMergePaletteBuilder, D>;
#[cfg(feature = "bit-merge")]
pub type BitMergeSixelEncoderBetter<D = Sierra> =
SixelEncoder<BitMergePaletteBuilder<{ 1 << 20 }>, D>;
#[cfg(feature = "bit-merge")]
pub type BitMergeSixelEncoderBest<D = Sierra> =
SixelEncoder<BitMergePaletteBuilder<{ 1 << 21 }>, D>;
pub type BitSixelEncoder<D = Sierra> = SixelEncoder<BitPaletteBuilder, D>;
#[cfg(feature = "focal")]
pub type FocalSixelEncoder<D = Sierra> = SixelEncoder<FocalPaletteBuilder, D>;
#[cfg(feature = "k-means")]
pub type KMeansSixelEncoder<D = Sierra> = SixelEncoder<KMeansPaletteBuilder, D>;
#[cfg(feature = "k-medians")]
pub type KMediansSixelEncoder<D = Sierra> = SixelEncoder<KMediansPaletteBuilder, D>;
#[cfg(feature = "median-cut")]
pub type MedianCutSixelEncoder<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder, D>;
#[cfg(feature = "octree")]
pub type OctreeSixelEncoder<D = Sierra, const USE_MIN_HEAP: bool = false> =
SixelEncoder<OctreePaletteBuilder<USE_MIN_HEAP>, D>;
#[cfg(feature = "wu")]
pub type WuSixelEncoder<D = Sierra> = SixelEncoder<WuPaletteBuilder, D>;
impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
pub fn encode(rgba: RgbaImage) -> String {
Self::encode_with_palette_size(rgba, 256)
}
pub fn encode_with_palette_size(
#[allow(unused_mut)] mut image: RgbaImage,
palette_size: usize,
) -> String {
#[cfg(feature = "partial-transparency")]
{
use std::time::Duration;
let bg_color = 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 {
P::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 = P::build_bucketer(&palette, palette_size);
let paletted_pixels = D::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", P::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));
}