Merge #6292: fix(render): a page of stencil masks painted with a tiling pattern draws in seconds instead of running out of memory (hayro patch 30)

# Conflicts:
#	vendor/README.md
This commit is contained in:
bflatastic
2026-11-09 08:04:05 +07:00
5 changed files with 581 additions and 121 deletions
+323
View File
@@ -7618,6 +7618,329 @@ trailer << /Root 1 0 R >>
}
}
/// A page of `/ImageMask` stencils painted with a tiling pattern: the 4 × 4 stencil inks a ring
/// (its centre 2 × 2 samples are `1`, unpainted under the default `/Decode [0 1]`), filled with
/// a red coloured pattern.
fn patterned_stencil_page(media: (u32, u32), content: &str) -> RenderedPage {
let tile = "1 0 0 rg 0 0 10 10 re f";
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents 4 0 R /Resources << /XObject << /Msk 5 0 R >> /Pattern << /P 6 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
00606000>
endstream endobj
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
{tile}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
media.0,
media.1,
content.len(),
tile.len()
);
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
page: 0,
kind: RequestKind::Pixels,
scale: 1.0,
..Default::default()
});
assert!(page.error.is_none(), "{:?}", page.error);
page
}
/// A stencil mask filled with a pattern shows the pattern where the stencil inks and nothing
/// elsewhere: not in its unpainted samples, and not around it, also when it is drawn smaller
/// than its samples and lands on whole device pixels.
#[test]
fn a_patterned_stencil_mask_paints_only_where_it_inks() {
// Pixel rows run top down: PDF (x, y) is pixel (x, 200 - y).
let px = |p: &RenderedPage, x: u32, y: u32| p.rgba[(((200 - y) * p.width + x) * 4) as usize..][..3].to_vec();
let page = patterned_stencil_page((200, 200), "/Pattern cs /P scn q 40 0 0 40 20 20 cm /Msk Do Q q 3 0 0 3 100.4 100.4 cm /Msk Do Q");
assert_eq!(px(&page, 25, 25), [255, 0, 0], "the ring is red");
assert_eq!(px(&page, 40, 40), [255, 255, 255], "the stencil's unpainted centre stays white");
for (x, y) in [(15, 40), (65, 40), (40, 15), (40, 65)] {
assert_eq!(px(&page, x, y), [255, 255, 255], "outside the stencil at ({x}, {y})");
}
assert!((100..104).any(|x| px(&page, x, 101) != [255, 255, 255]), "the shrunk stencil is drawn");
for edge in [97, 98, 106, 107] {
for along in 97..=107 {
assert_eq!(px(&page, edge, along), [255, 255, 255], "beside the shrunk stencil at ({edge}, {along})");
assert_eq!(px(&page, along, edge), [255, 255, 255], "above or below the shrunk stencil at ({along}, {edge})");
}
}
}
/// A patterned stencil mask under a soft mask is drawn at the soft mask's opacity once, as a
/// rectangle filled with the pattern beside it is: the stencil's coverage, which cuts the pattern
/// to the stencil's shape, is not masked a second time.
#[test]
fn a_patterned_stencil_mask_under_a_soft_mask_is_masked_once() {
let content = "/S gs /Pattern cs /P scn q 40 0 0 40 20 20 cm /Msk Do Q 120 20 40 40 re f";
let tile = "1 0 0 rg 0 0 10 10 re f";
let group = "0.5 g 0 0 200 200 re f";
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /XObject << /Msk 5 0 R >> /Pattern << /P 6 0 R >> /ExtGState << /S << /SMask << /S /Luminosity /G 7 0 R >> >> >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
00606000>
endstream endobj
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
{tile}
endstream endobj
7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 200 200] /Group << /S /Transparency /CS /DeviceGray >> /Length {} >> stream
{group}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
tile.len(),
group.len()
);
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
page: 0,
kind: RequestKind::Pixels,
scale: 1.0,
..Default::default()
});
assert!(page.error.is_none(), "{:?}", page.error);
// Pixel rows run top down: PDF (x, y) is pixel (x, 200 - y).
let px = |x: u32, y: u32| page.rgba[(((200 - y) * page.width + x) * 4) as usize..][..3].to_vec();
let (ring, rect) = (px(25, 25), px(140, 40));
assert!(rect != [255, 255, 255] && rect != [255, 0, 0], "the rectangle is half masked: {rect:?}");
assert!(ring.iter().zip(&rect).all(|(a, b)| a.abs_diff(*b) <= 2), "stencil {ring:?}, rectangle {rect:?}");
assert_eq!(px(40, 40), [255, 255, 255], "the stencil's unpainted centre stays white");
}
/// A patterned stencil mask in a Type 3 glyph is shrunk by area averaging, as it is outside
/// one (#4800): the glyph's own image rules are for its images, not for the coverage that cuts
/// the pattern. Here a one-column stroke of an 8-column stencil, drawn 4 pixels wide, is a
/// half-covered red; nearest-neighbour would read columns 1, 3, 5 and 7 and miss it.
#[test]
fn a_patterned_stencil_mask_in_a_type3_glyph_keeps_thin_strokes() {
let content = "BT /Pattern cs /P scn /T3 1 Tf 0 0 Td (a) Tj ET";
let glyph = "400 0 0 0 400 100 d1 q 400 0 0 100 0 0 cm /Im0 Do Q";
let tile = "1 0 0 rg 0 0 10 10 re f";
let pdf = format!(
"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 4 1] /Contents 4 0 R /Resources << /Font << /T3 6 0 R >> /Pattern << /P 8 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Image /Width 8 /Height 1 /ImageMask true /Filter /ASCIIHexDecode /Length 3 >> stream
DF>
endstream endobj
6 0 obj << /Type /Font /Subtype /Type3 /FontBBox [0 0 400 100] /FontMatrix [0.01 0 0 0.01 0 0] /CharProcs << /a 7 0 R >> /Encoding << /Type /Encoding /Differences [97 /a] >> /FirstChar 97 /LastChar 97 /Widths [400] /Resources << /XObject << /Im0 5 0 R >> >> >> endobj
7 0 obj << /Length {} >> stream
{glyph}
endstream endobj
8 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
{tile}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
glyph.len(),
tile.len()
);
let page = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default()).render(RenderRequest {
page: 0,
kind: RequestKind::Pixels,
scale: 1.0,
..Default::default()
});
assert!(page.error.is_none(), "{:?}", page.error);
assert_eq!((page.width, page.height), (4, 1));
let px = page.rgba.as_chunks::<4>().0.to_vec();
let [r, g, b, _] = px.get(1).copied().unwrap_or_default();
assert!(r >= 254 && g.abs_diff(128) <= 1 && b == g, "half-covered red over white, got {px:?}");
assert_eq!(px.get(3), Some(&[255, 255, 255, 255]), "{px:?}");
}
/// pdf.js issue 8078 paints 73,047 small stencil masks with one tiling pattern. Each was drawn
/// through a canvas-sized luminance mask (a full-page render and composite per stencil), which
/// took minutes for the page; a stencil now costs what its rectangle covers. Here 5,000
/// patterned stencils on a 1000 × 2000 page. What the page draws offscreen is counted (hayro
/// patch 30) rather than timed, so the check is the same on every machine: the one pattern cell,
/// and not a page-sized pixmap per stencil.
#[test]
fn many_patterned_stencil_masks_draw_no_page_sized_masks() {
let mut content = String::from("/Pattern cs /P scn ");
for i in 0..5000u32 {
content.push_str(&format!("q 16 0 0 16 {} {} cm /Msk Do Q ", (i % 50) * 20, (i / 50) * 20));
}
let before = hayro::offscreen_draws();
let page = patterned_stencil_page((1000, 2000), &content);
let after = hayro::offscreen_draws();
assert_eq!(after.tiling_cells - before.tiling_cells, 1, "one cell for the pattern's one setting");
let pixels = after.pixels - before.pixels;
assert!(pixels < 1000 * 2000, "5,000 patterned stencils drew {pixels} pixels offscreen");
// The last stencil sits at (980, 1980): its ring is red, its centre white.
let px = |x: u32, y: u32| page.rgba[(((2000 - y) * page.width + x) * 4) as usize..][..3].to_vec();
assert_eq!(px(981, 1981), [255, 0, 0]);
assert_eq!(px(988, 1988), [255, 255, 255]);
}
/// A coloured tiling pattern's cell is drawn once for all the fills of one setting (`scn`), as
/// counted by hayro patch 30: here 3,000 small squares filled with one pattern. Drawn again for
/// each fill, each cell was kept until the page was composited (pdf.js issue 8078 ran out of
/// memory that way, its 3.5 MB cell drawn for each of 73,047 stencils). The squares all show the
/// pattern in place. The same holds for a step so large that the fills show one cell, which is
/// drawn with a transparent border instead of repeated.
#[test]
fn fills_of_one_pattern_setting_share_its_cell() {
let mut content = String::from("/Pattern cs /P scn ");
for i in 0..3000u32 {
content.push_str(&format!("{} {} 4 4 re f ", (i % 60) * 5, (i / 60) * 5));
}
// The cell is blue with a red lower-left quarter; at `/Matrix` scale 10 a cell is 100 units,
// so with a step of 10 red shows where both coordinates, modulo 100, are below 50. With a
// step of 100000 the one cell shows and nothing past it.
let cell = "0 0 1 rg 0 0 10 10 re f 1 0 0 rg 0 0 5 5 re f";
for (step, past_the_first_cell) in [("10", [0, 0, 255]), ("100000", [255, 255, 255])] {
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Pattern << /P 5 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep {step} /YStep {step} /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
{cell}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
cell.len()
);
let before = hayro::offscreen_draws();
let page = render_first_page(pdf.into_bytes());
let cells = hayro::offscreen_draws().tiling_cells - before.tiling_cells;
assert_eq!(cells, 1, "step {step}: cells drawn for 3,000 fills of one setting");
// Pixel rows run top down: PDF (x, y) is pixel (x, 300 - y).
let px = |x: u32, y: u32| page.rgba[(((300 - y) * page.width + x) * 4) as usize..][..3].to_vec();
// The first square, at (0, 0), and the last, at (295, 245).
assert_eq!(px(1, 1), [255, 0, 0], "step {step}");
assert_eq!(px(296, 246), past_the_first_cell, "step {step}");
}
}
/// Fills interleaving the settings of two coloured patterns with cells of one size, and of one
/// of them at two cell scales, each show their own pattern in place: a cell kept for one setting
/// is not taken for another (hayro patch 30). `/P` is blue with a red lower-left quarter and `/Q`
/// green with a yellow one, both at `/Matrix` scale 10, so a cell is 100 units and its quarter
/// shows where both coordinates, modulo 100, are below 50. The form `/X`, drawn at scale 2, sets
/// `/P` in its own space, where a cell is 200 units, red where both page coordinates, modulo
/// 200, are below 100. Each of the six settings draws its cell once, for all its fills.
#[test]
fn fills_of_interleaved_pattern_settings_each_show_their_own_cell() {
let content = "/Pattern cs /P scn 60 60 4 4 re f 10 10 4 4 re f q 2 0 0 2 0 0 cm /X Do Q \
/Q scn 110 10 4 4 re f 160 160 4 4 re f /P scn 210 210 4 4 re f q 2 0 0 2 0 0 cm /X Do Q \
/Q scn 260 260 4 4 re f 110 110 4 4 re f";
// In page space, squares at (60, 260) and (260, 60), where `/P` at scale 1 is blue.
let form = "/Pattern cs /P scn 30 130 2 2 re f 130 30 2 2 re f";
let p = "0 0 1 rg 0 0 10 10 re f 1 0 0 rg 0 0 5 5 re f";
let q = "0 1 0 rg 0 0 10 10 re f 1 1 0 rg 0 0 5 5 re f";
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Pattern << /P 5 0 R /Q 6 0 R >> /XObject << /X 7 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
{p}
endstream endobj
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Matrix [10 0 0 10 0 0] /Resources << >> /Length {} >> stream
{q}
endstream endobj
7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 150 150] /Resources << /Pattern << /P 5 0 R >> >> /Length {} >> stream
{form}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
p.len(),
q.len(),
form.len()
);
let before = hayro::offscreen_draws();
let page = render_first_page(pdf.into_bytes());
assert_eq!(hayro::offscreen_draws().tiling_cells - before.tiling_cells, 6, "one cell per setting");
// The middle of the 4 × 4 square at PDF (x, y); pixel rows run top down.
let px = |x: u32, y: u32| page.rgba[(((300 - 2 - y) * page.width + x + 2) * 4) as usize..][..3].to_vec();
let (red, blue, green, yellow) = ([255, 0, 0], [0, 0, 255], [0, 255, 0], [255, 255, 0]);
for (x, y, colour, what) in [
(60, 60, blue, "/P"),
(10, 10, red, "/P"),
(60, 260, red, "/P at scale 2"),
(260, 60, red, "/P at scale 2"),
(110, 10, yellow, "/Q"),
(160, 160, green, "/Q"),
(210, 210, red, "/P set again"),
(260, 260, green, "/Q set again"),
(110, 110, yellow, "/Q set again"),
] {
assert_eq!(px(x, y), colour, "{what} at ({x}, {y})");
}
}
/// An uncoloured tiling pattern (`/PaintType 2`) takes the colour of each setting, so fills that
/// alternate red and green settings of it show both, each in place. Its cell also takes the
/// opacity the setting captured, which a patterned stencil mask's copy of the setting leaves out
/// (its transparency group applies it, hayro-interpret patch 46), so the cell drawn for one fill
/// of a setting is not the other's (hayro patch 30 keeps only coloured cells): a square and a
/// stencil painted with one half-opaque blue setting show the same half blue.
#[test]
fn an_uncoloured_pattern_shows_each_settings_colour() {
let content = "/C cs 1 0 0 /U scn 10 10 20 20 re f 0 1 0 /U scn 40 10 20 20 re f \
1 0 0 /U scn 70 10 20 20 re f 0 1 0 /U scn 100 10 20 20 re f \
/H gs 0 0 1 /U scn 10 50 20 20 re f q 20 0 0 20 40 50 cm /Msk Do Q";
let tile = "0 0 10 10 re f";
let pdf = format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 150 100] /Contents 4 0 R /Resources << /ColorSpace << /C [/Pattern /DeviceRGB] >> /Pattern << /U 6 0 R >> /XObject << /Msk 5 0 R >> /ExtGState << /H << /ca 0.5 >> >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}
endstream endobj
5 0 obj << /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true /Filter /ASCIIHexDecode /Length 9 >> stream
00000000>
endstream endobj
6 0 obj << /Type /Pattern /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 10 10] /XStep 10 /YStep 10 /Resources << >> /Length {} >> stream
{tile}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
tile.len()
);
let page = render_first_page(pdf.into_bytes());
// Pixel rows run top down: PDF (x, y) is pixel (x, 100 - y).
let px = |x: u32, y: u32| page.rgba[(((100 - y) * page.width + x) * 4) as usize..][..3].to_vec();
for (x, colour) in [(20, [255, 0, 0]), (50, [0, 255, 0]), (80, [255, 0, 0]), (110, [0, 255, 0])] {
assert_eq!(px(x, 20), colour, "the square at x {x}");
}
let (square, stencil) = (px(20, 60), px(50, 60));
assert!(square[2] == 255 && square[0].abs_diff(128) <= 1 && square[1] == square[0], "half blue: {square:?}");
assert!(square.iter().zip(&stencil).all(|(a, b)| a.abs_diff(*b) <= 2), "square {square:?}, stencil {stencil:?}");
}
/// #2971: in a knockout transparency group (`/K true`) each element composites with the
/// group's initial backdrop, not with the elements drawn before it in the group, over the
/// element's shape. A blue square at 50 % opacity over a red one at 50 % shows only the blue
+2 -2
View File
File diff suppressed because one or more lines are too long
+24
View File
@@ -23,6 +23,10 @@ use hayro_syntax::xref::XRef;
use kurbo::{Affine, BezPath, Rect, Shape};
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
/// PdfCraft patch (78): the source of [`TilingPattern::instance`].
static NEXT_TILING_INSTANCE: AtomicU64 = AtomicU64::new(0);
/// A PDF pattern.
#[derive(Debug, Clone)]
@@ -156,6 +160,8 @@ impl CacheKey for ShadingPattern {
#[derive(Clone)]
pub struct TilingPattern<'a> {
cache_key: u128,
/// PdfCraft patch (78): see [`Self::instance`].
instance: u64,
ctx_bbox: Rect,
/// The bbox of the tiling pattern.
pub bbox: Rect,
@@ -233,6 +239,7 @@ impl<'a> TilingPattern<'a> {
Some(Self {
cache_key,
instance: NEXT_TILING_INSTANCE.fetch_add(1, Ordering::Relaxed),
bbox,
x_step,
y_step,
@@ -310,6 +317,23 @@ impl<'a> TilingPattern<'a> {
}
}
impl TilingPattern<'_> {
/// PdfCraft patch (78): which setting of the pattern (`scn`/`SCN`) this is. Each setting is
/// numbered apart, and its copies keep the number. A coloured pattern's cell depends only on
/// what the setting captured (its stream, resources, settings and the bounding box then in
/// effect) and on the scale it is drawn at, so a renderer can draw the cell once per number
/// and scale for every fill of that setting. An uncoloured pattern's cell also takes the
/// colour and opacity of the paint, which copies can change.
pub fn instance(&self) -> u64 {
self.instance
}
/// Whether the pattern is coloured (`/PaintType 1`).
pub fn is_coloured(&self) -> bool {
self.is_color
}
}
impl CacheKey for TilingPattern<'_> {
fn cache_key(&self) -> u128 {
self.cache_key
+3
View File
@@ -65,6 +65,9 @@ pub use renderer::{image_resampling_size, tiling_cell_pixels, tiling_cell_scale,
/// PdfCraft patch: exported for its regression test.
#[doc(hidden)]
pub use renderer::set_skip_offscreen_paths;
/// PdfCraft patch (30): exported for its regression tests.
#[doc(hidden)]
pub use renderer::{OffscreenDraws, offscreen_draws};
/// A cache used by the renderer.
///
+229 -119
View File
@@ -637,6 +637,47 @@ pub fn set_skip_offscreen_paths(skip: bool) {
SKIP_OFFSCREEN_PATHS.with(|s| s.set(skip));
}
/// PdfCraft patch (30): what the renders on this thread have drawn offscreen, for tests that check
/// a page's cost by counting rather than by timing it (see [`offscreen_draws`]).
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct OffscreenDraws {
/// Tiling pattern cells drawn; a cell taken from `tiling_cells` isn't drawn again.
pub tiling_cells: u64,
/// The pixels of every pixmap drawn offscreen: tiling cells, masks, groups and the like.
pub pixels: u64,
}
thread_local! {
/// PdfCraft patch (30): see [`OffscreenDraws`].
static OFFSCREEN_DRAWS: std::cell::Cell<OffscreenDraws> =
const { std::cell::Cell::new(OffscreenDraws { tiling_cells: 0, pixels: 0 }) };
}
/// PdfCraft patch (30): exported for its regression tests (see [`OffscreenDraws`]). The counts
/// only grow; a test takes the difference across a render.
#[doc(hidden)]
pub fn offscreen_draws() -> OffscreenDraws {
OFFSCREEN_DRAWS
.try_with(std::cell::Cell::get)
.unwrap_or_default()
}
/// PdfCraft patch (30): a `width` × `height` pixmap to draw offscreen, counted in
/// [`OffscreenDraws`]. Every pixmap the renderer renders into, besides the page, is made here.
fn offscreen_pixmap(width: u16, height: u16, tiling_cell: bool) -> Pixmap {
// Gone only while the thread exits, when there is nothing left to count for.
let _ = OFFSCREEN_DRAWS.try_with(|draws| {
let mut counts = draws.get();
counts.pixels = counts
.pixels
.saturating_add(u64::from(width) * u64::from(height));
counts.tiling_cells = counts.tiling_cells.saturating_add(u64::from(tiling_cell));
draws.set(counts);
});
Pixmap::new(width, height)
}
/// PdfCraft patch: whether a device-space box can paint a pixel of a `width` × `height`
/// canvas. A box whose bounds miss the canvas is skipped without changing a pixel.
/// Bounds that aren't finite are drawn as before.
@@ -802,6 +843,9 @@ pub(crate) struct Renderer {
/// render, every nested renderer starting empty) and so to one `preview` setting: a mask
/// drawn for a preview, which may leave out tiny images, never reaches a final render.
pub(crate) soft_mask_cache: HashMap<u128, Mask>,
/// PdfCraft patch (30): coloured tiling cells drawn during this render (see [`TilingCells`]).
/// Like `soft_mask_cache` it belongs to one renderer, so to one `preview` setting.
tiling_cells: TilingCells,
pub(crate) outline_cache: Rc<std::cell::RefCell<HashMap<u128, Rc<BezPath>>>>,
pub(crate) cur_mask: Option<Mask>,
pub(crate) in_type3_glyph: bool,
@@ -815,6 +859,40 @@ pub(crate) struct Renderer {
ink: InkState,
}
/// PdfCraft patch (30): the cell pixmaps of coloured tiling patterns, keyed by the setting of the
/// pattern (`TilingPattern::instance`) and the cell's size and scale. A page that fills many
/// shapes with one pattern (pdf.js issue 8078 paints 73,047 stencil masks with a pattern whose
/// cell is 3.5 MB) drew and kept a cell per fill, which ran out of memory; the fills now share
/// one. A handful of entries covers a run of fills with one pattern, and bounds what the cache
/// keeps besides what the scene holds anyway.
#[derive(Default)]
struct TilingCells(Vec<(TilingCellKey, Arc<Pixmap>)>);
#[derive(Clone, Copy, PartialEq, Eq)]
struct TilingCellKey {
instance: u64,
/// Whether this is the bordered cell of a pattern whose step shows one cell (patch (20)).
one_cell: bool,
is_stroke: bool,
pixels: (u16, u16),
scale: (u64, u64),
}
impl TilingCells {
const MAX_ENTRIES: usize = 8;
fn get(&self, key: &TilingCellKey) -> Option<Arc<Pixmap>> {
self.0.iter().find(|(k, _)| k == key).map(|(_, cell)| cell.clone())
}
fn insert(&mut self, key: TilingCellKey, cell: Arc<Pixmap>) {
if self.0.len() >= Self::MAX_ENTRIES {
self.0.remove(0);
}
self.0.push((key, cell));
}
}
/// PdfCraft patch (27): which inks a pass of a `DeviceCMYK` group draws, and whether the
/// group asked for something a pass can't draw.
///
@@ -913,6 +991,7 @@ impl Renderer {
erase_groups: 0,
inside_pattern: false,
soft_mask_cache: HashMap::default(),
tiling_cells: TilingCells::default(),
outline_cache: cache.outline_cache.clone(),
cur_mask: None,
in_type3_glyph: false,
@@ -1019,6 +1098,7 @@ impl Renderer {
erase_groups: 0,
inside_pattern: false,
soft_mask_cache: HashMap::default(),
tiling_cells: TilingCells::default(),
outline_cache: self.outline_cache.clone(),
cur_mask: None,
in_type3_glyph: false,
@@ -1027,7 +1107,7 @@ impl Renderer {
image_transparency_stack: Vec::new(),
ink: InkState::default(),
};
let mut mask_pix = Pixmap::new(self.ctx.width(), self.ctx.height());
let mut mask_pix = offscreen_pixmap(self.ctx.width(), self.ctx.height(), false);
let rgb_data = ImageData::Rgb(RgbData {
data: dummy_rgb,
width: alpha_data.width,
@@ -1540,6 +1620,52 @@ impl Renderer {
self.draw_pixmap(Arc::new(pixmap), quality, draw_transform);
}
/// PdfCraft patch (30): the cell pixmap of tiling pattern `t`, `size` pixels drawn at
/// `initial_transform`; a cell with a `key` is drawn once and then taken from `tiling_cells`.
fn tiling_cell(
&mut self,
t: &hayro_interpret::pattern::TilingPattern<'_>,
key: Option<TilingCellKey>,
size: (u16, u16),
initial_transform: Affine,
is_stroke: bool,
) -> Arc<Pixmap> {
if let Some(cell) = key.and_then(|key| self.tiling_cells.get(&key)) {
return cell;
}
let mut renderer = Self {
ctx: RenderContext::new_with(size.0, size.1, derive_settings(self.ctx.render_settings())),
// PdfCraft patch: a pattern cell is painted as one artwork, so its content is never
// remapped by the reading-colours setting.
color_replacement: None,
in_annotations: false,
knockout: Vec::new(),
erasing: false,
knockout_passes: Vec::new(),
erase_groups: 0,
cur_mask: None,
inside_pattern: true,
soft_mask_cache: HashMap::default(),
tiling_cells: TilingCells::default(),
outline_cache: self.outline_cache.clone(),
in_type3_glyph: false,
scaler: self.scaler,
preview: self.preview,
image_transparency_stack: Vec::new(),
ink: InkState::default(),
};
t.interpret(&mut renderer, initial_transform, is_stroke);
let mut pix = offscreen_pixmap(size.0, size.1, true);
renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
let pix = Arc::new(pix);
if let Some(key) = key {
self.tiling_cells.insert(key, pix.clone());
}
pix
}
fn push_clip_path_inner(&mut self, clip_path: &BezPath, fill: FillRule) {
let old_transform = *self.ctx.transform();
@@ -1585,6 +1711,7 @@ impl Renderer {
in_annotations: self.in_annotations,
inside_pattern: self.inside_pattern,
soft_mask_cache: HashMap::default(),
tiling_cells: TilingCells::default(),
outline_cache: self.outline_cache.clone(),
cur_mask: None,
in_type3_glyph: false,
@@ -1609,7 +1736,7 @@ impl Renderer {
if renderer.ink.unsupported {
return None;
}
let mut pix = Pixmap::new(width, height);
let mut pix = offscreen_pixmap(width, height, false);
renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
@@ -1741,7 +1868,9 @@ impl Renderer {
AlphaColor::from_rgba8(c[0], c[1], c[2], c[3]).into()
}
Paint::Pattern(p) => {
let path_transform = self.ctx.transform();
// PdfCraft patch (30): a copy, so a tiling cell can be drawn and kept on `self`.
let current_transform = *self.ctx.transform();
let path_transform = &current_transform;
match *p {
Pattern::Shading(s) => {
@@ -1848,39 +1977,20 @@ impl Renderer {
const BORDER: f64 = 1.0;
let pix_width = ((bbox.width() as f32 * xs).ceil() as u16).saturating_add(2);
let pix_height = ((bbox.height() as f32 * ys).ceil() as u16).saturating_add(2);
let mut renderer = Self {
ctx: RenderContext::new_with(
pix_width,
pix_height,
derive_settings(self.ctx.render_settings()),
),
color_replacement: None,
in_annotations: false,
knockout: Vec::new(),
erasing: false,
knockout_passes: Vec::new(),
erase_groups: 0,
cur_mask: None,
inside_pattern: true,
soft_mask_cache: HashMap::default(),
outline_cache: self.outline_cache.clone(),
in_type3_glyph: false,
scaler: self.scaler,
preview: self.preview,
image_transparency_stack: Vec::new(),
ink: InkState::default(),
};
let initial_transform = Affine::translate((BORDER, BORDER))
* Affine::scale_non_uniform(xs as f64, ys as f64)
* Affine::translate((-bbox.x0, -bbox.y0));
t.interpret(&mut renderer, initial_transform, is_stroke);
let mut pix = Pixmap::new(pix_width, pix_height);
renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
let key = t.is_coloured().then(|| TilingCellKey {
instance: t.instance(),
one_cell: true,
is_stroke,
pixels: (pix_width, pix_height),
scale: (u64::from(xs.to_bits()), u64::from(ys.to_bits())),
});
let pix = self.tiling_cell(&t, key, (pix_width, pix_height), initial_transform, is_stroke);
paint_transform = path_transform.inverse() * t.matrix * initial_transform.inverse();
let image = Image {
image: ImageSource::Pixmap(Arc::new(pix)),
image: ImageSource::Pixmap(pix),
sampler: ImageSampler {
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
@@ -1910,37 +2020,17 @@ impl Renderer {
let scaled_width = bbox.width() * xs;
let scaled_height = bbox.height() * ys;
let mut renderer = Self {
ctx: RenderContext::new_with(
pix_width,
pix_height,
derive_settings(self.ctx.render_settings()),
),
// PdfCraft patch: a pattern cell is painted as one artwork, so its
// content is never remapped by the reading-colours setting.
color_replacement: None,
in_annotations: false,
knockout: Vec::new(),
erasing: false,
knockout_passes: Vec::new(),
erase_groups: 0,
cur_mask: None,
inside_pattern: true,
soft_mask_cache: HashMap::default(),
outline_cache: self.outline_cache.clone(),
in_type3_glyph: false,
scaler: self.scaler,
preview: self.preview,
image_transparency_stack: Vec::new(),
ink: InkState::default(),
};
let mut initial_transform = Affine::scale_non_uniform(xs, ys)
* Affine::translate((-bbox.x0, -bbox.y0));
t.interpret(&mut renderer, initial_transform, is_stroke);
let mut pix = Pixmap::new(pix_width, pix_height);
renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);
// PdfCraft patch (30): a coloured cell is drawn once per setting and scale.
let key = t.is_coloured().then(|| TilingCellKey {
instance: t.instance(),
one_cell: false,
is_stroke,
pixels: (pix_width, pix_height),
scale: (xs.to_bits(), ys.to_bits()),
});
let pix = self.tiling_cell(&t, key, (pix_width, pix_height), initial_transform, is_stroke);
// TODO: Fix these
if t.x_step < 0.0 {
@@ -1957,7 +2047,7 @@ impl Renderer {
path_transform.inverse() * t.matrix * initial_transform.inverse();
let image = Image {
image: ImageSource::Pixmap(Arc::new(pix)),
image: ImageSource::Pixmap(pix),
sampler: ImageSampler {
x_extend: peniko::Extend::Repeat,
y_extend: peniko::Extend::Repeat,
@@ -2195,69 +2285,57 @@ impl<'a> Device<'a> for Renderer {
self.ctx.set_fill_rule(old_rule);
}
Paint::Pattern(_) => {
let (width, height) = (self.ctx.width(), self.ctx.height());
let stencil_rect = Rect::new(
0.0,
0.0,
stencil.width as f64,
stencil.height as f64,
);
let mask_pix = {
let rgb_bytes = ImageData::Rgb(RgbData {
data: vec![
255;
stencil.width as usize
* stencil.height as usize
* 3
],
width: stencil.width,
height: stencil.height,
interpolate: stencil.interpolate,
scale_factors: stencil.scale_factors,
});
let mut sub_renderer = Self {
ctx: RenderContext::new_with(
width,
height,
derive_settings(self.ctx.render_settings()),
),
// PdfCraft patch: a stencil mask must keep its original
// coverage values, so its content is never remapped by
// the reading-colours setting.
color_replacement: None,
in_annotations: false,
knockout: Vec::new(),
erasing: false,
knockout_passes: Vec::new(),
erase_groups: 0,
inside_pattern: false,
soft_mask_cache: HashMap::default(),
outline_cache: self.outline_cache.clone(),
cur_mask: None,
in_type3_glyph: false,
scaler: self.scaler,
preview: self.preview,
image_transparency_stack: Vec::new(),
ink: InkState::default(),
};
let mut sub_pix = Pixmap::new(width, height);
sub_renderer.ctx.set_transform(transform);
sub_renderer.draw_image(rgb_bytes, Some(stencil));
sub_renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
sub_renderer
.ctx
.render_to_pixmap(&mut resources, &mut sub_pix);
sub_pix
let Some(len) = source_byte_len(
stencil.width,
stencil.height,
3,
MAX_IMAGE_PIXELS,
) else {
log::warn!(
"Stencil mask dimensions exceeded the buffer limit; image skipped"
);
return;
};
self.ctx.push_layer(
None,
Some(self.ctx.blend_mode()),
None,
Some(Mask::new_luminance(&mask_pix)),
None,
);
let Some(mut white) = image_buffer(len) else {
log::warn!(
"Stencil mask buffer could not be allocated; image skipped"
);
return;
};
white.resize(len, 255);
// PdfCraft patch (30): under a clip of the stencil's device
// rectangle, the pattern is painted into a layer of its own and
// cut to the stencil by a layer holding the stencil, composited
// with `Compose::DestIn`; a layer is only as large as the clip,
// so a draw costs what its rectangle covers. A luminance mask
// (which vello_cpu only takes at canvas size) cost a
// canvas-sized render, mask and composite per draw: a page of
// 73,047 patterned 16 × 16 stencils took minutes. The clip is
// padded past the rectangle so that a stencil snapped to whole
// device pixels lies inside it; the cut clears the padding.
let device_rect = (transform * stencil_rect.to_path(0.1))
.bounding_box()
.inflate(2.0, 2.0);
if ![device_rect.x0, device_rect.y0, device_rect.x1, device_rect.y1]
.iter()
.all(|v| v.is_finite())
{
log::warn!("Stencil mask has an invalid transform; image skipped");
return;
}
let layer_clip = device_rect.to_path(0.1);
let blend_mode = self.ctx.blend_mode();
self.ctx.set_transform(Affine::IDENTITY);
self.ctx.push_clip_layer(&layer_clip);
// Unclipped, so isolated: a layer with only a clip draws
// straight into what is under it, which the cut would clear.
self.ctx.push_layer(None, Some(blend_mode), None, None, None);
self.ctx.set_transform(transform);
// PdfCraft patch: a stencil mask is painted with the fill
@@ -2275,6 +2353,37 @@ impl<'a> Device<'a> for Renderer {
}
}
// The stencil, as white ink under its own coverage, keeps the
// pattern where it paints and clears the rest of the clip.
let paint_blend = self.ctx.blend_mode();
self.ctx.set_blend_mode(peniko::BlendMode::default());
// The pattern left its own paint transform, which would place
// the stencil's pixels.
self.ctx.set_paint_transform(Affine::IDENTITY);
self.ctx.push_blend_layer(peniko::BlendMode::new(
Mix::Normal,
Compose::DestIn,
));
self.ctx.set_transform(transform);
// The coverage is drawn as a stencil outside a Type 3 glyph is:
// a glyph's image rules (nearest shrink, padded frame) are for
// the glyph's own images.
let in_type3_glyph = std::mem::replace(&mut self.in_type3_glyph, false);
self.draw_image(
ImageData::Rgb(RgbData {
data: white,
width: stencil.width,
height: stencil.height,
interpolate: stencil.interpolate,
scale_factors: stencil.scale_factors,
}),
Some(stencil),
);
self.in_type3_glyph = in_type3_glyph;
self.ctx.pop_layer();
self.ctx.set_blend_mode(paint_blend);
self.ctx.pop_layer();
self.ctx.pop_layer();
}
};
@@ -2366,7 +2475,7 @@ impl<'a> Device<'a> for Renderer {
return false;
}
let mut result = Pixmap::new(width, height);
let mut result = offscreen_pixmap(width, height, false);
for ((px, rgb), coverage) in result.data_mut().iter_mut().zip(rgb.chunks_exact(3)).zip(cmy.data()) {
let a = u32::from(coverage.a);
let premul = |v: u8| ((u32::from(v) * a + 127) / 255) as u8;
@@ -2750,6 +2859,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei
inside_pattern: false,
cur_mask: None,
soft_mask_cache: HashMap::default(),
tiling_cells: TilingCells::default(),
outline_cache: Rc::new(std::cell::RefCell::new(HashMap::new())),
in_type3_glyph: false,
scaler: Scaler::new(ResamplingFunction::CatmullRom),
@@ -2777,7 +2887,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei
renderer.ctx.pop_layer();
}
let mut pix = Pixmap::new(width, height);
let mut pix = offscreen_pixmap(width, height, false);
renderer.ctx.flush();
let mut resources = vello_cpu::Resources::default();
renderer.ctx.render_to_pixmap(&mut resources, &mut pix);