diff --git a/crates/metafile/src/import/picture.rs b/crates/metafile/src/import/picture.rs index d269fd27ca..8373253ae4 100644 --- a/crates/metafile/src/import/picture.rs +++ b/crates/metafile/src/import/picture.rs @@ -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, Option>)>; /// The last clip region and the clip made for it. type MadeRegion = (Vec<[f32; 4]>, Option>); +/// 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, Option<[u8; 4]>)>; struct Mapper { /// Picture units → points. @@ -120,6 +124,7 @@ struct Mapper { paths: MadeClips, region: Option, pending: Option, + 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) { diff --git a/crates/metafile/src/tests/import.rs b/crates/metafile/src/tests/import.rs index 689037d277..69abf02e86 100644 --- a/crates/metafile/src/tests/import.rs +++ b/crates/metafile/src/tests/import.rs @@ -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