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,
|
||||
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}")
|
||||
|
|
|
|||
|
|
@ -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:");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue