mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
M2: an ICC profile's fallback isn't kept with the profile
The depth limit didn't hold. An ICC profile read was kept whole, with the alternate it fell back to, and a later read took it without counting its levels. A page selecting the last of a chain first built each space on the one before, and no read was deep: 20,000 levels, and the stack overflowed painting with it. In the other direction, a profile met at the limit kept the fallback it got there, and a read nearer the top got that instead of its alternate. Both come from keeping a result that depends on the depth. What's kept now is what the profile itself reads as, one level whatever it is, and the alternate is read where it's met. 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
b71914b1de
commit
4c815786c6
@@ -977,6 +977,76 @@ fn colour_spaces_are_read_to_the_limit_of_their_nesting() {
|
||||
assert!(!green(32), "one past it");
|
||||
}
|
||||
|
||||
/// A page with the colour space resources `spaces` and the other objects `objects`, drawn with
|
||||
/// `content`. Objects 5 and 6 are tint transforms: the tint itself, and paper to green.
|
||||
fn page_with_colour_spaces(spaces: &str, objects: &str, content: &str) -> super::RenderedPage {
|
||||
use super::*;
|
||||
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 << {spaces} >> >> /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
5 0 obj << /FunctionType 2 /Domain [0 1] /N 1 /C0 [0] /C1 [1] >> endobj\n\
|
||||
6 0 obj << /FunctionType 2 /Domain [0 1] /N 1 /C0 [1 1 1] /C1 [0 1 0] >> endobj\n\
|
||||
{objects}\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// What an ICC profile reads as is kept. If what it falls back to were kept with it, a page
|
||||
/// selecting the last of a chain first would build each space on the one before, and no read
|
||||
/// would be deep: 20,000 levels past the limit, for whatever walks the space to paint with it.
|
||||
#[test]
|
||||
fn a_chain_built_on_spaces_already_read_ends() {
|
||||
const LINKS: u32 = 20_000;
|
||||
let selected: String = (0..LINKS).rev().map(|i| format!("/C{i} cs\n")).collect();
|
||||
let content = format!("1 0 0 rg 0 0 100 100 re f\n{selected}0 sc 100 100 100 100 re f");
|
||||
let spaces: String = (0..LINKS).map(|i| format!("/C{i} [/ICCBased {} 0 R] ", 10 + i)).collect();
|
||||
let around: [&dyn Fn(&str) -> String; 4] = [
|
||||
&|next| format!("[/Pattern {next}]"),
|
||||
&|next| format!("[/Indexed {next} 1 <0011>]"),
|
||||
&|next| format!("[/Separation /A {next} 5 0 R]"),
|
||||
&|next| format!("[/DeviceN [/A] {next} 5 0 R]"),
|
||||
];
|
||||
for around in around {
|
||||
let mut objects: String = (0..LINKS)
|
||||
.map(|i| {
|
||||
let alternate = around(&format!("[/ICCBased {} 0 R]", 11 + i));
|
||||
format!("{} 0 obj << /N 1 /Alternate {alternate} /Length 1 >> stream\nx\nendstream endobj\n", 10 + i)
|
||||
})
|
||||
.collect();
|
||||
objects.push_str(&format!("{} 0 obj << /N 1 /Length 1 >> stream\nx\nendstream endobj\n", 10 + LINKS));
|
||||
let page = page_with_colour_spaces(&spaces, &objects, &content);
|
||||
assert!(page.error.is_none(), "{}: {:?}", around("next"), page.error);
|
||||
let start = ((150 * page.width as usize) + 50) * 4;
|
||||
assert_eq!(page.rgba.get(start..start + 4), Some(&[255, 0, 0, 255][..]), "{}: what comes before it drew", around("next"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A profile met at the limit can't read its alternate there, which says nothing of a read nearer
|
||||
/// the top: the space kept from the first read isn't what the second gets.
|
||||
#[test]
|
||||
fn a_profile_read_at_the_limit_keeps_its_alternate_for_a_read_nearer_the_top() {
|
||||
let chain: String = (0..31).map(|i| format!("{} 0 obj [/Separation /A {} 0 R 5 0 R] endobj\n", 10 + i, 11 + i)).collect();
|
||||
let objects = format!(
|
||||
"{chain}41 0 obj [/ICCBased 7 0 R] endobj\n\
|
||||
7 0 obj << /N 1 /Alternate [/Separation /A /DeviceRGB 6 0 R] /Length 1 >> stream\nx\nendstream endobj\n"
|
||||
);
|
||||
let page = page_with_colour_spaces("/Deep 10 0 R /Near 41 0 R", &objects, "/Deep cs /Near cs 1 scn 0 0 200 200 re f");
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!(page.rgba.get(..4), Some(&[0, 255, 0, 255][..]));
|
||||
}
|
||||
|
||||
/// An image's `/Decode` pairs can have a reference at the start of a pair. Its read took that for
|
||||
/// the whole pair, found no pair there, and ended the array, so the image didn't draw at all.
|
||||
#[test]
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+27
-27
@@ -198,6 +198,9 @@ impl ColorSpaceType {
|
||||
|
||||
// PdfCraft patch (48): cached by what the profile is read from (see
|
||||
// `IccProfileKey`), not by the stream dictionary's bytes alone.
|
||||
// PdfCraft patch (74): what's kept is what the profile itself reads as. What
|
||||
// it falls back to depends on how deep it was met, and kept with the profile
|
||||
// a page could build a chain past the limit on spaces already read.
|
||||
let read = || {
|
||||
let data = icc_stream.raw_data();
|
||||
let content = IccProfileKey::Content {
|
||||
@@ -206,40 +209,37 @@ impl ColorSpaceType {
|
||||
};
|
||||
cache.get_or_insert_with(hash128(&content), || {
|
||||
cache.count_icc_profile_read();
|
||||
if let Some(decoded) = icc_stream.decoded().ok().as_ref() {
|
||||
ICCProfile::new(decoded, num_components)
|
||||
.map(|icc| {
|
||||
// TODO: For SVG and PNG we can assume that the output color space is
|
||||
// sRGB. If we ever implement PDF-to-PDF, we probably want to
|
||||
// let the user pass the native color type and don't make this optimization
|
||||
// if it's not sRGB.
|
||||
if icc.is_srgb() {
|
||||
Self::DeviceRgb
|
||||
} else {
|
||||
Self::ICCBased(icc)
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
dict.get::<Object<'_>>(ALTERNATE)
|
||||
.and_then(|o| Self::new(o, cache, depth + 1))
|
||||
})
|
||||
.or_else(|| match dict.get::<u8>(N) {
|
||||
Some(1) => Some(Self::DeviceGray),
|
||||
Some(3) => Some(Self::DeviceRgb),
|
||||
Some(4) => Some(Self::DeviceCmyk),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let decoded = icc_stream.decoded().ok()?;
|
||||
Some(ICCProfile::new(&decoded, num_components).map(|icc| {
|
||||
// TODO: For SVG and PNG we can assume that the output color space is
|
||||
// sRGB. If we ever implement PDF-to-PDF, we probably want to
|
||||
// let the user pass the native color type and don't make this optimization
|
||||
// if it's not sRGB.
|
||||
if icc.is_srgb() {
|
||||
Self::DeviceRgb
|
||||
} else {
|
||||
Self::ICCBased(icc)
|
||||
}
|
||||
}))
|
||||
})
|
||||
};
|
||||
return match icc_stream_object(&color_array, &icc_stream) {
|
||||
let profile = match icc_stream_object(&color_array, &icc_stream) {
|
||||
Some(object) => {
|
||||
cache.get_or_insert_with(hash128(&IccProfileKey::Object(object)), read)
|
||||
}
|
||||
None => read(),
|
||||
};
|
||||
return profile?
|
||||
.or_else(|| {
|
||||
dict.get::<Object<'_>>(ALTERNATE)
|
||||
.and_then(|o| Self::new(o, cache, depth + 1))
|
||||
})
|
||||
.or_else(|| match dict.get::<u8>(N) {
|
||||
Some(1) => Some(Self::DeviceGray),
|
||||
Some(3) => Some(Self::DeviceRgb),
|
||||
Some(4) => Some(Self::DeviceCmyk),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
CALCMYK => return Some(Self::DeviceCmyk),
|
||||
CALGRAY => {
|
||||
|
||||
Reference in New Issue
Block a user