mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 18:01:08 -06:00
Merge the fixes to the PR below into this one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
@@ -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)) {
|
||||
@@ -1145,11 +1153,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()
|
||||
|
||||
@@ -3573,6 +3573,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