Preserve unmarked CMYK JPEG samples

This commit is contained in:
Dylan M. Taylor committed 2026-11-14 13:23:47 +00:00
1 parent d15bf1f35b
commit c2a69bbdc6
2 files changed
+30 -4

No files matched your search

+10 -4
View File
@@ -37,6 +37,8 @@ pub(crate) struct JpegMeta {
pub dpi: Option<(f32, f32)>,
/// The APP13 Photoshop image resource blocks (`8BIM` ...), segments joined, without the header.
pub photoshop: Option<Vec<u8>>,
/// Whether an Adobe APP14 marker identifies the four-component samples as inverted CMYK.
pub adobe_marker: bool,
pub adobe_transform: Option<u8>,
/// (width, height, components) from the first SOFn.
pub frame: Option<(u32, u32, u8)>,
@@ -148,8 +150,11 @@ pub(crate) fn scan_metadata(b: &[u8]) -> JpegMeta {
m.frame = Some((w, h, seg[5]));
sof = Some((marker, seg));
}
0xEE if seg.len() >= 12 && seg.starts_with(b"Adobe") => {
m.adobe_transform = Some(seg[11]);
0xEE if seg.starts_with(b"Adobe") => {
m.adobe_marker = true;
if seg.len() >= 12 {
m.adobe_transform = Some(seg[11]);
}
}
_ => {}
}
@@ -398,8 +403,9 @@ pub(crate) fn decode(bytes: &[u8], limits: &Limits) -> Result<Image, CodecError>
}
px.truncate(expected);
match input {
// Adobe-style CMYK is stored inverted (255 = no ink).
ColorSpace::CMYK => px.iter_mut().for_each(|v| *v = 255 - *v),
// Adobe-style CMYK is stored inverted (255 = no ink). Four-component JPEGs without
// APP14 are also encountered with their CMYK samples stored as ink values directly.
ColorSpace::CMYK if meta.adobe_marker => px.iter_mut().for_each(|v| *v = 255 - *v),
ColorSpace::YCCK => {
for p in px.as_chunks_mut::<4>().0 {
let (y, cb, cr) = (p[0] as f32, p[1] as f32 - 128.0, p[2] as f32 - 128.0);
+20
View File
@@ -115,6 +115,26 @@ fn jpeg_cmyk_roundtrip_with_adobe_marker() {
assert!(psnr(&img, &back) > 35.0, "{}", psnr(&img, &back));
}
#[test]
fn jpeg_cmyk_without_adobe_marker_keeps_samples_as_inks() {
let img = Image::from_u8(16, 16, ChannelLayout::Cmyk, [10u8, 100, 200, 50].repeat(256)).unwrap();
let marked = encode(&img, Format::Jpeg, &EncodeOptions { jpeg_quality: 95, ..Default::default() }).unwrap();
let signature = b"\xFF\xEE\x00\x0EAdobe";
let start = marked.windows(signature.len()).position(|part| part == signature).expect("Adobe APP14 marker");
// APP14's two marker bytes and two-byte length precede its 12-byte payload.
let mut unmarked = Vec::with_capacity(marked.len() - 16);
unmarked.extend_from_slice(&marked[..start]);
unmarked.extend_from_slice(&marked[start + 16..]);
let adobe_pixels = decode(&marked).unwrap().data().to_vec();
let unmarked_pixels = decode(&unmarked).unwrap().data().to_vec();
for (adobe, raw) in adobe_pixels.as_chunks::<4>().0.iter().zip(unmarked_pixels.as_chunks::<4>().0) {
for (&ink, &stored) in adobe.iter().zip(raw) {
assert_eq!(stored, 255 - ink, "unmarked CMYK samples are direct inks");
}
}
}
#[test]
fn jpeg_ycck_decodes_to_cmyk() {
let img = synth(40, 32, ChannelLayout::Cmyk, SampleType::U8, 4, 0.02);