Skip signed files or remove C2PA metadata

This commit is contained in:
Kornel 2024-12-23 17:22:47 +00:00 committed by Alejandro González
parent 910e779b09
commit 8270c6a034
6 changed files with 63 additions and 1 deletions

View file

@ -14,6 +14,7 @@ pub enum PngError {
ChunkMissing(&'static str),
InvalidDepthForType(BitDepth, ColorType),
IncorrectDataLength(usize, usize),
C2PAMetadataPreventsChanges,
Other(Box<str>),
}
@ -41,6 +42,9 @@ impl fmt::Display for PngError {
"Data length {} does not match the expected length {}",
l1, l2
),
PngError::C2PAMetadataPreventsChanges => f.write_str(
"The image contains C2PA manifest that would be invalidated by any file changes",
),
PngError::Other(ref s) => f.write_str(s),
}
}

View file

@ -75,6 +75,8 @@ pub struct Chunk {
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum StripChunks {
/// None
///
/// ...except caBX chunk if it contains a C2PA.org signature.
None,
/// Remove specific chunks
Strip(IndexSet<[u8; 4]>),
@ -111,6 +113,36 @@ pub struct RawChunk<'a> {
pub data: &'a [u8],
}
impl RawChunk<'_> {
// Is it a chunk for C2PA/CAI JUMBF metadata
pub(crate) fn is_c2pa(&self) -> bool {
if self.name == *b"caBX" {
if let Some((b"jumb", data)) = parse_jumbf_box(self.data) {
if let Some((b"jumd", data)) = parse_jumbf_box(data) {
if data.get(..4) == Some(b"c2pa") {
return true;
}
}
}
}
false
}
}
fn parse_jumbf_box(data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < 8 {
return None;
}
let (len, rest) = data.split_at(4);
let len = u32::from_be_bytes(len.try_into().unwrap()) as usize;
if len < 8 || len > data.len() {
return None;
}
let (box_name, data) = rest.split_at(4);
let data = data.get(..len - 8)?;
Some((box_name, data))
}
pub fn parse_next_chunk<'a>(
byte_data: &'a [u8],
byte_offset: &mut usize,

View file

@ -25,7 +25,7 @@ use clap::ArgMatches;
mod cli;
use indexmap::IndexSet;
use log::{error, warn, Level, LevelFilter};
use oxipng::{Deflaters, InFile, Options, OutFile, RowFilter, StripChunks};
use oxipng::{Deflaters, InFile, Options, OutFile, PngError, RowFilter, StripChunks};
use rayon::prelude::*;
use crate::cli::DISPLAY_CHUNKS;
@ -88,6 +88,10 @@ fn main() -> ExitCode {
// PNG files, and return an error for them.
// We don't really want to return an error code for those files.
Ok(_) => OptimizationResult::Ok,
Err(e @ PngError::C2PAMetadataPreventsChanges) => {
warn!("{input}: {e}");
OptimizationResult::Skipped
}
Err(e) => {
error!("{input}: {e}");
OptimizationResult::Failed

View file

@ -114,6 +114,15 @@ impl PngData {
}
_ => {
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);
}
aux_chunks.push(Chunk {
name: chunk.name,
data: chunk.data.to_owned(),

BIN
tests/files/c2pa-signed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -42,6 +42,19 @@ fn optimize() {
assert!(result.is_ok());
}
#[test]
fn skip_c2pa() {
let result = oxipng::optimize(
&"tests/files/c2pa-signed.png".into(),
&OutFile::None,
&Options {
strip: StripChunks::Keep(indexset! {*b"caBX"}),
..Options::default()
},
);
assert!(matches!(result, Err(PngError::C2PAMetadataPreventsChanges)));
}
#[test]
fn optimize_corrupted() {
let result = oxipng::optimize(