Parse fcTL/fdAT into frames
This commit is contained in:
parent
60911977f6
commit
fec6b2b9f3
4 changed files with 133 additions and 18 deletions
75
src/apng.rs
Normal file
75
src/apng.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::PngError,
|
||||||
|
headers::{read_be_u16, read_be_u32},
|
||||||
|
PngResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// Animated PNG frame
|
||||||
|
pub struct Frame {
|
||||||
|
/// Width of the frame
|
||||||
|
pub width: u32,
|
||||||
|
/// Height of the frame
|
||||||
|
pub height: u32,
|
||||||
|
/// X offset of the frame
|
||||||
|
pub x_offset: u32,
|
||||||
|
/// Y offset of the frame
|
||||||
|
pub y_offset: u32,
|
||||||
|
/// Frame delay numerator
|
||||||
|
pub delay_num: u16,
|
||||||
|
/// Frame delay denominator
|
||||||
|
pub delay_den: u16,
|
||||||
|
/// Frame disposal operation
|
||||||
|
pub dispose_op: u8,
|
||||||
|
/// Frame blend operation
|
||||||
|
pub blend_op: u8,
|
||||||
|
/// Frame data, from fdAT chunks
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
/// Construct a new Frame from the data in a fcTL chunk
|
||||||
|
pub fn from_fctl_data(byte_data: &[u8]) -> PngResult<Frame> {
|
||||||
|
if byte_data.len() < 26 {
|
||||||
|
return Err(PngError::TruncatedData);
|
||||||
|
}
|
||||||
|
Ok(Frame {
|
||||||
|
width: read_be_u32(&byte_data[4..8]),
|
||||||
|
height: read_be_u32(&byte_data[8..12]),
|
||||||
|
x_offset: read_be_u32(&byte_data[12..16]),
|
||||||
|
y_offset: read_be_u32(&byte_data[16..20]),
|
||||||
|
delay_num: read_be_u16(&byte_data[20..22]),
|
||||||
|
delay_den: read_be_u16(&byte_data[22..24]),
|
||||||
|
dispose_op: byte_data[24],
|
||||||
|
blend_op: byte_data[25],
|
||||||
|
data: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the data for a fcTL chunk using the given sequence number
|
||||||
|
#[must_use]
|
||||||
|
pub fn fctl_data(&self, sequence_number: u32) -> Vec<u8> {
|
||||||
|
let mut byte_data = Vec::with_capacity(26);
|
||||||
|
byte_data.write_all(&sequence_number.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.width.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.height.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.x_offset.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.y_offset.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.delay_num.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.delay_den.to_be_bytes()).unwrap();
|
||||||
|
byte_data.push(self.dispose_op);
|
||||||
|
byte_data.push(self.blend_op);
|
||||||
|
byte_data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the data for a fdAT chunk using the given sequence number
|
||||||
|
#[must_use]
|
||||||
|
pub fn fdat_data(&self, sequence_number: u32) -> Vec<u8> {
|
||||||
|
let mut byte_data = Vec::with_capacity(4 + self.data.len());
|
||||||
|
byte_data.write_all(&sequence_number.to_be_bytes()).unwrap();
|
||||||
|
byte_data.write_all(&self.data).unwrap();
|
||||||
|
byte_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ pub enum PngError {
|
||||||
TimedOut,
|
TimedOut,
|
||||||
NotPNG,
|
NotPNG,
|
||||||
APNGNotSupported,
|
APNGNotSupported,
|
||||||
|
APNGOutOfOrder,
|
||||||
InvalidData,
|
InvalidData,
|
||||||
TruncatedData,
|
TruncatedData,
|
||||||
ChunkMissing(&'static str),
|
ChunkMissing(&'static str),
|
||||||
|
|
@ -33,6 +34,7 @@ impl fmt::Display for PngError {
|
||||||
f.write_str("Missing data in the file; the file is truncated")
|
f.write_str("Missing data in the file; the file is truncated")
|
||||||
}
|
}
|
||||||
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"),
|
||||||
|
PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"),
|
||||||
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
|
PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"),
|
||||||
PngError::InvalidDepthForType(d, ref c) => {
|
PngError::InvalidDepthForType(d, ref c) => {
|
||||||
write!(f, "Invalid bit depth {d} for color type {c}")
|
write!(f, "Invalid bit depth {d} for color type {c}")
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ pub use crate::{
|
||||||
options::{InFile, Options, OutFile},
|
options::{InFile, Options, OutFile},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod apng;
|
||||||
mod atomicmin;
|
mod atomicmin;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod deflate;
|
mod deflate;
|
||||||
|
|
@ -530,6 +531,7 @@ fn optimize_raw(
|
||||||
raw: png,
|
raw: png,
|
||||||
idat_data,
|
idat_data,
|
||||||
aux_chunks: Vec::new(),
|
aux_chunks: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
};
|
};
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
||||||
debug!("Found better combination:");
|
debug!("Found better combination:");
|
||||||
|
|
@ -549,6 +551,7 @@ fn optimize_raw(
|
||||||
raw: result.image,
|
raw: result.image,
|
||||||
idat_data: result.idat_data,
|
idat_data: result.idat_data,
|
||||||
aux_chunks: Vec::new(),
|
aux_chunks: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
};
|
};
|
||||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
||||||
debug!("Found better combination:");
|
debug!("Found better combination:");
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use rgb::ComponentSlice;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
apng::*,
|
||||||
colors::{BitDepth, ColorType},
|
colors::{BitDepth, ColorType},
|
||||||
deflate,
|
deflate,
|
||||||
error::PngError,
|
error::PngError,
|
||||||
|
|
@ -47,6 +48,8 @@ pub struct PngData {
|
||||||
pub idat_data: Vec<u8>,
|
pub idat_data: Vec<u8>,
|
||||||
/// All non-critical chunks from the PNG are stored here
|
/// All non-critical chunks from the PNG are stored here
|
||||||
pub aux_chunks: Vec<Chunk>,
|
pub aux_chunks: Vec<Chunk>,
|
||||||
|
/// APNG frames
|
||||||
|
pub frames: Vec<Frame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PngData {
|
impl PngData {
|
||||||
|
|
@ -97,6 +100,8 @@ impl PngData {
|
||||||
let mut idat_data: Vec<u8> = Vec::new();
|
let mut idat_data: Vec<u8> = Vec::new();
|
||||||
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
|
let mut key_chunks: FxHashMap<[u8; 4], Vec<u8>> = FxHashMap::default();
|
||||||
let mut aux_chunks: Vec<Chunk> = Vec::new();
|
let mut aux_chunks: Vec<Chunk> = Vec::new();
|
||||||
|
let mut frames: Vec<Frame> = Vec::new();
|
||||||
|
let mut sequence_number = 0;
|
||||||
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
|
while let Some(chunk) = parse_next_chunk(byte_data, &mut byte_offset, opts.fix_errors)? {
|
||||||
match &chunk.name {
|
match &chunk.name {
|
||||||
b"IDAT" => {
|
b"IDAT" => {
|
||||||
|
|
@ -112,27 +117,46 @@ impl PngData {
|
||||||
b"IHDR" | b"PLTE" | b"tRNS" => {
|
b"IHDR" | b"PLTE" | b"tRNS" => {
|
||||||
key_chunks.insert(chunk.name, chunk.data.to_owned());
|
key_chunks.insert(chunk.name, chunk.data.to_owned());
|
||||||
}
|
}
|
||||||
_ => {
|
_ if opts.strip.keep(&chunk.name) => {
|
||||||
if opts.strip.keep(&chunk.name) {
|
if chunk.is_c2pa() {
|
||||||
if chunk.is_c2pa() {
|
// StripChunks::None is the default value, so to keep optimizing by default,
|
||||||
// StripChunks::None is the default value, so to keep optimizing by default,
|
// interpret it as stripping the C2PA metadata.
|
||||||
// interpret it as stripping the C2PA metadata.
|
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
|
||||||
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
|
if opts.strip == StripChunks::None {
|
||||||
if opts.strip == StripChunks::None {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Err(PngError::C2PAMetadataPreventsChanges);
|
|
||||||
}
|
}
|
||||||
aux_chunks.push(Chunk {
|
return Err(PngError::C2PAMetadataPreventsChanges);
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if chunk.name == *b"fcTL" || chunk.name == *b"fdAT" {
|
||||||
|
// Validate the sequence number
|
||||||
|
if read_be_u32(&chunk.data[0..4]) != sequence_number {
|
||||||
|
return Err(PngError::APNGOutOfOrder);
|
||||||
|
}
|
||||||
|
sequence_number += 1;
|
||||||
|
if chunk.name == *b"fcTL" && !idat_data.is_empty() {
|
||||||
|
// Only create a Frame if it's after the IDAT (else store it as an aux chunk)
|
||||||
|
frames.push(Frame::from_fctl_data(chunk.data)?);
|
||||||
|
continue;
|
||||||
|
} else if chunk.name == *b"fdAT" {
|
||||||
|
// Append the data to the last frame
|
||||||
|
frames
|
||||||
|
.last_mut()
|
||||||
|
.ok_or(PngError::APNGOutOfOrder)?
|
||||||
|
.data
|
||||||
|
.extend_from_slice(&chunk.data[4..]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regular ancillary chunk
|
||||||
|
aux_chunks.push(Chunk {
|
||||||
|
name: chunk.name,
|
||||||
|
data: chunk.data.to_owned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
b"acTL" => {
|
||||||
|
warn!("Stripping animation data from APNG - image will become standard PNG")
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,6 +190,7 @@ impl PngData {
|
||||||
idat_data,
|
idat_data,
|
||||||
raw: Arc::new(raw),
|
raw: Arc::new(raw),
|
||||||
aux_chunks,
|
aux_chunks,
|
||||||
|
frames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,14 +260,24 @@ impl PngData {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Special ancillary chunks that need to come after PLTE but before IDAT
|
// Special ancillary chunks that need to come after PLTE but before IDAT
|
||||||
|
let mut sequence_number = 0;
|
||||||
for chunk in aux_pre
|
for chunk in aux_pre
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
|
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
|
||||||
{
|
{
|
||||||
write_png_block(&chunk.name, &chunk.data, &mut output);
|
write_png_block(&chunk.name, &chunk.data, &mut output);
|
||||||
|
if &chunk.name == b"fcTL" {
|
||||||
|
sequence_number += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// IDAT data
|
// IDAT data
|
||||||
write_png_block(b"IDAT", &self.idat_data, &mut output);
|
write_png_block(b"IDAT", &self.idat_data, &mut output);
|
||||||
|
// APNG frames
|
||||||
|
for frame in self.frames.iter() {
|
||||||
|
write_png_block(b"fcTL", &frame.fctl_data(sequence_number), &mut output);
|
||||||
|
write_png_block(b"fdAT", &frame.fdat_data(sequence_number + 1), &mut output);
|
||||||
|
sequence_number += 2;
|
||||||
|
}
|
||||||
// Ancillary chunks that come after IDAT
|
// Ancillary chunks that come after IDAT
|
||||||
for aux_post in aux_split {
|
for aux_post in aux_split {
|
||||||
for chunk in aux_post {
|
for chunk in aux_post {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue