From 21d7c1267e89bcec63579b6913f950c74a7c9e9b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:22:05 +0000 Subject: [PATCH] metafile: draw GDI+ percentage hatch styles --- crates/metafile/src/canvas.rs | 37 +++++++++++++++++++++++++++++----- crates/metafile/src/emfplus.rs | 8 +++++--- crates/metafile/src/tests.rs | 29 ++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index b4ac445fc..51159bdf3 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -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> { - 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> { + hatch_tile_inner(style, fg, bg, true) +} + +fn hatch_tile_inner(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>, gdi_plus: bool) -> Option> { + 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]) }); } diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index 5c45721a7..af406aa30 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -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); diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 806c95662..1a2a30fc6 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -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]