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`], [`BitSixelEncoder`], or
18//!     [`OctreeSixelEncoder`].
19//! - I'm _really_ time constrained and can sacrifice a little quality:
20//!   - Use [`BitSixelEncoder<NoDither>`].
21//!
22//! For a more detailed breakdown, here's the encoders by average speed and
23//! quality against the test images (speed figures will vary) at 256 colors with
24//! Sierra dithering:
25//!
26//! | Algorithm         |    MSE | DSSIM  | Execution Time (ms) | Initial Buckets |
27//! | :---------------- | -----: | :----: | ------------------: | --------------: |
28//! | adu               |  15.56 | 0.0054 |                1473 |             N/A |
29//! | bit               |  36.42 | 0.0132 |                 367 |             N/A |
30//! | bit-merge-low     |  12.10 | 0.0046 |                 559 |            2^14 |
31//! | bit-merge         |  10.96 | 0.0040 |                1198 |            2^18 |
32//! | bit-merge-better  |  10.77 | 0.0039 |                2196 |            2^20 |
33//! | bit-merge-best    |  10.75 | 0.0039 |                2850 |            2^21 |
34//! | focal             |  11.72 | 0.0043 |                3253 |            2^21 |
35//! | k-means           |  10.86 | 0.0040 |                6208 |             N/A |
36//! | k-medians         |  18.68 | 0.0075 |               10688 |             N/A |
37//! | median-cut        |  20.27 | 0.0061 |                 627 |             N/A |
38//! | octree (max-heap) |  66.60 | 0.0163 |                 589 |             N/A |
39//! | octree (min-heap) | 332.29 | 0.0890 |                 552 |             N/A |
40//!
41//!
42//! Here's the encoders at 16 colors with Sierra dithering:
43//! | Algorithm         |    MSE | DSSIM  | Execution Time (ms) | Initial Buckets |
44//! | :---------------- | -----: | :----: | ------------------: | --------------: |
45//! | adu               | 120.45 | 0.0371 |                 280 |             N/A |
46//! | bit               | 182.07 | 0.0492 |                 247 |             N/A |
47//! | bit-merge         |  98.10 | 0.0307 |                 993 |            2^18 |
48//! | focal             | 107.17 | 0.0335 |                3045 |            2^21 |
49//! | k-means           |  97.02 | 0.0297 |                 664 |             N/A |
50//! | k-medians         | 171.20 | 0.0486 |                5792 |             N/A |
51//! | median-cut        | 168.88 | 0.0381 |                 317 |             N/A |
52//! | octree (max-heap) | 546.05 | 0.0922 |                 341 |             N/A |
53//! | octree (min-heap) | 879.79 | 0.2536 |                 349 |             N/A |
54
55pub mod adu;
56pub mod bit;
57pub mod bitmerge;
58pub mod dither;
59pub mod focal;
60pub mod kmeans;
61pub mod kmedians;
62pub mod median_cut;
63pub mod octree;
64
65use std::{
66    fmt::Write,
67    sync::atomic::{
68        AtomicBool,
69        Ordering,
70    },
71};
72
73use image::{
74    Rgb,
75    RgbImage,
76};
77use palette::{
78    encoding::Srgb,
79    Hsl,
80    IntoColor,
81    Lab,
82};
83use rayon::{
84    iter::{
85        IndexedParallelIterator,
86        IntoParallelRefIterator,
87        ParallelIterator,
88    },
89    slice::ParallelSlice,
90};
91
92pub use crate::{
93    adu::ADUPaletteBuilder,
94    bit::BitPaletteBuilder,
95    bitmerge::BitMergePaletteBuilder,
96    focal::FocalPaletteBuilder,
97    kmeans::KMeansPaletteBuilder,
98    kmedians::KMediansPaletteBuilder,
99    median_cut::MedianCutPaletteBuilder,
100    octree::OctreePaletteBuilder,
101};
102use crate::{
103    adu::ADUSixelEncoder256,
104    bit::BitSixelEncoder256,
105    bitmerge::BitMergeSixelEncoder256,
106    dither::{
107        Dither,
108        Sierra,
109    },
110    focal::FocalSixelEncoder256,
111    kmeans::KMeansSixelEncoder256,
112    kmedians::KMediansSixelEncoder256,
113    median_cut::MedianCutSixelEncoder256,
114    octree::OctreeSixelEncoder256,
115};
116
117struct SixelRow<'c> {
118    committed: &'c mut String,
119    pending: char,
120    count: usize,
121}
122
123impl<'c> SixelRow<'c> {
124    fn new(builder: &'c mut String, color: usize) -> Self {
125        builder
126            .write_fmt(format_args!("#{color}"))
127            .expect("Failed to write color selector");
128
129        Self {
130            committed: builder,
131            pending: num2six(0),
132            count: 0,
133        }
134    }
135
136    fn push(&mut self, ch: char) {
137        if ch == self.pending {
138            self.count += 1;
139        } else {
140            self.commit();
141            self.pending = ch;
142            self.count = 1;
143        }
144    }
145
146    fn commit(&mut self) {
147        if self.count > 3 {
148            self.committed
149                .write_fmt(format_args!("!{}{}", self.count, self.pending))
150                .expect("Failed to write to string");
151        } else {
152            for _ in 0..self.count {
153                self.committed.push(self.pending);
154            }
155        }
156    }
157
158    fn finalize(mut self) {
159        self.commit();
160        self.committed.push('$');
161    }
162}
163
164mod private {
165    pub trait Sealed {}
166}
167
168/// A trait for types that perform quantization of an image to a target palette
169/// size.
170pub trait PaletteBuilder: private::Sealed {
171    const PALETTE_SIZE: usize;
172    const NAME: &'static str;
173
174    /// Take in an image and return a quantized palette based on the colors in
175    /// the image. The returned vector may be `<= PALETTE_SIZE` in length.
176    fn build_palette(image: &RgbImage) -> Vec<Lab>;
177}
178
179const fn num2six(num: u8) -> char {
180    (0x3f + num) as char
181}
182
183/// The main type for performing sixel encoding.
184///
185/// It is provided with two generic parameters:
186/// - A [`PaletteBuilder`] to generate a color palette from the input image
187///   (sixel only supports up to 256 colors).
188/// - A [`Dither`] type to apply dithering to the reduced color image before
189///   encoding it into sixel format.
190///
191/// A number of type aliases are provided for common configurations, such as
192/// [`ADUSixelEncoder256`], which uses the [`ADUPaletteBuilder`] with 256
193/// colors.
194///
195/// # Choosing a `PaletteBuilder`
196/// - [`BitMergePaletteBuilder`] or [`KMeansPaletteBuilder`] are good default
197///   choices for minimizing the error across the image.
198/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
199///   other details that other encoders might squash, but is experimental. It is
200///   a weighted k-means implementation. Depending on the image, `KMeans` may be
201///   able to capture these highlights already, but it's worth trying if you're
202///   trying to preserve specific image characteristics.
203///
204/// # Choosing a `Dither`
205/// - [`Sierra`] is a good default choice for dithering, as it produces
206///   high-quality results with minimal artifacts.
207/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
208pub struct SixelEncoder<P: PaletteBuilder = FocalPaletteBuilder<256>, D: Dither = Sierra> {
209    _p: std::marker::PhantomData<P>,
210    _d: std::marker::PhantomData<D>,
211}
212
213pub type ADUSixelEncoder<D = Sierra> = ADUSixelEncoder256<D>;
214pub type BitMergeSixelEncoderLow<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 14 }>;
215pub type BitMergeSixelEncoder<D = Sierra> = BitMergeSixelEncoder256<D>;
216pub type BitMergeSixelEncoderBetter<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 20 }>;
217pub type BitMergeSixelEncoderBest<D = Sierra> = BitMergeSixelEncoder256<D, { 1 << 21 }>;
218pub type BitSixelEncoder<D = Sierra> = BitSixelEncoder256<D>;
219pub type FocalSixelEncoder<D = Sierra> = FocalSixelEncoder256<D>;
220pub type KMeansSixelEncoder<D = Sierra> = KMeansSixelEncoder256<D>;
221pub type KMediansSixelEncoder<D = Sierra> = KMediansSixelEncoder256<D>;
222pub type MedianCutSixelEncoder<D = Sierra> = MedianCutSixelEncoder256<D>;
223pub type OctreeSixelEncoder<D = Sierra, const USE_MIN_HEAP: bool = false> =
224    OctreeSixelEncoder256<D, USE_MIN_HEAP>;
225
226impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
227    pub fn encode(image: &RgbImage) -> String {
228        let palette = P::build_palette(image);
229
230        let mut sixel_string = r#"Pq"1;1;"#.to_string();
231        sixel_string
232            .write_fmt(format_args!("{};{}", image.height(), image.width()))
233            .expect("Failed to write sixel bounds");
234
235        for (i, lab) in palette.iter().copied().enumerate() {
236            let hsl: Hsl = lab.into_color();
237            // Sixel hue is offset by 120 degrees from the common hue values.
238            let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
239
240            sixel_string
241                .write_fmt(format_args!(
242                    "#{i};1;{deg};{};{}",
243                    (hsl.lightness * 100.0).round() as u8,
244                    (hsl.saturation * 100.0).round() as u8,
245                ))
246                .expect("Failed to palette entry");
247        }
248
249        let paletted_pixels = D::dither_and_palettize(image, &palette, P::PALETTE_SIZE);
250
251        #[cfg(feature = "dump_mse")]
252        {
253            let dequant = paletted_pixels
254                .iter()
255                .map(|&idx| palette[idx])
256                .collect::<Vec<_>>();
257            let mse = dequant
258                .par_iter()
259                .zip(image.par_pixels())
260                .map(|(l, rgb)| {
261                    use palette::color_difference::EuclideanDistance;
262                    let lab = rgb_to_lab(*rgb);
263                    lab.distance_squared(*l)
264                })
265                .sum::<f32>()
266                / (image.width() * image.height()) as f32;
267
268            println!("MSE: {:.2} ({} colors)", mse, P::PALETTE_SIZE);
269        }
270
271        #[cfg(feature = "dump_dssim")]
272        {
273            use dssim_core::Dssim;
274
275            let dssim = Dssim::new();
276            let image_pixels = image
277                .pixels()
278                .copied()
279                .map(|Rgb([r, g, b])| rgb::RGB::new(r, g, b))
280                .collect::<Vec<_>>();
281            let orig = dssim
282                .create_image_rgb(
283                    &image_pixels,
284                    image.width() as usize,
285                    image.height() as usize,
286                )
287                .unwrap();
288
289            let palette_pixels = paletted_pixels
290                .iter()
291                .map(|&idx| {
292                    let lab = palette[idx];
293                    let rgb: palette::Srgb = lab.into_color();
294                    let rgb = rgb.into_format::<u8>();
295                    rgb::RGB::new(rgb.red, rgb.green, rgb.blue)
296                })
297                .collect::<Vec<_>>();
298            let new = dssim
299                .create_image_rgb(
300                    &palette_pixels,
301                    image.width() as usize,
302                    image.height() as usize,
303                )
304                .unwrap();
305
306            let (dssim, _) = dssim.compare(&orig, &new);
307
308            println!("DSSIM: {:.4} ({} colors)", dssim, P::PALETTE_SIZE);
309        }
310
311        #[cfg(feature = "dump_image")]
312        {
313            use std::hash::{
314                BuildHasher,
315                Hasher,
316                RandomState,
317            };
318
319            let mut output_image = image::ImageBuffer::new(image.width(), image.height());
320            for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
321                let lab = palette[idx];
322                let rgb: palette::Srgb = lab.into_color();
323                let rgb = rgb.into_format::<u8>();
324                *pixel = Rgb([rgb.red, rgb.green, rgb.blue]);
325            }
326            let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
327
328            output_image
329                .save(format!("{}-{rand}.png", P::NAME))
330                .expect("Failed to save output image");
331        }
332
333        let rows: Vec<&[usize]> = paletted_pixels
334            .chunks(image.width() as usize)
335            .collect::<Vec<_>>();
336
337        let mut strings = vec![String::new(); rows.len().div_ceil(6)];
338        rows.par_chunks(6)
339            .zip(&mut strings)
340            .for_each(|(stack, sixel_string)| {
341                let row_palette =
342                    Vec::from_iter((0..P::PALETTE_SIZE).map(|_| AtomicBool::new(false)));
343                stack
344                    .par_iter()
345                    .flat_map(|row| row.par_iter().copied())
346                    .for_each(|idx| {
347                        row_palette[idx].store(true, Ordering::Relaxed);
348                    });
349
350                for (color, _) in row_palette
351                    .iter()
352                    .enumerate()
353                    .filter(|(_, v)| v.load(Ordering::Relaxed))
354                {
355                    let mut stack_string = SixelRow::new(sixel_string, color);
356                    for idx in 0..stack[0].len() {
357                        let bits = (stack[0][idx] == color) as u8
358                            | ((stack.get(1).map(|r| r[idx]) == Some(color)) as u8) << 1
359                            | ((stack.get(2).map(|r| r[idx]) == Some(color)) as u8) << 2
360                            | ((stack.get(3).map(|r| r[idx]) == Some(color)) as u8) << 3
361                            | ((stack.get(4).map(|r| r[idx]) == Some(color)) as u8) << 4
362                            | ((stack.get(5).map(|r| r[idx]) == Some(color)) as u8) << 5;
363                        let char = num2six(bits);
364                        stack_string.push(char);
365                    }
366                    stack_string.finalize();
367                }
368                sixel_string.push('-');
369            });
370
371        sixel_string.extend(strings);
372        sixel_string.push_str(r#"\"#);
373
374        sixel_string
375    }
376}
377
378fn rgb_to_lab(Rgb([r, g, b]): Rgb<u8>) -> Lab {
379    palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
380        .into_format::<f32>()
381        .into_color()
382}