a_sixel/lib.rs
1//! A sixel library for encoding images.
2//!
3//! ## Basic Usage
4//!
5//! ### Simple Encoding
6//!
7//! ```rust
8//! use a_sixel::BitMergeSixelEncoderBest;
9//! use image::RgbaImage;
10//!
11//! let img = RgbaImage::new(100, 100);
12//! println!("{}", <BitMergeSixelEncoderBest>::encode(img));
13//! ```
14//!
15//! ### Loading and Encoding an Image File
16//!
17//! ```rust
18//! use a_sixel::KMeansSixelEncoder;
19//! use image;
20//!
21//! // Load an image from file
22//! let image = image::open("examples/transparent.png").unwrap().to_rgba8();
23//!
24//! // Encode with default settings (256 colors, Sierra dithering)
25//! let sixel_output = <KMeansSixelEncoder>::encode(image);
26//! println!("{}", sixel_output);
27//! ```
28//!
29//! ### Custom Palette Size and Dithering
30//!
31//! ```rust
32//! use a_sixel::BitSixelEncoder;
33//! use a_sixel::dither::NoDither;
34//!
35//! let image = image::open("examples/transparent.png").unwrap().to_rgba8();
36//!
37//! // Use 16 colors with no dithering for faster encoding
38//! let sixel_output = BitSixelEncoder::<NoDither>::encode_with_palette_size(image, 16);
39//! println!("{}", sixel_output);
40//! ```
41//!
42//! ## Transparency
43//! By default, `a-sixel` handles transparency by setting any fully-transparent
44//! pixels to all-bits-zero. This translates to a transparent pixel in most
45//! sixel implementations, but some terminals may not support this.
46//!
47//! Sixel does not natively support partial transparency, but this library does
48//! have some support for rendering images as if partial transparency was
49//! supported. If the `partial-transparency` feature is enabled, `a-sixel` will
50//! query the terminal and attempt to determine the background color. Partially
51//! transparent pixels will then be blended with this background color before
52//! encoding. Note that with this approach, changing the terminal background
53//! color will not update partially transparent pixels to match. You will need
54//! to re-encode the image if the background color changes.
55//!
56//!
57//! ## Choosing an Encoder
58//! - I want good quality:
59//! - Use `BitMergeSixelEncoderBest` or `KMeansSixelEncoder`.
60//! - I'm time constrained:
61//! - Use `BitMergeSixelEncoderLow`, `BitSixelEncoder`, or
62//! `OctreeSixelEncoder`.
63//! - I'm _really_ time constrained and can sacrifice a little quality:
64//! - Use `BitSixelEncoder<NoDither>`.
65//!
66//! For a more detailed breakdown, here's the encoders by average speed and
67//! quality against the test images (speed figures will vary) at 256 colors with
68//! Sierra dithering:
69//!
70//! | Algorithm | MSE | DSSIM | PHash Distance | Mean ΔE | Max ΔE | ΔE >2.3 | ΔE >5.0 | Execution Time (ms) |
71//! | :--------------- | ----: | -----: | -------------: | ------: | -----: | ------: | ------: | ------------------: |
72//! | adu | 15.04 | 0.0052 | 8.86 | 1.79 | 12.80 | 31.8% | 4.4% | 1448 |
73//! | bit | 35.82 | 0.0132 | 31.14 | 3.16 | 11.03 | 64.5% | 15.1% | 468 |
74//! | bit-merge-low | 10.67 | 0.0038 | 13.97 | 1.95 | 9.98 | 32.4% | 2.2% | 855 |
75//! | bit-merge | 10.37 | 0.0037 | 13.48 | 1.89 | 10.03 | 31.0% | 2.2% | 1034 |
76//! | bit-merge-better | 10.30 | 0.0037 | 13.07 | 1.85 | 10.22 | 30.6% | 2.2% | 1301 |
77//! | bit-merge-best | 10.28 | 0.0037 | 13.59 | 1.83 | 10.20 | 30.5% | 2.2% | 1532 |
78//! | focal | 31.10 | 0.0091 | 19.72 | 3.34 | 8.41 | 73.9% | 13.1% | 821 |
79//! | k-means | 10.07 | 0.0036 | 13.28 | 1.80 | 10.14 | 29.1% | 2.2% | 3175 |
80//! | k-medians | 17.22 | 0.0067 | 19.10 | 2.56 | 9.98 | 50.8% | 4.7% | 9088 |
81//! | median-cut | 19.64 | 0.0059 | 16.45 | 2.24 | 10.36 | 42.2% | 5.9% | 740 |
82//! | octree | 54.48 | 0.0148 | 26.03 | 3.89 | 12.49 | 78.6% | 25.4% | 754 |
83//! | wu | 17.89 | 0.0068 | 21.03 | 2.34 | 10.24 | 46.3% | 5.1% | 1984 |
84//!
85//! **Note:** Execution time _includes_ the time taken to compute error
86//! statistics - this is non-trivial. For example, exclusive of error statistics
87//! computation, bit-no-dither takes <100ms on average. Performance figures will
88//! vary based on machine, etc. They are only useful for comparing algorithms
89//! against each other within this dataset.
90//!
91//! Here's the encoders at 16 colors with Sierra dithering:
92//!
93//! | Algorithm | MSE | DSSIM | PHash Distance | Mean ΔE | Max ΔE | ΔE >2.3 | ΔE >5.0 | Execution Time (ms) |
94//! | :--------------- | -----: | -----: | -------------: | ------: | -----: | ------: | ------: | ------------------: |
95//! | adu | 118.90 | 0.0364 | 40.86 | 4.04 | 18.38 | 65.7% | 33.8% | 357 |
96//! | bit | 178.47 | 0.0490 | 59.79 | 5.53 | 16.61 | 89.0% | 51.4% | 325 |
97//! | bit-merge-low | 95.61 | 0.0302 | 41.59 | 3.97 | 16.26 | 67.4% | 31.4% | 631 |
98//! | bit-merge | 94.53 | 0.0302 | 41.48 | 3.95 | 16.15 | 67.0% | 31.3% | 800 |
99//! | bit-merge-better | 96.11 | 0.0299 | 41.55 | 3.96 | 16.17 | 67.9% | 31.4% | 1078 |
100//! | bit-merge-best | 95.44 | 0.0297 | 41.69 | 3.96 | 16.46 | 67.0% | 31.4% | 1297 |
101//! | focal | 345.08 | 0.0585 | 56.66 | 7.23 | 16.65 | 95.6% | 74.8% | 433 |
102//! | k-means | 99.36 | 0.0309 | 42.83 | 3.99 | 16.39 | 66.9% | 31.4% | 702 |
103//! | k-medians | 173.95 | 0.0533 | 59.62 | 5.57 | 16.23 | 90.8% | 49.7% | 7255 |
104//! | median-cut | 164.52 | 0.0374 | 45.28 | 4.68 | 16.72 | 73.7% | 42.3% | 395 |
105//! | octree | 459.37 | 0.0845 | 75.03 | 7.69 | 18.87 | 98.3% | 73.5% | 477 |
106//! | wu | 125.84 | 0.0386 | 50.52 | 4.48 | 16.70 | 74.5% | 39.2% | 929 |
107#![cfg_attr(all(doc, ENABLE_DOC_AUTO_CFG), feature(doc_cfg))]
108
109#[cfg(feature = "adu")]
110pub mod adu;
111pub mod bit;
112#[cfg(feature = "bit-merge")]
113pub mod bitmerge;
114pub mod dither;
115#[cfg(feature = "focal")]
116pub mod focal;
117#[cfg(feature = "k-means")]
118pub mod kmeans;
119#[cfg(feature = "k-medians")]
120pub mod kmedians;
121#[cfg(feature = "median-cut")]
122pub mod median_cut;
123#[cfg(feature = "octree")]
124pub mod octree;
125#[cfg(feature = "wu")]
126pub mod wu;
127
128use image::Rgba;
129use image::RgbaImage;
130use palette::Hsl;
131use palette::IntoColor;
132use palette::Lab;
133use palette::encoding::Srgb;
134use rayon::iter::IndexedParallelIterator;
135use rayon::iter::IntoParallelRefMutIterator;
136use rayon::iter::ParallelIterator;
137use rayon::slice::ParallelSlice;
138
139#[cfg(feature = "adu")]
140pub use crate::adu::ADUPaletteBuilder;
141pub use crate::bit::BitPaletteBuilder;
142#[cfg(feature = "bit-merge")]
143pub use crate::bitmerge::BitMergePaletteBuilder;
144use crate::dither::Dither;
145use crate::dither::Sierra;
146#[cfg(feature = "focal")]
147pub use crate::focal::FocalPaletteBuilder;
148#[cfg(feature = "k-means")]
149pub use crate::kmeans::KMeansPaletteBuilder;
150#[cfg(feature = "k-medians")]
151pub use crate::kmedians::KMediansPaletteBuilder;
152#[cfg(feature = "median-cut")]
153pub use crate::median_cut::MedianCutPaletteBuilder;
154#[cfg(feature = "octree")]
155pub use crate::octree::OctreePaletteBuilder;
156#[cfg(feature = "wu")]
157pub use crate::wu::WuPaletteBuilder;
158
159mod private {
160 pub trait Sealed {}
161}
162
163pub trait PaletteBuilder: private::Sealed {
164 const NAME: &'static str;
165
166 /// Take in an image and return a quantized palette based on the colors in
167 /// the image. The returned vector may be `<= palette_size` in length.
168 fn build_palette(
169 image: &RgbaImage,
170 palette_size: usize,
171 ) -> Vec<Lab>;
172
173 /// Build a [`PaletteBucketer`](dither::PaletteBucketer) that maps pixels to
174 /// entries in the given palette. The default implementation returns a
175 /// [`KdTreeBucketer`](dither::KdTreeBucketer); palette builders with a
176 /// faster native mapping (e.g. [`BitPaletteBuilder`]) override this.
177 fn build_bucketer(
178 palette: &[Lab],
179 _palette_size: usize,
180 ) -> impl dither::PaletteBucketer {
181 dither::KdTreeBucketer::new(palette)
182 }
183}
184
185/// The main type for performing sixel encoding.
186///
187/// It is provided with two generic parameters:
188/// - A [`PaletteBuilder`] to generate a color palette from the input image
189/// (sixel only supports up to 256 colors).
190/// - A [`Dither`] type to apply dithering to the reduced color image before
191/// encoding it into sixel format.
192///
193/// A number of type aliases are provided for common configurations, such as
194/// [`ADUSixelEncoder`], which uses the [`ADUPaletteBuilder`].
195///
196/// # Choosing a `PaletteBuilder`
197/// - [`BitMergePaletteBuilder`] or [`KMeansPaletteBuilder`] are good default
198/// choices for minimizing the error across the image.
199/// - [`FocalPaletteBuilder`] is a good choice if the image has highlights and
200/// other details that other encoders might squash, but is experimental. It is
201/// a weighted k-means implementation. Depending on the image, `KMeans` may be
202/// able to capture these highlights already, but it's worth trying if you're
203/// trying to preserve specific image characteristics.
204///
205/// # Choosing a `Dither`
206/// - [`Sierra`] is a good default choice for dithering, as it produces
207/// high-quality results with minimal artifacts.
208/// - [`NoDither`](dither::NoDither) can be used if performance is a concern.
209pub struct SixelEncoder<P: PaletteBuilder = BitPaletteBuilder, D: Dither = Sierra> {
210 _p: std::marker::PhantomData<P>,
211 _d: std::marker::PhantomData<D>,
212}
213
214#[cfg(feature = "adu")]
215pub type ADUSixelEncoder<D = Sierra> = SixelEncoder<ADUPaletteBuilder, D>;
216#[cfg(feature = "bit-merge")]
217pub type BitMergeSixelEncoderLow<D = Sierra> = SixelEncoder<BitMergePaletteBuilder<{ 1 << 14 }>, D>;
218#[cfg(feature = "bit-merge")]
219pub type BitMergeSixelEncoder<D = Sierra> = SixelEncoder<BitMergePaletteBuilder, D>;
220#[cfg(feature = "bit-merge")]
221pub type BitMergeSixelEncoderBetter<D = Sierra> =
222 SixelEncoder<BitMergePaletteBuilder<{ 1 << 20 }>, D>;
223#[cfg(feature = "bit-merge")]
224pub type BitMergeSixelEncoderBest<D = Sierra> =
225 SixelEncoder<BitMergePaletteBuilder<{ 1 << 21 }>, D>;
226pub type BitSixelEncoder<D = Sierra> = SixelEncoder<BitPaletteBuilder, D>;
227#[cfg(feature = "focal")]
228pub type FocalSixelEncoder<D = Sierra> = SixelEncoder<FocalPaletteBuilder, D>;
229#[cfg(feature = "k-means")]
230pub type KMeansSixelEncoder<D = Sierra> = SixelEncoder<KMeansPaletteBuilder, D>;
231#[cfg(feature = "k-medians")]
232pub type KMediansSixelEncoder<D = Sierra> = SixelEncoder<KMediansPaletteBuilder, D>;
233#[cfg(feature = "median-cut")]
234pub type MedianCutSixelEncoder<D = Sierra> = SixelEncoder<MedianCutPaletteBuilder, D>;
235#[cfg(feature = "octree")]
236pub type OctreeSixelEncoder<D = Sierra, const USE_MIN_HEAP: bool = false> =
237 SixelEncoder<OctreePaletteBuilder<USE_MIN_HEAP>, D>;
238#[cfg(feature = "wu")]
239pub type WuSixelEncoder<D = Sierra> = SixelEncoder<WuPaletteBuilder, D>;
240
241impl<P: PaletteBuilder, D: Dither> SixelEncoder<P, D> {
242 /// Encode an RGBA image into sixel format with the default palette size of
243 /// 256 colors.
244 ///
245 /// This is a convenience method that calls
246 /// [`encode_with_palette_size`](Self::encode_with_palette_size)
247 /// with a palette size of 256.
248 ///
249 /// # Arguments
250 ///
251 /// * `rgba` - An RGBA image to encode. The alpha channel is used for
252 /// transparency handling.
253 ///
254 /// # Returns
255 ///
256 /// A `String` containing the sixel-encoded image data, ready to be printed
257 /// to a sixel-capable terminal.
258 ///
259 /// # Transparency Handling
260 ///
261 /// - **Fully transparent pixels** (alpha = 0): Encoded as transparent sixel
262 /// pixels
263 /// - **Partially transparent pixels**: If the `partial-transparency`
264 /// feature is enabled, these are blended with the detected terminal
265 /// background color. Otherwise, treated as opaque.
266 /// - **Opaque pixels** (alpha = 255): Encoded normally
267 pub fn encode(rgba: RgbaImage) -> String {
268 Self::encode_with_palette_size(rgba, 256)
269 }
270
271 /// Encode an RGBA image into sixel format with a custom palette size.
272 ///
273 /// This method provides full control over the color quantization process by
274 /// allowing you to specify the exact number of colors in the resulting
275 /// palette. The palette size directly affects both the quality and size
276 /// of the output.
277 ///
278 /// # Arguments
279 ///
280 /// * `rgba` - An RGBA image to encode. The alpha channel is used for
281 /// transparency handling.
282 /// * `palette_size` - The number of colors to use in the palette. Valid
283 /// range is 1-256. Will be automatically clamped if the image has fewer
284 /// unique colors.
285 ///
286 /// # Returns
287 ///
288 /// A `String` containing the sixel-encoded image data, ready to be printed
289 /// to a sixel-capable terminal.
290 ///
291 /// # Transparency Handling
292 ///
293 /// Same as [`encode`](Self::encode) - see that method's documentation for
294 /// details.
295 pub fn encode_with_palette_size(
296 #[allow(unused_mut)] mut image: RgbaImage,
297 palette_size: usize,
298 ) -> String {
299 #[cfg(feature = "partial-transparency")]
300 {
301 use std::time::Duration;
302
303 let bg_color = termbg::rgb(Duration::from_millis(100))
304 .map(|rgb| {
305 Rgba([
306 (rgb.r as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
307 (rgb.g as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
308 (rgb.b as f32 / u16::MAX as f32 * u8::MAX as f32) as u8,
309 u8::MAX,
310 ])
311 })
312 .unwrap_or(Rgba([0, 0, 0, u8::MAX]));
313 image.par_pixels_mut().for_each(|pixel| {
314 use image::Pixel;
315 use image::Rgba;
316
317 let mut color = Rgba([bg_color[0], bg_color[1], bg_color[2], pixel[3]]);
318 color.blend(pixel);
319 *pixel = color;
320 });
321 }
322 let palette = if image.width().saturating_mul(image.height()) < palette_size as u32 {
323 image.pixels().copied().map(rgba_to_lab).collect::<Vec<_>>()
324 } else {
325 P::build_palette(&image, palette_size)
326 };
327
328 let mut sixel_string = r#"P9;1q"1;1;"#.to_string();
329 push_usize(&mut sixel_string, image.width() as usize);
330 sixel_string.push(';');
331 push_usize(&mut sixel_string, image.height() as usize);
332
333 if image.width() > 0 && image.height() > 0 {
334 for (i, lab) in palette.iter().copied().enumerate() {
335 let hsl: Hsl = lab.into_color();
336 // Sixel hue is offset by 120 degrees from the common hue values.
337 let deg = (hsl.hue.into_positive_degrees().round() as u16 + 120) % 360;
338
339 sixel_string.push('#');
340 push_usize(&mut sixel_string, i);
341 sixel_string.push_str(";1;");
342 push_usize(&mut sixel_string, deg as usize);
343 sixel_string.push(';');
344 push_usize(&mut sixel_string, (hsl.lightness * 100.0).round() as usize);
345 sixel_string.push(';');
346 push_usize(&mut sixel_string, (hsl.saturation * 100.0).round() as usize);
347 }
348
349 let bucketer = P::build_bucketer(&palette, palette_size);
350 let paletted_pixels = D::dither_and_palettize(&image, &palette, &bucketer);
351
352 #[cfg(feature = "dump-mse")]
353 {
354 use rayon::iter::IntoParallelRefIterator;
355
356 let dequant = paletted_pixels
357 .iter()
358 .map(|&idx| palette[idx])
359 .collect::<Vec<_>>();
360 let mse = dequant
361 .par_iter()
362 .zip(image.par_pixels())
363 .map(|(l, rgb)| {
364 use palette::color_difference::EuclideanDistance;
365 let lab = rgba_to_lab(*rgb);
366 lab.distance_squared(*l)
367 })
368 .sum::<f32>()
369 / (image.width() * image.height()) as f32;
370
371 println!("MSE: {:.2} ({} colors)", mse, palette_size);
372 }
373
374 #[cfg(feature = "dump-delta-e")]
375 {
376 use rayon::iter::IntoParallelRefIterator;
377
378 let dequant = paletted_pixels
379 .iter()
380 .map(|&idx| palette[idx])
381 .collect::<Vec<_>>();
382 let differences = image
383 .par_pixels()
384 .copied()
385 .zip(dequant.par_iter())
386 .map(|(rgb, lab)| {
387 use palette::color_difference::ImprovedCiede2000;
388 let lab_rgb = rgba_to_lab(rgb);
389 lab_rgb.improved_difference(*lab)
390 })
391 .collect::<Vec<_>>();
392
393 let mean_diff =
394 differences.iter().sum::<f32>() / (image.width() * image.height()) as f32;
395 let max_diff = differences.iter().copied().fold(0.0, f32::max);
396 let two_three_threshold = differences.iter().copied().filter(|d| *d > 2.3).count()
397 as f32
398 / (image.width() * image.height()) as f32;
399 let five_threshold = differences.iter().copied().filter(|d| *d > 5.0).count()
400 as f32
401 / (image.width() * image.height()) as f32;
402
403 println!("Mean DeltaE: {:.2} ({} colors)", mean_diff, palette_size);
404 println!("Max DeltaE: {:.2} ({} colors)", max_diff, palette_size);
405 println!(
406 "DeltaE > 2.3: {:.2} ({} colors)",
407 two_three_threshold, palette_size
408 );
409 println!(
410 "DeltaE > 5.0: {:.2} ({} colors)",
411 five_threshold, palette_size
412 );
413 }
414
415 #[cfg(feature = "dump-dssim")]
416 {
417 use dssim_core::Dssim;
418
419 let dssim = Dssim::new();
420 let image_pixels = image
421 .pixels()
422 .copied()
423 .map(|Rgba([r, g, b, _])| rgb::RGB::new(r, g, b))
424 .collect::<Vec<_>>();
425 let orig = dssim
426 .create_image_rgb(
427 &image_pixels,
428 image.width() as usize,
429 image.height() as usize,
430 )
431 .unwrap();
432
433 let palette_pixels = paletted_pixels
434 .iter()
435 .map(|&idx| {
436 let lab = palette[idx];
437 let rgb: palette::Srgb = lab.into_color();
438 let rgb = rgb.into_format::<u8>();
439 rgb::RGB::new(rgb.red, rgb.green, rgb.blue)
440 })
441 .collect::<Vec<_>>();
442 let new = dssim
443 .create_image_rgb(
444 &palette_pixels,
445 image.width() as usize,
446 image.height() as usize,
447 )
448 .unwrap();
449
450 let (dssim, _) = dssim.compare(&orig, &new);
451
452 println!("DSSIM: {:.4} ({} colors)", dssim, palette_size);
453 }
454
455 #[cfg(feature = "dump-phash")]
456 {
457 use image_hasher::FilterType;
458 use image_hasher::HashAlg;
459 use image_hasher::HasherConfig;
460
461 let mut output_image = image::ImageBuffer::new(image.width(), image.height());
462 for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
463 let lab = palette[idx];
464 let rgb: palette::Srgb = lab.into_color();
465 let rgb = rgb.into_format::<u8>();
466 *pixel = Rgba([rgb.red, rgb.green, rgb.blue, u8::MAX]);
467 }
468
469 let hasher = HasherConfig::new()
470 .hash_alg(HashAlg::DoubleGradient)
471 .resize_filter(FilterType::Lanczos3)
472 .hash_size(32, 32)
473 .to_hasher();
474
475 let hash_in = hasher.hash_image(&image);
476 let hash_out = hasher.hash_image(&output_image);
477
478 println!(
479 "Hash Distance: {} ({} colors)",
480 hash_in.dist(&hash_out),
481 palette_size
482 );
483 }
484
485 #[cfg(feature = "dump-image")]
486 {
487 use std::hash::BuildHasher;
488 use std::hash::Hasher;
489 use std::hash::RandomState;
490
491 let mut output_image = image::ImageBuffer::new(image.width(), image.height());
492 for (pixel, &idx) in output_image.pixels_mut().zip(&paletted_pixels) {
493 let lab = palette[idx];
494 let rgb: palette::Srgb = lab.into_color();
495 let rgb = rgb.into_format::<u8>();
496 *pixel = Rgba([rgb.red, rgb.green, rgb.blue, u8::MAX]);
497 }
498 let rand = BuildHasher::build_hasher(&RandomState::new()).finish();
499
500 output_image
501 .save(format!("{}-{rand}.png", P::NAME))
502 .expect("Failed to save output image");
503 }
504
505 let width = image.width() as usize;
506
507 let num_chunks = (paletted_pixels.len() / width).div_ceil(6);
508 let chunk_capacity = width * 7;
509 let mut strings =
510 Vec::from_iter((0..num_chunks).map(|_| String::with_capacity(chunk_capacity)));
511
512 paletted_pixels
513 .par_chunks(width * 6)
514 .zip(image.into_raw().par_chunks(width * 6 * 4))
515 .zip(&mut strings)
516 .for_each(|((palette_chunk, rgba_chunk), sixel_string)| {
517 let mut color_bits = vec![0u8; palette_size * width];
518 let mut color_used = vec![false; palette_size];
519 let chunk_height = palette_chunk.len() / width;
520
521 for row in 0..chunk_height {
522 let bit = 1u8 << row;
523 let row_offset = row * width;
524 for col in 0..width {
525 let pixel_idx = row_offset + col;
526 if rgba_chunk[pixel_idx * 4 + 3] != 0 {
527 let color = palette_chunk[pixel_idx];
528 color_bits[color * width + col] |= bit;
529 color_used[color] = true;
530 }
531 }
532 }
533
534 color_bits.par_iter_mut().for_each(|d| {
535 *d += 0x3f;
536 });
537 // SAFETY: 0x3f..=0x7e are valid ASCII bytes
538 let color_bits = unsafe { String::from_utf8_unchecked(color_bits) };
539
540 for (color, _) in color_used.iter().enumerate().filter(|(_, u)| **u) {
541 sixel_string.push('#');
542 push_usize(sixel_string, color);
543 let base = color * width;
544 sixel_string.push_str(&color_bits[base..base + width]);
545 sixel_string.push('$');
546 }
547 sixel_string.push('-');
548 });
549
550 sixel_string.extend(strings);
551 }
552
553 sixel_string.push_str(r#"\"#);
554 sixel_string
555 }
556}
557
558fn rgba_to_lab(Rgba([r, g, b, _]): Rgba<u8>) -> Lab {
559 palette::rgb::Rgb::<Srgb, _>::new(r, g, b)
560 .into_format::<f32>()
561 .into_color()
562}
563
564fn push_usize(
565 s: &mut String,
566 n: usize,
567) {
568 s.push_str(itoa::Buffer::new().format(n));
569}