metafile: draw GDI+ percentage hatch styles

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 06:05:42 +00:00
1 parent d6ed8c20f1
commit 21d7c1267e
3 files changed
+64 -10

No files matched your search

+32 -5
View File
@@ -1295,13 +1295,35 @@ impl Canvas {
}
}
/// An 8×8 hatch tile (HS_HORIZONTAL, HS_VERTICAL, HS_FDIAGONAL, HS_BDIAGONAL, HS_CROSS,
/// HS_DIAGCROSS): one-pixel lines in `fg` over `bg`, or over nothing when the background is
/// transparent. None for an unknown style.
/// An 8×8 GDI hatch tile (HS_HORIZONTAL through HS_DIAGCROSS). None for an unknown GDI style.
pub(crate) fn hatch_tile(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>) -> Option<Vec<u8>> {
if style > 5 {
hatch_tile_inner(style, fg, bg, false)
}
/// An 8×8 GDI+ tile for the six basic and twelve percentage HatchStyle values. Percentage styles
/// use a Bayer tile with the specified foreground coverage. Other GDI+ styles still use the
/// fallback in `emfplus::Painter::fill` until their patterns have been mapped.
pub(crate) fn emfplus_hatch_tile(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>) -> Option<Vec<u8>> {
hatch_tile_inner(style, fg, bg, true)
}
fn hatch_tile_inner(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>, gdi_plus: bool) -> Option<Vec<u8>> {
if style > if gdi_plus { 17 } else { 5 } {
return None;
}
// An 8×8 Bayer matrix keeps percentage hatches evenly distributed in a small, repeatable
// tile. The twelve values are GDI+'s Percent05 through Percent90 styles.
const BAYER: [[u8; 8]; 8] = [
[0, 48, 12, 60, 3, 51, 15, 63],
[32, 16, 44, 28, 35, 19, 47, 31],
[8, 56, 4, 52, 11, 59, 7, 55],
[40, 24, 36, 20, 43, 27, 39, 23],
[2, 50, 14, 62, 1, 49, 13, 61],
[34, 18, 46, 30, 33, 17, 45, 29],
[10, 58, 6, 54, 9, 57, 5, 53],
[42, 26, 38, 22, 41, 25, 37, 21],
];
const PERCENT: [u32; 12] = [5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90];
let mut px = Vec::with_capacity(8 * 8 * 4);
for y in 0..8u32 {
for x in 0..8u32 {
@@ -1312,7 +1334,12 @@ pub(crate) fn hatch_tile(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>) -> Option
2 => f,
3 => b,
4 => h || v,
_ => f || b,
5 => f || b,
6..=17 if gdi_plus => {
let count = (PERCENT[(style - 6) as usize] * 64 + 50) / 100;
u32::from(BAYER[y as usize][x as usize]) < count
}
_ => return None,
};
px.extend(if on { fg } else { bg.unwrap_or([0, 0, 0, 0]) });
}
+5 -3
View File
@@ -27,7 +27,7 @@
use std::sync::Arc;
use crate::bytes::Bytes;
use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, LogFont, PS_NULL, Pen, RawTile, Xform, hatch_tile, mul};
use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, LogFont, PS_NULL, Pen, RawTile, Xform, emfplus_hatch_tile, mul};
use crate::canvas::{Raw, RawClip};
use crate::cliprect::{MAX_AREA_CLIP_WORK, clip_polygon_within, convex, flatten, ops_bbox, pts_bbox, rect_poly};
use crate::dib::Rgba;
@@ -845,9 +845,11 @@ impl Painter {
cv.set_brush(Brush::new(BS_SOLID, *c));
cv.shape(ops.to_vec(), true, false);
}
Fill::Hatch { style, fg, bg } => match hatch_tile(*style, *fg, Some(*bg)) {
Fill::Hatch { style, fg, bg } => match emfplus_hatch_tile(*style, *fg, Some(*bg)) {
Some(px) => cv.tile_shape(ops, Arc::new(RawTile { w: 8, h: 8, px, cell: cv.pattern_px })),
// GDI+ has many more hatch styles than GDI: those fill with the blend of both colours.
// Percentage styles are tiled above. The remaining GDI+ hatch styles have no tile
// mapping yet, so keep the existing blended-colour fallback instead of inventing
// a pattern that could disagree with the file.
None => {
cv.set_brush(Brush::new(BS_SOLID, lerp(*fg, *bg, 0.5)));
cv.shape(ops.to_vec(), true, false);
+27 -2
View File
@@ -2237,10 +2237,35 @@ fn emfplus_linear_gradient_fills_with_a_shaded_tile() {
#[test]
fn emfplus_hatch_brush_fills_with_an_8x8_tile() {
let brush = cat(vec![h32(0xDBC0_1002), h32(1), h32(4), h32(0xFF00_0000), h32(0xFFFF_FFFF)]);
// GDI+ HatchStyle.Percent50 (12), as used by System.Drawing's HatchBrush.
let brush = cat(vec![h32(0xDBC0_1002), h32(1), h32(12), h32(0xFF00_0000), h32(0xFFFF_FFFF)]);
let fill = prec(0x400A, 0, &cat(vec![h32(0), h32(1), f32s(&[0.0, 0.0, 16.0, 16.0])]));
let p = plus_pic(vec![prec(0x4008, 0x0100, &brush), fill]);
assert!(matches!(&p.items[0], Item::Pattern { tile, .. } if tile.width == 8 && tile.height == 8));
let tile = pattern_of(&p.items[0]);
assert_eq!((tile.width, tile.height), (8, 8));
let foreground = tile.rgba.as_chunks::<4>().0.iter().filter(|px| **px == [0, 0, 0, 255]).count();
assert_eq!(foreground, 32, "Percent50 uses equal foreground and background coverage");
}
#[test]
fn gdi_plus_percentage_hatch_styles_keep_the_documented_coverage() {
const PERCENT: [usize; 12] = [5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90];
let fg = [0, 0, 0, 255];
let bg = [255, 255, 255, 255];
for style in 0..=17 {
let tile = crate::canvas::emfplus_hatch_tile(style, fg, Some(bg)).expect("supported GDI+ hatch style");
let foreground = tile.as_chunks::<4>().0.iter().filter(|px| **px == fg).count();
assert!((1..64).contains(&foreground), "style {style} should mix both colours");
if (6..=17).contains(&style) {
let percent = PERCENT[(style - 6) as usize];
assert_eq!(foreground, (percent * 64 + 50) / 100, "percentage style {style}");
}
}
for style in 18..=52 {
assert!(crate::canvas::emfplus_hatch_tile(style, fg, Some(bg)).is_none(), "style {style} has no confirmed tile mapping");
}
assert!(crate::canvas::emfplus_hatch_tile(53, fg, Some(bg)).is_none(), "out-of-range values stay unsupported");
assert!(crate::canvas::hatch_tile(9, fg, Some(bg)).is_none(), "GDI's six-style range is unchanged");
}
#[test]