mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 00:41:21 +00:00
review: keep the mask of an over-chunked JPEG and size the stripped buffer from the kept bytes
This commit is contained in:
3 files changed
+28
-7
No files matched your search
@@ -329,6 +329,15 @@ fn jpeg_samples(data: &[u8], space: &Space, w: usize, h: usize) -> Option<(Vec<u
|
||||
Space::Cmyk => return cmyk_jpeg_samples(data, w, h).map(|px| (px, Space::Cmyk)),
|
||||
_ => {}
|
||||
}
|
||||
// Past the decoder's metadata bound, drop the Extended XMP chunks rather than the mask:
|
||||
// decoded as it stands, an over-chunked JPEG is written as is, without its /SMask.
|
||||
let stripped;
|
||||
let data = if crate::upright_image::decodable_metadata(data) {
|
||||
data
|
||||
} else {
|
||||
stripped = crate::upright_image::without_extended_xmp(data);
|
||||
&stripped[..]
|
||||
};
|
||||
let img = jpeg_of_size(data, w, h)?;
|
||||
Some(match space {
|
||||
Space::Gray => (img.into_luma8().into_raw(), Space::Gray),
|
||||
|
||||
@@ -845,10 +845,11 @@ fn damaged_masks_and_sizes_never_panic() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jpeg_with_thousands_of_extended_xmp_chunks_is_exported_without_decoding() {
|
||||
// Export decodes a JPEG with a soft mask, or a JPEG soft mask, to make a PNG with alpha: a JPEG
|
||||
// whose Extended XMP chunks would take the decoder quadratic time is written as it is instead,
|
||||
// and such a mask is left out, without reaching the decoder.
|
||||
fn a_jpeg_with_thousands_of_extended_xmp_chunks_keeps_its_mask() {
|
||||
// Export decodes a JPEG with a soft mask, or a JPEG soft mask, to make a PNG with alpha. A JPEG
|
||||
// whose Extended XMP chunks would take the decoder quadratic time is decoded without them,
|
||||
// which is linear in what is left, so the mask survives; a JPEG mask past the same bound is
|
||||
// still left out, without reaching the decoder.
|
||||
let flood = crate::upright_image::tests::with_extended_xmp_chunks(&real_jpeg(), 5_000);
|
||||
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
|
||||
with_image(&mut doc, |doc| {
|
||||
@@ -860,7 +861,12 @@ fn a_jpeg_with_thousands_of_extended_xmp_chunks_is_exported_without_decoding() {
|
||||
});
|
||||
crate::extract::JPEGS_DECODED.with(|n| n.set(0));
|
||||
let out = extract_images(&doc, &[0], 0);
|
||||
assert_eq!((out.images[0].extension, &out.images[0].data), ("jpg", &flood));
|
||||
assert_eq!(out.images[0].extension, "png");
|
||||
assert_eq!(crate::extract::JPEGS_DECODED.with(std::cell::Cell::get), 1);
|
||||
// The chunks themselves are gone, so the decoder never saw them.
|
||||
assert!(!out.images[0].data.windows(5).any(|w| w == b"http:"));
|
||||
|
||||
crate::extract::JPEGS_DECODED.with(|n| n.set(0));
|
||||
|
||||
let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap();
|
||||
with_image(&mut doc, |doc| {
|
||||
|
||||
@@ -82,7 +82,11 @@ fn extended_xmp_chunks(bytes: &[u8]) -> usize {
|
||||
/// JPEG `bytes` without their Extended XMP chunks, which nothing here reads: what is left decodes
|
||||
/// in time linear in its size however many chunks the file carried.
|
||||
pub(crate) fn without_extended_xmp(bytes: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(bytes.len());
|
||||
// Only the bytes that are kept: a CMYK JPEG can be mostly Extended XMP, and reserving the
|
||||
// whole file for the little that remains aborts the export under a tight address-space limit.
|
||||
let mut removed = 0usize;
|
||||
each_extended_xmp(bytes, |r| removed = removed.saturating_add(r.len()));
|
||||
let mut out = Vec::with_capacity(bytes.len().saturating_sub(removed));
|
||||
let mut at = 0;
|
||||
each_extended_xmp(bytes, |r| {
|
||||
out.extend_from_slice(bytes.get(at..r.start).unwrap_or_default());
|
||||
@@ -114,7 +118,9 @@ fn each_extended_xmp(bytes: &[u8], mut f: impl FnMut(std::ops::Range<usize>)) {
|
||||
let Some(&[hi, lo]) = bytes.get(i.saturating_add(2)..i.saturating_add(4)) else { break };
|
||||
let len = usize::from(u16::from_be_bytes([hi, lo]));
|
||||
let end = i.saturating_add(2).saturating_add(len.max(2));
|
||||
if marker == 0xE1 && bytes.get(i.saturating_add(4)..).is_some_and(|seg| seg.starts_with(PREFIX)) {
|
||||
// The prefix must start the APP1 segment's own payload: a zero-length header
|
||||
// followed by prefix bytes is not a chunk.
|
||||
if marker == 0xE1 && bytes.get(i.saturating_add(4)..end.min(bytes.len())).is_some_and(|seg| seg.starts_with(PREFIX)) {
|
||||
f(i..end.min(bytes.len()));
|
||||
}
|
||||
i = end;
|
||||
|
||||
Reference in new issue
Block a user