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