a_sixel/
lib.rs

1//! A-Sixel library for encoding sixel images.
2//!
3//! ### Basic Usage
4//!
5//! ```rust
6//! use a_sixel::BitMergeSixelEncoderBest;
7//! use image::RgbaImage;
8//!
9//! let img = RgbaImage::new(100, 100);
10//! println!("{}", <BitMergeSixelEncoderBest>::encode(img));
11//! ```
12//!
13//! ## Choosing an Encoder
14//! - I want good quality:
15//!   - Use [`BitMergeSixelEncoderBest`] or [`KMeansSixelEncoder`].
16//! - I'm time constrained:
17//!   - Use [`BitMergeSixelEncoderLow`], or [`BitSixelEncoder`].
18//! - I'm _really_ time constrained and can sacrifice a little quality:
19//!   - Use [`BitSixelEncoder<NoDither>`].
20//!
21//! For a more detailed breakdown, here's the encoders by average speed and
22//! quality against the test images (speed figures will vary) at 256 colors with
23//! Sierra dithering:
24//!
25//! | Algorithm        |   MSE | DSSIM  | PHash Distance | Mean ΔE | Max ΔE | ΔE >2.3 | ΔE >5.0 | Execution Time (ms) |
26//! | :--------------- | ----: | :----: | -------------: | ------: | -----: | ------: | ------: | ------------------: |
27//! | adu              | 15.07 | 0.0053 |           8.66 |    1.79 |  12.88 |   31.6% |    4.4% |                1416 |
28//! | bit              | 35.82 | 0.0132 |          31.14 |    3.16 |  11.03 |   64.5% |   15.1% |                 426 |
29//! | bit-no-dither    | 31.78 | 0.0214 |          39.10 |    3.09 |  10.23 |   64.0% |   13.4% |                 274 |
30//! | bit-merge-low    | 10.67 | 0.0038 |          13.97 |    1.95 |   9.98 |   32.4% |    2.2% |                 785 |
31//! | bit-merge        | 10.36 | 0.0037 |          13.55 |    1.89 |  10.01 |   31.0% |    2.2% |                 932 |
32//! | bit-merge-better | 10.31 | 0.0037 |          13.45 |    1.85 |  10.21 |   30.6% |    2.2% |                1275 |
33//! | bit-merge-best   | 10.29 | 0.0037 |          13.52 |    1.83 |  10.26 |   30.6% |    2.2% |                1496 |
34//! | focal            | 14.62 | 0.0056 |          19.97 |    2.30 |   9.16 |   45.3% |    3.3% |                2428 |
35//! | k-means          | 10.07 | 0.0036 |          13.07 |    1.80 |  10.17 |   29.1% |    2.2% |                2996 |
36//! | k-medians        | 17.67 | 0.0068 |          21.07 |    2.61 |  10.17 |   53.6% |    5.1% |                7305 |
37//! | median-cut       | 19.63 | 0.0059 |          16.45 |    2.24 |  10.36 |   42.2% |    5.9% |                 692 |
38//! | octree           | 54.48 | 0.0148 |          26.03 |    3.89 |  12.49 |   78.6% |   25.4% |                 682 |
39//! | wu               | 17.89 | 0.0068 |          21.03 |    2.34 |  10.24 |   46.3% |    5.1% |                1853 |
40//!
41//! **Note:** Execution time _includes_ the time taken to compute error
42//! statistics - this is non-trivial. For example, exclusive of error statistics
43//! computation, bit-no-dither takes <100ms on average. Performance figures will
44//! vary based on machine, etc. They are only useful for comparing algorithms
45//! against each other within this dataset.
46//!
47//! Here's the encoders at 16 colors with Sierra dithering:
48//!
49//! | Algorithm  |    MSE | DSSIM | PHash Distance | Mean ΔE | Max ΔE | ΔE >2.3 | ΔE >5.0 | Execution Time (ms) |
50//! | :--------- | -----: | :---: | -------------: | ------: | -----: | ------: | ------: | ------------------: |
51//! | adu        | 116.85 | 0.036 |          39.83 |    4.02 |  18.39 |     66% |     33% |                 332 |
52//! | bit        | 178.47 | 0.049 |          59.79 |    5.53 |  16.61 |     89% |     51% |                 307 |
53//! | bit-merge  |  95.17 | 0.030 |          41.52 |    3.95 |  16.16 |     67% |     31% |                 712 |
54//! | focal      | 118.57 | 0.035 |          48.59 |    4.36 |  16.88 |     72% |     34% |                2150 |
55//! | k-means    |  99.36 | 0.031 |          43.10 |    3.99 |  16.41 |     67% |     31% |                 637 |
56//! | k-medians  | 166.88 | 0.050 |          60.59 |    5.48 |  16.77 |     88% |     52% |                5447 |
57//! | median-cut | 164.52 | 0.037 |          45.28 |    4.68 |  16.72 |     74% |     42% |                 374 |
58//! | octree     | 459.37 | 0.085 |          75.07 |    7.69 |  18.89 |     98% |     74% |                 446 |
59//! | wu         | 125.84 | 0.039 |          50.52 |    4.48 |  16.70 |     75% |     39% |                 906 |
60
61#![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
202/// A trait for types that perform quantization of an image to a target palette
203/// size.
204pub trait PaletteBuilder: private::Sealed {
205    const PALETTE_SIZE: usize;
206    const NAME: &'static str;
207
208    /// Take in an image and return a quantized palette based on the colors in
209    /// the image. The returned vector may be `<= PALETTE_SIZE` in length.
210    fn build_palette(image: &RgbImage) -> Vec<Lab>;
211}
212
213const fn num2six(num: u8) -> char {
214    (0x3f + num) as char
215}
216
217/// The main type for performing sixel encoding.
218///
219/// It is provided with two generic parameters:
220/// - A [`PaletteBuilder`] to generate a color palette from the input image
221///   (sixel only supports up to 256 colors).
222/// - A [`Dither`] type to apply dithering to the reduced color image before
223///   encoding it into sixel format.
224///
225/// A number of type aliases are provided for common configurations, such as
226/// [`ADUSixelEncoder256`], which uses the [`ADUPaletteBuilder`] with 256
227/// colors.
228///
229/// # Choosing a `PaletteBuilder`
230/// - [`BitMergePaletteBuilder`] or [`KMeansPaletteBuilder`] are good default
231///   choices for minimizing the error across the image.
232/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
233///   other details that other encoders might squash, but is experimental. It is
234///   a weighted k-means implementation. Depending on the image, `KMeans` may be
235///   able to capture these highlights already, but it's worth trying if you're
236///   trying to preserve specific image characteristics.
237///
238/// # Choosing a `Dither`
239/// - [`Sierra`] is a good default choice for dithering, as it produces
240///   high-quality results with minimal artifacts.
241/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
242pub 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 = &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            // Sixel hue is offset by 120 degrees from the common hue values.
321            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}