M2: the soft-mask tests count image and mask decodes apart and check a truncated mask's size

decoded_images_serve_every_size_they_are_drawn_at asserts image and mask
decodes separately, through a new InterpreterCache::decoded_mask_decodes
(hayro-interpret patch 82), so a wrong split behind the same total fails.
The truncated-stream case of
an_averaged_down_soft_mask_follows_its_samples_and_its_drawn_size asserts
the exact mask handed over: 32 x 10 (the decoded rows), all opaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
mtlcl410
2026-11-07 14:45:30 +08:00
co-authored by Claude Opus 5.5
parent 327fddec5b
commit 30164b6973
4 changed files with 30 additions and 8 deletions
+9 -7
View File
@@ -8187,13 +8187,14 @@ fn decoded_images_serve_every_size_they_are_drawn_at() {
let mut device = Draws::default();
interpret_page(page, &mut ctx, &mut device);
assert_eq!(device.0, 5, "every draw of /{name} was handed its pixels");
cache.decoded_image_decodes()
// (image decodes, mask decodes)
let masks = cache.decoded_mask_decodes();
(cache.decoded_image_decodes() - masks, masks)
};
assert_eq!(decodes("A"), 1, "a Flate image decodes the same at every size");
// The image and its JPX soft mask each decode once for each of the five sizes: the mask goes
// through the cache too since hayro-interpret patch 82, so its decodes are counted with the
// image's.
assert_eq!(decodes("B"), 10, "a JPX soft mask decodes for the size it is drawn at");
assert_eq!(decodes("A"), (1, 0), "a Flate image decodes the same at every size");
// The image and its JPX soft mask each decode once for each of the five sizes (the mask goes
// through the cache too since hayro-interpret patch 82).
assert_eq!(decodes("B"), (5, 5), "an image with a JPX soft mask, and the mask, decode for the size they are drawn at");
}
/// A soft mask far larger than where its image is drawn is averaged down to that size and cached
@@ -8356,7 +8357,8 @@ fn an_averaged_down_soft_mask_follows_its_samples_and_its_drawn_size() {
let (handed, _) = soft_masks_handed(7, 1, &[0, 0, 0, 255, 0, 0, 0], &[(3, 1)]);
assert_eq!(handed, vec![(4, 1, vec![0, 0, 127, 0])], "centres, not left edges");
let (handed, _) = soft_masks_handed(100, 100, &[255; 100 * 10], &[(20, 20)]);
assert!(!handed.is_empty() && handed.iter().all(|(_, _, data)| data.iter().all(|&a| a == 255)), "no uncovered rows: {handed:?}");
// 32 wide (20 rounded up to a power of two), and 10 rows, the ones decoded, not 32: all opaque.
assert_eq!(handed, vec![(32, 10, vec![255; 32 * 10])], "no uncovered rows");
let sizes = |draws: &[(u32, u32)]| -> Vec<(u32, u32)> {
soft_masks_handed(100, 100, &[255; 100 * 100], draws).0.iter().map(|(w, h, _)| (*w, *h)).collect()
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+14
View File
@@ -119,6 +119,8 @@ struct DecodedImageCacheInner {
bytes: usize,
/// PdfCraft patch (32): how many keyed decodes ran (cache misses).
decodes: usize,
/// PdfCraft patch (82): how many of `decodes` were of masks (keys with `mask_output`).
mask_decodes: usize,
}
impl DecodedImageCache {
@@ -135,6 +137,15 @@ impl DecodedImageCache {
self.inner.lock().map(|locked| locked.decodes).unwrap_or(0)
}
/// PdfCraft patch (82): how many of [`Self::decodes`] were of soft masks and `/Mask` images,
/// for tests. 0 when the lock is poisoned.
pub(crate) fn mask_decodes(&self) -> usize {
self.inner
.lock()
.map(|locked| locked.mask_decodes)
.unwrap_or(0)
}
/// The same cache, for a preview (`true`) or a final render.
pub(crate) fn for_render(&self, preview: bool) -> Self {
Self {
@@ -172,6 +183,9 @@ impl DecodedImageCache {
return val;
};
locked.decodes = locked.decodes.saturating_add(1);
if key.mask_output {
locked.mask_decodes = locked.mask_decodes.saturating_add(1);
}
// Another thread may have decoded it meanwhile: keep the first copy.
if let Some(existing) = locked.map.get(&key)
&& let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::<T>())
+6
View File
@@ -147,6 +147,12 @@ impl<'a> InterpreterCache<'a> {
self.decoded_cache.decodes()
}
/// PdfCraft patch (82): how many of [`Self::decoded_image_decodes`] were of soft masks and
/// `/Mask` images, for diagnostics and tests.
pub fn decoded_mask_decodes(&self) -> usize {
self.decoded_cache.mask_decodes()
}
/// PdfCraft patch (48): how many `/ICCBased` profiles were read through this cache, for
/// diagnostics and tests. A profile used again, by the same stream object or by a copy with
/// the same dictionary and bytes, is read once.