Skip to content

Commit d1dea3d

Browse files
authored
Merge pull request #223 from Shnatsel/encode-la8
Add a helper for encoding La8 images
2 parents 706fcb3 + 5d49bdc commit d1dea3d

File tree

1 file changed

+90
-0
lines changed

1 file changed

+90
-0
lines changed

src/common.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,96 @@ impl Frame<'static> {
278278
}
279279
}
280280

281+
/// Creates a frame from pixels in LumaAlpha format (grayscale pixels with transparency).
282+
///
283+
/// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
284+
/// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
285+
/// pixel. The least used color will be used to indicate alpha and replace with the closest other color
286+
/// in the image. Different frames have independent palettes.
287+
///
288+
/// # Panics:
289+
/// * If the length of pixels does not equal `width * height * 2`.
290+
pub fn from_grayscale_with_alpha(width: u16, height: u16, pixels: &[u8]) -> Self {
291+
assert_eq!(width as usize * height as usize * 2, pixels.len(), "Too much or too little pixel data for the given width and height to create a GIF Frame");
292+
293+
// Input is in LumaA format.
294+
// Count the occurrences of all the colors, then pick the least common color as alpha.
295+
let mut num_transparent_pixels: u32 = 0;
296+
let mut color_frequencies: [u32; 256] = [0; 256];
297+
for pixel in pixels.chunks_exact(2) {
298+
let color = pixel[0];
299+
let alpha = pixel[1];
300+
// do not count colors in fully transparent pixels
301+
if alpha == 0 {
302+
num_transparent_pixels += 1;
303+
} else {
304+
color_frequencies[color as usize] += 1;
305+
}
306+
}
307+
308+
let grayscale_palette: Vec<u8> = (0..=255).flat_map(|i| [i, i, i]).collect();
309+
310+
// If there were no fully transparent pixels, do not allocate a color to transparency in the GIF
311+
// and return immediately with the generic grayscale palette
312+
if num_transparent_pixels == 0 {
313+
let stripped_alpha: Vec<u8> = pixels.chunks_exact(2).map(|pixel| pixel[0]).collect();
314+
return Frame {
315+
width,
316+
height,
317+
buffer: Cow::Owned(stripped_alpha),
318+
palette: Some(grayscale_palette),
319+
transparent: None,
320+
..Frame::default()
321+
};
322+
}
323+
324+
// Choose the color that will be our alpha color
325+
let least_used_color = color_frequencies
326+
.iter()
327+
.enumerate()
328+
.min_by_key(|(_, &value)| value)
329+
.map(|(index, _)| index as u8)
330+
.expect("input slice is empty");
331+
332+
// pick the less used color out of the neighbours as the replacement color
333+
let replacement_color = if least_used_color == 255 {
334+
254
335+
} else if least_used_color == 0 {
336+
1
337+
} else if color_frequencies[(least_used_color - 1) as usize]
338+
< color_frequencies[(least_used_color + 1) as usize]
339+
{
340+
least_used_color - 1
341+
} else {
342+
least_used_color + 1
343+
};
344+
345+
// Strip alpha and replace fully transparent pixels with the chosen color
346+
let paletted: Vec<u8> = pixels
347+
.chunks_exact(2)
348+
.map(|pixel| {
349+
let color = pixel[0];
350+
let alpha = pixel[1];
351+
if alpha == 0 {
352+
least_used_color
353+
} else if color == least_used_color {
354+
replacement_color
355+
} else {
356+
color
357+
}
358+
})
359+
.collect();
360+
361+
Frame {
362+
width,
363+
height,
364+
buffer: Cow::Owned(paletted),
365+
palette: Some(grayscale_palette),
366+
transparent: Some(least_used_color),
367+
..Frame::default()
368+
}
369+
}
370+
281371
/// Creates a frame from a palette and indexed pixels.
282372
///
283373
/// # Panics:

0 commit comments

Comments
 (0)