Merge branch 'master' into heuristic-evaluation
This commit is contained in:
commit
54c3b5c17e
7 changed files with 91 additions and 63 deletions
|
|
@ -11,7 +11,7 @@ matrix:
|
||||||
env: TARGET=x86_64-apple-darwin
|
env: TARGET=x86_64-apple-darwin
|
||||||
cache: cargo
|
cache: cargo
|
||||||
- os: linux
|
- os: linux
|
||||||
rust: 1.34.0
|
rust: 1.36.0
|
||||||
env: TARGET=x86_64-unknown-linux-gnu
|
env: TARGET=x86_64-unknown-linux-gnu
|
||||||
cache: cargo
|
cache: cargo
|
||||||
- os: linux
|
- os: linux
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ cargo build --release
|
||||||
cp target/release/oxipng /usr/local/bin
|
cp target/release/oxipng /usr/local/bin
|
||||||
```
|
```
|
||||||
|
|
||||||
The current minimum supported Rust version is **1.34.0**. Oxipng may compile on earlier versions of Rust,
|
The current minimum supported Rust version is **1.36.0**. Oxipng may compile on earlier versions of Rust,
|
||||||
but there is no guarantee.
|
but there is no guarantee.
|
||||||
|
|
||||||
Oxipng follows Semantic Versioning.
|
Oxipng follows Semantic Versioning.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
|
pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec<u8>) {
|
||||||
let mut filtered = Vec::with_capacity(data.len());
|
buf.reserve(data.len());
|
||||||
match filter {
|
match filter {
|
||||||
0 => {
|
0 => {
|
||||||
filtered.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
filtered.extend_from_slice(&data[0..bpp]);
|
buf.extend_from_slice(&data[0..bpp]);
|
||||||
filtered.extend(
|
buf.extend(
|
||||||
data.iter()
|
data.iter()
|
||||||
.skip(bpp)
|
.skip(bpp)
|
||||||
.zip(data.iter())
|
.zip(data.iter())
|
||||||
|
|
@ -15,9 +15,9 @@ pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
filtered.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
} else {
|
} else {
|
||||||
filtered.extend(
|
buf.extend(
|
||||||
data.iter()
|
data.iter()
|
||||||
.zip(last_line.iter())
|
.zip(last_line.iter())
|
||||||
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
.map(|(cur, last)| cur.wrapping_sub(*last)),
|
||||||
|
|
@ -27,12 +27,12 @@ pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec
|
||||||
3 => {
|
3 => {
|
||||||
for (i, byte) in data.iter().enumerate() {
|
for (i, byte) in data.iter().enumerate() {
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
filtered.push(match i.checked_sub(bpp) {
|
buf.push(match i.checked_sub(bpp) {
|
||||||
Some(x) => byte.wrapping_sub(data[x] >> 1),
|
Some(x) => byte.wrapping_sub(data[x] >> 1),
|
||||||
None => *byte,
|
None => *byte,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
filtered.push(match i.checked_sub(bpp) {
|
buf.push(match i.checked_sub(bpp) {
|
||||||
Some(x) => byte.wrapping_sub(
|
Some(x) => byte.wrapping_sub(
|
||||||
((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8,
|
((u16::from(data[x]) + u16::from(last_line[i])) >> 1) as u8,
|
||||||
),
|
),
|
||||||
|
|
@ -44,12 +44,12 @@ pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec
|
||||||
4 => {
|
4 => {
|
||||||
for (i, byte) in data.iter().enumerate() {
|
for (i, byte) in data.iter().enumerate() {
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
filtered.push(match i.checked_sub(bpp) {
|
buf.push(match i.checked_sub(bpp) {
|
||||||
Some(x) => byte.wrapping_sub(data[x]),
|
Some(x) => byte.wrapping_sub(data[x]),
|
||||||
None => *byte,
|
None => *byte,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
filtered.push(match i.checked_sub(bpp) {
|
buf.push(match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x]))
|
byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x]))
|
||||||
}
|
}
|
||||||
|
|
@ -60,33 +60,33 @@ pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
filtered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
|
pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec<u8>) {
|
||||||
let mut unfiltered = Vec::with_capacity(data.len());
|
assert_eq!(buf.len(), 0);
|
||||||
|
buf.reserve(data.len());
|
||||||
match filter {
|
match filter {
|
||||||
0 => {
|
0 => {
|
||||||
unfiltered.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
for (i, byte) in data.iter().enumerate() {
|
for (i, byte) in data.iter().enumerate() {
|
||||||
match i.checked_sub(bpp) {
|
match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let b = unfiltered[x];
|
let b = buf[x];
|
||||||
unfiltered.push(byte.wrapping_add(b));
|
buf.push(byte.wrapping_add(b));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
unfiltered.push(*byte);
|
buf.push(*byte);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
unfiltered.extend_from_slice(data);
|
buf.extend_from_slice(data);
|
||||||
} else {
|
} else {
|
||||||
unfiltered.extend(
|
buf.extend(
|
||||||
data.iter()
|
data.iter()
|
||||||
.zip(last_line.iter())
|
.zip(last_line.iter())
|
||||||
.map(|(cur, last)| cur.wrapping_add(*last)),
|
.map(|(cur, last)| cur.wrapping_add(*last)),
|
||||||
|
|
@ -98,23 +98,23 @@ pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> V
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
match i.checked_sub(bpp) {
|
match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let b = unfiltered[x];
|
let b = buf[x];
|
||||||
unfiltered.push(byte.wrapping_add(b >> 1));
|
buf.push(byte.wrapping_add(b >> 1));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
unfiltered.push(*byte);
|
buf.push(*byte);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match i.checked_sub(bpp) {
|
match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let b = unfiltered[x];
|
let b = buf[x];
|
||||||
unfiltered.push(byte.wrapping_add(
|
buf.push(byte.wrapping_add(
|
||||||
((u16::from(b) + u16::from(last_line[i])) >> 1) as u8,
|
((u16::from(b) + u16::from(last_line[i])) >> 1) as u8,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
unfiltered.push(byte.wrapping_add(last_line[i] >> 1));
|
buf.push(byte.wrapping_add(last_line[i] >> 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -125,25 +125,25 @@ pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> V
|
||||||
if last_line.is_empty() {
|
if last_line.is_empty() {
|
||||||
match i.checked_sub(bpp) {
|
match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let b = unfiltered[x];
|
let b = buf[x];
|
||||||
unfiltered.push(byte.wrapping_add(b));
|
buf.push(byte.wrapping_add(b));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
unfiltered.push(*byte);
|
buf.push(*byte);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match i.checked_sub(bpp) {
|
match i.checked_sub(bpp) {
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let b = unfiltered[x];
|
let b = buf[x];
|
||||||
unfiltered.push(byte.wrapping_add(paeth_predictor(
|
buf.push(byte.wrapping_add(paeth_predictor(
|
||||||
b,
|
b,
|
||||||
last_line[i],
|
last_line[i],
|
||||||
last_line[x],
|
last_line[x],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
unfiltered.push(byte.wrapping_add(last_line[i]));
|
buf.push(byte.wrapping_add(last_line[i]));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -151,7 +151,6 @@ pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> V
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
unfiltered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
|
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
|
||||||
|
|
|
||||||
|
|
@ -250,11 +250,6 @@ impl Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_preset_4(mut self) -> Self {
|
fn apply_preset_4(mut self) -> Self {
|
||||||
self.alphas.insert(AlphaOptim::White);
|
|
||||||
self.alphas.insert(AlphaOptim::Up);
|
|
||||||
self.alphas.insert(AlphaOptim::Down);
|
|
||||||
self.alphas.insert(AlphaOptim::Left);
|
|
||||||
self.alphas.insert(AlphaOptim::Right);
|
|
||||||
self.apply_preset_3()
|
self.apply_preset_3()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,9 +280,9 @@ impl Default for Options {
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
strategies.insert(i);
|
strategies.insert(i);
|
||||||
}
|
}
|
||||||
|
// We always need NoOp to be present
|
||||||
let mut alphas = HashSet::new();
|
let mut alphas = HashSet::new();
|
||||||
alphas.insert(colors::AlphaOptim::NoOp);
|
alphas.insert(AlphaOptim::NoOp);
|
||||||
alphas.insert(colors::AlphaOptim::Black);
|
|
||||||
|
|
||||||
Options {
|
Options {
|
||||||
backup: false,
|
backup: false,
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,7 @@ fn parse_opts_into_struct(
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches.is_present("alpha") {
|
if matches.is_present("alpha") {
|
||||||
|
opts.alphas.insert(AlphaOptim::Black);
|
||||||
opts.alphas.insert(AlphaOptim::White);
|
opts.alphas.insert(AlphaOptim::White);
|
||||||
opts.alphas.insert(AlphaOptim::Up);
|
opts.alphas.insert(AlphaOptim::Up);
|
||||||
opts.alphas.insert(AlphaOptim::Down);
|
opts.alphas.insert(AlphaOptim::Down);
|
||||||
|
|
|
||||||
|
|
@ -282,17 +282,19 @@ impl PngImage {
|
||||||
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
|
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
|
||||||
let mut last_line: Vec<u8> = Vec::new();
|
let mut last_line: Vec<u8> = Vec::new();
|
||||||
let mut last_pass = 1;
|
let mut last_pass = 1;
|
||||||
|
let mut unfiltered_buf = Vec::new();
|
||||||
for line in self.scan_lines() {
|
for line in self.scan_lines() {
|
||||||
if let Some(pass) = line.pass {
|
if let Some(pass) = line.pass {
|
||||||
if pass != last_pass {
|
if pass != last_pass {
|
||||||
last_line = Vec::new();
|
last_line.clear();
|
||||||
last_pass = pass;
|
last_pass = pass;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let unfiltered_line = unfilter_line(line.filter, bpp, &line.data, &last_line);
|
unfilter_line(line.filter, bpp, &line.data, &last_line, &mut unfiltered_buf);
|
||||||
unfiltered.push(0);
|
unfiltered.push(0);
|
||||||
unfiltered.extend_from_slice(&unfiltered_line);
|
unfiltered.extend_from_slice(&unfiltered_buf);
|
||||||
last_line = unfiltered_line;
|
std::mem::swap(&mut last_line, &mut unfiltered_buf);
|
||||||
|
unfiltered_buf.clear();
|
||||||
}
|
}
|
||||||
unfiltered
|
unfiltered
|
||||||
}
|
}
|
||||||
|
|
@ -309,7 +311,9 @@ impl PngImage {
|
||||||
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
|
let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize;
|
||||||
let mut last_line: &[u8] = &[];
|
let mut last_line: &[u8] = &[];
|
||||||
let mut last_pass: Option<u8> = None;
|
let mut last_pass: Option<u8> = None;
|
||||||
|
let mut f_buf = Vec::new();
|
||||||
for line in self.scan_lines() {
|
for line in self.scan_lines() {
|
||||||
|
f_buf.clear();
|
||||||
match filter {
|
match filter {
|
||||||
0 | 1 | 2 | 3 | 4 => {
|
0 | 1 | 2 | 3 | 4 => {
|
||||||
let filter = if last_pass == line.pass || filter <= 1 {
|
let filter = if last_pass == line.pass || filter <= 1 {
|
||||||
|
|
@ -318,28 +322,34 @@ impl PngImage {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
filtered.push(filter);
|
filtered.push(filter);
|
||||||
filtered.extend_from_slice(&filter_line(filter, bpp, &line.data, last_line));
|
filter_line(filter, bpp, &line.data, last_line, &mut f_buf);
|
||||||
|
filtered.extend_from_slice(&f_buf);
|
||||||
}
|
}
|
||||||
5 => {
|
5 => {
|
||||||
// Heuristically guess best filter per line
|
// Heuristically guess best filter per line
|
||||||
// Uses MSAD algorithm mentioned in libpng reference docs
|
// Uses MSAD algorithm mentioned in libpng reference docs
|
||||||
// http://www.libpng.org/pub/png/book/chapter09.html
|
// http://www.libpng.org/pub/png/book/chapter09.html
|
||||||
let mut trials: Vec<(u8, Vec<u8>)> = Vec::with_capacity(5);
|
let mut best_filter = 0;
|
||||||
|
let mut best_line = Vec::new();
|
||||||
|
let mut best_size = std::u64::MAX;
|
||||||
|
|
||||||
// Avoid vertical filtering on first line of each interlacing pass
|
// Avoid vertical filtering on first line of each interlacing pass
|
||||||
for filter in if last_pass == line.pass { 0..5 } else { 0..2 } {
|
for filter in if last_pass == line.pass { 0..5 } else { 0..2 } {
|
||||||
trials.push((filter, filter_line(filter, bpp, &line.data, last_line)));
|
filter_line(filter, bpp, &line.data, last_line, &mut f_buf);
|
||||||
}
|
let size = f_buf.iter().fold(0u64, |acc, &x| {
|
||||||
let (best_filter, best_line) = trials
|
|
||||||
.iter()
|
|
||||||
.min_by_key(|(_, line)| {
|
|
||||||
line.iter().fold(0u64, |acc, &x| {
|
|
||||||
let signed = x as i8;
|
let signed = x as i8;
|
||||||
acc + i16::from(signed).abs() as u64
|
acc + i16::from(signed).abs() as u64
|
||||||
})
|
});
|
||||||
})
|
if size < best_size {
|
||||||
.unwrap();
|
best_size = size;
|
||||||
filtered.push(*best_filter);
|
best_filter = filter;
|
||||||
filtered.extend_from_slice(best_line);
|
std::mem::swap(&mut best_line, &mut f_buf);
|
||||||
|
}
|
||||||
|
f_buf.clear() //discard buffer, and start again
|
||||||
|
|
||||||
|
}
|
||||||
|
filtered.push(best_filter);
|
||||||
|
filtered.extend_from_slice(&best_line);
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,24 @@ pub fn reduced_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut used_enumerated : Vec<(usize, &bool)>= used.iter().enumerate().collect();
|
||||||
|
used_enumerated.sort_by(|a, b| {
|
||||||
|
//Sort by ascending alpha and descending luma.
|
||||||
|
let color_val = |i| {
|
||||||
|
let color = palette.get(i).copied()
|
||||||
|
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
|
||||||
|
((color.a as i32) << 18)
|
||||||
|
// These are coefficients for standard sRGB to luma conversion
|
||||||
|
- (color.r as i32) * 299
|
||||||
|
- (color.g as i32) * 587
|
||||||
|
- (color.b as i32) * 114
|
||||||
|
};
|
||||||
|
color_val(a.0).cmp(&color_val(b.0))
|
||||||
|
});
|
||||||
|
|
||||||
let mut next_index = 0u16;
|
let mut next_index = 0u16;
|
||||||
let mut seen = HashMap::with_capacity(palette.len());
|
let mut seen = HashMap::with_capacity(palette.len());
|
||||||
for (i, (used, palette_map)) in used.iter().cloned().zip(palette_map.iter_mut()).enumerate()
|
for (i, used) in used_enumerated.iter().cloned()
|
||||||
{
|
{
|
||||||
if !used {
|
if !used {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -75,12 +90,12 @@ pub fn reduced_palette(png: &PngImage) -> Option<PngImage> {
|
||||||
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
|
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
|
||||||
match seen.entry(color) {
|
match seen.entry(color) {
|
||||||
Vacant(new) => {
|
Vacant(new) => {
|
||||||
*palette_map = Some(next_index as u8);
|
palette_map[i] = Some(next_index as u8);
|
||||||
new.insert(next_index as u8);
|
new.insert(next_index as u8);
|
||||||
next_index += 1;
|
next_index += 1;
|
||||||
}
|
}
|
||||||
Occupied(remap_to) => {
|
Occupied(remap_to) => {
|
||||||
*palette_map = Some(*remap_to.get());
|
palette_map[i] = Some(*remap_to.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,6 +226,14 @@ pub fn reduce_color_type(png: &PngImage) -> Option<PngImage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Make sure that palette gets sorted. Ideally, this should be done within reduced_color_to_palette.
|
||||||
|
if should_reduce_bit_depth && reduced.ihdr.color_type == ColorType::Indexed {
|
||||||
|
if let Some(r) = reduced_palette(&reduced) {
|
||||||
|
reduced = Cow::Owned(r);
|
||||||
|
should_reduce_bit_depth = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if should_reduce_bit_depth {
|
if should_reduce_bit_depth {
|
||||||
// Some conversions will allow us to perform bit depth reduction that
|
// Some conversions will allow us to perform bit depth reduction that
|
||||||
// wasn't possible before
|
// wasn't possible before
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue