use log::warn;
use rustc_hash::FxHashMap;
use std::{fs, path::Path, sync::Arc};
use crate::{
Options, PngResult,
apng::*,
colors::{BitDepth, ColorType},
deflate,
error::PngError,
filters::*,
headers::*,
};
pub(crate) mod scan_lines;
use self::scan_lines::ScanLines;
#[derive(Debug, Clone)]
pub struct PngImage {
pub ihdr: IhdrData,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct PngData {
pub raw: Arc<PngImage>,
pub idat_data: Vec<u8>,
pub aux_chunks: Vec<Chunk>,
pub frames: Vec<Frame>,
}
impl PngData {
#[inline]
pub fn new(filepath: &Path, opts: &Options) -> PngResult<Self> {
let byte_data = Self::read_file(filepath)?;
Self::from_slice(&byte_data, opts)
}
pub fn read_file(filepath: &Path) -> PngResult<Vec<u8>> {
fs::read(filepath).map_err(|e| PngError::ReadFailed(filepath.display().to_string(), e))
}
pub fn from_slice(byte_data: &[u8], opts: &Options) -> PngResult<Self> {
let mut byte_offset: usize = 0;
let header = byte_data.get(0..8).ok_or(PngError::TruncatedData)?;
if !file_header_is_valid(header) {
return Err(PngError::NotPNG);
}
byte_offset += 8;
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" => {
if idat_data.is_empty() {
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) => {
if chunk.name == *b"caBX" || chunk.name == *b"iDOT" {
if matches!(opts.strip, StripChunks::Keep(_)) {
return Err(PngError::ChunkPreventsChanges(chunk.name));
}
warn!(
"Stripping {} chunk which will be invalidated by file changes",
std::str::from_utf8(&chunk.name).unwrap()
);
continue;
}
if chunk.name == *b"fcTL" || chunk.name == *b"fdAT" {
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() {
frames.push(Frame::from_fctl_data(chunk.data)?);
continue;
} else if chunk.name == *b"fdAT" {
frames
.last_mut()
.ok_or(PngError::APNGOutOfOrder)?
.data
.extend_from_slice(&chunk.data[4..]);
continue;
}
}
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");
}
_ => (),
}
}
if idat_data.is_empty() {
return Err(PngError::ChunkMissing("IDAT"));
}
let Some(ihdr_chunk) = key_chunks.remove(b"IHDR") else {
return Err(PngError::ChunkMissing("IHDR"));
};
let ihdr = parse_ihdr_chunk(
&ihdr_chunk,
key_chunks.remove(b"PLTE"),
key_chunks.remove(b"tRNS"),
)?;
if let Some(max) = opts.max_decompressed_size
&& ihdr.raw_data_size() > max
{
return Err(PngError::InflatedDataTooLong(max));
}
let raw = PngImage::new(ihdr, &idat_data)?;
Ok(Self {
idat_data,
raw: Arc::new(raw),
aux_chunks,
frames,
})
}
#[must_use]
pub fn output(&self) -> Vec<u8> {
let mut output = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
let mut ihdr_data = Vec::with_capacity(13);
ihdr_data.extend_from_slice(&self.raw.ihdr.width.to_be_bytes());
ihdr_data.extend_from_slice(&self.raw.ihdr.height.to_be_bytes());
ihdr_data.extend_from_slice(&[self.raw.ihdr.bit_depth as u8]);
ihdr_data.extend_from_slice(&[
self.raw.ihdr.color_type.png_header_code(),
0, 0, self.raw.ihdr.interlaced as u8,
]);
write_png_block(b"IHDR", &ihdr_data, &mut output);
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| !matches!(&c.name, b"bKGD" | b"hIST" | b"tRNS" | b"fcTL"))
{
write_png_block(&chunk.name, &chunk.data, &mut output);
}
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_ref());
}
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),
} => {
write_png_block(b"tRNS", &trns.to_be_bytes(), &mut output);
}
ColorType::RGB {
transparent_color: Some(trns),
} => {
let trns_data: Vec<_> = trns.iter().flat_map(u16::to_be_bytes).collect();
write_png_block(b"tRNS", &trns_data, &mut output);
}
_ => {}
}
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;
}
}
write_png_block(b"IDAT", &self.idat_data, &mut output);
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;
}
for aux_post in aux_split {
for chunk in aux_post {
write_png_block(&chunk.name, &chunk.data, &mut output);
}
}
write_png_block(b"IEND", &[], &mut output);
output
}
}
impl PngImage {
pub fn new(ihdr: IhdrData, compressed_data: &[u8]) -> PngResult<Self> {
let raw_data = deflate::inflate(compressed_data, ihdr.raw_data_size())?;
if raw_data.len() != ihdr.raw_data_size() {
return Err(PngError::TruncatedData);
}
let mut image = Self {
ihdr,
data: raw_data,
};
image.data = image.unfilter_image()?;
Ok(image)
}
#[inline]
#[must_use]
pub const fn channels_per_pixel(&self) -> usize {
self.ihdr.color_type.channels_per_pixel() as usize
}
#[inline]
#[must_use]
pub const fn bytes_per_channel(&self) -> usize {
match self.ihdr.bit_depth {
BitDepth::Sixteen => 2,
_ => 1,
}
}
#[must_use]
pub fn key_chunks_size(&self) -> usize {
match &self.ihdr.color_type {
ColorType::Indexed { palette } => {
let plte = 12 + palette.len() * 3;
palette
.iter()
.rposition(|p| p.a != 255)
.map_or(plte, |trns| plte + 12 + trns + 1)
}
ColorType::Grayscale { transparent_shade } if transparent_shade.is_some() => 12 + 2,
ColorType::RGB { transparent_color } if transparent_color.is_some() => 12 + 6,
_ => 0,
}
}
#[must_use]
pub fn estimated_output_size(&self, idat_data: &[u8]) -> usize {
idat_data.len() + self.key_chunks_size()
}
#[inline]
#[must_use]
pub fn scan_lines(&self, has_filter: bool) -> ScanLines<'_> {
ScanLines::new(self, has_filter)
}
fn unfilter_image(&self) -> PngResult<Vec<u8>> {
let mut unfiltered = Vec::with_capacity(self.data.len());
let bpp = self.bytes_per_channel() * self.channels_per_pixel();
let mut prev_line: Vec<u8> = Vec::new();
let mut prev_pass = None;
for line in self.scan_lines(true) {
if prev_pass != line.pass || prev_line.is_empty() {
prev_line = vec![0; line.data.len()];
prev_pass = line.pass;
}
let offset = unfiltered.len();
let filter = RowFilter::try_from(line.filter).map_err(|()| PngError::InvalidData)?;
filter.unfilter_line(bpp, line.data, &prev_line, &mut unfiltered);
prev_line.clone_from_slice(&unfiltered[offset..]);
}
Ok(unfiltered)
}
#[must_use]
pub fn filter_image(
&self,
strategy: FilterStrategy,
optimize_alpha: bool,
) -> (Vec<u8>, FilterStrategy) {
let mut output = Vec::with_capacity(self.ihdr.raw_data_size());
let bpp = self.bytes_per_channel() * self.channels_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 filters_used = Vec::new();
let mut strategy_evaluator = strategy.evaluator();
for (i, line) in self.scan_lines(false).enumerate() {
if prev_pass != line.pass || prev_line.is_empty() {
prev_line = vec![0; line.data.len()];
prev_pass = line.pass;
}
let mut line_data = line.data.to_vec();
if let FilterStrategy::Basic(filter) = strategy {
filter.filter_line(bpp, &mut line_data, &prev_line, &mut output, alpha_bytes);
prev_line = line_data;
continue;
} else if let FilterStrategy::Predefined(lines) = &strategy {
let filter = lines.get(i).unwrap_or(&RowFilter::None);
filter.filter_line(bpp, &mut line_data, &prev_line, &mut output, alpha_bytes);
prev_line = line_data;
continue;
}
let mut best_filter = RowFilter::None;
if line_data.iter().all(|&x| x == 0) {
best_filter.filter_line(bpp, &mut line_data, &prev_line, &mut output, alpha_bytes);
prev_line = line_data;
filters_used.push(best_filter);
continue;
}
let line_len = line.data.len() + 1;
let mut best_line = vec![0; line_len];
let mut best_line_raw = Vec::with_capacity(line.data.len());
let offset = output.len();
let evaluator = strategy_evaluator.as_mut().unwrap();
evaluator.reset(line_len);
for f in RowFilter::ALL {
f.filter_line(bpp, &mut line_data, &prev_line, &mut output, alpha_bytes);
if evaluator.evaluate(&output, offset) {
best_line.clone_from_slice(&output[offset..]);
best_line_raw.clone_from(&line_data);
best_filter = f;
}
output.truncate(offset);
}
output.extend_from_slice(&best_line);
prev_line = best_line_raw;
filters_used.push(best_filter);
}
if filters_used.is_empty() {
(output, strategy)
} else {
(output, FilterStrategy::Predefined(filters_used))
}
}
}
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());
}