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::RgbImage;
8//!
9//! let img = RgbImage::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};
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
201/// A trait for types that perform quantization of an image to a target palette
202/// size.
203pub trait PaletteBuilder: private::Sealed {
204    const PALETTE_SIZE: usize;
205    const NAME: &'static str;
206
207    /// Take in an image and return a quantized palette based on the colors in
208    /// the image. The returned vector may be `<= PALETTE_SIZE` in length.
209    fn build_palette(image: &RgbImage) -> Vec<Lab>;
210}
211
212const fn num2six(num: u8) -> char {
213    (0x3f + num) as char
214}
215
216/// The main type for performing sixel encoding.
217///
218/// It is provided with two generic parameters:
219/// - A [`PaletteBuilder`] to generate a color palette from the input image
220///   (sixel only supports up to 256 colors).
221/// - A [`Dither`] type to apply dithering to the reduced color image before
222///   encoding it into sixel format.
223///
224/// A number of type aliases are provided for common configurations, such as
225/// [`ADUSixelEncoder256`], which uses the [`ADUPaletteBuilder`] with 256
226/// colors.
227///
228/// # Choosing a `PaletteBuilder`
229/// - [`BitMergePaletteBuilder`] or [`KMeansPaletteBuilder`] are good default
230///   choices for minimizing the error across the image.
231/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
232///   other details that other encoders might squash, but is experimental. It is
233///   a weighted k-means implementation. Depending on the image, `KMeans` may be
234///   able to capture these highlights already, but it's worth trying if you're
235///   trying to preserve specific image characteristics.
236///
237/// # Choosing a `Dither`
238/// - [`Sierra`] is a good default choice for dithering, as it produces
239///   high-quality results with minimal artifacts.
240/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
241pub 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            // Sixel hue is offset by 120 degrees from the common hue values.
283            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}