a_sixel/
lib.rs

1//! A-Sixel library for encoding sixel images.
2//!
3//! ### Basic Usage
4//!
5//! ```rust
6//! use a_sixel::ADUSixelEncoder;
7//! use image::RgbImage;
8//!
9//! let img = RgbImage::new(100, 100);
10//! println!("{}", <ADUSixelEncoder>::encode(&img));
11//! ```
12//!
13//! ## Choosing an Encoder
14//! - I want fast encoding with good quality:
15//!   - Use [`ADUSixelEncoder`]
16//!   - [`BitSixelEncoder`] can also produce pretty good results for 256 colors
17//!     depending on the image while being over 10x faster.
18//! - I'm time constrained:
19//!   - Use [`ADUSixelEncoder`] or [`BitSixelEncoder`]. You can customize `ADU`
20//!     by lowering the `STEPS` parameter to run faster if necessary while still
21//!     getting good results.
22//! - I'm _really_ time constrained and can sacrifice a little quality:
23//!   - Use [`BitSixelEncoder<NoDither>`].
24//! - I want high quality encoding, and don't mind a bit more computation:
25//!   - Use [`FocalSixelEncoder`].
26//!   - This matters a lot less if you're not crunching the palette down below
27//!     256 colors.
28//!   - Note that this an experimental encoder. It will *likely* produce better
29//!     results than just [`ADUSixelEncoder`], but it may not always do so. On
30//!     the test images, for my personal preferences, I think it's slightly
31//!     better - particularly at small palette sizes.
32//!
33//!     <details>
34//!     <summary>How it works</summary>
35//!
36//!     Under the hood, it is a modified version of the `ADUSixelEncoder`
37//!     that uses a weighted selection algorithm for its sample pixels. These
38//!     weights are determined based on saliency maps and measures of
39//!     statistical noise in the image.
40//!
41//!     In addition to the weighted selection, the distance metric used to
42//!     determine which cluster to place a pixel into also incorporates the
43//!     weight. Similar pixels with different weights will be nudged towards
44//!     clusters with similar weights. This is a mild effect, but it seems
45//!     to improve things over basic clustering when there are a lot of similar
46//!     colors in an image.
47//!
48//!     </details>
49
50mod adu;
51mod bit;
52pub mod dither;
53mod focal;
54mod median_cut;
55
56use std::fmt::Write;
57
58use image::{
59    Rgb,
60    RgbImage,
61};
62use palette::{
63    encoding::Srgb,
64    Hsl,
65    IntoColor,
66    Lab,
67};
68use rayon::{
69    iter::{
70        IndexedParallelIterator,
71        ParallelIterator,
72    },
73    slice::ParallelSlice,
74};
75
76use crate::dither::{
77    Dither,
78    Sierra,
79};
80pub use crate::{
81    adu::ADUPaletteBuilder,
82    bit::BitPaletteBuilder,
83    focal::FocalPaletteBuilder,
84    median_cut::MedianCutPaletteBuilder,
85};
86
87struct SixelRow<'c> {
88    committed: &'c mut String,
89    pending: char,
90    count: usize,
91}
92
93impl<'c> SixelRow<'c> {
94    fn new(builder: &'c mut String, color: usize) -> Self {
95        builder
96            .write_fmt(format_args!("#{color}"))
97            .expect("Failed to write color selector");
98
99        Self {
100            committed: builder,
101            pending: num2six(0),
102            count: 0,
103        }
104    }
105
106    fn push(&mut self, ch: char) {
107        if ch == self.pending {
108            self.count += 1;
109        } else {
110            self.commit();
111            self.pending = ch;
112            self.count = 1;
113        }
114    }
115
116    fn commit(&mut self) {
117        if self.count > 3 {
118            self.committed
119                .write_fmt(format_args!("!{}{}", self.count, self.pending))
120                .expect("Failed to write to string");
121        } else {
122            for _ in 0..self.count {
123                self.committed.push(self.pending);
124            }
125        }
126    }
127
128    fn finalize(mut self) {
129        self.commit();
130        self.committed.push('$');
131    }
132}
133
134mod private {
135    pub trait Sealed {}
136}
137
138pub trait PaletteBuilder: private::Sealed {
139    const PALETTE_SIZE: usize;
140
141    fn build_palette(image: &RgbImage) -> Vec<Lab>;
142}
143
144const fn num2six(num: u8) -> char {
145    (0x3f + num) as char
146}
147
148/// The main type for performing sixel encoding.
149///
150/// It is provided with two generic parameters:
151/// - A [`PaletteBuilder`] to generate a color palette from the input image
152///   (sixel only supports up to 256 colors).
153/// - A [`Dither`] type to apply dithering to the reduced color image before
154///   encoding it into sixel format.
155///
156/// A number of type aliases are provided for common configurations, such as
157/// [`ADUSixelEncoder256`], which uses the [`ADUPaletteBuilder`] with 256
158/// colors.
159///
160/// # Choosing a `PaletteBuilder`
161/// - [`ADUPaletteBuilder`] is a good default choice for minimizing the error
162///   across the image. For maximum color accuracy at the cost of speed, you can
163///   use [`ADUSixelEncoder256High`].
164/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
165///   other color details that ADU might squash, but is experimental and much
166///   slower. You can increase the number of steps to improve accuracy even
167///   futher, as is done by [`FocalSixelEncoder256High`].
168/// - Other palette builders are available, but are likely to perform less well
169///   at image accuracy than either of these two.
170///
171/// # Choosing a `Dither`
172/// - [`Sierra`] is a good default choice for dithering, as it produces
173///   high-quality results with minimal artifacts.
174/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
175pub struct SixelEncoder<P: PaletteBuilder = FocalPaletteBuilder, D: Dither = Sierra> {
176    _p: std::marker::PhantomData<P>,
177    _d: std::marker::PhantomData<D>,
178}
179
180pub type ADUSixelEncoder8<D = Sierra> = SixelEncoder<ADUPaletteBuilder<8, 1, { 1 << 17 }>, D>;
181pub type ADUSixelEncoder16<D = Sierra> = SixelEncoder<ADUPaletteBuilder<16, 1, { 1 << 17 }>, D>;
182pub type ADUSixelEncoder32<D = Sierra> = SixelEncoder<ADUPaletteBuilder<32, 2, { 1 << 17 }>, D>;
183pub type ADUSixelEncoder64<D = Sierra> = SixelEncoder<ADUPaletteBuilder<64, 4, { 1 << 17 }>, D>;
184pub type ADUSixelEncoder128<D = Sierra> = SixelEncoder<ADUPaletteBuilder<128, 8, { 1 << 17 }>, D>;
185pub type ADUSixelEncoder256<D = Sierra> = SixelEncoder<ADUPaletteBuilder<256, 16, { 1 << 17 }>, D>;
186pub type ADUSixelEncoder256High<D = Sierra> = SixelEncoder<ADUPaletteBuilder, D>;
187pub type ADUSixelEncoder<D = Sierra> = ADUSixelEncoder256<D>;
188
189pub type FocalSixelEncoderMono<D = Sierra> = SixelEncoder<FocalPaletteBuilder<2>, D>;
190pub type FocalSixelEncoder4<D = Sierra> = SixelEncoder<FocalPaletteBuilder<4>, D>;
191pub type FocalSixelEncoder8<D = Sierra> = SixelEncoder<FocalPaletteBuilder<8>, D>;
192pub type FocalSixelEncoder16<D = Sierra> = SixelEncoder<FocalPaletteBuilder<16>, D>;
193pub type FocalSixelEncoder32<D = Sierra> = SixelEncoder<FocalPaletteBuilder<32>, D>;
194pub type FocalSixelEncoder64<D = Sierra> = SixelEncoder<FocalPaletteBuilder<64>, D>;
195pub type FocalSixelEncoder128<D = Sierra> = SixelEncoder<FocalPaletteBuilder<128>, D>;
196pub type FocalSixelEncoder256<D = Sierra> = SixelEncoder<FocalPaletteBuilder<256>, D>;
197pub type FocalSixelEncoder256High<D = Sierra> =
198    SixelEncoder<FocalPaletteBuilder<256, { 1 << 12 }, { 1 << 22 }>, D>;
199pub type FocalSixelEncoder<D = Sierra> = FocalSixelEncoder256<D>;
200
201pub type MedianCutSixelEncoderMono<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<2>, D>;
202pub type MedianCutSixelEncoder4<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<4>, D>;
203pub type MedianCutSixelEncoder8<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<8>, D>;
204pub type MedianCutSixelEncoder16<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<16>, D>;
205pub type MedianCutSixelEncoder32<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<32>, D>;
206pub type MedianCutSixelEncoder64<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<64>, D>;
207pub type MedianCutSixelEncoder128<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<128>, D>;
208pub type MedianCutSixelEncoder256<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder<256>, D>;
209pub type MedianCutSixelEncoder<D = Sierra> = MedianCutSixelEncoder256<D>;
210
211pub type BitSixelEncoderMono<D = Sierra> = SixelEncoder<BitPaletteBuilder<2>, D>;
212pub type BitSixelEncoder4<D = Sierra> = SixelEncoder<BitPaletteBuilder<4>, D>;
213pub type BitSixelEncoder8<D = Sierra> = SixelEncoder<BitPaletteBuilder<8>, D>;
214pub type BitSixelEncoder16<D = Sierra> = SixelEncoder<BitPaletteBuilder<16>, D>;
215pub type BitSixelEncoder32<D = Sierra> = SixelEncoder<BitPaletteBuilder<32>, D>;
216pub type BitSixelEncoder64<D = Sierra> = SixelEncoder<BitPaletteBuilder<64>, D>;
217pub type BitSixelEncoder128<D = Sierra> = SixelEncoder<BitPaletteBuilder<128>, D>;
218pub type BitSixelEncoder256<D = Sierra> = SixelEncoder<BitPaletteBuilder<256>, D>;
219pub type BitSixelEncoder<D = Sierra> = BitSixelEncoder256<D>;
220
221impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
222    pub fn encode(image: RgbImage) -> String {
223        let palette = P::build_palette(&image);
224
225        let mut sixel_string = r#"Pq"1;1;"#.to_string();
226        sixel_string
227            .write_fmt(format_args!("{};{}", image.height(), image.width()))
228            .expect("Failed to write sixel bounds");
229
230        for (i, lab) in palette.iter().copied().enumerate() {
231            let hsl: Hsl = lab.into_color();
232            // Sixel hue is offset by 120 degrees from the common hue values.
233            let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
234
235            sixel_string
236                .write_fmt(format_args!(
237                    "#{i};1;{deg};{};{}",
238                    (hsl.lightness * 100.0).round() as u8,
239                    (hsl.saturation * 100.0).round() as u8,
240                ))
241                .expect("Failed to palette entry");
242        }
243
244        let paletted_pixels = D::dither_and_palettize(&image, &palette, P::PALETTE_SIZE);
245
246        let rows: Vec<&[usize]> = paletted_pixels
247            .chunks(image.width() as usize)
248            .collect::<Vec<_>>();
249
250        let mut strings = vec![String::new(); rows.len().div_ceil(6)];
251        rows.par_chunks(6)
252            .zip(&mut strings)
253            .for_each(|(stack, sixel_string)| {
254                let mut row_palette = vec![false; P::PALETTE_SIZE];
255                row_palette.fill(false);
256                for idx in
257                    stack_iter(stack).flat_map(|(((((zero, one), two), three), four), five)| {
258                        std::iter::once(zero)
259                            .chain(one)
260                            .chain(two)
261                            .chain(three)
262                            .chain(four)
263                            .chain(five)
264                    })
265                {
266                    row_palette[idx] = true;
267                }
268
269                for (color, _) in row_palette.iter().copied().enumerate().filter(|(_, v)| *v) {
270                    let mut stack_string = SixelRow::new(sixel_string, color);
271                    for (((((zero, one), two), three), four), five) in stack_iter(stack) {
272                        let bits = (zero == color) as u8
273                            | ((one == Some(color)) as u8) << 1
274                            | ((two == Some(color)) as u8) << 2
275                            | ((three == Some(color)) as u8) << 3
276                            | ((four == Some(color)) as u8) << 4
277                            | ((five == Some(color)) as u8) << 5;
278                        let char = num2six(bits);
279                        stack_string.push(char);
280                    }
281                    stack_string.finalize();
282                }
283                sixel_string.push('-');
284            });
285
286        sixel_string.extend(strings);
287        sixel_string.push_str(r#"\"#);
288
289        sixel_string
290    }
291}
292
293type StackTuple = (
294    (
295        (((usize, Option<usize>), Option<usize>), Option<usize>),
296        Option<usize>,
297    ),
298    Option<usize>,
299);
300
301fn stack_iter<'a>(stack: &'a [&[usize]]) -> impl Iterator<Item = StackTuple> + 'a {
302    stack
303        .first()
304        .into_iter()
305        .cloned()
306        .flatten()
307        .copied()
308        .zip(
309            stack
310                .get(1)
311                .into_iter()
312                .cloned()
313                .flatten()
314                .copied()
315                .map(Some)
316                .chain(std::iter::repeat(None)),
317        )
318        .zip(
319            stack
320                .get(2)
321                .into_iter()
322                .cloned()
323                .flatten()
324                .copied()
325                .map(Some)
326                .chain(std::iter::repeat(None)),
327        )
328        .zip(
329            stack
330                .get(3)
331                .into_iter()
332                .cloned()
333                .flatten()
334                .copied()
335                .map(Some)
336                .chain(std::iter::repeat(None)),
337        )
338        .zip(
339            stack
340                .get(4)
341                .into_iter()
342                .cloned()
343                .flatten()
344                .copied()
345                .map(Some)
346                .chain(std::iter::repeat(None)),
347        )
348        .zip(
349            stack
350                .get(5)
351                .into_iter()
352                .cloned()
353                .flatten()
354                .copied()
355                .map(Some)
356                .chain(std::iter::repeat(None)),
357        )
358}
359
360fn rgb_to_lab(Rgb([r, g, b]): Rgb<u8>) -> Lab {
361    palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
362        .into_format::<f32>()
363        .into_color()
364}