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