1pub 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
168pub trait PaletteBuilder: private::Sealed {
171 const PALETTE_SIZE: usize;
172 const NAME: &'static str;
173
174 fn build_palette(image: &RgbImage) -> Vec<Lab>;
177}
178
179const fn num2six(num: u8) -> char {
180 (0x3f + num) as char
181}
182
183pub 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 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}