Metafile import: translucent strokes stay apart, and each pattern tile is averaged once

Strokes drawn one after another with the same pen join into one path only
when the pen is opaque. A translucent stroke keeps its own opacity, so where
two of them overlap both show.

Items filled with the same pattern brush share its tile, and the tile's
average color is now worked out once per tile instead of once per item.
This commit is contained in:
dotCooCoo
2026-11-14 07:35:25 -08:00
parent 0df1c3f284
commit f8e70c1415
2 changed files with 37 additions and 4 deletions
+14 -4
View File
@@ -2,8 +2,8 @@
//! picture, and the picture's items become document objects here.
//!
//! - A filled or stroked path becomes a path with a fill and a stroke (caps, joins, miter limit
//! and dashes from the pen). Strokes drawn one after another, each starting where the last one
//! ended, with the same pen and clip, join into one path.
//! and dashes from the pen). Opaque strokes drawn one after another, each starting where the
//! last one ended, with the same pen and clip, join into one path.
//! - A path filled with a pattern or hatched brush is filled with the average color of the
//! brush's tile.
//! - A bitmap becomes an image (its pixels as PNG) placed where the picture draws it, turned or
@@ -18,7 +18,7 @@
//! at its device's pixels a millimetre (96 pixels an inch when the header doesn't say).
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::{Arc, Weak};
use craft_metafile::{Cap, ClipPath, HAlign, Item, Join, LineStyle, Seg, VAlign};
use vectorcraft_color::Paint;
@@ -106,6 +106,10 @@ struct Pending {
type MadeClips = HashMap<*const ClipPath, (Arc<ClipPath>, Option<Arc<Clip>>)>;
/// The last clip region and the clip made for it.
type MadeRegion = (Vec<[f32; 4]>, Option<Arc<Clip>>);
/// Brush tiles already averaged, by address: items filled with the same brush share its tile. The
/// `Weak` keeps the address from being reused, and the tile's pixels are freed once the last item
/// that uses it has been read.
type Averaged = HashMap<*const craft_metafile::Tile, (Weak<craft_metafile::Tile>, Option<[u8; 4]>)>;
struct Mapper {
/// Picture units → points.
@@ -120,6 +124,7 @@ struct Mapper {
paths: MadeClips,
region: Option<MadeRegion>,
pending: Option<Pending>,
averaged: Averaged,
}
fn color(c: [u8; 4]) -> Paint {
@@ -161,6 +166,7 @@ impl Mapper {
paths: HashMap::new(),
region: None,
pending: None,
averaged: HashMap::new(),
}
}
@@ -317,8 +323,10 @@ impl Mapper {
PathEl::MoveTo(p) => Some(*p),
_ => None,
});
// A translucent stroke stays apart, so where strokes overlap each one shows.
if let Some(p) = &mut self.pending
&& p.pen == pen
&& pen.color[3] == u8::MAX
&& start.is_some()
&& p.path.elements().last().and_then(|e| e.end_point()) == start
{
@@ -338,7 +346,9 @@ impl Mapper {
Item::Pattern { segs, even_odd, tile, region, clips } => {
self.flush();
let Some(chain) = self.chain(region, &clips) else { return };
let Some(c) = average(&tile.rgba) else { return };
let Some(c) = self.averaged.entry(Arc::as_ptr(&tile)).or_insert_with(|| (Arc::downgrade(&tile), average(&tile.rgba))).1 else {
return;
};
self.warn(PATTERNS);
let bp = self.path(&segs);
if let Some(n) = self.path_node(bp, Some(c), None, even_odd) {
+23
View File
@@ -548,6 +548,29 @@ fn dual_emf_plus_files_draw_their_emf_plus_records() {
assert!((f.opacity - 128.0 / 255.0).abs() < 1e-6, "{}", f.opacity);
}
#[test]
fn translucent_strokes_drawn_one_after_another_stay_apart() {
// EmfPlusObject: pen 0, 1 pixel wide, with a solid brush of `argb`; then two EmfPlusDrawLines
// with it, from A to B and from B back to A.
let header = le(&[0xDBC0_1002u32 as i32, 0, 96, 96]);
let lines = |argb: u32| {
let pen = [le(&[0xDBC0_1002u32 as i32, 0, 0, 2]), le_f32(&[1.0]), le(&[0xDBC0_1002u32 as i32, 0, argb as i32])].concat();
let a_to_b = [le(&[2]), le_f32(&[1000.0, 1000.0, 3000.0, 1000.0])].concat();
let b_to_a = [le(&[2]), le_f32(&[3000.0, 1000.0, 1000.0, 1000.0])].concat();
let comment = emf_plus(&[(0x4001, 0, header.clone()), (0x4008, 0x0200, pen), (0x400D, 0, a_to_b), (0x400D, 0, b_to_a), (0x4002, 0, vec![])]);
paths(&import(&hand_emf(5000, 3000, &[(70, comment)])).unwrap().document)
};
// Each translucent stroke keeps its own opacity, so where they overlap both show.
let p = lines(0x80FF_0000);
assert_eq!(p.len(), 2, "{p:?}");
for n in &p {
let AppearanceItem::Stroke(st) = &n.appearance.items[0] else { panic!("a stroke") };
assert!((st.opacity - 128.0 / 255.0).abs() < 1e-6, "{}", st.opacity);
}
// Opaque strokes still join into one path.
assert_eq!(lines(0xFFFF_0000).len(), 1);
}
#[test]
fn pattern_brushes_fill_with_their_average_color() {
// CREATEDIBPATTERNBRUSHPT: a 2 × 2 tile of 24-bit pixels, two red and two blue (bottom-up rows