use image::metadata::Orientation;
use std::borrow::Cow;
const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
const JPEG_SOI: &[u8; 2] = &[0xFF, 0xD8];
const RIFF_MAGIC: &[u8; 4] = b"RIFF";
const WEBP_MAGIC: &[u8; 4] = b"WEBP";
const JPEG_TEM: u8 = 0x01;
const JPEG_EOI: u8 = 0xD9;
const JPEG_SOS: u8 = 0xDA;
const JPEG_APP1: u8 = 0xE1;
const JPEG_APP11: u8 = 0xEB;
const JPEG_APP13: u8 = 0xED;
const JPEG_COM: u8 = 0xFE;
const EXIF_PREFIX: &[u8] = b"Exif\0\0";
const VP8X_FLAG_EXIF: u8 = 0x08;
const VP8X_FLAG_XMP: u8 = 0x04;
#[must_use]
pub(crate) fn strip_image_provenance(bytes: &[u8]) -> Cow<'_, [u8]> {
let stripped = if bytes.starts_with(PNG_SIGNATURE) {
strip_png(bytes)
} else if bytes.starts_with(JPEG_SOI) && bytes.get(2) == Some(&0xFF) {
strip_jpeg(bytes)
} else if bytes.len() >= 12 && bytes.starts_with(RIFF_MAGIC) && &bytes[8..12] == WEBP_MAGIC {
strip_webp(bytes)
} else {
None
};
match stripped {
Some(rewritten) if rewritten != bytes => Cow::Owned(rewritten),
_ => Cow::Borrowed(bytes),
}
}
fn strip_png(bytes: &[u8]) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(bytes.len());
out.extend_from_slice(PNG_SIGNATURE);
let mut pos = PNG_SIGNATURE.len();
let mut removed = false;
loop {
let length = read_be_u32(bytes, pos)? as usize;
let chunk_type = read_fourcc(bytes, pos.checked_add(4)?)?;
let payload_start = pos.checked_add(8)?;
let payload_end = payload_start.checked_add(length)?;
let chunk_end = payload_end.checked_add(4)?;
if chunk_end > bytes.len() {
return None;
}
if pos == PNG_SIGNATURE.len() && &chunk_type != b"IHDR" {
return None;
}
match &chunk_type {
b"IEND" => {
if length != 0 {
return None;
}
out.extend_from_slice(&bytes[pos..chunk_end]);
out.extend_from_slice(&bytes[chunk_end..]);
return removed.then_some(out);
}
b"eXIf" => {
removed = true;
if let Some(minimal) =
minimal_exif_with_orientation(&bytes[payload_start..payload_end])
{
push_png_chunk(&mut out, b"eXIf", &minimal);
}
}
b"tEXt" | b"zTXt" | b"iTXt" | b"caBX" => removed = true,
_ => out.extend_from_slice(&bytes[pos..chunk_end]),
}
pos = chunk_end;
}
}
fn push_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8], payload: &[u8]) {
let mut crc = flate2::Crc::new();
crc.update(chunk_type);
crc.update(payload);
let length = u32::try_from(payload.len()).expect("rebuilt metadata fits a chunk length");
out.extend_from_slice(&length.to_be_bytes());
out.extend_from_slice(chunk_type);
out.extend_from_slice(payload);
out.extend_from_slice(&crc.sum().to_be_bytes());
}
fn strip_jpeg(bytes: &[u8]) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(bytes.len());
out.extend_from_slice(JPEG_SOI);
let mut pos = JPEG_SOI.len();
let mut removed = false;
loop {
if bytes.get(pos) != Some(&0xFF) {
return None;
}
let marker_start = pos;
while bytes.get(pos) == Some(&0xFF) {
pos += 1;
}
let marker = *bytes.get(pos)?;
pos += 1;
if marker == 0x00 {
return None;
}
match marker {
JPEG_EOI => {
out.extend_from_slice(&bytes[marker_start..]);
return removed.then_some(out);
}
JPEG_TEM | 0xD0..=0xD7 => out.extend_from_slice(&bytes[marker_start..pos]),
JPEG_SOS => {
let length = usize::from(read_be_u16(bytes, pos)?);
if length < 2 {
return None;
}
let scan_start = pos.checked_add(length)?;
if scan_start > bytes.len() {
return None;
}
out.extend_from_slice(&bytes[marker_start..scan_start]);
let mut scan_pos = scan_start;
let scan_end = loop {
let i = scan_pos + bytes.get(scan_pos..)?.iter().position(|&b| b == 0xFF)?;
match bytes.get(i + 1) {
None => return None,
Some(&next)
if next == 0x00 || next == 0xFF || (0xD0..=0xD7).contains(&next) =>
{
scan_pos = i + 1;
}
Some(_) => break i,
}
};
out.extend_from_slice(&bytes[scan_start..scan_end]);
pos = scan_end;
}
_ => {
let length = usize::from(read_be_u16(bytes, pos)?);
if length < 2 {
return None;
}
let payload_start = pos.checked_add(2)?;
let payload_end = pos.checked_add(length)?;
if payload_end > bytes.len() {
return None;
}
let payload = &bytes[payload_start..payload_end];
match marker {
JPEG_APP1 => {
removed = true;
if payload.starts_with(EXIF_PREFIX)
&& let Some(minimal) = minimal_exif_with_orientation(payload)
{
push_jpeg_segment(&mut out, marker, &minimal);
}
}
JPEG_APP11 if payload.starts_with(b"JP") => removed = true,
JPEG_APP13
if payload.starts_with(b"Photoshop 3.0\0")
|| payload.starts_with(b"Adobe_Photoshop2.5") =>
{
removed = true;
}
JPEG_COM => removed = true,
_ => out.extend_from_slice(&bytes[marker_start..payload_end]),
}
pos = payload_end;
}
}
}
}
fn push_jpeg_segment(out: &mut Vec<u8>, marker: u8, payload: &[u8]) {
out.push(0xFF);
out.push(marker);
let length = u16::try_from(payload.len() + 2).expect("rebuilt metadata fits a segment length");
out.extend_from_slice(&length.to_be_bytes());
out.extend_from_slice(payload);
}
fn strip_webp(bytes: &[u8]) -> Option<Vec<u8>> {
let riff_end = (read_le_u32(bytes, 4)? as usize).checked_add(8)?;
if riff_end > bytes.len() {
return None;
}
let mut out = Vec::with_capacity(bytes.len());
out.extend_from_slice(RIFF_MAGIC);
out.extend_from_slice(&[0; 4]); out.extend_from_slice(WEBP_MAGIC);
let mut flags_at = None;
let (mut saw_exif, mut saw_xmp, mut saw_c2pa) = (false, false, false);
let mut kept_exif = false;
let mut pos = 12;
while pos < riff_end {
let (fourcc, payload_start, payload_end, chunk_end) =
read_webp_chunk(bytes, pos, riff_end)?;
match &fourcc {
b"EXIF" => {
saw_exif = true;
if let Some(minimal) =
minimal_exif_with_orientation(&bytes[payload_start..payload_end])
{
kept_exif = true;
push_webp_chunk(&mut out, b"EXIF", &minimal);
}
}
b"XMP " => saw_xmp = true,
b"C2PA" => saw_c2pa = true,
b"VP8X" => {
if payload_end > payload_start {
flags_at = Some(out.len() + 8);
}
out.extend_from_slice(&bytes[pos..chunk_end]);
}
_ => out.extend_from_slice(&bytes[pos..chunk_end]),
}
pos = chunk_end;
}
if !(saw_exif || saw_xmp || saw_c2pa) {
return None;
}
if let Some(flags) = flags_at.and_then(|at| out.get_mut(at)) {
*flags &= !(VP8X_FLAG_EXIF | VP8X_FLAG_XMP);
if kept_exif {
*flags |= VP8X_FLAG_EXIF;
}
}
let riff_size = u32::try_from(out.len().checked_sub(8)?).ok()?;
out[4..8].copy_from_slice(&riff_size.to_le_bytes());
out.extend_from_slice(&bytes[riff_end..]);
Some(out)
}
fn read_webp_chunk(
bytes: &[u8],
pos: usize,
riff_end: usize,
) -> Option<([u8; 4], usize, usize, usize)> {
let fourcc = read_fourcc(bytes, pos)?;
let size = read_le_u32(bytes, pos.checked_add(4)?)? as usize;
let payload_start = pos.checked_add(8)?;
let payload_end = payload_start.checked_add(size)?;
let chunk_end = payload_end.checked_add(size & 1)?;
if chunk_end > riff_end {
return None;
}
Some((fourcc, payload_start, payload_end, chunk_end))
}
fn push_webp_chunk(out: &mut Vec<u8>, fourcc: &[u8], payload: &[u8]) {
let size = u32::try_from(payload.len()).expect("rebuilt metadata fits a chunk length");
out.extend_from_slice(fourcc);
out.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(payload);
if size & 1 != 0 {
out.push(0);
}
}
fn minimal_exif_with_orientation(payload: &[u8]) -> Option<Vec<u8>> {
let tiff = payload.strip_prefix(EXIF_PREFIX).unwrap_or(payload);
let orientation = Orientation::from_exif_chunk(tiff)?;
if orientation == Orientation::NoTransforms {
return None;
}
let orientation = orientation.to_exif();
let prefixed = payload.starts_with(EXIF_PREFIX);
let mut out = Vec::with_capacity(if prefixed { 32 } else { 26 });
if prefixed {
out.extend_from_slice(EXIF_PREFIX);
}
out.extend_from_slice(b"II\x2A\x00"); out.extend_from_slice(&8u32.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&[0x12, 0x01, 0x03, 0x00]); out.extend_from_slice(&1u32.to_le_bytes()); out.extend_from_slice(&[orientation, 0x00, 0x00, 0x00]); out.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); Some(out)
}
fn read_fourcc(bytes: &[u8], offset: usize) -> Option<[u8; 4]> {
let end = offset.checked_add(4)?;
bytes.get(offset..end)?.try_into().ok()
}
fn read_be_u16(bytes: &[u8], offset: usize) -> Option<u16> {
let end = offset.checked_add(2)?;
let raw: [u8; 2] = bytes.get(offset..end)?.try_into().ok()?;
Some(u16::from_be_bytes(raw))
}
fn read_be_u32(bytes: &[u8], offset: usize) -> Option<u32> {
let raw: [u8; 4] = bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?;
Some(u32::from_be_bytes(raw))
}
fn read_le_u32(bytes: &[u8], offset: usize) -> Option<u32> {
let raw: [u8; 4] = bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?;
Some(u32::from_le_bytes(raw))
}
#[cfg(test)]
mod tests {
use super::*;
use image::ImageEncoder as _;
fn sample_rgba() -> image::RgbaImage {
image::RgbaImage::from_raw(
2,
2,
vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ],
)
.expect("valid 2x2 RGBA buffer")
}
fn sample_rgb() -> image::RgbImage {
image::RgbImage::from_raw(
2,
2,
vec![
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0, ],
)
.expect("valid 2x2 RGB buffer")
}
fn rgba_pixels(bytes: &[u8]) -> Vec<u8> {
image::load_from_memory(bytes)
.expect("fixture must decode")
.to_rgba8()
.into_raw()
}
fn rewritten(bytes: &[u8]) -> Vec<u8> {
match strip_image_provenance(bytes) {
Cow::Owned(out) => out,
Cow::Borrowed(_) => panic!("a metadata-bearing fixture must be rewritten"),
}
}
fn encode_png(img: &image::RgbaImage) -> Vec<u8> {
let mut out = Vec::new();
image::codecs::png::PngEncoder::new(&mut out)
.write_image(
img.as_raw(),
img.width(),
img.height(),
image::ExtendedColorType::Rgba8,
)
.expect("PNG encode");
out
}
fn encode_jpeg(img: &image::RgbImage) -> Vec<u8> {
let mut out = Vec::new();
image::codecs::jpeg::JpegEncoder::new(&mut out)
.encode_image(&image::DynamicImage::ImageRgb8(img.clone()))
.expect("JPEG encode");
out
}
fn encode_webp_lossless(img: &image::RgbImage) -> Vec<u8> {
let mut out = Vec::new();
image::codecs::webp::WebPEncoder::new_lossless(&mut out)
.encode(
img.as_raw(),
img.width(),
img.height(),
image::ExtendedColorType::Rgb8,
)
.expect("WebP encode");
out
}
fn png_with_extra_chunks(base: &[u8], extra: &[u8]) -> Vec<u8> {
assert_eq!(&base[12..16], b"IHDR");
let mut out = base[..33].to_vec();
out.extend_from_slice(extra);
out.extend_from_slice(&base[33..]);
out
}
fn png_with_metadata() -> Vec<u8> {
let base = encode_png(&sample_rgba());
let mut extra = Vec::new();
push_png_chunk(&mut extra, b"gAMA", &[0x00, 0x00, 0xB1, 0x8F]);
push_png_chunk(&mut extra, b"iCCP", b"icc\0\0not-a-zlib-stream");
push_png_chunk(
&mut extra,
b"zTXt",
b"Raw profile type iptc\0\0not-a-zlib-stream",
);
push_png_chunk(
&mut extra,
b"iTXt",
b"XML:com.adobe.xmp\0\x00\x00\0\0<x:xmpmeta/>",
);
push_png_chunk(&mut extra, b"caBX", b"jumb\0\x10\0\0c2pa\x11manifest");
png_with_extra_chunks(&base, &extra)
}
fn png_chunk_types(bytes: &[u8]) -> Vec<[u8; 4]> {
let mut types = Vec::new();
let mut pos = 8;
while pos + 12 <= bytes.len() {
let length = u32::from_be_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
types.push(bytes[pos + 4..pos + 8].try_into().unwrap());
pos += 12 + length;
}
types
}
const MAKE_TAG_VALUE: &[u8] = b"ABC\0";
fn carries_make_tag(payload: &[u8]) -> bool {
payload
.windows(MAKE_TAG_VALUE.len())
.any(|w| w == MAKE_TAG_VALUE)
}
fn exif_with_orientation_and_make_tag(orientation: u8) -> Vec<u8> {
let mut out = b"II*\0".to_vec();
out.extend_from_slice(&8u32.to_le_bytes()); out.extend_from_slice(&2u16.to_le_bytes()); out.extend_from_slice(&[0x12, 0x01, 0x03, 0x00]); out.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); out.extend_from_slice(&[orientation, 0x00, 0x00, 0x00]); out.extend_from_slice(&[0x0F, 0x01, 0x02, 0x00]); out.extend_from_slice(&[0x04, 0x00, 0x00, 0x00]); out.extend_from_slice(MAKE_TAG_VALUE); out.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); out
}
fn jpeg_with_segments(base: &[u8], segments: &[Vec<u8>]) -> Vec<u8> {
assert!(base.starts_with(JPEG_SOI));
let mut out = base[..2].to_vec();
for segment in segments {
out.extend_from_slice(segment);
}
out.extend_from_slice(&base[2..]);
out
}
fn jpeg_segment(marker: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
push_jpeg_segment(&mut out, marker, payload);
out
}
fn jpeg_header_segments(bytes: &[u8]) -> Vec<(u8, Vec<u8>)> {
let mut segments = Vec::new();
let mut pos = 2;
while bytes.get(pos) == Some(&0xFF) {
while bytes.get(pos) == Some(&0xFF) {
pos += 1;
}
let Some(&marker) = bytes.get(pos) else {
break;
};
pos += 1;
if marker == JPEG_SOS || marker == JPEG_EOI {
break;
}
if marker == JPEG_TEM || (0xD0..=0xD7).contains(&marker) {
continue;
}
let Some(length) = read_be_u16(bytes, pos) else {
break;
};
let end = pos + usize::from(length);
segments.push((marker, bytes[pos + 2..end].to_vec()));
pos = end;
}
segments
}
fn has_segment(segments: &[(u8, Vec<u8>)], marker: u8, prefix: &[u8]) -> bool {
segments
.iter()
.any(|(m, payload)| *m == marker && payload.starts_with(prefix))
}
fn webp_chunks(bytes: &[u8]) -> Vec<([u8; 4], Vec<u8>)> {
let mut chunks = Vec::new();
let riff_end = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize + 8;
let mut pos = 12;
while pos < riff_end {
let fourcc = bytes[pos..pos + 4].try_into().unwrap();
let size = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
chunks.push((fourcc, bytes[pos + 8..pos + 8 + size].to_vec()));
pos += 8 + size + (size & 1);
}
chunks
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "call sites pass fourcc literals, which read better by reference"
)]
fn webp_chunk<'a>(chunks: &'a [([u8; 4], Vec<u8>)], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
chunks
.iter()
.find(|(id, _)| id == fourcc)
.map(|(_, payload)| payload.as_slice())
}
fn vp8x_payload(flags: u8, width: u32, height: u32) -> Vec<u8> {
let mut payload = vec![flags, 0, 0, 0];
payload.extend_from_slice(&(width - 1).to_le_bytes()[..3]);
payload.extend_from_slice(&(height - 1).to_le_bytes()[..3]);
payload
}
fn write_riff_size(bytes: &mut [u8]) {
let size = u32::try_from(bytes.len() - 8).expect("fixture fits u32");
bytes[4..8].copy_from_slice(&size.to_le_bytes());
}
fn webp_vp8l(img: &image::RgbImage) -> Vec<u8> {
webp_chunks(&encode_webp_lossless(img))
.into_iter()
.find(|(fourcc, _)| fourcc == b"VP8L")
.map(|(_, payload)| payload)
.expect("lossless encoder emits a VP8L chunk")
}
fn webp_fixture(
img: &image::RgbImage,
vp8l: &[u8],
flags: u8,
extra: &[(&[u8], &[u8])],
) -> Vec<u8> {
let mut fixture = Vec::new();
fixture.extend_from_slice(RIFF_MAGIC);
fixture.extend_from_slice(&[0; 4]);
fixture.extend_from_slice(WEBP_MAGIC);
push_webp_chunk(
&mut fixture,
b"VP8X",
&vp8x_payload(flags, img.width(), img.height()),
);
push_webp_chunk(&mut fixture, b"VP8L", vp8l);
for (fourcc, payload) in extra {
push_webp_chunk(&mut fixture, fourcc, payload);
}
write_riff_size(&mut fixture);
fixture
}
#[test]
fn png_drops_metadata_chunks_and_keeps_rendering_state() {
let base = encode_png(&sample_rgba());
let fixture = png_with_metadata();
assert_eq!(rgba_pixels(&fixture), rgba_pixels(&base));
let out = rewritten(&fixture);
let types = png_chunk_types(&out);
assert!(types.contains(b"IHDR"));
assert!(types.contains(b"IDAT"));
assert!(types.contains(b"IEND"));
assert!(types.contains(b"gAMA"));
assert!(types.contains(b"iCCP"));
for gone in [b"zTXt", b"iTXt", b"caBX"] {
assert!(!types.contains(gone), "{gone:?} must be dropped");
}
assert_eq!(rgba_pixels(&out), rgba_pixels(&base));
}
#[test]
fn png_keeps_non_identity_orientation_and_drops_the_rest() {
let base = encode_png(&sample_rgba());
let mut extra = Vec::new();
push_png_chunk(&mut extra, b"eXIf", &exif_with_orientation_and_make_tag(6));
let fixture = png_with_extra_chunks(&base, &extra);
assert_eq!(rgba_pixels(&fixture), rgba_pixels(&base));
let out = rewritten(&fixture);
let types = png_chunk_types(&out);
assert!(types.contains(b"eXIf"));
assert_eq!(
crate::util::exif_orientation(&out).map(Orientation::to_exif),
Some(6)
);
assert_eq!(rgba_pixels(&out), rgba_pixels(&base));
for payload in [
exif_with_orientation_and_make_tag(1),
b"not a tiff block".to_vec(),
] {
let mut extra = Vec::new();
push_png_chunk(&mut extra, b"eXIf", &payload);
let fixture = png_with_extra_chunks(&base, &extra);
let out = rewritten(&fixture);
assert!(!png_chunk_types(&out).contains(b"eXIf"));
assert_eq!(rgba_pixels(&out), rgba_pixels(&base));
}
}
#[test]
fn jpeg_drops_metadata_segments_and_keeps_rendering_state() {
let base = encode_jpeg(&sample_rgb());
let fixture = jpeg_with_segments(
&base,
&[
jpeg_segment(0xE1, b"http://ns.adobe.com/xap/1.0/\0<x:xmpmeta/>"),
jpeg_segment(0xED, b"Photoshop 3.0\0iptc"),
jpeg_segment(0xEB, b"JP\0\x01\0\0jumb-c2pa"),
jpeg_segment(JPEG_COM, b"generated by a model"),
jpeg_segment(0xE2, b"ICC_PROFILE\0\x01\x01fake-icc-bytes"),
jpeg_segment(0xEE, b"Adobe\x00\x64\x00\x00\x00\x00\x01"),
],
);
assert_eq!(rgba_pixels(&fixture), rgba_pixels(&base));
let out = rewritten(&fixture);
let segments = jpeg_header_segments(&out);
assert!(has_segment(&segments, 0xE0, b"JFIF\0"));
assert!(has_segment(&segments, 0xE2, b"ICC_PROFILE\0"));
assert!(has_segment(&segments, 0xEE, b"Adobe"));
assert!(!has_segment(
&segments,
0xE1,
b"http://ns.adobe.com/xap/1.0/"
));
assert!(!has_segment(&segments, 0xED, b"Photoshop 3.0\0"));
assert!(!has_segment(&segments, 0xEB, b"JP"));
assert!(!segments.iter().any(|(marker, _)| *marker == JPEG_COM));
assert_eq!(rgba_pixels(&out), rgba_pixels(&base));
let truncated = &base[..base.len() / 2];
assert_eq!(strip_image_provenance(truncated), truncated);
}
#[test]
fn jpeg_keeps_non_identity_orientation_and_drops_the_rest() {
let base = encode_jpeg(&sample_rgb());
let mut exif = EXIF_PREFIX.to_vec();
exif.extend_from_slice(&exif_with_orientation_and_make_tag(6));
let fixture = jpeg_with_segments(&base, &[jpeg_segment(JPEG_APP1, &exif)]);
let out = rewritten(&fixture);
let segments = jpeg_header_segments(&out);
assert_eq!(
crate::util::exif_orientation(&out).map(Orientation::to_exif),
Some(6)
);
let app1: Vec<_> = segments
.iter()
.filter(|(marker, _)| *marker == JPEG_APP1)
.collect();
assert_eq!(app1.len(), 1);
assert!(app1[0].1.starts_with(EXIF_PREFIX));
assert!(!carries_make_tag(&app1[0].1));
let mut exif = EXIF_PREFIX.to_vec();
exif.extend_from_slice(&exif_with_orientation_and_make_tag(1));
let fixture = jpeg_with_segments(&base, &[jpeg_segment(JPEG_APP1, &exif)]);
let out = rewritten(&fixture);
assert!(
!jpeg_header_segments(&out)
.iter()
.any(|(marker, _)| *marker == JPEG_APP1)
);
assert_eq!(rgba_pixels(&out), rgba_pixels(&base));
}
#[test]
fn jpeg_copies_entropy_data_and_scans_verbatim() {
let soi = JPEG_SOI.to_vec();
let head_com = jpeg_segment(JPEG_COM, b"generated by a model");
let dqt = jpeg_segment(0xDB, b"quant tables");
let sos_1 = jpeg_segment(JPEG_SOS, b"scan-1 header");
let scan_1 = vec![
0x01, 0xFF, 0x00, 0x02, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0xD0, 0x04, 0xFF, 0xD3, 0x05,
];
let mid_com = jpeg_segment(JPEG_COM, b"comment between scans");
let dht = jpeg_segment(0xC4, b"huffman tables");
let sos_2 = jpeg_segment(JPEG_SOS, b"scan-2 header");
let scan_2 = vec![0x06, 0xFF, 0x00, 0x07, 0xFF, 0x00];
let tail = vec![0xFF, 0xD9];
let trailer = b"TRAILER".to_vec();
let parts: [&[u8]; 11] = [
&soi, &head_com, &dqt, &sos_1, &scan_1, &mid_com, &dht, &sos_2, &scan_2, &tail,
&trailer,
];
let kept: [&[u8]; 9] = [
&soi, &dqt, &sos_1, &scan_1, &dht, &sos_2, &scan_2, &tail, &trailer,
];
let fixture = parts.concat();
let expected = kept.concat();
let out = rewritten(&fixture);
assert_eq!(out, expected);
}
#[test]
fn webp_drops_metadata_chunks_and_patches_vp8x_flags() {
let img = sample_rgb();
let encoded = encode_webp_lossless(&img);
let vp8l = webp_vp8l(&img);
let fixture = webp_fixture(
&img,
&vp8l,
0x2C,
&[
(b"EXIF", &exif_with_orientation_and_make_tag(1)),
(b"XMP ", b"<x:xmpmeta/>"),
(b"C2PA", b"jumb\0\x10\0\0c2pa"),
],
);
assert_eq!(rgba_pixels(&fixture), rgba_pixels(&encoded));
let out = rewritten(&fixture);
let chunks = webp_chunks(&out);
assert_eq!(webp_chunk(&chunks, b"VP8L"), Some(vp8l.as_slice()));
assert_eq!(webp_chunk(&chunks, b"EXIF"), None);
assert_eq!(webp_chunk(&chunks, b"XMP "), None);
assert_eq!(webp_chunk(&chunks, b"C2PA"), None);
assert_eq!(webp_chunk(&chunks, b"VP8X").unwrap()[0], 0x20);
assert_eq!(
out.len() - 8,
u32::from_le_bytes(out[4..8].try_into().unwrap()) as usize
);
assert_eq!(rgba_pixels(&out), rgba_pixels(&encoded));
let fixture = webp_fixture(&img, &vp8l, 0x0C, &[(b"C2PA", b"jumb\0\x10\0\0c2pa")]);
let out = rewritten(&fixture);
assert_eq!(webp_chunk(&webp_chunks(&out), b"VP8X").unwrap()[0], 0x00);
}
#[test]
fn webp_keeps_non_identity_orientation_and_sets_the_flag() {
let img = sample_rgb();
let encoded = encode_webp_lossless(&img);
let vp8l = webp_vp8l(&img);
let fixture = webp_fixture(
&img,
&vp8l,
0x24,
&[
(b"EXIF", &exif_with_orientation_and_make_tag(6)),
(b"XMP ", b"<x:xmpmeta/>"),
],
);
assert_eq!(rgba_pixels(&fixture), rgba_pixels(&encoded));
let out = rewritten(&fixture);
let chunks = webp_chunks(&out);
assert_eq!(
crate::util::exif_orientation(&out).map(Orientation::to_exif),
Some(6)
);
let exif = webp_chunk(&chunks, b"EXIF").unwrap();
assert!(!exif.starts_with(EXIF_PREFIX));
assert!(!carries_make_tag(exif));
assert_eq!(webp_chunk(&chunks, b"XMP "), None);
assert_eq!(webp_chunk(&chunks, b"VP8X").unwrap()[0], 0x28);
assert_eq!(rgba_pixels(&out), rgba_pixels(&encoded));
}
#[test]
fn webp_leaves_a_vp8x_without_a_flags_byte_unpatched() {
let img = sample_rgb();
let vp8l = webp_vp8l(&img);
let mut fixture = Vec::new();
fixture.extend_from_slice(RIFF_MAGIC);
fixture.extend_from_slice(&[0; 4]);
fixture.extend_from_slice(WEBP_MAGIC);
push_webp_chunk(&mut fixture, b"VP8X", &[]);
push_webp_chunk(&mut fixture, b"VP8L", &vp8l);
push_webp_chunk(&mut fixture, b"XMP ", b"<x:xmpmeta/>");
write_riff_size(&mut fixture);
let out = rewritten(&fixture);
let chunks = webp_chunks(&out);
assert_eq!(webp_chunk(&chunks, b"VP8L"), Some(vp8l.as_slice()));
assert_eq!(webp_chunk(&chunks, b"XMP "), None);
}
#[test]
fn webp_leaves_a_trailer_outside_the_declared_size() {
let img = sample_rgb();
let encoded = encode_webp_lossless(&img);
let mut fixture = webp_fixture(&img, &webp_vp8l(&img), 0x20, &[(b"XMP ", b"<x:xmpmeta/>")]);
let trailer = b"TRAILER";
fixture.extend_from_slice(trailer);
let out = rewritten(&fixture);
assert!(out.ends_with(trailer));
let declared = u32::from_le_bytes(out[4..8].try_into().unwrap()) as usize;
assert_eq!(declared, out.len() - 8 - trailer.len());
assert_eq!(rgba_pixels(&out), rgba_pixels(&encoded));
}
#[test]
fn non_images_and_unparsable_payloads_pass_through_unchanged() {
let png = encode_png(&sample_rgba());
let mp4 = b"\0\0\0\x18ftypmp42\0\0\0\0mp42isom avc1".as_slice();
let cases: [&[u8]; 3] = [
b"<svg xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>",
mp4,
&png[..20], ];
for bytes in cases {
let stripped = strip_image_provenance(bytes);
assert!(matches!(stripped, Cow::Borrowed(_)));
assert_eq!(stripped.as_ref(), bytes);
}
let mut extra = Vec::new();
push_png_chunk(&mut extra, b"eXIf", &exif_with_orientation_and_make_tag(6));
for fixture in [
png_with_metadata(),
png_with_extra_chunks(&encode_png(&sample_rgba()), &extra),
] {
let stripped = rewritten(&fixture);
assert!(matches!(
strip_image_provenance(&stripped),
Cow::Borrowed(_)
));
}
}
}