Allow APNG with reductions disabled (#511)
This commit is contained in:
parent
d81236f71e
commit
75a0f0de95
7 changed files with 134 additions and 31 deletions
|
|
@ -86,7 +86,9 @@ pub enum StripChunks {
|
|||
|
||||
impl StripChunks {
|
||||
/// List of chunks that will be kept when using the `Safe` option
|
||||
pub const KEEP_SAFE: [[u8; 4]; 4] = [*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs"];
|
||||
pub const KEEP_SAFE: [[u8; 4]; 7] = [
|
||||
*b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT",
|
||||
];
|
||||
|
||||
pub(crate) fn keep(&self, name: &[u8; 4]) -> bool {
|
||||
match &self {
|
||||
|
|
|
|||
61
src/lib.rs
61
src/lib.rs
|
|
@ -32,6 +32,7 @@ use crate::png::PngImage;
|
|||
use crate::reduction::*;
|
||||
use log::{debug, info, trace, warn};
|
||||
use rayon::prelude::*;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::fs::{copy, File, Metadata};
|
||||
use std::io::{stdin, stdout, BufWriter, Read, Write};
|
||||
|
|
@ -388,7 +389,7 @@ impl RawImage {
|
|||
/// Create an optimized png from the raw image data using the options provided
|
||||
pub fn create_optimized_png(&self, opts: &Options) -> PngResult<Vec<u8>> {
|
||||
let deadline = Arc::new(Deadline::new(opts.timeout));
|
||||
let mut png = optimize_raw(self.png.clone(), opts, deadline, None)
|
||||
let mut png = optimize_raw(self.png.clone(), opts, deadline.clone(), None)
|
||||
.ok_or_else(|| PngError::new("Failed to optimize input data"))?;
|
||||
|
||||
// Process aux chunks
|
||||
|
|
@ -398,7 +399,7 @@ impl RawImage {
|
|||
.filter(|c| opts.strip.keep(&c.name))
|
||||
.cloned()
|
||||
.collect();
|
||||
postprocess_chunks(&mut png, opts, &self.png.ihdr);
|
||||
postprocess_chunks(&mut png, opts, deadline, &self.png.ihdr);
|
||||
|
||||
Ok(png.output())
|
||||
}
|
||||
|
|
@ -564,17 +565,30 @@ fn optimize_png(
|
|||
debug!(" IDAT size = {} bytes", idat_original_size);
|
||||
debug!(" File size = {} bytes", file_original_size);
|
||||
|
||||
// Check for APNG by presence of acTL chunk
|
||||
let opts = if png.aux_chunks.iter().any(|c| &c.name == b"acTL") {
|
||||
warn!("APNG detected, disabling all reductions");
|
||||
let mut opts = opts.to_owned();
|
||||
opts.interlace = None;
|
||||
opts.bit_depth_reduction = false;
|
||||
opts.color_type_reduction = false;
|
||||
opts.palette_reduction = false;
|
||||
opts.grayscale_reduction = false;
|
||||
Cow::Owned(opts)
|
||||
} else {
|
||||
Cow::Borrowed(opts)
|
||||
};
|
||||
let max_size = if opts.force {
|
||||
None
|
||||
} else {
|
||||
Some(png.estimated_output_size())
|
||||
};
|
||||
if let Some(new_png) = optimize_raw(raw.clone(), opts, deadline, max_size) {
|
||||
if let Some(new_png) = optimize_raw(raw.clone(), &opts, deadline.clone(), max_size) {
|
||||
png.raw = new_png.raw;
|
||||
png.idat_data = new_png.idat_data;
|
||||
}
|
||||
|
||||
postprocess_chunks(png, opts, &raw.ihdr);
|
||||
postprocess_chunks(png, &opts, deadline, &raw.ihdr);
|
||||
|
||||
let output = png.output();
|
||||
|
||||
|
|
@ -844,7 +858,12 @@ fn report_format(prefix: &str, png: &PngImage) {
|
|||
}
|
||||
|
||||
/// Perform cleanup of certain chunks from the `PngData` object, after optimization has been completed
|
||||
fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) {
|
||||
fn postprocess_chunks(
|
||||
png: &mut PngData,
|
||||
opts: &Options,
|
||||
deadline: Arc<Deadline>,
|
||||
orig_ihdr: &IhdrData,
|
||||
) {
|
||||
if let Some(iccp_idx) = png.aux_chunks.iter().position(|c| &c.name == b"iCCP") {
|
||||
// See if we can replace an iCCP chunk with an sRGB chunk
|
||||
let may_replace_iccp = opts.strip != StripChunks::None && opts.strip.keep(b"sRGB");
|
||||
|
|
@ -897,6 +916,38 @@ fn postprocess_chunks(png: &mut PngData, opts: &Options, orig_ihdr: &IhdrData) {
|
|||
!invalid
|
||||
});
|
||||
}
|
||||
|
||||
// Find fdAT chunks and attempt to recompress them
|
||||
// Note if there are multiple fdATs per frame then decompression will fail and nothing will change
|
||||
let mut fdat: Vec<_> = png
|
||||
.aux_chunks
|
||||
.iter_mut()
|
||||
.filter(|c| &c.name == b"fdAT")
|
||||
.collect();
|
||||
if !fdat.is_empty() {
|
||||
let buffer_size = orig_ihdr.raw_data_size();
|
||||
fdat.par_iter_mut()
|
||||
.with_max_len(1)
|
||||
.enumerate()
|
||||
.for_each(|(i, c)| {
|
||||
if deadline.passed() || c.data.len() <= 4 {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut data) = deflate::inflate(&c.data[4..], buffer_size).and_then(|data| {
|
||||
let max_size = AtomicMin::new(Some(c.data.len() - 5));
|
||||
opts.deflate.deflate(&data, &max_size)
|
||||
}) {
|
||||
debug!(
|
||||
"Recompressed fdAT #{:<2}: {} ({} bytes decrease)",
|
||||
i,
|
||||
c.data.len(),
|
||||
c.data.len() - 4 - data.len()
|
||||
);
|
||||
c.data.truncate(4);
|
||||
c.data.append(&mut data);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an image was already optimized prior to oxipng's operations
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ fn main() {
|
|||
)
|
||||
.arg(
|
||||
Arg::new("strip")
|
||||
.help("Strip metadata objects ['safe', 'all', or comma-separated list]")
|
||||
.help("Strip metadata objects ['safe', 'all', or comma-separated list]\nCAUTION: stripping 'all' will convert APNGs to standard PNGs")
|
||||
.long("strip")
|
||||
.value_name("mode")
|
||||
.conflicts_with("strip-safe"),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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;
|
||||
|
|
@ -93,8 +94,16 @@ impl PngData {
|
|||
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" => idat_data.extend_from_slice(chunk.data),
|
||||
b"acTL" => return Err(PngError::APNGNotSupported),
|
||||
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());
|
||||
}
|
||||
|
|
@ -104,6 +113,10 @@ impl PngData {
|
|||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,9 +178,10 @@ impl PngData {
|
|||
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
|
||||
for chunk in self
|
||||
.aux_chunks
|
||||
// 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"))
|
||||
{
|
||||
|
|
@ -202,8 +216,7 @@ impl PngData {
|
|||
_ => {}
|
||||
}
|
||||
// Special ancillary chunks that need to come after PLTE but before IDAT
|
||||
for chunk in self
|
||||
.aux_chunks
|
||||
for chunk in aux_pre
|
||||
.iter()
|
||||
.filter(|c| &c.name == b"bKGD" || &c.name == b"hIST" || &c.name == b"tRNS")
|
||||
{
|
||||
|
|
@ -211,6 +224,12 @@ impl PngData {
|
|||
}
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
18
src/rayon.rs
18
src/rayon.rs
|
|
@ -26,6 +26,12 @@ pub trait IntoParallelRefIterator<'data> {
|
|||
fn par_iter(&'data self) -> Self::Iter;
|
||||
}
|
||||
|
||||
pub trait IntoParallelRefMutIterator<'data> {
|
||||
type Iter: ParallelIterator<Item = Self::Item>;
|
||||
type Item: Send + 'data;
|
||||
fn par_iter_mut(&'data mut self) -> Self::Iter;
|
||||
}
|
||||
|
||||
impl<I: IntoIterator> IntoParallelIterator for I
|
||||
where
|
||||
I::Item: Send,
|
||||
|
|
@ -50,6 +56,18 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I
|
||||
where
|
||||
&'data mut I: IntoParallelIterator,
|
||||
{
|
||||
type Iter = <&'data mut I as IntoParallelIterator>::Iter;
|
||||
type Item = <&'data mut I as IntoParallelIterator>::Item;
|
||||
|
||||
fn par_iter_mut(&'data mut self) -> Self::Iter {
|
||||
self.into_par_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator> ParallelIterator for I {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
use image::{DynamicImage, GenericImageView, ImageFormat, Pixel};
|
||||
use image::{codecs::png::PngDecoder, *};
|
||||
use log::{error, warn};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Validate that the output png data still matches the original image
|
||||
pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
||||
let (old_png, new_png) = rayon::join(
|
||||
let (old_frames, new_frames) = rayon::join(
|
||||
|| load_png_image_from_memory(original_data),
|
||||
|| load_png_image_from_memory(output),
|
||||
);
|
||||
|
||||
match (new_png, old_png) {
|
||||
match (new_frames, old_frames) {
|
||||
(Err(new_err), _) => {
|
||||
error!("Failed to read output image for validation: {}", new_err);
|
||||
false
|
||||
|
|
@ -21,26 +20,40 @@ pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool {
|
|||
warn!("Failed to read input image for validation: {}", old_err);
|
||||
true
|
||||
}
|
||||
(Ok(new_png), Ok(old_png)) => images_equal(&old_png, &new_png),
|
||||
(Ok(new_frames), Ok(old_frames)) if new_frames.len() != old_frames.len() => false,
|
||||
(Ok(new_frames), Ok(old_frames)) => {
|
||||
for (a, b) in old_frames.iter().zip(new_frames) {
|
||||
if !images_equal(&a, &b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a PNG image from memory to a [DynamicImage]
|
||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<DynamicImage, image::ImageError> {
|
||||
let mut reader = image::io::Reader::new(Cursor::new(png_data));
|
||||
reader.set_format(ImageFormat::Png);
|
||||
reader.no_limits();
|
||||
reader.decode()
|
||||
/// Loads a PNG image from memory to frames of [RgbaImage]
|
||||
fn load_png_image_from_memory(png_data: &[u8]) -> Result<Vec<RgbaImage>, image::ImageError> {
|
||||
let decoder = PngDecoder::new(png_data)?;
|
||||
if decoder.is_apng() {
|
||||
decoder
|
||||
.apng()
|
||||
.into_frames()
|
||||
.map(|f| f.map(|f| f.into_buffer()))
|
||||
.collect()
|
||||
} else {
|
||||
DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()])
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares images pixel by pixel for equivalent content
|
||||
fn images_equal(old_png: &DynamicImage, new_png: &DynamicImage) -> bool {
|
||||
fn images_equal(old_png: &RgbaImage, new_png: &RgbaImage) -> bool {
|
||||
let a = old_png.pixels().filter(|x| {
|
||||
let p = x.2.channels();
|
||||
let p = x.channels();
|
||||
!(p.len() == 4 && p[3] == 0)
|
||||
});
|
||||
let b = new_png.pixels().filter(|x| {
|
||||
let p = x.2.channels();
|
||||
let p = x.channels();
|
||||
!(p.len() == 4 && p[3] == 0)
|
||||
});
|
||||
a.eq(b)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn optimize_from_memory_apng() {
|
|||
in_file.read_to_end(&mut in_file_buf).unwrap();
|
||||
|
||||
let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default());
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -58,9 +58,9 @@ fn optimize_apng() {
|
|||
let result = oxipng::optimize(
|
||||
&"tests/files/apng_file.png".into(),
|
||||
&OutFile::Path(None),
|
||||
&Options::default(),
|
||||
&Options::from_preset(0),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue