diff --git a/.travis.yml b/.travis.yml index f3548d3c..626b2b9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ matrix: env: TARGET=x86_64-apple-darwin cache: cargo - os: linux - rust: 1.34.0 + rust: 1.36.0 env: TARGET=x86_64-unknown-linux-gnu cache: cargo - os: linux diff --git a/README.md b/README.md index dbb9ebfd..b3d252c7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ cargo build --release 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. Oxipng follows Semantic Versioning. diff --git a/src/filters.rs b/src/filters.rs index 875c3420..54b9bb63 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,12 +1,12 @@ -pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec { - let mut filtered = Vec::with_capacity(data.len()); +pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec) { + buf.reserve(data.len()); match filter { 0 => { - filtered.extend_from_slice(data); + buf.extend_from_slice(data); } 1 => { - filtered.extend_from_slice(&data[0..bpp]); - filtered.extend( + buf.extend_from_slice(&data[0..bpp]); + buf.extend( data.iter() .skip(bpp) .zip(data.iter()) @@ -15,9 +15,9 @@ pub fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec } 2 => { if last_line.is_empty() { - filtered.extend_from_slice(data); + buf.extend_from_slice(data); } else { - filtered.extend( + buf.extend( data.iter() .zip(last_line.iter()) .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 => { for (i, byte) in data.iter().enumerate() { 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), None => *byte, }); } else { - filtered.push(match i.checked_sub(bpp) { + buf.push(match i.checked_sub(bpp) { Some(x) => byte.wrapping_sub( ((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 => { for (i, byte) in data.iter().enumerate() { 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]), None => *byte, }); } else { - filtered.push(match i.checked_sub(bpp) { + buf.push(match i.checked_sub(bpp) { Some(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!(), } - filtered } -pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec { - let mut unfiltered = Vec::with_capacity(data.len()); +pub fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8], buf: &mut Vec) { + assert_eq!(buf.len(), 0); + buf.reserve(data.len()); match filter { 0 => { - unfiltered.extend_from_slice(data); + buf.extend_from_slice(data); } 1 => { for (i, byte) in data.iter().enumerate() { match i.checked_sub(bpp) { Some(x) => { - let b = unfiltered[x]; - unfiltered.push(byte.wrapping_add(b)); + let b = buf[x]; + buf.push(byte.wrapping_add(b)); } None => { - unfiltered.push(*byte); + buf.push(*byte); } }; } } 2 => { if last_line.is_empty() { - unfiltered.extend_from_slice(data); + buf.extend_from_slice(data); } else { - unfiltered.extend( + buf.extend( data.iter() .zip(last_line.iter()) .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() { match i.checked_sub(bpp) { Some(x) => { - let b = unfiltered[x]; - unfiltered.push(byte.wrapping_add(b >> 1)); + let b = buf[x]; + buf.push(byte.wrapping_add(b >> 1)); } None => { - unfiltered.push(*byte); + buf.push(*byte); } }; } else { match i.checked_sub(bpp) { Some(x) => { - let b = unfiltered[x]; - unfiltered.push(byte.wrapping_add( + let b = buf[x]; + buf.push(byte.wrapping_add( ((u16::from(b) + u16::from(last_line[i])) >> 1) as u8, )); } 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() { match i.checked_sub(bpp) { Some(x) => { - let b = unfiltered[x]; - unfiltered.push(byte.wrapping_add(b)); + let b = buf[x]; + buf.push(byte.wrapping_add(b)); } None => { - unfiltered.push(*byte); + buf.push(*byte); } }; } else { match i.checked_sub(bpp) { Some(x) => { - let b = unfiltered[x]; - unfiltered.push(byte.wrapping_add(paeth_predictor( + let b = buf[x]; + buf.push(byte.wrapping_add(paeth_predictor( b, last_line[i], last_line[x], ))); } 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!(), } - unfiltered } fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 { diff --git a/src/lib.rs b/src/lib.rs index f4179808..4b0ccd8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,11 +250,6 @@ impl Options { } 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() } @@ -285,9 +280,9 @@ impl Default for Options { for i in 0..4 { strategies.insert(i); } + // We always need NoOp to be present let mut alphas = HashSet::new(); - alphas.insert(colors::AlphaOptim::NoOp); - alphas.insert(colors::AlphaOptim::Black); + alphas.insert(AlphaOptim::NoOp); Options { backup: false, diff --git a/src/main.rs b/src/main.rs index a83ea18c..e7f37171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -363,6 +363,7 @@ fn parse_opts_into_struct( }; if matches.is_present("alpha") { + opts.alphas.insert(AlphaOptim::Black); opts.alphas.insert(AlphaOptim::White); opts.alphas.insert(AlphaOptim::Up); opts.alphas.insert(AlphaOptim::Down); diff --git a/src/png/mod.rs b/src/png/mod.rs index 2fd5184f..75760930 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -282,17 +282,19 @@ impl PngImage { let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; let mut last_line: Vec = Vec::new(); let mut last_pass = 1; + let mut unfiltered_buf = Vec::new(); for line in self.scan_lines() { if let Some(pass) = line.pass { if pass != last_pass { - last_line = Vec::new(); + last_line.clear(); 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.extend_from_slice(&unfiltered_line); - last_line = unfiltered_line; + unfiltered.extend_from_slice(&unfiltered_buf); + std::mem::swap(&mut last_line, &mut unfiltered_buf); + unfiltered_buf.clear(); } unfiltered } @@ -309,7 +311,9 @@ impl PngImage { let bpp = ((self.ihdr.bit_depth.as_u8() * self.channels_per_pixel() + 7) / 8) as usize; let mut last_line: &[u8] = &[]; let mut last_pass: Option = None; + let mut f_buf = Vec::new(); for line in self.scan_lines() { + f_buf.clear(); match filter { 0 | 1 | 2 | 3 | 4 => { let filter = if last_pass == line.pass || filter <= 1 { @@ -318,28 +322,34 @@ impl PngImage { 0 }; 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 => { // Heuristically guess best filter per line // Uses MSAD algorithm mentioned in libpng reference docs // http://www.libpng.org/pub/png/book/chapter09.html - let mut trials: Vec<(u8, Vec)> = 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 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 signed = x as i8; + acc + i16::from(signed).abs() as u64 + }); + if size < best_size { + best_size = size; + best_filter = filter; + std::mem::swap(&mut best_line, &mut f_buf); + } + f_buf.clear() //discard buffer, and start again + } - let (best_filter, best_line) = trials - .iter() - .min_by_key(|(_, line)| { - line.iter().fold(0u64, |acc, &x| { - let signed = x as i8; - acc + i16::from(signed).abs() as u64 - }) - }) - .unwrap(); - filtered.push(*best_filter); - filtered.extend_from_slice(best_line); + filtered.push(best_filter); + filtered.extend_from_slice(&best_line); } _ => unreachable!(), } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index 8401b539..88ff72c4 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -61,9 +61,24 @@ pub fn reduced_palette(png: &PngImage) -> Option { } } + 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 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 { continue; @@ -75,12 +90,12 @@ pub fn reduced_palette(png: &PngImage) -> Option { .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); match seen.entry(color) { Vacant(new) => { - *palette_map = Some(next_index as u8); + palette_map[i] = Some(next_index as u8); new.insert(next_index as u8); next_index += 1; } 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 { } } + //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 { // Some conversions will allow us to perform bit depth reduction that // wasn't possible before