Parse fcTL/fdAT into frames

This commit is contained in:
Andrew 2025-01-14 19:44:28 +13:00 committed by Alejandro González
parent 60911977f6
commit fec6b2b9f3
4 changed files with 133 additions and 18 deletions

75
src/apng.rs Normal file
View 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
}
}

View file

@ -9,6 +9,7 @@ pub enum PngError {
TimedOut,
NotPNG,
APNGNotSupported,
APNGOutOfOrder,
InvalidData,
TruncatedData,
ChunkMissing(&'static str),
@ -33,6 +34,7 @@ impl fmt::Display for PngError {
f.write_str("Missing data in the file; the file is truncated")
}
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::InvalidDepthForType(d, ref c) => {
write!(f, "Invalid bit depth {d} for color type {c}")

View file

@ -59,6 +59,7 @@ pub use crate::{
options::{InFile, Options, OutFile},
};
mod apng;
mod atomicmin;
mod colors;
mod deflate;
@ -530,6 +531,7 @@ fn optimize_raw(
raw: png,
idat_data,
aux_chunks: Vec::new(),
frames: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");
@ -549,6 +551,7 @@ fn optimize_raw(
raw: result.image,
idat_data: result.idat_data,
aux_chunks: Vec::new(),
frames: Vec::new(),
};
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
debug!("Found better combination:");

View file

@ -12,6 +12,7 @@ use rgb::ComponentSlice;
use rustc_hash::FxHashMap;
use crate::{
apng::*,
colors::{BitDepth, ColorType},
deflate,
error::PngError,
@ -47,6 +48,8 @@ pub struct PngData {
pub idat_data: Vec<u8>,
/// All non-critical chunks from the PNG are stored here
pub aux_chunks: Vec<Chunk>,
/// APNG frames
pub frames: Vec<Frame>,
}
impl PngData {
@ -97,6 +100,8 @@ impl PngData {
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();
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)? {
match &chunk.name {
b"IDAT" => {
@ -112,27 +117,46 @@ impl PngData {
b"IHDR" | b"PLTE" | b"tRNS" => {
key_chunks.insert(chunk.name, chunk.data.to_owned());
}
_ => {
if opts.strip.keep(&chunk.name) {
if chunk.is_c2pa() {
// StripChunks::None is the default value, so to keep optimizing by default,
// interpret it as stripping the C2PA metadata.
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
if opts.strip == StripChunks::None {
continue;
}
return Err(PngError::C2PAMetadataPreventsChanges);
_ if opts.strip.keep(&chunk.name) => {
if chunk.is_c2pa() {
// StripChunks::None is the default value, so to keep optimizing by default,
// interpret it as stripping the C2PA metadata.
// The C2PA metadata is invalidated if the file changes, so it shouldn't be kept.
if opts.strip == StripChunks::None {
continue;
}
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"
);
return Err(PngError::C2PAMetadataPreventsChanges);
}
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,
raw: Arc::new(raw),
aux_chunks,
frames,
})
}
@ -235,14 +260,24 @@ impl PngData {
_ => {}
}
// Special ancillary chunks that need to come after PLTE but before IDAT
let mut sequence_number = 0;
for chunk in aux_pre
.iter()
.filter(|c| matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
if &chunk.name == b"fcTL" {
sequence_number += 1;
}
}
// IDAT data
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
for aux_post in aux_split {
for chunk in aux_post {