oxipng/src/png/mod.rs
Josh Holmer 2a59419bdf
Provide BufferedZopfliDeflater and allow user to pass in a custom Deflater (#530)
* Add .whitesource configuration file

* Experimental: allow Zopfli to use any size BufWriter

* Allow user to specify the output buffer size as well

* Allow user to specify maximum block splits

* Reformat and fix warnings

* Use deflater on iCCP chunk as well

* Bug fix: need to implement Zlib format

* Make functions const when possible

* Switch to using zopfli::Options in prep for https://github.com/zopfli-rs/zopfli/pull/21

* Switch to using zopfli::Options in prep for https://github.com/zopfli-rs/zopfli/pull/21

* Cargo fmt

* Fix compilation

* Fix tests

* Fix more lints

* Fix more lints

* Fix compilation more

---------

Co-authored-by: mend-bolt-for-github[bot] <42819689+mend-bolt-for-github[bot]@users.noreply.github.com>
Co-authored-by: Chris Hennick <hennickc@amazon.com>
Co-authored-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
2023-07-07 22:35:01 -04:00

497 lines
21 KiB
Rust

use crate::colors::{BitDepth, ColorType};
use crate::deflate;
use crate::error::PngError;
use crate::filters::*;
use crate::headers::*;
use crate::interlace::{deinterlace_image, interlace_image, Interlacing};
use crate::Options;
use bitvec::bitarr;
use libdeflater::{CompressionLvl, Compressor};
use log::warn;
use rgb::ComponentSlice;
use rustc_hash::FxHashMap;
use std::fs::File;
use std::io::{BufReader, Read, Write};
use std::iter::Iterator;
use std::path::Path;
use std::sync::Arc;
pub(crate) mod scan_lines;
use self::scan_lines::ScanLines;
/// Compression level to use for the Brute filter strategy
const BRUTE_LEVEL: i32 = 1; // 1 is fastest, 2-4 are not useful, 5 is slower but more effective
/// Number of lines to compress with the Brute filter strategy
const BRUTE_LINES: usize = 4; // Values over 8 are generally not useful
#[derive(Debug, Clone)]
pub struct PngImage {
/// The headers stored in the IHDR chunk
pub ihdr: IhdrData,
/// The uncompressed, unfiltered data from the IDAT chunk
pub data: Vec<u8>,
}
/// Contains all data relevant to a PNG image
#[derive(Debug, Clone)]
pub struct PngData {
/// Uncompressed image data
pub raw: Arc<PngImage>,
/// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>,
/// All non-critical chunks from the PNG are stored here
pub aux_chunks: Vec<Chunk>,
}
impl PngData {
/// Create a new `PngData` struct by opening a file
#[inline]
pub fn new(filepath: &Path, opts: &Options) -> Result<Self, PngError> {
let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts)
}
pub fn read_file(filepath: &Path) -> Result<Vec<u8>, PngError> {
let file = match File::open(filepath) {
Ok(f) => f,
Err(_) => return Err(PngError::new("Failed to open file for reading")),
};
let file_len = file.metadata().map(|m| m.len() as usize).unwrap_or(0);
let mut reader = BufReader::new(file);
// Check file for PNG header
let mut header = [0; 8];
if reader.read_exact(&mut header).is_err() {
return Err(PngError::new("Not a PNG file: too small"));
}
if !file_header_is_valid(&header) {
return Err(PngError::new("Invalid PNG header detected"));
}
// Read raw png data into memory
let mut byte_data: Vec<u8> = Vec::with_capacity(file_len);
byte_data.extend_from_slice(&header);
match reader.read_to_end(&mut byte_data) {
Ok(_) => (),
Err(_) => return Err(PngError::new("Failed to read from file")),
}
Ok(byte_data)
}
/// Create a new `PngData` struct by reading a slice
pub fn from_slice(byte_data: &[u8], opts: &Options) -> Result<Self, PngError> {
let mut byte_offset: usize = 0;
// Test that png header is valid
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
if !file_header_is_valid(header) {
return Err(PngError::NotPNG);
}
byte_offset += 8;
// Read the data chunks
let mut idat_data: Vec<u8> = Vec::new();
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
let mut aux_chunks: Vec<Chunk> = Vec::new();
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
match &chunk.name {
b"IDAT" => {
if idat_data.is_empty() {
// Keep track of where the first IDAT sits relative to other chunks
aux_chunks.push(Chunk {
name: chunk.name,
data: Vec::new(),
})
}
idat_data.extend_from_slice(chunk.data);
}
b"IHDR" | b"PLTE" | b"tRNS" => {
key_chunks.insert(chunk.name, chunk.data.to_owned());
}
_ => {
if opts.strip.keep(&chunk.name) {
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),
})
} else if chunk.name == *b"acTL" {
warn!(
"Stripping animation data from APNG - image will become standard PNG"
);
}
}
}
}
// Parse the chunks into our PngData
if idat_data.is_empty() {
return Err(PngError::ChunkMissing("IDAT"));
}
let ihdr_chunk = match key_chunks.remove(b"IHDR") {
Some(ihdr) => ihdr,
None => return Err(PngError::ChunkMissing("IHDR")),
};
let ihdr = parse_ihdr_chunk(
&ihdr_chunk,
key_chunks.remove(b"PLTE"),
key_chunks.remove(b"tRNS"),
)?;
let raw_data = deflate::inflate(idat_data.as_ref(), ihdr.raw_data_size())?;
// Reject files with incorrect width/height or truncated data
if raw_data.len() != ihdr.raw_data_size() {
return Err(PngError::TruncatedData);
}
let mut raw = PngImage {
ihdr,
data: raw_data,
};
raw.data = raw.unfilter_image()?;
// Return the PngData
Ok(Self {
idat_data,
raw: Arc::new(raw),
aux_chunks,
})
}
/// Return an estimate of the output size which can help with evaluation of very small data
pub fn estimated_output_size(&self) -> usize {
self.idat_data.len() + self.raw.key_chunks_size()
}
/// Format the `PngData` struct into a valid PNG bytestream
pub fn output(&self) -> Vec<u8> {
// PNG header
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
// IHDR
let mut ihdr_data = Vec::with_capacity(13);
ihdr_data.write_all(&self.raw.ihdr.width.to_be_bytes()).ok();
ihdr_data
.write_all(&self.raw.ihdr.height.to_be_bytes())
.ok();
ihdr_data.write_all(&[self.raw.ihdr.bit_depth as u8]).ok();
ihdr_data
.write_all(&[self.raw.ihdr.color_type.png_header_code()])
.ok();
ihdr_data.write_all(&[0]).ok(); // Compression -- deflate
ihdr_data.write_all(&[0]).ok(); // Filter method -- 5-way adaptive filtering
ihdr_data.write_all(&[self.raw.ihdr.interlaced as u8]).ok();
write_png_block(b"IHDR", &ihdr_data, &mut output);
// Ancillary chunks - split into those that come before IDAT and those that come after
let mut aux_split = self.aux_chunks.split(|c| &c.name == b"IDAT");
let aux_pre = aux_split.next().unwrap();
for chunk in aux_pre
.iter()
.filter(|c| !(&c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
}
// Palette and transparency
match &self.raw.ihdr.color_type {
ColorType::Indexed { palette } => {
let mut palette_data = Vec::with_capacity(palette.len() * 3);
for px in palette {
palette_data.extend_from_slice(px.rgb().as_slice());
}
write_png_block(b"PLTE", &palette_data, &mut output);
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
let trns_data: Vec<_> = palette[0..=last_trns].iter().map(|px| px.a).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
}
ColorType::Grayscale {
transparent_shade: Some(trns),
} => {
// Transparency pixel - 2 byte u16
write_png_block(b"tRNS", &trns.to_be_bytes(), &mut output);
}
ColorType::RGB {
transparent_color: Some(trns),
} => {
// Transparency pixel - 6 byte RGB16
let trns_data: Vec<_> = trns.iter().flat_map(|c| c.to_be_bytes()).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
_ => {}
}
// Special ancillary chunks that need to come after PLTE but before IDAT
for chunk in aux_pre
.iter()
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
{
write_png_block(&chunk.name, &chunk.data, &mut output);
}
// IDAT data
write_png_block(b"IDAT", &self.idat_data, &mut output);
// Ancillary chunks that come after IDAT
for aux_post in aux_split {
for chunk in aux_post {
write_png_block(&chunk.name, &chunk.data, &mut output);
}
}
// Stream end
write_png_block(b"IEND", &[], &mut output);
output
}
}
impl PngImage {
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered
#[inline]
#[must_use]
pub fn change_interlacing(&self, interlace: Interlacing) -> Option<PngImage> {
if interlace == self.ihdr.interlaced {
return None;
}
Some(if interlace == Interlacing::Adam7 {
// Convert progressive to interlaced data
interlace_image(self)
} else {
// Convert interlaced to progressive data
deinterlace_image(self)
})
}
/// Return the number of channels in the image, based on color type
#[inline]
pub const fn channels_per_pixel(&self) -> usize {
self.ihdr.color_type.channels_per_pixel() as usize
}
/// Return the number of bytes per channel in the image
#[inline]
pub const fn bytes_per_channel(&self) -> usize {
match self.ihdr.bit_depth {
BitDepth::Sixteen => 2,
// Depths lower than 8 will round up to 1 byte
_ => 1,
}
}
/// Calculate the size of the PLTE and tRNS chunks
pub fn key_chunks_size(&self) -> usize {
match &self.ihdr.color_type {
ColorType::Indexed { palette } => {
let plte = 12 + palette.len() * 3;
let trns = palette.iter().filter(|p| p.a != 255).count();
if trns != 0 {
plte + 12 + trns
} else {
plte
}
}
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
_ => 0,
}
}
/// Return an iterator over the scanlines of the image
#[inline]
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
ScanLines::new(self, has_filter)
}
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
fn unfilter_image(&self) -> Result<Vec<u8>, PngError> {
let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
let mut last_line: Vec<u8> = Vec::new();
let mut last_pass = None;
let mut unfiltered_buf = Vec::new();
for line in self.scan_lines(true) {
if last_pass != line.pass {
last_line.clear();
last_pass = line.pass;
}
last_line.resize(line.data.len(), 0);
let filter = RowFilter::try_from(line.filter).map_err(|_| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &last_line, &mut unfiltered_buf)?;
unfiltered.extend_from_slice(&unfiltered_buf);
std::mem::swap(&mut last_line, &mut unfiltered_buf);
unfiltered_buf.clear();
}
Ok(unfiltered)
}
/// Apply the specified filter type to all rows in the image
pub fn filter_image(&self, filter: RowFilter, optimize_alpha: bool) -> Vec<u8> {
let mut filtered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
// If alpha optimization is enabled, determine how many bytes of alpha there are per pixel
let alpha_bytes = if optimize_alpha && self.ihdr.color_type.has_alpha() {
self.bytes_per_channel()
} else {
0
};
let mut prev_line = Vec::new();
let mut prev_pass: Option<u8> = None;
let mut f_buf = Vec::new();
for line in self.scan_lines(false) {
if prev_pass != line.pass || line.data.len() != prev_line.len() {
prev_line = vec![0; line.data.len()];
}
// Alpha optimisation may alter the line data, so we need a mutable copy of it
let mut line_data = line.data.to_vec();
if filter <= RowFilter::Paeth {
// Standard filters
let filter = if prev_pass == line.pass || filter <= RowFilter::Sub {
filter
} else {
RowFilter::None
};
filter.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered.extend_from_slice(&f_buf);
prev_line = line_data;
} else {
// Heuristic filter selection strategies
let mut best_line = Vec::new();
let mut best_line_raw = Vec::new();
// Avoid vertical filtering on first line of each interlacing pass
let try_filters = if prev_pass == line.pass {
RowFilter::STANDARD.iter()
} else {
RowFilter::SINGLE_LINE.iter()
};
match filter {
RowFilter::MinSum => {
// MSAD algorithm mentioned in libpng reference docs
// http://www.libpng.org/pub/png/book/chapter09.html
let mut best_size = usize::MAX;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let size = f_buf.iter().fold(0, |acc, &x| {
let signed = x as i8;
acc + signed.unsigned_abs() as usize
});
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw = line_data.clone();
}
}
}
RowFilter::Entropy => {
// Shannon entropy algorithm, from LodePNG
// https://github.com/lvandeve/lodepng
let mut best_size = i32::MIN;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut counts = vec![0; 0x100];
for &i in f_buf.iter() {
counts[i as usize] += 1;
}
let size = counts.into_iter().fold(0, |acc, x| {
if x == 0 {
return acc;
}
acc + ilog2i(x)
}) as i32;
if size > best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw = line_data.clone();
}
}
}
RowFilter::Bigrams => {
// Count distinct bigrams, from pngwolf
// https://bjoern.hoehrmann.de/pngwolf/
let mut best_size = usize::MAX;
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
let mut set = bitarr![0; 0x10000];
for pair in f_buf.windows(2) {
let bigram = (pair[0] as usize) << 8 | pair[1] as usize;
set.set(bigram, true);
}
let size = set.count_ones();
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw = line_data.clone();
}
}
}
RowFilter::BigEnt => {
// Bigram entropy, combined from Entropy and Bigrams filters
let mut best_size = i32::MIN;
// FxHasher is the fastest rust hasher currently available for this purpose
let mut counts = FxHashMap::<u16, u32>::default();
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
counts.clear();
for pair in f_buf.windows(2) {
let bigram = (pair[0] as u16) << 8 | pair[1] as u16;
counts.entry(bigram).and_modify(|e| *e += 1).or_insert(1);
}
let size = counts.values().fold(0, |acc, &x| acc + ilog2i(x)) as i32;
if size > best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw = line_data.clone();
}
}
}
RowFilter::Brute => {
// Brute force by compressing each filter attempt
// Similar to that of LodePNG but includes some previous lines for context
let mut best_size = usize::MAX;
let line_start = filtered.len();
filtered.resize(filtered.len() + line.data.len() + 1, 0);
let mut compressor =
Compressor::new(CompressionLvl::new(BRUTE_LEVEL).unwrap());
let limit = filtered.len().min((line.data.len() + 1) * BRUTE_LINES);
let capacity = compressor.zlib_compress_bound(limit);
let mut dest = vec![0; capacity];
for f in try_filters {
f.filter_line(bpp, &mut line_data, &prev_line, &mut f_buf, alpha_bytes);
filtered[line_start..].copy_from_slice(&f_buf);
let size = compressor
.zlib_compress(&filtered[filtered.len() - limit..], &mut dest)
.unwrap_or(usize::MAX);
if size < best_size {
best_size = size;
std::mem::swap(&mut best_line, &mut f_buf);
best_line_raw = line_data.clone();
}
}
filtered.resize(line_start, 0);
}
_ => unreachable!(),
}
filtered.extend_from_slice(&best_line);
prev_line = best_line_raw;
}
prev_pass = line.pass;
}
filtered
}
}
fn write_png_block(key: &[u8], chunk: &[u8], output: &mut Vec<u8>) {
let mut chunk_data = Vec::with_capacity(chunk.len() + 4);
chunk_data.extend_from_slice(key);
chunk_data.extend_from_slice(chunk);
output.reserve(chunk_data.len() + 8);
output.extend_from_slice(&(chunk_data.len() as u32 - 4).to_be_bytes());
let crc = deflate::crc32(&chunk_data);
output.append(&mut chunk_data);
output.extend_from_slice(&crc.to_be_bytes());
}
// Integer approximation for i * log2(i) - much faster than float calculations
const fn ilog2i(i: u32) -> u32 {
let log = 32 - i.leading_zeros() - 1;
i * log + ((i - (1 << log)) << 1)
}