1#![cfg_attr(all(doc, ENABLE_DOC_AUTO_CFG), feature(doc_auto_cfg))]
62
63#[cfg(feature = "adu")]
64pub mod adu;
65pub mod bit;
66#[cfg(feature = "bit-merge")]
67pub mod bitmerge;
68pub mod dither;
69#[cfg(feature = "focal")]
70pub mod focal;
71#[cfg(feature = "k-means")]
72pub mod kmeans;
73#[cfg(feature = "k-medians")]
74pub mod kmedians;
75#[cfg(feature = "median-cut")]
76pub mod median_cut;
77#[cfg(feature = "octree")]
78pub mod octree;
79#[cfg(feature = "wu")]
80pub mod wu;
81
82use std::{
83 fmt::Write,
84 sync::atomic::{
85 AtomicBool,
86 Ordering,
87 },
88};
89
90use image::{
91 Rgb,
92 RgbImage,
93};
94use palette::{
95 encoding::Srgb,
96 Hsl,
97 IntoColor,
98 Lab,
99};
100use rayon::{
101 iter::{
102 IndexedParallelIterator,
103 IntoParallelRefIterator,
104 ParallelIterator,
105 },
106 slice::ParallelSlice,
107};
108
109#[cfg(feature = "adu")]
110pub use crate::adu::ADUPaletteBuilder;
111#[cfg(feature = "adu")]
112use crate::adu::ADUSixelEncoder256;
113pub use crate::bit::BitPaletteBuilder;
114#[cfg(feature = "bit-merge")]
115pub use crate::bitmerge::BitMergePaletteBuilder;
116#[cfg(feature = "bit-merge")]
117use crate::bitmerge::BitMergeSixelEncoder256;
118#[cfg(feature = "focal")]
119pub use crate::focal::FocalPaletteBuilder;
120#[cfg(feature = "focal")]
121use crate::focal::FocalSixelEncoder256;
122#[cfg(feature = "k-means")]
123pub use crate::kmeans::KMeansPaletteBuilder;
124#[cfg(feature = "k-means")]
125use crate::kmeans::KMeansSixelEncoder256;
126#[cfg(feature = "k-medians")]
127pub use crate::kmedians::KMediansPaletteBuilder;
128#[cfg(feature = "k-medians")]
129use crate::kmedians::KMediansSixelEncoder256;
130#[cfg(feature = "median-cut")]
131pub use crate::median_cut::MedianCutPaletteBuilder;
132#[cfg(feature = "median-cut")]
133use crate::median_cut::MedianCutSixelEncoder256;
134#[cfg(feature = "octree")]
135pub use crate::octree::OctreePaletteBuilder;
136#[cfg(feature = "octree")]
137use crate::octree::OctreeSixelEncoder256;
138#[cfg(feature = "wu")]
139pub use crate::wu::WuPaletteBuilder;
140#[cfg(feature = "wu")]
141use crate::wu::WuSixelEncoder256;
142use crate::{
143 bit::BitSixelEncoder256,
144 dither::{
145 Dither,
146 Sierra,
147 },
148};
149
150struct SixelRow<'c> {
151 committed: &'c mut String,
152 pending: char,
153 count: usize,
154}
155
156impl<'c> SixelRow<'c> {
157 fn new(builder: &'c mut String, color: usize) -> Self {
158 builder
159 .write_fmt(format_args!("#{color}"))
160 .expect("Failed to write color selector");
161
162 Self {
163 committed: builder,
164 pending: num2six(0),
165 count: 0,
166 }
167 }
168
169 fn push(&mut self, ch: char) {
170 if ch == self.pending {
171 self.count += 1;
172 } else {
173 self.commit();
174 self.pending = ch;
175 self.count = 1;
176 }
177 }
178
179 fn commit(&mut self) {
180 if self.count > 3 {
181 self.committed
182 .write_fmt(format_args!("!{}{}", self.count, self.pending))
183 .expect("Failed to write to string");
184 } else {
185 for _ in 0..self.count {
186 self.committed.push(self.pending);
187 }
188 }
189 }
190
191 fn finalize(mut self) {
192 self.commit();
193 self.committed.push('$');
194 }
195}
196
197mod private {
198 pub trait Sealed {}
199}
200
201pub trait PaletteBuilder: private::Sealed {
204 const PALETTE_SIZE: usize;
205 const NAME: &'static str;
206
207 fn build_palette(image: &RgbImage) -> Vec<Lab>;
210}
211
212const fn num2six(num: u8) -> char {
213 (0x3f + num) as char
214}
215
216pub struct SixelEncoder<P: PaletteBuilder = BitPaletteBuilder<256>, D: Dither = Sierra> {
242 _p: std::marker::PhantomData<P>,
243 _d: std::marker::PhantomData<D>,
244}
245
246#[cfg(feature = "adu")]
247pub type ADUSixelEncoder<D = Sierra> = ADUSixelEncoder256<D>;
248#[cfg(feature = "bit-merge")]
249pub type BitMergeSixelEncoderLow<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 14 }>;
250#[cfg(feature = "bit-merge")]
251pub type BitMergeSixelEncoder<D = Sierra> = BitMergeSixelEncoder256<D>;
252#[cfg(feature = "bit-merge")]
253pub type BitMergeSixelEncoderBetter<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 20 }>;
254#[cfg(feature = "bit-merge")]
255pub type BitMergeSixelEncoderBest<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 21 }>;
256pub type BitSixelEncoder<D = Sierra> = BitSixelEncoder256<D>;
257#[cfg(feature = "focal")]
258pub type FocalSixelEncoder<D = Sierra> = FocalSixelEncoder256<D>;
259#[cfg(feature = "k-means")]
260pub type KMeansSixelEncoder<D = Sierra> = KMeansSixelEncoder256<D>;
261#[cfg(feature = "k-medians")]
262pub type KMediansSixelEncoder<D = Sierra> = KMediansSixelEncoder256<D>;
263#[cfg(feature = "median-cut")]
264pub type MedianCutSixelEncoder<D = Sierra> = MedianCutSixelEncoder256<D>;
265#[cfg(feature = "octree")]
266pub type OctreeSixelEncoder<D = Sierra, const USE_MIN_HEAP: bool = false> =
267 OctreeSixelEncoder256<D, USE_MIN_HEAP>;
268#[cfg(feature = "wu")]
269pub type WuSixelEncoder<D = Sierra> = WuSixelEncoder256<D>;
270
271impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
272 pub fn encode(image: &RgbImage) -> String {
273 let palette = P::build_palette(image);
274
275 let mut sixel_string = r#"Pq"1;1;"#.to_string();
276 sixel_string
277 .write_fmt(format_args!("{};{}", image.height(), image.width()))
278 .expect("Failed to write sixel bounds");
279
280 for (i, lab) in palette.iter().copied().enumerate() {
281 let hsl: Hsl = lab.into_color();
282 let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
284
285 sixel_string
286 .write_fmt(format_args!(
287 "#{i};1;{deg};{};{}",
288 (hsl.lightness * 100.0).round() as u8,
289 (hsl.saturation * 100.0).round() as u8,
290 ))
291 .expect("Failed to palette entry");
292 }
293
294 let paletted_pixels = D::dither_and_palettize(image, &palette, P::PALETTE_SIZE);
295
296 #[cfg(feature = "dump-mse")]
297 {
298 let dequant = paletted_pixels
299 .iter()
300 .map(|&idx| palette[idx])
301 .collect::<Vec<_>>();
302 let mse = dequant
303 .par_iter()
304 .zip(image.par_pixels())
305 .map(|(l, rgb)| {
306 use palette::color_difference::EuclideanDistance;
307 let lab = rgb_to_lab(*rgb);
308 lab.distance_squared(*l)
309 })
310 .sum::<f32>()
311 / (image.width() * image.height()) as f32;
312
313 println!("MSE: {:.2} ({} colors)", mse, P::PALETTE_SIZE);
314 }
315
316 #[cfg(feature = "dump-delta-e")]
317 {
318 let dequant = paletted_pixels
319 .iter()
320 .map(|&idx| palette[idx])
321 .collect::<Vec<_>>();
322 let differences = image
323 .par_pixels()
324 .copied()
325 .zip(dequant.par_iter())
326 .map(|(rgb, lab)| {
327 use palette::color_difference::ImprovedCiede2000;
328 let lab_rgb = rgb_to_lab(rgb);
329 lab_rgb.improved_difference(*lab)
330 })
331 .collect::<Vec<_>>();
332
333 let mean_diff =
334 differences.iter().sum::<f32>() / (image.width() * image.height()) as f32;
335 let max_diff = differences.iter().copied().fold(0.0, f32::max);
336 let two_three_threshold = differences.iter().copied().filter(|d| *d > 2.3).count()
337 as f32
338 / (image.width() * image.height()) as f32;
339 let five_threshold = differences.iter().copied().filter(|d| *d > 5.0).count() as f32
340 / (image.width() * image.height()) as f32;
341
342 println!("Mean DeltaE: {:.2} ({} colors)", mean_diff, P::PALETTE_SIZE);
343 println!("Max DeltaE: {:.2} ({} colors)", max_diff, P::PALETTE_SIZE);
344 println!(
345 "DeltaE > 2.3: {:.2} ({} colors)",
346 two_three_threshold,
347 P::PALETTE_SIZE
348 );
349 println!(
350 "DeltaE > 5.0: {:.2} ({} colors)",
351 five_threshold,
352 P::PALETTE_SIZE
353 );
354 }
355
356 #[cfg(feature = "dump-dssim")]
357 {
358 use dssim_core::Dssim;
359
360 let dssim = Dssim::new();
361 let image_pixels = image
362 .pixels()
363 .copied()
364 .map(|Rgb([r, g, b])| rgb::RGB::new(r, g, b))
365 .collect::<Vec<_>>();
366 let orig = dssim
367 .create_image_rgb(
368 &image_pixels,
369 image.width() as usize,
370 image.height() as usize,
371 )
372 .unwrap();
373
374 let palette_pixels = paletted_pixels
375 .iter()
376 .map(|&idx| {
377 let lab = palette[idx];
378 let rgb: palette::Srgb = lab.into_color();
379 let rgb = rgb.into_format::<u8>();
380 rgb::RGB::new(rgb.red, rgb.green, rgb.blue)
381 })
382 .collect::<Vec<_>>();
383 let new = dssim
384 .create_image_rgb(
385 &palette_pixels,
386 image.width() as usize,
387 image.height() as usize,
388 )
389 .unwrap();
390
391 let (dssim, _) = dssim.compare(&orig, &new);
392
393 println!("DSSIM: {:.4} ({} colors)", dssim, P::PALETTE_SIZE);
394 }
395
396 #[cfg(feature = "dump-phash")]
397 {
398 use image_hasher::{
399 FilterType,
400 HashAlg,
401 HasherConfig,
402 };
403
404 let mut output_image = image::ImageBuffer::new(image.width(), image.height());
405 for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
406 let lab = palette[idx];
407 let rgb: palette::Srgb = lab.into_color();
408 let rgb = rgb.into_format::<u8>();
409 *pixel = Rgb([rgb.red, rgb.green, rgb.blue]);
410 }
411
412 let hasher = HasherConfig::new()
413 .hash_alg(HashAlg::DoubleGradient)
414 .resize_filter(FilterType::Lanczos3)
415 .hash_size(32, 32)
416 .to_hasher();
417
418 let hash_in = hasher.hash_image(image);
419 let hash_out = hasher.hash_image(&output_image);
420
421 println!("Hash Distance: {}", hash_in.dist(&hash_out));
422 }
423
424 #[cfg(feature = "dump-image")]
425 {
426 use std::hash::{
427 BuildHasher,
428 Hasher,
429 RandomState,
430 };
431
432 let mut output_image = image::ImageBuffer::new(image.width(), image.height());
433 for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
434 let lab = palette[idx];
435 let rgb: palette::Srgb = lab.into_color();
436 let rgb = rgb.into_format::<u8>();
437 *pixel = Rgb([rgb.red, rgb.green, rgb.blue]);
438 }
439 let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
440
441 output_image
442 .save(format!("{}-{rand}.png", P::NAME))
443 .expect("Failed to save output image");
444 }
445
446 let rows: Vec<&[usize]> = paletted_pixels
447 .chunks(image.width() as usize)
448 .collect::<Vec<_>>();
449
450 let mut strings = vec![String::new(); rows.len().div_ceil(6)];
451 rows.par_chunks(6)
452 .zip(&mut strings)
453 .for_each(|(stack, sixel_string)| {
454 let row_palette =
455 Vec::from_iter((0..P::PALETTE_SIZE).map(|_| AtomicBool::new(false)));
456 stack
457 .par_iter()
458 .flat_map(|row| row.par_iter().copied())
459 .for_each(|idx| {
460 row_palette[idx].store(true, Ordering::Relaxed);
461 });
462
463 for (color, _) in row_palette
464 .iter()
465 .enumerate()
466 .filter(|(_, v)| v.load(Ordering::Relaxed))
467 {
468 let mut stack_string = SixelRow::new(sixel_string, color);
469 for idx in 0..stack[0].len() {
470 let bits = (stack[0][idx] == color) as u8
471 | ((stack.get(1).map(|r| r[idx]) == Some(color)) as u8) << 1
472 | ((stack.get(2).map(|r| r[idx]) == Some(color)) as u8) << 2
473 | ((stack.get(3).map(|r| r[idx]) == Some(color)) as u8) << 3
474 | ((stack.get(4).map(|r| r[idx]) == Some(color)) as u8) << 4
475 | ((stack.get(5).map(|r| r[idx]) == Some(color)) as u8) << 5;
476 let char = num2six(bits);
477 stack_string.push(char);
478 }
479 stack_string.finalize();
480 }
481 sixel_string.push('-');
482 });
483
484 sixel_string.extend(strings);
485 sixel_string.push_str(r#"\"#);
486
487 sixel_string
488 }
489}
490
491fn rgb_to_lab(Rgb([r, g, b]): Rgb<u8>) -> Lab {
492 palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
493 .into_format::<f32>()
494 .into_color()
495}