mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 14:38:13 -06:00
M10: export reads a /Decode array's numbers through references
Since the renderer now draws an image with /Decode [7 0 R 0], export disagreed with it. Its check of which way round a stencil mask or a CMYK JPEG is compared the two numbers as given, and a reference reads as none, so the file came out a negative of what the page draws. Tests for the function arrays the previous commit's tuple fix also covers: a function's /Domain, /Range, /Encode and /Decode are read a pair at a time too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa7acd3156
commit
ae94b54505
@@ -236,11 +236,12 @@ fn image(doc: &Document, s: &Stream) -> Result<(&'static str, Vec<u8>), String>
|
||||
let mut alpha = if mask { None } else { soft_mask(doc, s, w, h).or_else(|| stencil_mask(doc, s, w, h)) };
|
||||
let key = if mask { None } else { colour_key(doc, s) };
|
||||
// Keep the existing JPEG and stencil-mask Decode handling separate from lossless colour images.
|
||||
let endpoint = |value: Option<&Object>| value.map(|v| doc.resolve(v)).and_then(|v| v.as_f64());
|
||||
let inverted = (jpeg || mask)
|
||||
&& s.dict
|
||||
.get(b"Decode")
|
||||
.map(|d| doc.resolve(d))
|
||||
.and_then(|d| d.as_array().map(|a| a.first().and_then(Object::as_f64) > a.get(1).and_then(Object::as_f64)))
|
||||
.and_then(|d| d.as_array().map(|a| endpoint(a.first()) > endpoint(a.get(1))))
|
||||
.unwrap_or(false);
|
||||
// In a PDF a CMYK JPEG's samples are ink amounts unless /Decode inverts them, while image
|
||||
// viewers read a CMYK JPEG file as inverted (Adobe's convention): as is, it would show as
|
||||
|
||||
@@ -2209,3 +2209,31 @@ fn jbig2_globals_are_decoded_within_a_limit_of_their_own() {
|
||||
assert!(why.starts_with("the JBIG2 globals can't be read") && why.contains(&format!("limit of {LIMIT} bytes")), "{why}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `/Decode` says which way round a stencil mask is, and its numbers can be given by reference. Read
|
||||
/// as absent, they gave the wrong answer, so the file came out a negative of what the page draws.
|
||||
#[test]
|
||||
fn a_stencil_masks_decode_array_can_hold_references() {
|
||||
use pdfcraft_cos::Document;
|
||||
let export = |decode: &str| {
|
||||
let pdf = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /XObject << /Im 4 0 R >> >> >> endobj\n\
|
||||
4 0 obj << /Subtype /Image /ImageMask true /Width 8 /Height 1 /BitsPerComponent 1 /Decode {decode} /Filter /ASCIIHexDecode /Length 3 >> stream\nF0>\nendstream endobj\n\
|
||||
5 0 obj 1 endobj\n6 0 obj 0 endobj\ntrailer << /Root 1 0 R >>\n%%EOF"
|
||||
);
|
||||
let doc = Document::open(std::sync::Arc::new(pdf.into_bytes())).unwrap();
|
||||
let mut export = crate::extract::extract_images(&doc, &[0], 1);
|
||||
assert_eq!(export.images.len(), 1, "{decode}: {:?}", export.skipped);
|
||||
export.images.remove(0).data
|
||||
};
|
||||
let (inverted, identity) = (export("[1 0]"), export("[0 1]"));
|
||||
assert_ne!(inverted, identity, "the two ways round differ");
|
||||
for decode in ["[5 0 R 0]", "[1 6 0 R]", "[5 0 R 6 0 R]"] {
|
||||
assert_eq!(export(decode), inverted, "{decode}");
|
||||
}
|
||||
for decode in ["[0 5 0 R]", "[6 0 R 1]", "[6 0 R 5 0 R]"] {
|
||||
assert_eq!(export(decode), identity, "{decode}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,6 +904,53 @@ fn a_decode_array_with_references_in_it_is_read() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A function's `/Encode`, `/Decode`, `/Domain` and `/Range` are read a pair at a time too, so they
|
||||
/// lost a reference at the start of a pair the same way, and the array with it. Each form is compared
|
||||
/// with the direct one, which differs from the identity so the array is shown to count.
|
||||
#[test]
|
||||
fn a_functions_arrays_can_start_with_a_reference() {
|
||||
use super::*;
|
||||
let render = |function: String| -> [u8; 4] {
|
||||
let content = "/C cs 0.25 sc 0 0 200 200 re f";
|
||||
let bytes = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /ColorSpace << /C [/Separation /X /DeviceRGB 9 0 R] >> >> /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
5 0 obj 1 endobj\n6 0 obj 0 endobj\n\
|
||||
7 0 obj << /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 0 0] /N 1 >> endobj\n\
|
||||
8 0 obj << /FunctionType 2 /Domain [0 1] /C0 [0 1 0] /C1 [0 0 1] /N 1 >> endobj\n\
|
||||
9 0 obj {function} endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
assert!(page.error.is_none(), "{function}: {:?}", page.error);
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
let sampled = |encode: &str| {
|
||||
format!(
|
||||
"<< /FunctionType 0 /Size [2] /BitsPerSample 8 /Domain [0 1] /Range [0 1 0 1 0 1] /Encode {encode} /Filter /ASCIIHexDecode /Length 13 >> stream\n000000FF8040>\nendstream"
|
||||
)
|
||||
};
|
||||
let stitched = |encode: &str| format!("<< /FunctionType 3 /Domain [0 1] /Functions [7 0 R 8 0 R] /Bounds [.5] /Encode {encode} >>");
|
||||
assert_ne!(render(sampled("[1 0]")), render(sampled("[0 1]")), "/Encode counts");
|
||||
assert_eq!(render(sampled("[5 0 R 0]")), render(sampled("[1 0]")), "a sampled function");
|
||||
assert_ne!(render(stitched("[1 0 0 1]")), render(stitched("[0 1 0 1]")), "/Encode counts");
|
||||
assert_eq!(render(stitched("[5 0 R 0 0 1]")), render(stitched("[1 0 0 1]")), "a stitching function");
|
||||
}
|
||||
|
||||
/// The lookup table of an `/Indexed` space is a string or a stream, and any part of the space can
|
||||
/// be indirect (ISO 32000-2 §8.6.6.3, §7.3.10). A table in an indirect string was lost: reading it
|
||||
/// as a stream used up the reference, so the space couldn't be read and drew in grey.
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user