mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M2: a soft mask far larger than where its image is drawn is averaged down to that size once (hayro-interpret patch 77)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
1 parent
863af864a7
commit
be5a9209cc
3 files changed
+140
-1
No files matched your search
@@ -8193,6 +8193,79 @@ fn decoded_images_serve_every_size_they_are_drawn_at() {
|
||||
assert_eq!(decodes("B"), 5, "a JPX soft mask decodes for the size it is drawn at");
|
||||
}
|
||||
|
||||
/// A soft mask far larger than where its image is drawn is averaged down to that size and cached
|
||||
/// there (vendored hayro-interpret patch 77): a 2 × 2 image whose mask is 34,862 × 4,332, drawn 40
|
||||
/// times on one slide (pdf.js `issue16263.pdf`), was decoded and drawn at full size each time and
|
||||
/// the page took minutes. Drawn ten times at one size, the mask is now decoded once and handed
|
||||
/// over at that size; a mask no larger than twice its drawn size keeps its own.
|
||||
#[test]
|
||||
fn a_soft_mask_far_larger_than_where_it_is_drawn_is_shrunk_once() {
|
||||
use hayro::hayro_interpret::font::Glyph;
|
||||
use hayro::hayro_interpret::hayro_syntax::Pdf;
|
||||
use hayro::hayro_interpret::{
|
||||
BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, Paint, PathDrawMode, SoftMask, TransformExt, interpret_page,
|
||||
};
|
||||
use kurbo::{Affine, BezPath, Rect};
|
||||
use std::io::Write;
|
||||
|
||||
/// Asks for every raster image at the size it is drawn at, as the renderer does, and keeps
|
||||
/// the size of each alpha channel it was handed.
|
||||
#[derive(Default)]
|
||||
struct Draws(Vec<(u32, u32)>);
|
||||
impl<'a> Device<'a> for Draws {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'a>, _: &PathDrawMode) {}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'a>, _: Affine, _: Affine, _: &Paint<'a>, _: &GlyphDrawMode) {}
|
||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
||||
let [a, b, c, d, ..] = transform.as_coeffs();
|
||||
let target = ((a.hypot(b) * f64::from(image.width())).ceil() as u32, (c.hypot(d) * f64::from(image.height())).ceil() as u32);
|
||||
if let Image::Raster(r) = image {
|
||||
r.with_rgba(|_, alpha| self.0.push(alpha.map_or((0, 0), |a| (a.width, a.height))), Some(target));
|
||||
}
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
|
||||
let flate = |data: &[u8]| {
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
z.write_all(data).unwrap();
|
||||
z.finish().unwrap()
|
||||
};
|
||||
let rgb = flate(&[0u8; 2 * 2 * 3]);
|
||||
let image = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /SMask 6 0 R /Length {} >>",
|
||||
rgb.len()
|
||||
);
|
||||
// `side` × `side` mask samples, drawn ten times at 20 × 20 points (device pixels at 72 dpi).
|
||||
let draws = |side: u32| {
|
||||
let alpha = flate(&(0..side * side).map(|i| (i % 256) as u8).collect::<Vec<_>>());
|
||||
let mask = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width {side} /Height {side} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>",
|
||||
alpha.len()
|
||||
);
|
||||
let content = (0..10).map(|i| format!("q 20 0 0 20 {} 0 cm /A Do Q", i * 22)).collect::<Vec<_>>().join(" ");
|
||||
let bytes = image_cache_pdf(&content, "<< /XObject << /A 5 0 R >> >>", &[(&image, &rgb), (&mask, &alpha)]);
|
||||
let parsed = Pdf::new(Arc::new(bytes)).expect("parses");
|
||||
let page = &parsed.pages()[0];
|
||||
let cache = InterpreterCache::new();
|
||||
let initial = page.initial_transform(true).to_kurbo();
|
||||
let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 230.0, 30.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
(device.0, cache.decoded_image_decodes())
|
||||
};
|
||||
let (alphas, decodes) = draws(1000);
|
||||
assert_eq!(alphas, vec![(20, 20); 10], "averaged down to the size it is drawn at");
|
||||
// One decode for the image and one for its averaged-down mask, shared by all ten draws.
|
||||
assert_eq!(decodes, 2, "decoded once");
|
||||
let (alphas, _) = draws(40);
|
||||
assert_eq!(alphas, vec![(40, 40); 10], "no more than twice the drawn size: kept as it is");
|
||||
}
|
||||
|
||||
// ICC profiles whose stream dictionaries are the same (vendored hayro-interpret patch 48).
|
||||
mod icc_profile_cache;
|
||||
|
||||
|
||||
Vendored
+3
@@ -108,6 +108,9 @@ pub(crate) struct DecodedImageKey {
|
||||
pub(crate) default_space: Option<u128>,
|
||||
pub(crate) target: Option<(u32, u32)>,
|
||||
pub(crate) preview: bool,
|
||||
/// PdfCraft patch (77): the size a soft mask far larger than where it is drawn was averaged
|
||||
/// down to (`ImageXObject::decoded_soft_mask`), `None` for a mask kept at its own size.
|
||||
pub(crate) shrunk_to: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
+64
-1
@@ -634,9 +634,39 @@ impl<'a> ImageXObject<'a> {
|
||||
None
|
||||
},
|
||||
preview: cache.preview,
|
||||
shrunk_to: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch (77): this soft mask (or `/Mask` image) for an image drawn at
|
||||
/// `target_dimension` device pixels. A mask more than [`SOFT_MASK_SHRINK_FACTOR`] times larger
|
||||
/// than that on a side is averaged down to it, and the result is cached at that size: drawn
|
||||
/// through a form many times (a 2 × 2 image whose mask is 34,862 × 4,332, drawn 40 times on
|
||||
/// one slide, pdf.js `issue16263.pdf`), it was decoded, copied and drawn at full size each
|
||||
/// time, about 3.6 s apiece, and the page took minutes. Any other mask decodes as before.
|
||||
pub(crate) fn decoded_soft_mask(&self, target_dimension: Option<(u32, u32)>) -> Option<DecodedMask> {
|
||||
let shrink_to = target_dimension.filter(|&(tw, th)| {
|
||||
tw > 0
|
||||
&& th > 0
|
||||
&& (u64::from(self.width) > u64::from(tw) * SOFT_MASK_SHRINK_FACTOR
|
||||
|| u64::from(self.height) > u64::from(th) * SOFT_MASK_SHRINK_FACTOR)
|
||||
});
|
||||
let Some((tw, th)) = shrink_to else {
|
||||
return self.decoded_mask(target_dimension);
|
||||
};
|
||||
// Averaged down only on the sides that are larger than where they are drawn.
|
||||
let to = (tw.min(self.width).max(1), th.min(self.height).max(1));
|
||||
let shrink = || {
|
||||
let full = decode_mask(self, target_dimension)?;
|
||||
Some(DecodedMask { luma: shrink_luma(full.luma, to)? })
|
||||
};
|
||||
let Some(cache) = &self.decoded_cache else {
|
||||
return shrink();
|
||||
};
|
||||
let key = self.decode_key(cache, true, target_dimension).map(|key| DecodedImageKey { shrunk_to: Some(to), ..key });
|
||||
cache.get_or_insert_with(key, MAX_DECODED_IMAGE_BYTES, decoded_mask_size, shrink)
|
||||
}
|
||||
|
||||
/// PdfCraft patch (32): whether decoding this image depends on the size it is drawn at.
|
||||
/// Only the JPX filter decodes at a reduced resolution for a target size, in the image
|
||||
/// itself or in the soft mask or mask decoded with it; every other filter decodes the whole
|
||||
@@ -672,6 +702,38 @@ impl<'a> ImageXObject<'a> {
|
||||
/// image pixels shared across tiles and scales. Past it the cache starts a fresh epoch.
|
||||
const MAX_DECODED_IMAGE_BYTES: usize = 64 << 20;
|
||||
|
||||
/// PdfCraft patch (77): how many times larger than where it is drawn, on a side, a soft mask must be
|
||||
/// before it is averaged down to that size (`ImageXObject::decoded_soft_mask`). Below it the mask
|
||||
/// keeps its own resolution, which the renderer filters as it draws.
|
||||
const SOFT_MASK_SHRINK_FACTOR: u64 = 2;
|
||||
|
||||
/// PdfCraft patch (77): `luma` averaged down to `to` (no larger than it on either side): each
|
||||
/// target sample is the mean of the source samples whose centres fall in it.
|
||||
fn shrink_luma(luma: LumaData, to: (u32, u32)) -> Option<LumaData> {
|
||||
let (sw, sh) = (luma.width as usize, luma.height as usize);
|
||||
let (tw, th) = (to.0 as usize, to.1 as usize);
|
||||
if sw == 0 || sh == 0 || tw == 0 || th == 0 || luma.data.len() != sw.checked_mul(sh)? {
|
||||
return None;
|
||||
}
|
||||
let mut sums = vec![0u64; tw.checked_mul(th)?];
|
||||
let mut counts = vec![0u32; tw.checked_mul(th)?];
|
||||
// Which target column each source column falls in, worked out once.
|
||||
let column: Vec<usize> = (0..sw).map(|x| (x * tw / sw).min(tw - 1)).collect();
|
||||
for (y, row) in luma.data.chunks_exact(sw).enumerate() {
|
||||
let ty = (y * th / sh).min(th - 1);
|
||||
let base = ty * tw;
|
||||
for (&v, &tx) in row.iter().zip(&column) {
|
||||
let i = base + tx;
|
||||
if let (Some(sum), Some(count)) = (sums.get_mut(i), counts.get_mut(i)) {
|
||||
*sum += u64::from(v);
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let data = sums.iter().zip(&counts).map(|(&sum, &count)| if count == 0 { 0 } else { (sum / u64::from(count)) as u8 }).collect();
|
||||
Some(LumaData { data, width: to.0, height: to.1, ..luma })
|
||||
}
|
||||
|
||||
/// PdfCraft patch: pixel bytes held by a decoded mask.
|
||||
fn decoded_mask_size(mask: &DecodedMask) -> usize {
|
||||
mask.luma.width as usize * mask.luma.height as usize
|
||||
@@ -1127,7 +1189,8 @@ fn resolve_alpha(
|
||||
{
|
||||
let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None, None)?;
|
||||
|
||||
decode_mask(&obj, target_dimension).map(|decoded| decoded.luma)
|
||||
// PdfCraft patch (77): averaged down when far larger than where it is drawn, and cached.
|
||||
obj.decoded_soft_mask(target_dimension).map(|decoded| decoded.luma)
|
||||
} else if let Some(color_key_mask) = dict.get::<SmallVec<[u16; 4]>>(MASK) {
|
||||
let mut mask_data = vec![];
|
||||
|
||||
|
||||
Reference in new issue
Block a user