Reduce alpha to tRNS (#477)
This commit is contained in:
parent
2599b9fe82
commit
2008d09915
7 changed files with 93 additions and 19 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::colors::ColorType;
|
use crate::colors::{BitDepth, ColorType};
|
||||||
use crate::headers::IhdrData;
|
use crate::headers::IhdrData;
|
||||||
use crate::png::PngImage;
|
use crate::png::PngImage;
|
||||||
|
|
||||||
|
|
@ -37,37 +37,61 @@ pub fn cleaned_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn reduced_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
||||||
let target_color_type = match png.ihdr.color_type {
|
let target_color_type = match png.ihdr.color_type {
|
||||||
ColorType::GrayscaleAlpha => ColorType::Grayscale,
|
ColorType::GrayscaleAlpha => ColorType::Grayscale,
|
||||||
ColorType::RGBA => ColorType::RGB,
|
ColorType::RGBA => ColorType::RGB,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let byte_depth = png.ihdr.bit_depth.as_u8() >> 3;
|
let byte_depth = (png.ihdr.bit_depth.as_u8() >> 3) as usize;
|
||||||
let channels = png.channels_per_pixel();
|
let channels = png.channels_per_pixel() as usize;
|
||||||
let bpp = channels * byte_depth;
|
let bpp = channels * byte_depth;
|
||||||
let bpp_mask = bpp - 1;
|
|
||||||
if 0 != bpp & bpp_mask {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let colored_bytes = bpp - byte_depth;
|
let colored_bytes = bpp - byte_depth;
|
||||||
|
|
||||||
|
// If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels.
|
||||||
|
// In case this occurs, we want to try and find an unused color we can use for the tRNS chunk.
|
||||||
|
// Rather than an exhaustive search, we will just keep track of 256 shades of gray, which should cover many cases.
|
||||||
|
let mut has_transparency = false;
|
||||||
|
let mut used_colors = vec![false; 256];
|
||||||
|
|
||||||
for line in png.scan_lines() {
|
for line in png.scan_lines() {
|
||||||
for (i, &byte) in line.data.iter().enumerate() {
|
for pixel in line.data.chunks(bpp) {
|
||||||
if i as u8 & bpp_mask >= colored_bytes && byte != 255 {
|
if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) {
|
||||||
|
// Fully transparent, we may be able to reduce with tRNS
|
||||||
|
has_transparency = true;
|
||||||
|
} else if pixel.iter().skip(colored_bytes).any(|b| *b != 255) {
|
||||||
|
// Partially transparent, the image is not reducible
|
||||||
return None;
|
return None;
|
||||||
|
} else if optimize_alpha && pixel.iter().take(colored_bytes).all(|b| *b == pixel[0]) {
|
||||||
|
// Opaque shade of gray, we can't use this color for tRNS
|
||||||
|
used_colors[pixel[0] as usize] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transparency_pixel = if has_transparency {
|
||||||
|
// If no unused color was found we will have to fail here
|
||||||
|
// Otherwise, proceed to construct the tRNS chunk
|
||||||
|
let unused_color = used_colors.iter().position(|b| !*b)? as u8;
|
||||||
|
Some(match png.ihdr.bit_depth {
|
||||||
|
BitDepth::Sixteen => vec![unused_color; colored_bytes],
|
||||||
|
// 8-bit is still stored as 16-bit, with the high byte set to 0
|
||||||
|
_ => [0, unused_color].repeat(colored_bytes),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut raw_data = Vec::with_capacity(png.data.len());
|
let mut raw_data = Vec::with_capacity(png.data.len());
|
||||||
for line in png.scan_lines() {
|
for line in png.scan_lines() {
|
||||||
raw_data.push(line.filter);
|
raw_data.push(line.filter);
|
||||||
for (i, &byte) in line.data.iter().enumerate() {
|
for pixel in line.data.chunks(bpp) {
|
||||||
if i as u8 & bpp_mask >= colored_bytes {
|
match transparency_pixel {
|
||||||
continue;
|
Some(ref trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => {
|
||||||
}
|
raw_data.resize(raw_data.len() + colored_bytes, trns[1]);
|
||||||
|
}
|
||||||
raw_data.push(byte);
|
_ => raw_data.extend_from_slice(&pixel[0..colored_bytes]),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +110,7 @@ pub fn reduced_alpha_channel(png: &PngImage) -> Option<PngImage> {
|
||||||
..png.ihdr
|
..png.ihdr
|
||||||
},
|
},
|
||||||
aux_headers,
|
aux_headers,
|
||||||
transparency_pixel: None,
|
transparency_pixel,
|
||||||
palette: None,
|
palette: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ pub fn reduce_color_type(
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
.or_else(|| reduced_alpha_channel(&reduced))
|
.or_else(|| reduced_alpha_channel(&reduced, optimize_alpha))
|
||||||
{
|
{
|
||||||
reduced = Cow::Owned(r);
|
reduced = Cow::Owned(r);
|
||||||
} else if let Some(r) = reduce_to_palette(&reduced) {
|
} else if let Some(r) = reduce_to_palette(&reduced) {
|
||||||
|
|
@ -223,7 +223,9 @@ pub fn reduce_color_type(
|
||||||
}
|
}
|
||||||
|
|
||||||
if reduced.ihdr.color_type == ColorType::GrayscaleAlpha {
|
if reduced.ihdr.color_type == ColorType::GrayscaleAlpha {
|
||||||
if let Some(r) = reduced_alpha_channel(&reduced).or_else(|| reduce_to_palette(&reduced)) {
|
if let Some(r) =
|
||||||
|
reduced_alpha_channel(&reduced, optimize_alpha).or_else(|| reduce_to_palette(&reduced))
|
||||||
|
{
|
||||||
reduced = Cow::Owned(r);
|
reduced = Cow::Owned(r);
|
||||||
should_reduce_bit_depth = true;
|
should_reduce_bit_depth = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png
Normal file
BIN
tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 247 KiB |
BIN
tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png
Normal file
BIN
tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
tests/files/rgba_16_should_be_rgb_trns_16.png
Normal file
BIN
tests/files/rgba_16_should_be_rgb_trns_16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
tests/files/rgba_8_should_be_rgb_trns_8.png
Normal file
BIN
tests/files/rgba_8_should_be_rgb_trns_8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
|
|
@ -130,6 +130,30 @@ fn rgba_8_should_be_rgb_8() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgba_16_should_be_rgb_trns_16() {
|
||||||
|
test_it_converts(
|
||||||
|
"tests/files/rgba_16_should_be_rgb_trns_16.png",
|
||||||
|
true,
|
||||||
|
ColorType::RGBA,
|
||||||
|
BitDepth::Sixteen,
|
||||||
|
ColorType::RGB,
|
||||||
|
BitDepth::Sixteen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgba_8_should_be_rgb_trns_8() {
|
||||||
|
test_it_converts(
|
||||||
|
"tests/files/rgba_8_should_be_rgb_trns_8.png",
|
||||||
|
true,
|
||||||
|
ColorType::RGBA,
|
||||||
|
BitDepth::Eight,
|
||||||
|
ColorType::RGB,
|
||||||
|
BitDepth::Eight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rgba_16_should_be_palette_8() {
|
fn rgba_16_should_be_palette_8() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
|
|
@ -694,6 +718,30 @@ fn grayscale_8_should_be_grayscale_8() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_alpha_16_should_be_grayscale_trns_16() {
|
||||||
|
test_it_converts(
|
||||||
|
"tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png",
|
||||||
|
true,
|
||||||
|
ColorType::GrayscaleAlpha,
|
||||||
|
BitDepth::Sixteen,
|
||||||
|
ColorType::Grayscale,
|
||||||
|
BitDepth::Sixteen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grayscale_alpha_8_should_be_grayscale_trns_8() {
|
||||||
|
test_it_converts(
|
||||||
|
"tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png",
|
||||||
|
true,
|
||||||
|
ColorType::GrayscaleAlpha,
|
||||||
|
BitDepth::Eight,
|
||||||
|
ColorType::Grayscale,
|
||||||
|
BitDepth::Eight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn small_files() {
|
fn small_files() {
|
||||||
let input = PathBuf::from("tests/files/small_files.png");
|
let input = PathBuf::from("tests/files/small_files.png");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue