a_sixel/
lib.rs

1//! A-Sixel library for encoding sixel images.
2//!
3//! ### Basic Usage
4//!
5//! ```rust
6//! use a_sixel::KMeansSixelEncoder;
7//! use image::RgbImage;
8//!
9//! let img = RgbImage::new(100, 100);
10//! println!("{}", <KMeansSixelEncoder>::encode(&img));
11//! ```
12//!
13//! ## Choosing an Encoder
14//! - I want fast encoding with good quality:
15//!   - Use `KMeansSixelEncoder` or `ADUSixelEncoder`.
16//! - I'm time constrained:
17//!   - Use `ADUSixelEncoder`, `BitSixelEncoder`, or `OctreeSixelEncoder`. You
18//!     can customize `ADU` by lowering the `STEPS` parameter to run faster if
19//!     necessary while still getting good results.
20//! - I'm _really_ time constrained and can sacrifice a little quality:
21//!   - Use `BitSixelEncoder<NoDither>`.
22//! - I want high quality encoding, and don't mind a bit more computation:
23//!   - Use `FocalSixelEncoder`.
24//!   - This matters a lot less if you're not crunching the palette down below
25//!     256 colors.
26//!   - Note that this an experimental encoder. It will *likely* produce
27//!     comparable or better results than other encoders, but may not always do
28//!     so. On the test images, for my personal preferences, I think it's
29//!     slightly better - particularly at small palette sizes. It works by
30//!     computing weights for each pixel based on saliancy maps and measures of
31//!     local statistics. These weighted pixels are then fed into a weighted
32//!     k-means algorithm to produce a palette.
33
34pub mod adu;
35pub mod bit;
36pub mod dither;
37pub mod focal;
38pub mod kmeans;
39pub mod median_cut;
40pub mod octree;
41
42use std::{
43    fmt::Write,
44    sync::atomic::{
45        AtomicBool,
46        Ordering,
47    },
48};
49
50use image::{
51    Rgb,
52    RgbImage,
53};
54use palette::{
55    encoding::Srgb,
56    Hsl,
57    IntoColor,
58    Lab,
59};
60use rayon::{
61    iter::{
62        IndexedParallelIterator,
63        IntoParallelRefIterator,
64        ParallelIterator,
65    },
66    slice::ParallelSlice,
67};
68
69pub use crate::{
70    adu::ADUPaletteBuilder,
71    bit::BitPaletteBuilder,
72    focal::FocalPaletteBuilder,
73    kmeans::KMeansPaletteBuilder,
74    median_cut::MedianCutPaletteBuilder,
75    octree::OctreePaletteBuilder,
76};
77use crate::{
78    adu::ADUSixelEncoder256,
79    bit::BitSixelEncoder256,
80    dither::{
81        Dither,
82        Sierra,
83    },
84    focal::FocalSixelEncoder256,
85    kmeans::KMeansSixelEncoder256,
86    median_cut::MedianCutSixelEncoder256,
87    octree::OctreeSixelEncoder256,
88};
89
90struct SixelRow<'c> {
91    committed: &'c mut String,
92    pending: char,
93    count: usize,
94}
95
96impl<'c> SixelRow<'c> {
97    fn new(builder: &'c mut String, color: usize) -> Self {
98        builder
99            .write_fmt(format_args!("#{color}"))
100            .expect("Failed to write color selector");
101
102        Self {
103            committed: builder,
104            pending: num2six(0),
105            count: 0,
106        }
107    }
108
109    fn push(&mut self, ch: char) {
110        if ch == self.pending {
111            self.count += 1;
112        } else {
113            self.commit();
114            self.pending = ch;
115            self.count = 1;
116        }
117    }
118
119    fn commit(&mut self) {
120        if self.count > 3 {
121            self.committed
122                .write_fmt(format_args!("!{}{}", self.count, self.pending))
123                .expect("Failed to write to string");
124        } else {
125            for _ in 0..self.count {
126                self.committed.push(self.pending);
127            }
128        }
129    }
130
131    fn finalize(mut self) {
132        self.commit();
133        self.committed.push('$');
134    }
135}
136
137mod private {
138    pub trait Sealed {}
139}
140
141/// A trait for types that perform quantization of an image to a target palette
142/// size.
143pub trait PaletteBuilder: private::Sealed {
144    const PALETTE_SIZE: usize;
145
146    /// Take in an image and return a quantized palette based on the colors in
147    /// the image. The returned vector may be `<= PALETTE_SIZE` in length.
148    fn build_palette(image: &RgbImage) -> Vec<Lab>;
149}
150
151const fn num2six(num: u8) -> char {
152    (0x3f + num) as char
153}
154
155/// The main type for performing sixel encoding.
156///
157/// It is provided with two generic parameters:
158/// - A [`PaletteBuilder`] to generate a color palette from the input image
159///   (sixel only supports up to 256 colors).
160/// - A [`Dither`] type to apply dithering to the reduced color image before
161///   encoding it into sixel format.
162///
163/// A number of type aliases are provided for common configurations, such as
164/// [`ADUSixelEncoder256`], which uses the [`ADUPaletteBuilder`] with 256
165/// colors.
166///
167/// # Choosing a `PaletteBuilder`
168/// - [`ADUPaletteBuilder`] or [`KMeansPaletteBuilder`] are a good default
169///   choices for minimizing the error across the image.
170/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
171///   other color details that ADU might squash, but is experimental.
172/// - Other palette builders are available, but are likely to perform less well
173///   at image accuracy than these choices.
174///
175/// # Choosing a `Dither`
176/// - [`Sierra`] is a good default choice for dithering, as it produces
177///   high-quality results with minimal artifacts.
178/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
179pub struct SixelEncoder<P: PaletteBuilder = FocalPaletteBuilder, D: Dither = Sierra> {
180    _p: std::marker::PhantomData<P>,
181    _d: std::marker::PhantomData<D>,
182}
183
184pub type ADUSixelEncoder<D = Sierra> = ADUSixelEncoder256<D>;
185
186pub type FocalSixelEncoder<D = Sierra> = FocalSixelEncoder256<D>;
187
188pub type MedianCutSixelEncoder<D = Sierra> = MedianCutSixelEncoder256<D>;
189
190pub type BitSixelEncoder<D = Sierra> = BitSixelEncoder256<D>;
191
192pub type OctreeSixelEncoder<D = Sierra, const USE_MIN_HEAP: bool = false> =
193    OctreeSixelEncoder256<D, USE_MIN_HEAP>;
194
195pub type KMeansSixelEncoder<D = Sierra> = KMeansSixelEncoder256<D>;
196
197impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
198    pub fn encode(image: &RgbImage) -> String {
199        let palette = P::build_palette(image);
200
201        let mut sixel_string = r#"Pq"1;1;"#.to_string();
202        sixel_string
203            .write_fmt(format_args!("{};{}", image.height(), image.width()))
204            .expect("Failed to write sixel bounds");
205
206        for (i, lab) in palette.iter().copied().enumerate() {
207            let hsl: Hsl = lab.into_color();
208            // Sixel hue is offset by 120 degrees from the common hue values.
209            let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
210
211            sixel_string
212                .write_fmt(format_args!(
213                    "#{i};1;{deg};{};{}",
214                    (hsl.lightness * 100.0).round() as u8,
215                    (hsl.saturation * 100.0).round() as u8,
216                ))
217                .expect("Failed to palette entry");
218        }
219
220        let paletted_pixels = D::dither_and_palettize(image, &palette, P::PALETTE_SIZE);
221
222        let rows: Vec<&[usize]> = paletted_pixels
223            .chunks(image.width() as usize)
224            .collect::<Vec<_>>();
225
226        let mut strings = vec![String::new(); rows.len().div_ceil(6)];
227        rows.par_chunks(6)
228            .zip(&mut strings)
229            .for_each(|(stack, sixel_string)| {
230                let row_palette =
231                    Vec::from_iter((0..P::PALETTE_SIZE).map(|_| AtomicBool::new(false)));
232                stack
233                    .par_iter()
234                    .flat_map(|row| row.par_iter().copied())
235                    .for_each(|idx| {
236                        row_palette[idx].store(true, Ordering::Relaxed);
237                    });
238
239                for (color, _) in row_palette
240                    .iter()
241                    .enumerate()
242                    .filter(|(_, v)| v.load(Ordering::Relaxed))
243                {
244                    let mut stack_string = SixelRow::new(sixel_string, color);
245                    for idx in 0..stack[0].len() {
246                        let bits = (stack[0][idx] == color) as u8
247                            | ((stack.get(1).map(|r| r[idx]) == Some(color)) as u8) << 1
248                            | ((stack.get(2).map(|r| r[idx]) == Some(color)) as u8) << 2
249                            | ((stack.get(3).map(|r| r[idx]) == Some(color)) as u8) << 3
250                            | ((stack.get(4).map(|r| r[idx]) == Some(color)) as u8) << 4
251                            | ((stack.get(5).map(|r| r[idx]) == Some(color)) as u8) << 5;
252                        let char = num2six(bits);
253                        stack_string.push(char);
254                    }
255                    stack_string.finalize();
256                }
257                sixel_string.push('-');
258            });
259
260        sixel_string.extend(strings);
261        sixel_string.push_str(r#"\"#);
262
263        sixel_string
264    }
265}
266
267fn rgb_to_lab(Rgb([r, g, b]): Rgb<u8>) -> Lab {
268    palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
269        .into_format::<f32>()
270        .into_color()
271}