mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-15 05:54:50 +00:00
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:
@@ -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
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+24
@@ -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
|
||||
|
||||
Vendored
+3
@@ -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.
|
||||
///
|
||||
|
||||
Vendored
+229
-119
@@ -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 = ¤t_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);
|
||||
|
||||
Reference in New Issue
Block a user