Optimize RGB/A to palette conversion (#148)
This commit is contained in:
parent
588d5bd52b
commit
686adcdebd
6 changed files with 143 additions and 231 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -264,6 +264,7 @@ dependencies = [
|
||||||
"miniz_oxide 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"miniz_oxide 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rayon 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rgb 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"wild 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"zopfli 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
@ -313,6 +314,11 @@ dependencies = [
|
||||||
"redox_syscall 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"redox_syscall 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -435,6 +441,7 @@ dependencies = [
|
||||||
"checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356"
|
"checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356"
|
||||||
"checksum redox_syscall 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "cf8fb82a4d1c9b28f1c26c574a5b541f5ffb4315f6c9a791fa47b6a04438fe93"
|
"checksum redox_syscall 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "cf8fb82a4d1c9b28f1c26c574a5b541f5ffb4315f6c9a791fa47b6a04438fe93"
|
||||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||||
|
"checksum rgb 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)" = "002bebda58b24482d6911a59512e8a17fa1defecf5a2162521113b7cc5422dd1"
|
||||||
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
|
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
|
||||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ itertools = "^0.7.7"
|
||||||
num_cpus = "^1.0.0"
|
num_cpus = "^1.0.0"
|
||||||
zopfli = "^0.3.4"
|
zopfli = "^0.3.4"
|
||||||
miniz_oxide = "0.2.0"
|
miniz_oxide = "0.2.0"
|
||||||
|
rgb = "0.8.11"
|
||||||
|
|
||||||
[dependencies.rayon]
|
[dependencies.rayon]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extern crate byteorder;
|
||||||
extern crate cloudflare_zlib_sys;
|
extern crate cloudflare_zlib_sys;
|
||||||
extern crate crc;
|
extern crate crc;
|
||||||
extern crate image;
|
extern crate image;
|
||||||
|
extern crate rgb;
|
||||||
extern crate itertools;
|
extern crate itertools;
|
||||||
extern crate miniz_oxide;
|
extern crate miniz_oxide;
|
||||||
extern crate num_cpus;
|
extern crate num_cpus;
|
||||||
|
|
|
||||||
155
src/png/mod.rs
155
src/png/mod.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
use rgb::RGBA8;
|
||||||
|
use rgb::ComponentSlice;
|
||||||
use atomicmin::AtomicMin;
|
use atomicmin::AtomicMin;
|
||||||
use bit_vec::BitVec;
|
use bit_vec::BitVec;
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
|
|
@ -16,7 +18,7 @@ use reduction::color::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
use std::iter::{Iterator, repeat};
|
use std::iter::Iterator;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
const STD_COMPRESSION: u8 = 8;
|
const STD_COMPRESSION: u8 = 8;
|
||||||
|
|
@ -40,11 +42,9 @@ pub struct PngData {
|
||||||
pub raw_data: Vec<u8>,
|
pub raw_data: Vec<u8>,
|
||||||
/// The palette containing colors used in an Indexed image
|
/// The palette containing colors used in an Indexed image
|
||||||
/// Contains 3 bytes per color (R+G+B), up to 768
|
/// Contains 3 bytes per color (R+G+B), up to 768
|
||||||
pub palette: Option<Vec<u8>>,
|
pub palette: Option<Vec<RGBA8>>,
|
||||||
/// The pixel value that should be rendered as transparent
|
/// The pixel value that should be rendered as transparent
|
||||||
pub transparency_pixel: Option<Vec<u8>>,
|
pub transparency_pixel: Option<Vec<u8>>,
|
||||||
/// A map of how transparent each color in the palette should be
|
|
||||||
pub transparency_palette: Option<Vec<u8>>,
|
|
||||||
/// All non-critical headers from the PNG are stored here
|
/// All non-critical headers from the PNG are stored here
|
||||||
pub aux_headers: HashMap<[u8; 4], Vec<u8>>,
|
pub aux_headers: HashMap<[u8; 4], Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +52,10 @@ pub struct PngData {
|
||||||
impl PngData {
|
impl PngData {
|
||||||
/// Create a new `PngData` struct by opening a file
|
/// Create a new `PngData` struct by opening a file
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(filepath: &Path, fix_errors: bool) -> Result<PngData, PngError> {
|
pub fn new(filepath: &Path, fix_errors: bool) -> Result<Self, PngError> {
|
||||||
let byte_data = PngData::read_file(filepath)?;
|
let byte_data = Self::read_file(filepath)?;
|
||||||
|
|
||||||
PngData::from_slice(&byte_data, fix_errors)
|
Self::from_slice(&byte_data, fix_errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
|
||||||
|
|
@ -85,7 +85,7 @@ impl PngData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `PngData` struct by reading a slice
|
/// Create a new `PngData` struct by reading a slice
|
||||||
pub fn from_slice(byte_data: &[u8], fix_errors: bool) -> Result<PngData, PngError> {
|
pub fn from_slice(byte_data: &[u8], fix_errors: bool) -> Result<Self, PngError> {
|
||||||
let mut byte_offset: usize = 0;
|
let mut byte_offset: usize = 0;
|
||||||
// Test that png header is valid
|
// Test that png header is valid
|
||||||
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
|
||||||
|
|
@ -115,31 +115,15 @@ impl PngData {
|
||||||
};
|
};
|
||||||
let ihdr_header = parse_ihdr_header(&ihdr)?;
|
let ihdr_header = parse_ihdr_header(&ihdr)?;
|
||||||
let raw_data = deflate::inflate(idat_headers.as_ref())?;
|
let raw_data = deflate::inflate(idat_headers.as_ref())?;
|
||||||
// Handle transparency header
|
|
||||||
let mut has_transparency_pixel = false;
|
let (palette, transparency_pixel) = Self::palette_to_rgba(ihdr_header.color_type, aux_headers.remove(b"PLTE"), aux_headers.remove(b"tRNS"))?;
|
||||||
let mut has_transparency_palette = false;
|
|
||||||
if aux_headers.contains_key(b"tRNS") {
|
let mut png_data = Self {
|
||||||
if ihdr_header.color_type == ColorType::Indexed {
|
|
||||||
has_transparency_palette = true;
|
|
||||||
} else {
|
|
||||||
has_transparency_pixel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut png_data = PngData {
|
|
||||||
idat_data: idat_headers,
|
idat_data: idat_headers,
|
||||||
ihdr_data: ihdr_header,
|
ihdr_data: ihdr_header,
|
||||||
raw_data,
|
raw_data,
|
||||||
palette: aux_headers.remove(b"PLTE"),
|
palette,
|
||||||
transparency_pixel: if has_transparency_pixel {
|
transparency_pixel,
|
||||||
aux_headers.remove(b"tRNS")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
transparency_palette: if has_transparency_palette {
|
|
||||||
aux_headers.remove(b"tRNS")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
aux_headers,
|
aux_headers,
|
||||||
};
|
};
|
||||||
png_data.raw_data = png_data.unfilter_image();
|
png_data.raw_data = png_data.unfilter_image();
|
||||||
|
|
@ -147,13 +131,31 @@ impl PngData {
|
||||||
Ok(png_data)
|
Ok(png_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn reset_from_original(&mut self, original: &PngData) {
|
/// Handle transparency header
|
||||||
|
fn palette_to_rgba(color_type: ColorType, palette_data: Option<Vec<u8>>, trns_data: Option<Vec<u8>>) -> Result<(Option<Vec<RGBA8>>, Option<Vec<u8>>), PngError> {
|
||||||
|
if color_type == ColorType::Indexed {
|
||||||
|
let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?;
|
||||||
|
let mut palette: Vec<_> = palette_data.chunks(3)
|
||||||
|
.map(|color| RGBA8::new(color[0], color[1], color[2], 255))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(trns_data) = trns_data {
|
||||||
|
for (color, trns) in palette.iter_mut().zip(trns_data) {
|
||||||
|
color.a = trns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((Some(palette), None))
|
||||||
|
} else {
|
||||||
|
Ok((None, trns_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_from_original(&mut self, original: &Self) {
|
||||||
self.idat_data = original.idat_data.clone();
|
self.idat_data = original.idat_data.clone();
|
||||||
self.ihdr_data = original.ihdr_data;
|
self.ihdr_data = original.ihdr_data;
|
||||||
self.raw_data = original.raw_data.clone();
|
self.raw_data = original.raw_data.clone();
|
||||||
self.palette = original.palette.clone();
|
self.palette = original.palette.clone();
|
||||||
self.transparency_pixel = original.transparency_pixel.clone();
|
self.transparency_pixel = original.transparency_pixel.clone();
|
||||||
self.transparency_palette = original.transparency_palette.clone();
|
|
||||||
self.aux_headers = original.aux_headers.clone();
|
self.aux_headers = original.aux_headers.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,10 +189,17 @@ impl PngData {
|
||||||
}
|
}
|
||||||
// Palette
|
// Palette
|
||||||
if let Some(ref palette) = self.palette {
|
if let Some(ref palette) = self.palette {
|
||||||
write_png_block(b"PLTE", palette, &mut output);
|
let mut palette_data = Vec::with_capacity(palette.len()*3);
|
||||||
if let Some(ref transparency_palette) = self.transparency_palette {
|
for px in palette {
|
||||||
// Transparency pixel
|
palette_data.extend_from_slice(px.rgb().as_slice());
|
||||||
write_png_block(b"tRNS", transparency_palette, &mut output);
|
}
|
||||||
|
write_png_block(b"PLTE", &palette_data, &mut output);
|
||||||
|
let num_transparent = palette.iter().enumerate().fold(0, |prev, (index, px)| {
|
||||||
|
if px.a != 255 {index+1} else {prev}
|
||||||
|
});
|
||||||
|
if num_transparent > 0 {
|
||||||
|
let trns_data: Vec<_> = palette[0..num_transparent].iter().map(|px| px.a).collect();
|
||||||
|
write_png_block(b"tRNS", &trns_data, &mut output);
|
||||||
}
|
}
|
||||||
} else if let Some(ref transparency_pixel) = self.transparency_pixel {
|
} else if let Some(ref transparency_pixel) = self.transparency_pixel {
|
||||||
// Transparency pixel
|
// Transparency pixel
|
||||||
|
|
@ -349,32 +358,22 @@ impl PngData {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A palette with RGB or RGBA slices
|
|
||||||
let mut palette_tmp;
|
|
||||||
let mut indexed_palette: Vec<_> = if let Some(ref trns) = self.transparency_palette {
|
|
||||||
palette_tmp = Vec::with_capacity(1024);
|
|
||||||
for (pixel, trns) in self.palette.as_ref().unwrap().chunks(3)
|
|
||||||
.zip(trns.iter().cloned().chain(repeat(255))) {
|
|
||||||
palette_tmp.extend_from_slice(pixel);
|
|
||||||
palette_tmp.push(trns);
|
|
||||||
}
|
|
||||||
palette_tmp.chunks(4).collect()
|
|
||||||
} else {
|
|
||||||
palette_tmp = self.palette.clone().unwrap();
|
|
||||||
palette_tmp.chunks(3).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
// A map of old indexes to new ones, for any moved
|
// A map of old indexes to new ones, for any moved
|
||||||
let mut index_map: HashMap<u8, u8> = HashMap::new();
|
let mut index_map: HashMap<u8, u8> = HashMap::new();
|
||||||
|
|
||||||
|
let mut palette = match self.palette {
|
||||||
|
Some(ref p) => p.clone(),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
|
||||||
// A list of (original) indices that are duplicates and no longer needed
|
// A list of (original) indices that are duplicates and no longer needed
|
||||||
let mut duplicates: Vec<u8> = Vec::new();
|
let mut duplicates: Vec<u8> = Vec::new();
|
||||||
{
|
{
|
||||||
// Find duplicate entries in the palette
|
// Find duplicate entries in the palette
|
||||||
let mut seen: HashMap<&[u8], u8> = HashMap::with_capacity(indexed_palette.len());
|
let mut seen: HashMap<RGBA8, u8> = HashMap::with_capacity(palette.len());
|
||||||
for (i, color) in indexed_palette.iter().cloned().enumerate() {
|
for (i, color) in palette.iter().cloned().enumerate() {
|
||||||
if seen.contains_key(color) {
|
if seen.contains_key(&color) {
|
||||||
let index = seen[color];
|
let index = seen[&color];
|
||||||
duplicates.push(i as u8);
|
duplicates.push(i as u8);
|
||||||
index_map.insert(i as u8, index);
|
index_map.insert(i as u8, index);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -385,11 +384,11 @@ impl PngData {
|
||||||
|
|
||||||
// Remove duplicates from the data
|
// Remove duplicates from the data
|
||||||
if !duplicates.is_empty() {
|
if !duplicates.is_empty() {
|
||||||
self.do_palette_reduction(&duplicates, &mut index_map, &mut indexed_palette);
|
self.do_palette_reduction(&duplicates, &mut index_map, &mut palette);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find palette entries that are never used
|
// Find palette entries that are never used
|
||||||
let mut seen = HashSet::with_capacity(indexed_palette.len());
|
let mut seen = HashSet::with_capacity(palette.len());
|
||||||
for line in self.scan_lines() {
|
for line in self.scan_lines() {
|
||||||
match self.ihdr_data.bit_depth {
|
match self.ihdr_data.bit_depth {
|
||||||
BitDepth::Eight => for &byte in line.data {
|
BitDepth::Eight => for &byte in line.data {
|
||||||
|
|
@ -426,7 +425,7 @@ impl PngData {
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if seen.len() == indexed_palette.len() {
|
if seen.len() == palette.len() {
|
||||||
// Exit early if no further possible optimizations
|
// Exit early if no further possible optimizations
|
||||||
// Check at the end of each line
|
// Check at the end of each line
|
||||||
// Checking after every pixel would be overly expensive
|
// Checking after every pixel would be overly expensive
|
||||||
|
|
@ -434,12 +433,12 @@ impl PngData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let unused: Vec<u8> = (0..indexed_palette.len() as u8)
|
let unused: Vec<u8> = (0..palette.len() as u8)
|
||||||
.filter(|i| !seen.contains(i))
|
.filter(|i| !seen.contains(i))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Remove unused palette indices
|
// Remove unused palette indices
|
||||||
self.do_palette_reduction(&unused, &mut index_map, &mut indexed_palette);
|
self.do_palette_reduction(&unused, &mut index_map, &mut palette);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -448,29 +447,20 @@ impl PngData {
|
||||||
&mut self,
|
&mut self,
|
||||||
indices_to_remove: &[u8],
|
indices_to_remove: &[u8],
|
||||||
index_map: &mut HashMap<u8, u8>,
|
index_map: &mut HashMap<u8, u8>,
|
||||||
indexed_palette: &mut Vec<&[u8]>,
|
palette: &mut Vec<RGBA8>,
|
||||||
) {
|
) {
|
||||||
let mut new_data = Vec::with_capacity(self.raw_data.len());
|
let mut new_data = Vec::with_capacity(self.raw_data.len());
|
||||||
let original_len = indexed_palette.len();
|
let original_len = palette.len();
|
||||||
for idx in indices_to_remove.iter().sorted_by(|a, b| b.cmp(a)) {
|
for idx in indices_to_remove.iter().cloned().sorted_by(|a, b| b.cmp(a)) {
|
||||||
for i in (*idx as usize + 1)..original_len {
|
for i in (idx as usize + 1)..original_len {
|
||||||
let existing = index_map.entry(i as u8).or_insert(i as u8);
|
let existing = index_map.entry(i as u8).or_insert(i as u8);
|
||||||
if *existing >= *idx {
|
if *existing >= idx {
|
||||||
*existing -= 1;
|
*existing -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
indexed_palette.remove(*idx as usize);
|
palette.remove(idx as usize);
|
||||||
if let Some(ref mut alpha) = self.transparency_palette {
|
|
||||||
if (*idx as usize) < alpha.len() {
|
|
||||||
alpha.remove(*idx as usize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(ref mut alpha) = self.transparency_palette {
|
|
||||||
while let Some(255) = alpha.last().cloned() {
|
|
||||||
alpha.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reassign data bytes to new indices
|
// Reassign data bytes to new indices
|
||||||
for line in self.scan_lines() {
|
for line in self.scan_lines() {
|
||||||
new_data.push(line.filter);
|
new_data.push(line.filter);
|
||||||
|
|
@ -531,12 +521,8 @@ impl PngData {
|
||||||
}
|
}
|
||||||
index_map.clear();
|
index_map.clear();
|
||||||
self.raw_data = new_data;
|
self.raw_data = new_data;
|
||||||
let new_palette = flatten(indexed_palette.iter().cloned())
|
self.transparency_pixel = None;
|
||||||
.enumerate()
|
self.palette = Some(palette.clone());
|
||||||
.filter(|&(i, _)| !(self.transparency_palette.is_some() && i % 4 == 3))
|
|
||||||
.map(|(_, x)| *x)
|
|
||||||
.collect::<Vec<u8>>();
|
|
||||||
self.palette = Some(new_palette);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to reduce the color type of the image
|
/// Attempt to reduce the color type of the image
|
||||||
|
|
@ -550,7 +536,7 @@ impl PngData {
|
||||||
if self.ihdr_data.color_type == ColorType::RGBA {
|
if self.ihdr_data.color_type == ColorType::RGBA {
|
||||||
if reduce_rgba_to_grayscale_alpha(self) || reduce_rgba_to_rgb(self) {
|
if reduce_rgba_to_grayscale_alpha(self) || reduce_rgba_to_rgb(self) {
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if reduce_rgba_to_palette(self) {
|
} else if reduce_color_to_palette(self) {
|
||||||
changed = true;
|
changed = true;
|
||||||
should_reduce_bit_depth = true;
|
should_reduce_bit_depth = true;
|
||||||
}
|
}
|
||||||
|
|
@ -564,7 +550,7 @@ impl PngData {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.ihdr_data.color_type == ColorType::RGB
|
if self.ihdr_data.color_type == ColorType::RGB
|
||||||
&& (reduce_rgb_to_grayscale(self) || reduce_rgb_to_palette(self))
|
&& (reduce_rgb_to_grayscale(self) || reduce_color_to_palette(self))
|
||||||
{
|
{
|
||||||
changed = true;
|
changed = true;
|
||||||
should_reduce_bit_depth = true;
|
should_reduce_bit_depth = true;
|
||||||
|
|
@ -652,7 +638,6 @@ impl PngData {
|
||||||
ihdr_data: self.ihdr_data,
|
ihdr_data: self.ihdr_data,
|
||||||
palette: self.palette.clone(),
|
palette: self.palette.clone(),
|
||||||
transparency_pixel: self.transparency_pixel.clone(),
|
transparency_pixel: self.transparency_pixel.clone(),
|
||||||
transparency_palette: self.transparency_palette.clone(),
|
|
||||||
aux_headers: self.aux_headers.clone(),
|
aux_headers: self.aux_headers.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
use std::hash::Hash;
|
||||||
|
use rgb::{RGBA8, RGB8, FromSlice};
|
||||||
use colors::{BitDepth, ColorType};
|
use colors::{BitDepth, ColorType};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use png::PngData;
|
use png::PngData;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::alpha::reduce_alpha_channel;
|
use super::alpha::reduce_alpha_channel;
|
||||||
|
|
||||||
|
|
@ -71,173 +74,88 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reduce_rgba_to_palette(png: &mut PngData) -> bool {
|
fn reduce_scanline_to_palette<T>(iter: impl IntoIterator<Item=T>, palette: &mut HashMap<T, u8>, reduced: &mut Vec<u8>) -> bool
|
||||||
|
where T: Eq + Hash {
|
||||||
|
for pixel in iter {
|
||||||
|
let idx = if let Some(&idx) = palette.get(&pixel) {
|
||||||
|
idx
|
||||||
|
} else {
|
||||||
|
let len = palette.len();
|
||||||
|
if len == 256 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let idx = len as u8;
|
||||||
|
palette.insert(pixel, idx);
|
||||||
|
idx
|
||||||
|
};
|
||||||
|
reduced.push(idx);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reduce_color_to_palette(png: &mut PngData) -> bool {
|
||||||
if png.ihdr_data.bit_depth != BitDepth::Eight {
|
if png.ihdr_data.bit_depth != BitDepth::Eight {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
||||||
let mut palette = Vec::with_capacity(256);
|
let mut palette = HashMap::with_capacity(257);
|
||||||
let bpp: usize = (4 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
|
let transparency_pixel = png.transparency_pixel.as_ref().map(|t| RGB8::new(t[1], t[3], t[5]));
|
||||||
for line in png.scan_lines() {
|
for line in png.scan_lines() {
|
||||||
reduced.push(line.filter);
|
reduced.push(line.filter);
|
||||||
let mut cur_pixel = Vec::with_capacity(bpp);
|
let ok = if png.ihdr_data.color_type == ColorType::RGB {
|
||||||
for (i, byte) in line.data.iter().enumerate() {
|
reduce_scanline_to_palette(line.data.as_rgb().iter().cloned().map(|px| {
|
||||||
cur_pixel.push(*byte);
|
px.alpha(if Some(px) != transparency_pixel {255} else {0})
|
||||||
if i % bpp == bpp - 1 {
|
}), &mut palette, &mut reduced)
|
||||||
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
|
|
||||||
reduced.push(idx as u8);
|
|
||||||
} else {
|
|
||||||
let len = palette.len();
|
|
||||||
if len == 256 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
palette.push(cur_pixel);
|
|
||||||
reduced.push(len as u8);
|
|
||||||
}
|
|
||||||
cur_pixel = Vec::with_capacity(bpp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut color_palette = Vec::with_capacity(
|
|
||||||
palette.len() * 3 + if png.aux_headers.contains_key(b"bKGD") {
|
|
||||||
6
|
|
||||||
} else {
|
} else {
|
||||||
0
|
debug_assert_eq!(png.ihdr_data.color_type, ColorType::RGBA);
|
||||||
},
|
reduce_scanline_to_palette(line.data.as_rgba().iter().cloned(), &mut palette, &mut reduced)
|
||||||
);
|
};
|
||||||
let mut trans_palette = Vec::with_capacity(palette.len());
|
if !ok {
|
||||||
for color in &palette {
|
return false;
|
||||||
for (i, byte) in color.iter().enumerate() {
|
|
||||||
if i < 3 {
|
|
||||||
color_palette.push(*byte);
|
|
||||||
} else {
|
|
||||||
trans_palette.push(*byte);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let headers_size = color_palette.len() + trans_palette.len() + 8;
|
let num_transparent = palette.iter()
|
||||||
if reduced.len() + headers_size > png.raw_data.len() * 4 {
|
.filter_map(|(px, &idx)| if px.a != 255 {Some(idx as usize +1)} else {None})
|
||||||
|
.max();
|
||||||
|
let trns_size = num_transparent.map(|n| n+8).unwrap_or(0);
|
||||||
|
|
||||||
|
let headers_size = palette.len()*3 + 8 + trns_size;
|
||||||
|
if reduced.len() + headers_size > png.raw_data.len() {
|
||||||
// Reduction would result in a larger image
|
// Reduction would result in a larger image
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
||||||
assert_eq!(bkgd_header.len(), 6);
|
assert_eq!(bkgd_header.len(), 6);
|
||||||
let header_pixels = bkgd_header
|
// In bKGD 16-bit values are used even for 8-bit images
|
||||||
.iter()
|
let bg = RGBA8::new(bkgd_header[1], bkgd_header[3], bkgd_header[5], 255);
|
||||||
.skip(1)
|
let entry = if let Some(&entry) = palette.get(&bg) {
|
||||||
.step(2)
|
entry
|
||||||
.cloned()
|
} else if palette.len() < 256 {
|
||||||
.collect::<Vec<u8>>();
|
let entry = palette.len() as u8;
|
||||||
if let Some(entry) = color_palette
|
palette.insert(bg, entry);
|
||||||
.chunks(3)
|
entry
|
||||||
.position(|x| x == header_pixels.as_slice())
|
|
||||||
{
|
|
||||||
*bkgd_header = vec![entry as u8];
|
|
||||||
} else if color_palette.len() / 3 == 256 {
|
|
||||||
return false;
|
|
||||||
} else {
|
} else {
|
||||||
let entry = color_palette.len() / 3;
|
return false;
|
||||||
color_palette.extend_from_slice(&header_pixels);
|
};
|
||||||
*bkgd_header = vec![entry as u8];
|
*bkgd_header = vec![entry];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") {
|
||||||
assert_eq!(sbit_header.len(), 4);
|
assert_eq!(sbit_header.len(), 4);
|
||||||
sbit_header.pop();
|
sbit_header.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
png.raw_data = reduced;
|
let mut palette_vec = vec![RGBA8::new(0,0,0,0); palette.len()];
|
||||||
png.palette = Some(color_palette);
|
for (color, idx) in palette {
|
||||||
if trans_palette.iter().any(|x| *x != 255) {
|
palette_vec[idx as usize] = color;
|
||||||
while let Some(255) = trans_palette.last().cloned() {
|
|
||||||
trans_palette.pop();
|
|
||||||
}
|
|
||||||
png.transparency_palette = Some(trans_palette);
|
|
||||||
} else {
|
|
||||||
png.transparency_palette = None;
|
|
||||||
}
|
|
||||||
png.ihdr_data.color_type = ColorType::Indexed;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reduce_rgb_to_palette(png: &mut PngData) -> bool {
|
|
||||||
if png.ihdr_data.bit_depth != BitDepth::Eight {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let mut reduced = Vec::with_capacity(png.raw_data.len());
|
|
||||||
let mut palette = Vec::with_capacity(256);
|
|
||||||
if let Some(ref trns) = png.transparency_pixel {
|
|
||||||
assert_eq!(trns.len(), 6);
|
|
||||||
if trns[0] != trns[1] || trns[2] != trns[3] || trns[4] != trns[5] {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
palette.push(vec![trns[0], trns[2], trns[4]]);
|
|
||||||
}
|
|
||||||
let bpp: usize = (3 * png.ihdr_data.bit_depth.as_u8() as usize) >> 3;
|
|
||||||
for line in png.scan_lines() {
|
|
||||||
reduced.push(line.filter);
|
|
||||||
let mut cur_pixel = Vec::with_capacity(bpp);
|
|
||||||
for (i, byte) in line.data.iter().enumerate() {
|
|
||||||
cur_pixel.push(*byte);
|
|
||||||
if i % bpp == bpp - 1 {
|
|
||||||
if let Some(idx) = palette.iter().position(|x| x == &cur_pixel) {
|
|
||||||
reduced.push(idx as u8);
|
|
||||||
} else {
|
|
||||||
let len = palette.len();
|
|
||||||
if len == 256 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
palette.push(cur_pixel);
|
|
||||||
reduced.push(len as u8);
|
|
||||||
}
|
|
||||||
cur_pixel = Vec::with_capacity(bpp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut color_palette = Vec::with_capacity(palette.len() * 3);
|
|
||||||
for color in &palette {
|
|
||||||
color_palette.extend_from_slice(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
let headers_size = color_palette.len() + 4;
|
|
||||||
if reduced.len() + headers_size > png.raw_data.len() * 3 {
|
|
||||||
// Reduction would result in a larger image
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") {
|
|
||||||
assert_eq!(bkgd_header.len(), 6);
|
|
||||||
let header_pixels = bkgd_header
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.step(2)
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<u8>>();
|
|
||||||
if let Some(entry) = color_palette
|
|
||||||
.chunks(3)
|
|
||||||
.position(|x| x == header_pixels.as_slice())
|
|
||||||
{
|
|
||||||
*bkgd_header = vec![entry as u8];
|
|
||||||
} else if color_palette.len() == 255 {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
let entry = color_palette.len() / 3;
|
|
||||||
color_palette.extend_from_slice(&header_pixels);
|
|
||||||
*bkgd_header = vec![entry as u8];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
png.raw_data = reduced;
|
png.raw_data = reduced;
|
||||||
png.palette = Some(color_palette);
|
png.transparency_pixel = None;
|
||||||
|
png.palette = Some(palette_vec);
|
||||||
png.ihdr_data.color_type = ColorType::Indexed;
|
png.ihdr_data.color_type = ColorType::Indexed;
|
||||||
if png.transparency_pixel.is_some() {
|
|
||||||
png.transparency_pixel = None;
|
|
||||||
png.transparency_palette = Some(vec![0]);
|
|
||||||
};
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -718,7 +718,7 @@ fn palette_should_be_reduced_with_dupes() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 43 * 3);
|
assert_eq!(png.palette.unwrap().len(), 43);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
@ -737,7 +737,7 @@ fn palette_should_be_reduced_with_dupes() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 35 * 3);
|
assert_eq!(png.palette.unwrap().len(), 35);
|
||||||
|
|
||||||
remove_file(output).ok();
|
remove_file(output).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -751,7 +751,7 @@ fn palette_should_be_reduced_with_unused() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 35 * 3);
|
assert_eq!(png.palette.unwrap().len(), 35);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
@ -770,7 +770,7 @@ fn palette_should_be_reduced_with_unused() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 33 * 3);
|
assert_eq!(png.palette.unwrap().len(), 33);
|
||||||
|
|
||||||
remove_file(output).ok();
|
remove_file(output).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -784,7 +784,7 @@ fn palette_should_be_reduced_with_both() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 43 * 3);
|
assert_eq!(png.palette.unwrap().len(), 43);
|
||||||
|
|
||||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|
@ -803,7 +803,7 @@ fn palette_should_be_reduced_with_both() {
|
||||||
|
|
||||||
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
assert_eq!(png.ihdr_data.color_type, ColorType::Indexed);
|
||||||
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
assert_eq!(png.ihdr_data.bit_depth, BitDepth::Eight);
|
||||||
assert_eq!(png.palette.unwrap().len(), 33 * 3);
|
assert_eq!(png.palette.unwrap().len(), 33);
|
||||||
|
|
||||||
remove_file(output).ok();
|
remove_file(output).ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue