Skip signed files or remove C2PA metadata
This commit is contained in:
parent
6153cc1029
commit
fad3679364
6 changed files with 63 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ pub enum PngError {
|
||||||
ChunkMissing(&'static str),
|
ChunkMissing(&'static str),
|
||||||
InvalidDepthForType(BitDepth, ColorType),
|
InvalidDepthForType(BitDepth, ColorType),
|
||||||
IncorrectDataLength(usize, usize),
|
IncorrectDataLength(usize, usize),
|
||||||
|
C2PAMetadataPreventsChanges,
|
||||||
Other(Box<str>),
|
Other(Box<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +42,9 @@ impl fmt::Display for PngError {
|
||||||
"Data length {} does not match the expected length {}",
|
"Data length {} does not match the expected length {}",
|
||||||
l1, l2
|
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),
|
PngError::Other(ref s) => f.write_str(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ pub struct Chunk {
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum StripChunks {
|
pub enum StripChunks {
|
||||||
/// None
|
/// None
|
||||||
|
///
|
||||||
|
/// ...except caBX chunk if it contains a C2PA.org signature.
|
||||||
None,
|
None,
|
||||||
/// Remove specific chunks
|
/// Remove specific chunks
|
||||||
Strip(IndexSet<[u8; 4]>),
|
Strip(IndexSet<[u8; 4]>),
|
||||||
|
|
@ -111,6 +113,36 @@ pub struct RawChunk<'a> {
|
||||||
pub data: &'a [u8],
|
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>(
|
pub fn parse_next_chunk<'a>(
|
||||||
byte_data: &'a [u8],
|
byte_data: &'a [u8],
|
||||||
byte_offset: &mut usize,
|
byte_offset: &mut usize,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use clap::ArgMatches;
|
||||||
mod cli;
|
mod cli;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use log::{error, warn, Level, LevelFilter};
|
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 rayon::prelude::*;
|
||||||
|
|
||||||
use crate::cli::DISPLAY_CHUNKS;
|
use crate::cli::DISPLAY_CHUNKS;
|
||||||
|
|
@ -88,6 +88,10 @@ fn main() -> ExitCode {
|
||||||
// PNG files, and return an error for them.
|
// PNG files, and return an error for them.
|
||||||
// We don't really want to return an error code for those files.
|
// We don't really want to return an error code for those files.
|
||||||
Ok(_) => OptimizationResult::Ok,
|
Ok(_) => OptimizationResult::Ok,
|
||||||
|
Err(e @ PngError::C2PAMetadataPreventsChanges) => {
|
||||||
|
warn!("{input}: {e}");
|
||||||
|
OptimizationResult::Skipped
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{input}: {e}");
|
error!("{input}: {e}");
|
||||||
OptimizationResult::Failed
|
OptimizationResult::Failed
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,15 @@ impl PngData {
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if opts.strip.keep(&chunk.name) {
|
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 {
|
aux_chunks.push(Chunk {
|
||||||
name: chunk.name,
|
name: chunk.name,
|
||||||
data: chunk.data.to_owned(),
|
data: chunk.data.to_owned(),
|
||||||
|
|
|
||||||
BIN
tests/files/c2pa-signed.png
Normal file
BIN
tests/files/c2pa-signed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
13
tests/lib.rs
13
tests/lib.rs
|
|
@ -42,6 +42,19 @@ fn optimize() {
|
||||||
assert!(result.is_ok());
|
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]
|
#[test]
|
||||||
fn optimize_corrupted() {
|
fn optimize_corrupted() {
|
||||||
let result = oxipng::optimize(
|
let result = oxipng::optimize(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue