mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 11:51:59 +00:00
Merge #6287: fix(render): a small image with a soft mask far larger than it draws in seconds, its mask averaged down once (hayro-interpret patch 82)
# Conflicts: # vendor/README.md
This commit is contained in:
@@ -8613,10 +8613,199 @@ 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");
|
||||
assert_eq!(decodes("B"), 5, "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
|
||||
/// there (vendored hayro-interpret patch 82): 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 rounded up to a power of two; 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);
|
||||
// Drawn 20 pixels square, averaged down to that rounded up to a power of two.
|
||||
assert_eq!(alphas, vec![(32, 32); 10], "averaged down to the size it is drawn at");
|
||||
// One decode for the image and one for its mask (here no larger than the intermediate the
|
||||
// averaged-down sizes are made from, so that is the mask itself), plus the averaged-down mask,
|
||||
// all shared by the ten draws.
|
||||
assert_eq!(decodes, 3, "decoded once");
|
||||
let (alphas, _) = draws(40);
|
||||
assert_eq!(alphas, vec![(40, 40); 10], "no more than twice the drawn size: kept as it is");
|
||||
}
|
||||
|
||||
/// The soft masks a small image is handed, drawn at each of `sizes` (width × height points at 72
|
||||
/// dpi): its `mask` (`width`, `height`, 8-bit samples) is larger than the 2 × 1 image, so it may
|
||||
/// be averaged down (vendored hayro-interpret patch 82). Each mask is given as its size and
|
||||
/// samples.
|
||||
fn soft_masks_handed(width: u32, height: u32, mask: &[u8], sizes: &[(u32, u32)]) -> (Vec<(u32, u32, Vec<u8>)>, usize) {
|
||||
soft_masks_handed_keyed(width, height, mask, sizes, ("Width", "Height"))
|
||||
}
|
||||
|
||||
/// `soft_masks_handed`, with the mask's dimensions under `keys` (`/W` and `/H`, say).
|
||||
fn soft_masks_handed_keyed(width: u32, height: u32, mask: &[u8], sizes: &[(u32, u32)], keys: (&str, &str)) -> (Vec<(u32, u32, Vec<u8>)>, usize) {
|
||||
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;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Draws(Vec<(u32, u32, Vec<u8>)>);
|
||||
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.extend(alpha.map(|a| (a.width, a.height, a.data))), 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 * 3]);
|
||||
let image = format!(
|
||||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /SMask 6 0 R /Length {} >>",
|
||||
rgb.len()
|
||||
);
|
||||
let alpha = flate(mask);
|
||||
let mask_dict = format!(
|
||||
"<< /Type /XObject /Subtype /Image /{} {width} /{} {height} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {} >>",
|
||||
keys.0,
|
||||
keys.1,
|
||||
alpha.len()
|
||||
);
|
||||
let content = sizes.iter().map(|(w, h)| format!("q {w} 0 0 {h} 0 0 cm /A Do Q")).collect::<Vec<_>>().join(" ");
|
||||
let bytes = image_cache_pdf(&content, "<< /XObject << /A 5 0 R >> >>", &[(&image, &rgb), (&mask_dict, &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, 400.0, 400.0), &cache, page.xref(), InterpreterSettings::default());
|
||||
let mut device = Draws::default();
|
||||
interpret_page(page, &mut ctx, &mut device);
|
||||
(device.0, cache.decoded_image_decodes())
|
||||
}
|
||||
|
||||
/// An averaged-down soft mask (vendored hayro-interpret patch 82):
|
||||
/// - takes each source sample into the target sample its centre falls in: seven columns
|
||||
/// `0 0 0 255 0 0 0`, drawn three wide and so averaged to four, give `0 0 127 0` (by left edges,
|
||||
/// `0 127 0 0`);
|
||||
/// - is never sized past the samples a truncated stream decodes: a mask that says it is 100 rows
|
||||
/// but holds 10, drawn 20 high, is handed no rows that no sample covers;
|
||||
/// - is cached per drawn size, rounded up to a power of two: the same image drawn 20 and then 200
|
||||
/// points wide is handed its mask at 32 and then at its own 100, not the first draw's 32 again,
|
||||
/// and drawn 20 and 21 wide it is handed one mask of 32 both times, whether the mask's
|
||||
/// dimensions are under `/Width` and `/Height` or `/W` and `/H`;
|
||||
/// - is made, at every size, from one intermediate mask at most 4096 on a side, so a mask wider
|
||||
/// than that, drawn at four sizes, is decoded in full once and not four times.
|
||||
#[test]
|
||||
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)]);
|
||||
// 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()
|
||||
};
|
||||
assert_eq!(sizes(&[(20, 20), (200, 200)]), vec![(32, 32), (100, 100)], "each draw at its own size");
|
||||
assert_eq!(sizes(&[(20, 20), (21, 21)]), vec![(32, 32), (32, 32)], "draws a pixel apart share one");
|
||||
// The same with the mask's dimensions under the abbreviated `/W` and `/H`, which an image is
|
||||
// read with too.
|
||||
let (handed, _) = soft_masks_handed_keyed(100, 100, &[255; 100 * 100], &[(20, 20), (200, 200)], ("W", "H"));
|
||||
let abbreviated: Vec<(u32, u32)> = handed.iter().map(|(w, h, _)| (*w, *h)).collect();
|
||||
assert_eq!(abbreviated, vec![(32, 32), (100, 100)], "each draw at its own size, under /W and /H");
|
||||
// Columns alternating 0 and 255, so every averaged-down sample is about half: within 8, as a
|
||||
// sample covering 19 or 20 columns, say, holds 9 or 10 opaque ones.
|
||||
let wide: Vec<u8> = (0..5000 * 2).map(|i| if i % 2 == 0 { 0 } else { 255 }).collect();
|
||||
let (handed, decodes) = soft_masks_handed(5000, 2, &wide, &[(20, 1), (40, 1), (80, 1), (160, 1)]);
|
||||
let sizes: Vec<(u32, u32)> = handed.iter().map(|(w, h, _)| (*w, *h)).collect();
|
||||
assert_eq!(sizes, vec![(32, 1), (64, 1), (128, 1), (256, 1)]);
|
||||
assert!(handed.iter().all(|(_, _, data)| data.iter().all(|&a| a.abs_diff(127) <= 8)), "{handed:?}");
|
||||
// The image at each of the four sizes, the 4096-wide intermediate once, and the mask at each
|
||||
// of the four sizes made from it. Without the intermediate there are 8, each mask size a full
|
||||
// decode.
|
||||
assert_eq!(decodes, 9, "the full mask decoded once");
|
||||
}
|
||||
|
||||
// ICC profiles whose stream dictionaries are the same (vendored hayro-interpret patch 48).
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+17
@@ -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 (82): 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)]
|
||||
@@ -116,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 {
|
||||
@@ -132,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 {
|
||||
@@ -169,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
@@ -153,6 +153,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.
|
||||
|
||||
+136
-2
@@ -645,13 +645,66 @@ impl<'a> ImageXObject<'a> {
|
||||
interpolate: self.interpolate,
|
||||
// PdfCraft patch (38): the same image drawn in a default colour space or without one.
|
||||
default_space: self.default_space,
|
||||
// PdfCraft patch (32): see `decode_reads_target`.
|
||||
// PdfCraft patch (32): see `decode_reads_target`; patch (82): see `has_larger_mask`.
|
||||
target: if self.decode_reads_target() {
|
||||
target_dimension
|
||||
} else if self.has_larger_mask() {
|
||||
target_dimension.map(size_bucket)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
preview: cache.preview,
|
||||
shrunk_to: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): this soft mask (or `/Mask` image) for an image of `parent` samples
|
||||
/// drawn at `target_dimension` device pixels. A mask larger than its image and more than
|
||||
/// [`SOFT_MASK_SHRINK_FACTOR`] times larger than where it is drawn, on a side, is averaged down
|
||||
/// to that size, 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; a mask
|
||||
/// no larger than its image keeps its size, so the renderer still samples it onto the image's
|
||||
/// grid.
|
||||
pub(crate) fn decoded_soft_mask(&self, target_dimension: Option<(u32, u32)>, parent: (u32, u32)) -> Option<DecodedMask> {
|
||||
let larger_than_image = self.width > parent.0 || self.height > parent.1;
|
||||
let shrink_to = target_dimension.filter(|&(tw, th)| {
|
||||
larger_than_image
|
||||
&& 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 to the drawn size rounded up to a power of two (`size_bucket`), so draws a
|
||||
// pixel apart share one mask and the renderer filters the rest, and only on the sides that
|
||||
// are larger than that.
|
||||
let (bw, bh) = size_bucket((tw, th));
|
||||
let to = (bw.min(self.width).max(1), bh.min(self.height).max(1));
|
||||
// Decoded in full and averaged down to `to`. A truncated stream decodes fewer rows than
|
||||
// the dictionary says: never more target samples than decoded ones on a side.
|
||||
let from_full = |to: (u32, u32)| {
|
||||
let full = decode_mask(self, target_dimension)?;
|
||||
Some(DecodedMask { luma: shrink_luma_within(full.luma, to)? })
|
||||
};
|
||||
let Some(cache) = &self.decoded_cache else {
|
||||
return from_full(to);
|
||||
};
|
||||
let key = |to| self.decode_key(cache, true, target_dimension).map(|key| DecodedImageKey { shrunk_to: Some(to), ..key });
|
||||
// One image drawn at several sizes (rotated and scaled copies on one slide) needs the
|
||||
// mask at several sizes. Each comes from one intermediate averaged-down mask, cached at
|
||||
// most `SOFT_MASK_INTERMEDIATE` on a side, so the full mask is decoded once, not once per
|
||||
// size; a size larger than the intermediate comes from the full mask.
|
||||
let between = (self.width.min(SOFT_MASK_INTERMEDIATE), self.height.min(SOFT_MASK_INTERMEDIATE));
|
||||
if to == between || to.0 > between.0 || to.1 > between.1 {
|
||||
return cache.get_or_insert_with(key(to), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, || from_full(to));
|
||||
}
|
||||
cache.get_or_insert_with(key(to), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, || {
|
||||
let mid = cache.get_or_insert_with(key(between), MAX_DECODED_IMAGE_BYTES, decoded_mask_size, || from_full(between))?;
|
||||
Some(DecodedMask { luma: shrink_luma_within(mid.luma, to)? })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -667,6 +720,21 @@ impl<'a> ImageXObject<'a> {
|
||||
|| dict.get::<Stream<'_>>(MASK).is_some_and(|s| uses_jpx(&s))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): whether this image has a soft mask (or `/Mask` image) larger than
|
||||
/// itself, which may be averaged down to the size the image is drawn at
|
||||
/// (`decoded_soft_mask`): the decoded raster holds it as its alpha, so it depends on that size,
|
||||
/// to the [`size_bucket`] it is averaged down to.
|
||||
fn has_larger_mask(&self) -> bool {
|
||||
let dict = self.stream.dict();
|
||||
let larger = |s: &Stream<'_>| {
|
||||
let d = s.dict();
|
||||
// The dimension keys as `ImageXObject::new` reads them, abbreviations first.
|
||||
let (w, h) = (d.get::<u32>(W).or_else(|| d.get::<u32>(WIDTH)), d.get::<u32>(H).or_else(|| d.get::<u32>(HEIGHT)));
|
||||
w.is_some_and(|w| w > self.width) || h.is_some_and(|h| h > self.height)
|
||||
};
|
||||
dict.get::<Stream<'_>>(SMASK).is_some_and(|s| larger(&s)) || dict.get::<Stream<'_>>(MASK).is_some_and(|s| larger(&s))
|
||||
}
|
||||
|
||||
pub(crate) fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
@@ -690,6 +758,71 @@ 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 (82): 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 (82): the largest side of the intermediate mask that every smaller averaged-down
|
||||
/// size of one soft mask is made from (`ImageXObject::decoded_soft_mask`): 4096 × 4096 one-byte
|
||||
/// samples are 16 MiB, a quarter of the decoded-image budget, so it stays cached between draws.
|
||||
const SOFT_MASK_INTERMEDIATE: u32 = 4096;
|
||||
|
||||
/// PdfCraft patch (82): the size a large soft mask is averaged down to for an image drawn at
|
||||
/// `(w, h)` device pixels: each side rounded up to a power of two. Draws of one image a pixel or so
|
||||
/// apart (scaled by 0.36 and 0.36167, say) share one averaged mask and one cache entry, and a mask
|
||||
/// is never averaged below where it is drawn, so the renderer shrinks the rest at most twofold.
|
||||
fn size_bucket((w, h): (u32, u32)) -> (u32, u32) {
|
||||
(w.checked_next_power_of_two().unwrap_or(u32::MAX), h.checked_next_power_of_two().unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (82): `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. Source sample `x`
|
||||
/// of `sw`, centred at `x + ½`, falls in target sample `⌊(2x + 1)·tw / 2sw⌋`. The only buffers are
|
||||
/// the target's sums and counts, reserved fallibly; nothing is sized by the source's width.
|
||||
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 || tw > sw || th > sh || luma.data.len() != sw.checked_mul(sh)? {
|
||||
return None;
|
||||
}
|
||||
let cells = tw.checked_mul(th)?;
|
||||
let (mut sums, mut counts): (Vec<u64>, Vec<u32>) = (Vec::new(), Vec::new());
|
||||
sums.try_reserve_exact(cells).ok()?;
|
||||
counts.try_reserve_exact(cells).ok()?;
|
||||
sums.resize(cells, 0);
|
||||
counts.resize(cells, 0);
|
||||
// The target sample a source sample's centre falls in, along an axis of `src` samples
|
||||
// shrunk to `dst`. `2·i + 1 < 2·src` and `dst ≤ src ≤ 2^32`, so the product fits a u64.
|
||||
let centre = |i: usize, src: usize, dst: usize| -> usize {
|
||||
let at = (2 * i as u64 + 1) * dst as u64 / (2 * src as u64);
|
||||
(at as usize).min(dst - 1)
|
||||
};
|
||||
for (y, row) in luma.data.chunks_exact(sw).enumerate() {
|
||||
let base = centre(y, sh, th) * tw;
|
||||
for (x, &v) in row.iter().enumerate() {
|
||||
let i = base + centre(x, sw, tw);
|
||||
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 (82): `luma` averaged down to `to`, but never to more samples than it has on a
|
||||
/// side (a truncated stream decodes fewer rows than its dictionary says), and returned as it is
|
||||
/// when that is its own size.
|
||||
fn shrink_luma_within(luma: LumaData, to: (u32, u32)) -> Option<LumaData> {
|
||||
let to = (to.0.min(luma.width).max(1), to.1.min(luma.height).max(1));
|
||||
if to == (luma.width, luma.height) {
|
||||
return Some(luma);
|
||||
}
|
||||
shrink_luma(luma, to)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -1145,7 +1278,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 (82): averaged down when far larger than where it is drawn, and cached.
|
||||
obj.decoded_soft_mask(target_dimension, (width, *height)).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