mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
M11: an indexed image also drawn where the optimizer doesn't read keeps its resolution
From CodeRabbit's review: default colour spaces apply to an indexed colour space's device base, so one in force where an annotation's appearance (or a pattern, or another image's /Alternates) draws the image may change which palette entry is darker, and the tie a resampled image keeps. Only black-and-white RGB went through used_elsewhere; indexed images now do too when downsampling is on, and one used beyond the page and form resources the walk read keeps its resolution, as such RGB keeps its colour space. A grey image stays out of this check, since sample 0 stays dark in a grey space, and #5426's rule for grey is unchanged: the test now has a grey image drawn on the page and in an annotation. 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
7fbb40a2c1
commit
d1677b4b3f
@@ -65,7 +65,8 @@ let report = optimize_with_progress(&mut doc, &settings, &mut |stage| keep_going
|
||||
RGB packed to one bit among them), image masks, and indexed images over `DeviceGray` or
|
||||
`DeviceRGB`, whose palette is read (Rec. 601 luma for RGB), with no decode array or one of two
|
||||
different numbers, and not drawn or listed where `/DefaultGray` or `/DefaultRGB` is or may be
|
||||
in force. Not a separation or a DeviceN, whose tint transform isn't read, calibrated or ICC
|
||||
in force (an indexed image referred to from beyond the page and form resources read, as an
|
||||
annotation's appearance, may be). Not a separation or a DeviceN, whose tint transform isn't read, calibrated or ICC
|
||||
grey, or an indexed image over another base or whose two entries look alike. Not one with a soft mask, whose own size would have to
|
||||
follow; not a halftone or a dither, most of whose new pixels would cover both black and white,
|
||||
so that taking the sample covering more would turn its tones solid (a 50% screen black, a 25%
|
||||
|
||||
@@ -300,6 +300,14 @@ fn device_rgb(doc: &Document, r: ObjRef) -> bool {
|
||||
&& cs.as_deref().and_then(Object::as_name).is_some_and(|n| matches!(n, b"DeviceRGB" | b"RGB"))
|
||||
}
|
||||
|
||||
/// Whether image `r` is indexed: its palette says which of its samples is dark ([`dark_is_one`]),
|
||||
/// which default colour spaces in force where it is drawn may change.
|
||||
fn indexed(doc: &Document, r: ObjRef) -> bool {
|
||||
let Object::Stream(s) = &*doc.get(r) else { return false };
|
||||
let cs = s.dict.get(b"ColorSpace").map(|cs| doc.resolve(cs));
|
||||
matches!(cs.as_deref(), Some(Object::Array(a)) if matches!(a.first().and_then(Object::as_name), Some(b"Indexed" | b"I")))
|
||||
}
|
||||
|
||||
/// Call `f` with every reference in `o`, a dictionary's or array's included, a stream's dictionary
|
||||
/// too, but not those in objects referred to.
|
||||
fn each_ref(o: &Object, mut f: impl FnMut(ObjRef)) {
|
||||
@@ -1144,11 +1152,13 @@ pub(crate) fn run(
|
||||
progress: &mut dyn FnMut(Stage) -> bool,
|
||||
) -> Result<(), OptimizeError> {
|
||||
let (ppi, mut defaults, read) = walk_pages(doc, pages);
|
||||
// Black-and-white RGB becomes grey, which default colour spaces would draw otherwise: one used
|
||||
// where the walk didn't look may be drawn so.
|
||||
// Black-and-white RGB becomes grey, which default colour spaces would draw otherwise, and an
|
||||
// indexed image's palette says which of its samples a resampled one keeps, which they may
|
||||
// change: one used where the walk didn't look may be drawn so.
|
||||
let packs = settings.mono.compression != MonoCompression::Retain;
|
||||
let rgb: HashSet<u32> = ppi.keys().filter(|r| packs && device_rgb(doc, **r)).map(|r| r.num).collect();
|
||||
defaults.extend(used_elsewhere(doc, &rgb, &read));
|
||||
let checked: HashSet<u32> =
|
||||
ppi.keys().filter(|r| (packs && device_rgb(doc, **r)) || (settings.mono.downsample && indexed(doc, **r))).map(|r| r.num).collect();
|
||||
defaults.extend(used_elsewhere(doc, &checked, &read));
|
||||
// Soft masks are processed with their image, not on their own.
|
||||
let masks: HashSet<ObjRef> = ppi
|
||||
.keys()
|
||||
|
||||
@@ -3504,6 +3504,41 @@ fn indexed_thin_rules_survive_averaging_for_both_palette_orders_and_decodes() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An indexed image also drawn where the optimizer doesn't read (an annotation's appearance, where
|
||||
/// other default colour spaces may be in force and change which palette entry is darker) keeps its
|
||||
/// resolution, as black-and-white RGB keeps its colour space; drawn only on the page, it is
|
||||
/// averaged. A grey image is averaged either way: sample 0 stays dark in a grey space, and only an
|
||||
/// indexed image's palette depends on what's in force where it is drawn.
|
||||
#[test]
|
||||
fn an_indexed_image_used_beyond_the_page_content_read_is_not_averaged() {
|
||||
let (w, h) = (1200u32, 480u32);
|
||||
let stride = (w as usize).div_ceil(8);
|
||||
let mut data = vec![0xFF; stride * h as usize];
|
||||
for y in 0..h as usize {
|
||||
data[y * stride + 101 / 8] &= !(0x80 >> (101 % 8));
|
||||
}
|
||||
let palette = Object::String(pdfcraft_cos::PdfString::literal(vec![0, 0, 0, 255, 255, 255]));
|
||||
let indexed = Object::Array(vec![Object::name("Indexed"), Object::name("DeviceRGB"), 1.into(), palette]);
|
||||
let cases = [(&indexed, false, 600), (&indexed, true, 1200), (&Object::name("DeviceGray"), false, 600), (&Object::name("DeviceGray"), true, 600)];
|
||||
for (cs, in_annotation, width) in cases {
|
||||
let mut doc = Document::new_empty();
|
||||
let image = mono_image(&mut doc, w, h, &data, None, &[("BitsPerComponent", 1.into()), ("ColorSpace", cs.clone())]);
|
||||
// 144 × 57.6 pt is 600 ppi.
|
||||
let p = page(&mut doc, &[("Im", image)], "q 144 0 0 57.6 20 600 cm /Im Do Q");
|
||||
if in_annotation {
|
||||
let resources = Object::Dict({
|
||||
let mut r = Dict::new();
|
||||
r.set(b"XObject".to_vec(), Object::Dict(xobject_dict(&[("Im", image)])));
|
||||
r
|
||||
});
|
||||
let appearance = drawing(&mut doc, Some(resources), &form_entries());
|
||||
annotate(&mut doc, p, appearance);
|
||||
}
|
||||
optimize(&mut doc, &Settings::default()).unwrap();
|
||||
assert_eq!(stream(&doc, image).dict.int(b"Width"), Some(width), "{cs:?}, in an annotation: {in_annotation}");
|
||||
}
|
||||
}
|
||||
|
||||
/// White margins must not hide a small screened photograph from the tone guard.
|
||||
#[test]
|
||||
fn mixed_text_and_small_halftone_regions_keep_their_tones_with_wide_white_margins() {
|
||||
|
||||
Reference in New Issue
Block a user