#![cfg_attr(all(doc, ENABLE_DOC_AUTO_CFG), feature(doc_auto_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 std::fmt::Write;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use image::Rgb;
use image::RgbImage;
use image::RgbaImage;
use palette::Hsl;
use palette::IntoColor;
use palette::Lab;
use palette::encoding::Srgb;
use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator;
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;
#[cfg(feature = "bit-merge")]
use crate::dither::Dither;
#[cfg(feature = "bit-merge")]
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;
struct SixelRow<'c> {
committed: &'c mut String,
pending: char,
count: usize,
}
impl<'c> SixelRow<'c> {
fn new(
builder: &'c mut String,
color: usize,
) -> Self {
builder
.write_fmt(format_args!("#{color}"))
.expect("Failed to write color selector");
Self {
committed: builder,
pending: num2six(0),
count: 0,
}
}
fn push(
&mut self,
ch: char,
) {
if ch == self.pending {
self.count += 1;
} else {
self.commit();
self.pending = ch;
self.count = 1;
}
}
fn commit(&mut self) {
if self.count > 3 {
self.committed
.write_fmt(format_args!("!{}{}", self.count, self.pending))
.expect("Failed to write to string");
} else {
for _ in 0..self.count {
self.committed.push(self.pending);
}
}
}
fn finalize(mut self) {
self.commit();
self.committed.push('$');
}
}
mod private {
pub trait Sealed {}
}
pub trait PaletteBuilder: private::Sealed {
const NAME: &'static str;
fn build_palette(
image: &RgbImage,
palette_size: usize,
) -> Vec<Lab>;
}
const fn num2six(num: u8) -> char {
(0x3f + num) as char
}
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 rgba: RgbaImage,
palette_size: usize,
) -> String {
#[cfg(feature = "partial-transparency")]
{
use std::time::Duration;
let bg_color = termbg::rgb(Duration::from_millis(100))
.map(|rgb| {
Rgb([
(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,
])
})
.unwrap_or(Rgb([0, 0, 0]));
rgba.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 image = RgbImage::from_raw(
rgba.width(),
rgba.height(),
rgba.pixels()
.flat_map(|p| [p[0], p[1], p[2]])
.collect::<Vec<_>>(),
)
.unwrap();
let image = ℑ
let palette = if image.width().saturating_mul(image.height()) < palette_size as u32 {
image.pixels().copied().map(rgb_to_lab).collect::<Vec<_>>()
} else {
P::build_palette(image, palette_size)
};
let mut sixel_string = r#"P9;1q"1;1;"#.to_string();
sixel_string
.write_fmt(format_args!("{};{}", image.width(), image.height()))
.expect("Failed to write sixel bounds");
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
.write_fmt(format_args!(
"#{i};1;{deg};{};{}",
(hsl.lightness * 100.0).round() as u8,
(hsl.saturation * 100.0).round() as u8,
))
.expect("Failed to palette entry");
}
let paletted_pixels = D::dither_and_palettize(image, &palette);
#[cfg(feature = "dump-mse")]
{
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 = rgb_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")]
{
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 = rgb_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(|Rgb([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 = Rgb([rgb.red, rgb.green, rgb.blue]);
}
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 = Rgb([rgb.red, rgb.green, rgb.blue]);
}
let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
output_image
.save(format!("{}-{rand}.png", P::NAME))
.expect("Failed to save output image");
}
let rgba_pixels = rgba.pixels().collect::<Vec<_>>();
let rows = paletted_pixels
.chunks(image.width() as usize)
.zip(rgba_pixels.chunks(image.width() as usize))
.collect::<Vec<_>>();
let mut strings = vec![String::new(); rows.len().div_ceil(6)];
rows.par_chunks(6)
.zip(&mut strings)
.for_each(|(stack, sixel_string)| {
let row_palette =
Vec::from_iter((0..palette_size).map(|_| AtomicBool::new(false)));
stack
.par_iter()
.flat_map(|(row, _)| row.par_iter().copied())
.for_each(|idx| {
row_palette[idx].store(true, Ordering::Relaxed);
});
for (color, _) in row_palette
.iter()
.enumerate()
.filter(|(_, v)| v.load(Ordering::Relaxed))
{
let mut stack_string = SixelRow::new(sixel_string, color);
for idx in 0..stack[0].0.len() {
let bit0 = (stack[0].0[idx] == color && stack[0].1[idx][3] != 0) as u8;
let bit1 = (stack
.get(1)
.filter(|(_, v)| v[idx][3] != 0)
.map(|(r, _)| r[idx])
== Some(color)) as u8;
let bit2 = (stack
.get(2)
.filter(|(_, v)| v[idx][3] != 0)
.map(|(r, _)| r[idx])
== Some(color)) as u8;
let bit3 = (stack
.get(3)
.filter(|(_, v)| v[idx][3] != 0)
.map(|(r, _)| r[idx])
== Some(color)) as u8;
let bit4 = (stack
.get(4)
.filter(|(_, v)| v[idx][3] != 0)
.map(|(r, _)| r[idx])
== Some(color)) as u8;
let bit5 = (stack
.get(5)
.filter(|(_, v)| v[idx][3] != 0)
.map(|(r, _)| r[idx])
== Some(color)) as u8;
let bits = bit0
| (bit1 << 1)
| (bit2 << 2)
| (bit3 << 3)
| (bit4 << 4)
| (bit5 << 5);
let char = num2six(bits);
stack_string.push(char);
}
stack_string.finalize();
}
sixel_string.push('-');
});
sixel_string.extend(strings);
}
sixel_string.push_str(r#"\"#);
sixel_string
}
}
fn rgb_to_lab(Rgb([r, g, b]): Rgb<u8>) -> Lab {
palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
.into_format::<f32>()
.into_color()
}