From ac3e46b748a7a968bb12d30b3bbb9bedfd1f0dbd Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 01:09:45 +0000 Subject: [PATCH] Metafile: read WMF, EMF and EMF+ with craft-metafile; delete crates/metafile The renderer now draws what the shared reader adds: pen caps, joins and dashes, clip paths (SELECTCLIPPATH) and turned or skewed bitmaps. --- Cargo.lock | 22 +- Cargo.toml | 3 +- crates/metafile/Cargo.toml | 14 - crates/metafile/src/bytes.rs | 59 - crates/metafile/src/canvas.rs | 1109 ---------------- crates/metafile/src/clip.rs | 193 --- crates/metafile/src/cliprect.rs | 357 ----- crates/metafile/src/dib.rs | 259 ---- crates/metafile/src/emf.rs | 478 ------- crates/metafile/src/emfplus.rs | 1641 ----------------------- crates/metafile/src/geom.rs | 118 -- crates/metafile/src/lib.rs | 306 ----- crates/metafile/src/place.rs | 206 --- crates/metafile/src/tests.rs | 2196 ------------------------------- crates/metafile/src/text.rs | 203 --- crates/metafile/src/wmf.rs | 383 ------ crates/render/Cargo.toml | 2 +- crates/render/src/metafile.rs | 69 +- crates/render/src/tests.rs | 13 + docs/architecture.md | 3 +- xtask/src/layers.rs | 2 - 21 files changed, 81 insertions(+), 7555 deletions(-) delete mode 100644 crates/metafile/Cargo.toml delete mode 100644 crates/metafile/src/bytes.rs delete mode 100644 crates/metafile/src/canvas.rs delete mode 100644 crates/metafile/src/clip.rs delete mode 100644 crates/metafile/src/cliprect.rs delete mode 100644 crates/metafile/src/dib.rs delete mode 100644 crates/metafile/src/emf.rs delete mode 100644 crates/metafile/src/emfplus.rs delete mode 100644 crates/metafile/src/geom.rs delete mode 100644 crates/metafile/src/lib.rs delete mode 100644 crates/metafile/src/place.rs delete mode 100644 crates/metafile/src/tests.rs delete mode 100644 crates/metafile/src/text.rs delete mode 100644 crates/metafile/src/wmf.rs diff --git a/Cargo.lock b/Cargo.lock index 61cc76481..a21129af4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -889,6 +889,17 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "craft-metafile" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=8079878ae2b166c1ea8229e40eff7e08c916e532#8079878ae2b166c1ea8229e40eff7e08c916e532" +dependencies = [ + "encoding_rs", + "image", + "kurbo", + "thiserror 2.0.21", +] + [[package]] name = "craft-pdf" version = "0.1.0" @@ -1201,15 +1212,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "deckcraft-metafile" -version = "0.18.1" -dependencies = [ - "image", - "kurbo", - "thiserror 2.0.21", -] - [[package]] name = "deckcraft-model" version = "0.18.1" @@ -1260,11 +1262,11 @@ dependencies = [ name = "deckcraft-render" version = "0.18.1" dependencies = [ + "craft-metafile", "deckcraft-anim", "deckcraft-color", "deckcraft-fonts", "deckcraft-geom", - "deckcraft-metafile", "deckcraft-model", "deckcraft-text", "image", diff --git a/Cargo.toml b/Cargo.toml index 647c7af5c..67118c880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ repository = "https://github.com/storytold/deckcraft" # internal deckcraft-geom = { path = "crates/geom" } deckcraft-color = { path = "crates/color" } -deckcraft-metafile = { path = "crates/metafile" } deckcraft-model = { path = "crates/model" } deckcraft-fonts = { path = "crates/fonts" } deckcraft-text = { path = "crates/text" } @@ -58,6 +57,8 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg", # craft-media and craft-audio share types and stay on one rev. craft-media = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false } craft-audio = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false, features = ["opus-decode"] } +# WMF, EMF and EMF+ reader. +craft-metafile = { git = "https://github.com/storytold/craft-libs", rev = "8079878ae2b166c1ea8229e40eff7e08c916e532", version = "=0.1.0" } craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } # craft-update: app-specific pretend versions apply to startup and manual checks. craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] } diff --git a/crates/metafile/Cargo.toml b/crates/metafile/Cargo.toml deleted file mode 100644 index 1b5645393..000000000 --- a/crates/metafile/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "deckcraft-metafile" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "DeckCraft metafiles (ported from WordCraft): parses Windows WMF and EMF records into a neutral vector model (paths and bitmaps)" - -[lints] -workspace = true - -[dependencies] -kurbo = { workspace = true } -thiserror = { workspace = true } -image = { workspace = true } diff --git a/crates/metafile/src/bytes.rs b/crates/metafile/src/bytes.rs deleted file mode 100644 index 5584d6dff..000000000 --- a/crates/metafile/src/bytes.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Bounds-checked little-endian access to hostile input. Every read returns `None` past the end, so -//! a truncated or lying size field stops the parse instead of panicking. - -#[derive(Clone, Copy)] -pub(crate) struct Bytes<'a>(pub(crate) &'a [u8]); - -impl<'a> Bytes<'a> { - pub fn slice(&self, off: usize, len: usize) -> Option<&'a [u8]> { - self.0.get(off..off.checked_add(len)?) - } - - pub fn tail(&self, off: usize) -> Option> { - self.0.get(off..).map(Bytes) - } - - fn arr(&self, off: usize) -> Option<[u8; N]> { - self.slice(off, N)?.try_into().ok() - } - - pub fn u16(&self, off: usize) -> Option { - Some(u16::from_le_bytes(self.arr(off)?)) - } - - pub fn i16(&self, off: usize) -> Option { - Some(i16::from_le_bytes(self.arr(off)?)) - } - - pub fn u32(&self, off: usize) -> Option { - Some(u32::from_le_bytes(self.arr(off)?)) - } - - pub fn i32(&self, off: usize) -> Option { - Some(i32::from_le_bytes(self.arr(off)?)) - } - - pub fn f32(&self, off: usize) -> Option { - Some(f32::from_le_bytes(self.arr(off)?)) - } - - /// `n` consecutive (x, y) pairs of 16- or 32-bit signed integers starting at `off`. - pub fn points(&self, off: usize, n: usize, wide: bool) -> Option> { - let sz = if wide { 8 } else { 4 }; - let raw = self.slice(off, n.checked_mul(sz)?)?; - let mut out = Vec::with_capacity(n); - for c in raw.chunks_exact(sz) { - let p = if wide { - let x = i32::from_le_bytes(c.get(0..4)?.try_into().ok()?); - let y = i32::from_le_bytes(c.get(4..8)?.try_into().ok()?); - (f64::from(x), f64::from(y)) - } else { - let x = i16::from_le_bytes(c.get(0..2)?.try_into().ok()?); - let y = i16::from_le_bytes(c.get(2..4)?.try_into().ok()?); - (f64::from(x), f64::from(y)) - }; - out.push(p); - } - Some(out) - } -} diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs deleted file mode 100644 index b0c62887c..000000000 --- a/crates/metafile/src/canvas.rs +++ /dev/null @@ -1,1109 +0,0 @@ -//! Drawing state shared by the WMF and EMF readers: window/viewport mapping and the EMF world -//! transform, pen and brush selection through an object table, the DC save stack and path brackets. -//! Output is device-space items; `lib.rs` maps them into the picture frame. - -use std::sync::{Arc, OnceLock}; - -use crate::clip::{self, MAX_CLIP_RECTS}; -use crate::dib::Rgba; -use crate::geom::{Op, P}; -use crate::text::{ETO_CLIPPED, ETO_OPAQUE, Font, TA_UPDATECP, Text, align}; -use crate::{MAX_BYTES, MAX_ITEMS, MAX_OBJECTS, MAX_PATH_OPS, MAX_STACK}; - -pub(crate) const MM_TEXT: u32 = 1; -const MM_ISOTROPIC: u32 = 7; -const MM_ANISOTROPIC: u32 = 8; -const STOCK: u32 = 0x8000_0000; -pub(crate) const PS_NULL: u32 = 5; -/// Pen style bits; the rest of the style word holds end-cap, join and type flags. -pub(crate) const PS_STYLE_MASK: u32 = 0x0F; -pub(crate) const BS_SOLID: u32 = 0; -/// Cells on the longer side of a computed (gradient or texture) fill tile. -const SHADE_RES: usize = 128; -pub(crate) const BS_NULL: u32 = 1; -pub(crate) const BS_HATCHED: u32 = 2; -/// BS_PATTERN, BS_DIBPATTERN and BS_DIBPATTERNPT: the brush paints a bitmap tile. -const BS_PATTERN: u32 = 3; -const BS_DIBPATTERN: u32 = 5; -const BS_DIBPATTERNPT: u32 = 6; -const WINDING: u32 = 2; -/// SETROP2 mix modes. R2_NOP leaves the destination alone: outlines and fills draw nothing. -/// R2_XORPEN and R2_MASKPEN draw the classic see-through shadow (XOR a colour, AND a pattern, XOR -/// the colour again); the others draw as R2_COPYPEN. -const R2_XORPEN: u32 = 7; -const R2_MASKPEN: u32 = 9; -const R2_NOP: u32 = 11; -const R2_COPYPEN: u32 = 13; -/// SETBKMODE OPAQUE: text cells and hatch gaps are filled with the background colour. -const OPAQUE: u32 = 2; -const PATCOPY: u32 = 0x00F0_0021; -const BLACKNESS: u32 = 0x0000_0042; -const WHITENESS: u32 = 0x00FF_0062; -/// Device pixels per millimetre assumed for WMF (96 dpi), and for EMF files without a device size. -const DEFAULT_PX_PER_MM: f64 = 96.0 / 25.4; - -/// Affine transform `[m11, m12, m21, m22, dx, dy]` in EMF XFORM order: `x' = x·m11 + y·m21 + dx`. -pub(crate) type Xform = [f64; 6]; -const IDENTITY: Xform = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; - -/// `a` then `b` in EMF row-vector convention: the matrix product `a × b`. -pub(crate) fn mul(a: Xform, b: Xform) -> Xform { - [ - a[0] * b[0] + a[1] * b[2], - a[0] * b[1] + a[1] * b[3], - a[2] * b[0] + a[3] * b[2], - a[2] * b[1] + a[3] * b[3], - a[4] * b[0] + a[5] * b[2] + b[4], - a[4] * b[1] + a[5] * b[3] + b[5], - ] -} - -#[derive(Clone, Copy, PartialEq, Debug)] -pub(crate) struct Pen { - pub style: u32, - /// Logical width; 0 means a cosmetic one-pixel hairline. - pub width: f64, - pub color: [u8; 4], -} - -#[derive(Clone, PartialEq, Debug)] -pub(crate) struct Brush { - pub style: u32, - pub color: [u8; 4], - /// HS_* hatch style of a BS_HATCHED brush. - pub hatch: u32, - /// The tile of a pattern brush. - pub pattern: Option>, -} - -impl Brush { - pub fn new(style: u32, color: [u8; 4]) -> Self { - Brush { style, color, hatch: 0, pattern: None } - } - - /// A pattern brush painting `img`. A monochrome brush paints its dark pixels in the text - /// colour and its light ones in the background colour. None for an empty or oversized tile. - pub fn pattern(img: Rgba, mono: bool) -> Option { - if img.w == 0 || img.h == 0 || img.w > MAX_TILE_SIDE || img.h > MAX_TILE_SIDE || img.px.len() != img.w * img.h * 4 { - return None; - } - let pattern = Arc::new(Pattern { w: img.w, h: img.h, px: img.px, mono, plain: OnceLock::new() }); - Some(Brush { style: BS_DIBPATTERNPT, color: [0, 0, 0, 255], hatch: 0, pattern: Some(pattern) }) - } -} - -/// A pattern brush's tile, top-down RGBA. -#[derive(PartialEq, Debug)] -pub(crate) struct Pattern { - pub w: usize, - pub h: usize, - pub px: Vec, - pub mono: bool, - /// The tile as drawn when nothing recolours it, shared by every fill with this brush. - pub plain: OnceLock>, -} - -/// Largest pattern tile side kept (GDI brushes are typically 8×8). -pub(crate) const MAX_TILE_SIDE: usize = 256; - -#[derive(Clone, Debug)] -pub(crate) enum Obj { - Pen(Pen), - Brush(Brush), - Font(Arc), - /// Palettes and regions: they take a table slot but draw nothing. - Other, -} - -/// A font object: the neutral [`Font`] and its escapement (tenths of a degree, counter-clockwise), -/// which the text transform takes. -#[derive(Debug, Clone, PartialEq, Default)] -pub(crate) struct LogFont { - pub font: Font, - pub escapement: f64, -} - -/// A brush tile in device units: `cell` device units per tile pixel, laid from the device origin. -#[derive(Debug, PartialEq)] -pub(crate) struct RawTile { - pub w: usize, - pub h: usize, - pub px: Vec, - pub cell: f64, -} - -/// What a fill paints. -enum Paint { - Solid([u8; 4]), - Tile(Arc), -} - -/// Converts a COLORREF (0x00BBGGRR) to opaque RGBA. -pub(crate) fn colorref(c: u32) -> [u8; 4] { - [(c & 0xFF) as u8, ((c >> 8) & 0xFF) as u8, ((c >> 16) & 0xFF) as u8, 255] -} - -/// Millimetres per logical unit of the metric map modes (LOMETRIC, HIMETRIC, LOENGLISH, HIENGLISH, TWIPS). -fn metric_mm(mode: u32) -> Option { - match mode { - 2 => Some(0.1), - 3 => Some(0.01), - 4 => Some(25.4 / 100.0), - 5 => Some(25.4 / 1000.0), - 6 => Some(25.4 / 1440.0), - _ => None, - } -} - -#[derive(Clone, Copy)] -pub(crate) struct Mapping { - wo: P, - we: P, - vo: P, - ve: P, - mode: u32, - world: Xform, - /// Device pixels per millimetre on each axis (EMF: from szlDevice and szlMillimeters). - px_mm: P, -} - -impl Mapping { - fn identity() -> Self { - Mapping { - wo: (0.0, 0.0), - we: (1.0, 1.0), - vo: (0.0, 0.0), - ve: (1.0, 1.0), - mode: MM_TEXT, - world: IDENTITY, - px_mm: (DEFAULT_PX_PER_MM, DEFAULT_PX_PER_MM), - } - } - - /// Logical point to device point: world transform, then window to viewport. - pub fn device(&self, p: P) -> P { - let w = self.world; - let (x, y) = (p.0 * w[0] + p.1 * w[2] + w[4], p.0 * w[1] + p.1 * w[3] + w[5]); - let (sx, sy) = self.ratios(); - ((x - self.wo.0) * sx + self.vo.0, (y - self.wo.1) * sy + self.vo.1) - } - - /// Window-to-viewport scale per axis. MM_TEXT fixes the scale at 1; the metric modes use a fixed - /// scale with y up; MM_ISOTROPIC uses one scale on both axes (the smaller magnitude, each axis's - /// sign kept); anisotropic and unknown modes use the extents as given. - fn ratios(&self) -> (f64, f64) { - if self.mode == MM_TEXT { - return (1.0, 1.0); - } - if let Some(mm) = metric_mm(self.mode) { - return (mm * self.px_mm.0, -mm * self.px_mm.1); - } - let (rx, ry) = (ratio(self.ve.0, self.we.0), ratio(self.ve.1, self.we.1)); - if self.mode == MM_ISOTROPIC { - let s = rx.abs().min(ry.abs()); - return (if rx < 0.0 { -s } else { s }, if ry < 0.0 { -s } else { s }); - } - (rx, ry) - } - - /// Approximate device length per logical length, for pen widths. - fn scale(&self) -> f64 { - let w = self.world; - let det = (w[0] * w[3] - w[1] * w[2]).abs().sqrt(); - let (sx, sy) = self.ratios(); - det * (sx.abs() + sy.abs()) / 2.0 - } - - /// True when the window-to-viewport extents set the scale (the isotropic and anisotropic modes). - pub fn uses_extents(&self) -> bool { - matches!(self.mode, MM_ISOTROPIC | MM_ANISOTROPIC) - } - - pub fn window_ext(&self) -> P { - self.we - } - - pub fn viewport_ext(&self) -> P { - self.ve - } - - /// The window in device units as `[x, y, w, h]` with positive extents, used as the picture frame - /// of a plain WMF. - pub fn window_rect(&self) -> [f64; 4] { - let a = self.device(self.wo); - let b = self.device((self.wo.0 + self.we.0, self.wo.1 + self.we.1)); - [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()] - } -} - -fn ratio(num: f64, den: f64) -> f64 { - if den == 0.0 { 1.0 } else { num / den } -} - -/// Output of drawing, in device units. Path points are already mapped; `lib.rs` normalises them. -pub(crate) enum Raw { - Path { - ops: Vec, - fill: Option<[u8; 4]>, - stroke: Option<([u8; 4], f64)>, - even_odd: bool, - /// Device-space clip region. None is the default clip. - region: Option>, - }, - /// A path filled with a repeating tile. - Pattern { - ops: Vec, - even_odd: bool, - tile: Arc, - region: Option>, - }, - Bitmap { - rect: [f64; 4], - img: Rgba, - region: Option>, - }, - /// Text whose transform and clip are in device units. `region` is the device clip. - Text { - text: Box, - region: Option>, - }, -} - -impl Raw { - fn set_region(&mut self, region: Option>) { - match self { - Raw::Path { region: slot, .. } - | Raw::Pattern { region: slot, .. } - | Raw::Bitmap { region: slot, .. } - | Raw::Text { region: slot, .. } => { - *slot = region; - } - } - } -} - -/// Text drawing state: colours, background mode, alignment and the selected font. -#[derive(Clone)] -struct TextState { - color: [u8; 4], - bk_color: [u8; 4], - bk_opaque: bool, - align: u32, - font: Option>, -} - -#[derive(Clone)] -struct Saved { - map: Mapping, - pen: Pen, - brush: Brush, - even_odd: bool, - rop2: u32, - text: TextState, - clip: Option>, -} - -pub(crate) struct Canvas { - pub items: Vec, - pub map: Mapping, - /// True once SETWINDOWEXT was seen, so a plain WMF has a frame to use. - pub window_set: bool, - /// True once the file set the viewport extent itself (not the player, see [`Canvas::play_into`]). - pub viewport_set: bool, - saved: Vec, - /// Bytes held by the items so far (path ops and pixels), capped at [`MAX_BYTES`]. - used: usize, - /// Bytes retained outside the canvas (EMF+ objects) that count against the same cap. - held: usize, - objects: Vec>, - pen: Pen, - brush: Brush, - even_odd: bool, - /// SETROP2 mix mode. - rop2: u32, - /// The colour of the last fill drawn with R2_XORPEN, for the see-through shadow idiom. - xor_color: Option<[u8; 4]>, - text: TextState, - /// Device units per pattern-brush pixel: brushes are laid out in 96-dpi pixels. - pub pattern_px: f64, - pos: P, - in_path: bool, - path: Vec, - full: bool, - /// Current clip in device units. None is the default (the whole surface); Some([]) draws nothing. - clip: Option>, - /// Device bounds of the picture, so excluding from the default clip has a surface to cut. - surface: Option<[f64; 4]>, - /// EMF+ convex device clip polygon: drawing is cut to it geometrically. - plus_clip: Option>, -} - -impl Canvas { - pub fn new() -> Self { - Canvas { - items: Vec::new(), - map: Mapping::identity(), - window_set: false, - viewport_set: false, - saved: Vec::new(), - used: 0, - held: 0, - objects: Vec::new(), - pen: Pen { style: 0, width: 0.0, color: [0, 0, 0, 255] }, - brush: Brush::new(BS_SOLID, [255, 255, 255, 255]), - even_odd: true, - rop2: R2_COPYPEN, - xor_color: None, - text: TextState { color: [0, 0, 0, 255], bk_color: [255, 255, 255, 255], bk_opaque: true, align: 0, font: None }, - pattern_px: 1.0, - pos: (0.0, 0.0), - in_path: false, - path: Vec::new(), - full: false, - clip: None, - surface: None, - plus_clip: None, - } - } - - /// Device bounds `[x0, y0, x1, y1]` of the picture. Used when a clip record cuts the default clip. - pub fn set_clip_surface(&mut self, rect: [f64; 4]) { - self.surface = clip::normalize(rect); - } - - /// Bytes retained outside the canvas that share its budget (see [`Canvas::hold`]). - #[cfg(test)] - pub(crate) fn held(&self) -> usize { - self.held - } - - /// Bytes the canvas has retained so far. - pub fn used(&self) -> usize { - self.used - } - - /// True once the item cap is hit; the readers stop there and keep what they have. - pub fn full(&self) -> bool { - self.full - } - - fn push(&mut self, raw: Raw) { - match self.plus_clip.take() { - None => self.push_raw(raw), - Some(c) => { - for r in crate::cliprect::clip_raw(raw, &c) { - self.push_raw(r); - } - self.plus_clip = Some(c); - } - } - } - - /// Records the bytes another part of the player retains, so the items and they stay within - /// [`MAX_BYTES`] together. - pub fn hold(&mut self, bytes: usize) { - self.held = bytes; - } - - fn push_raw(&mut self, mut raw: Raw) { - if self.clip.as_ref().is_some_and(Vec::is_empty) { - return; - } - raw.set_region(self.clip.clone()); - let region_cost = self.clip.as_ref().map_or(0, |c| c.len() * std::mem::size_of::<[f64; 4]>()); - let cost = match &raw { - Raw::Path { ops, .. } => ops.len() * std::mem::size_of::(), - // A tile shared with its brush is counted once (by the brush); one made for this fill - // counts here. - Raw::Pattern { ops, tile, .. } => { - let own = if Arc::strong_count(tile) == 1 { tile.px.len() } else { 0 }; - ops.len() * std::mem::size_of::() + 64 + own - } - Raw::Bitmap { img, .. } => img.px.len(), - Raw::Text { text: t, .. } => t.text.len() + t.dx.len() * 4 + t.font.family.len() + 128, - } + region_cost; - if self.items.len() >= MAX_ITEMS || self.used.saturating_add(self.held).saturating_add(cost) > MAX_BYTES { - self.full = true; - } else { - self.used += cost; - self.items.push(raw); - } - } - - pub fn set_window_org(&mut self, p: P) { - self.map.wo = p; - } - - pub fn set_window_ext(&mut self, e: P) { - self.map.we = e; - self.window_set = true; - } - - pub fn set_viewport_org(&mut self, p: P) { - self.map.vo = p; - } - - pub fn set_viewport_ext(&mut self, e: P) { - self.map.ve = e; - self.viewport_set = true; - } - - /// Sets up the mapping the way a player does before it plays a WMF into a target: anisotropic, - /// `window` (`[x, y, w, h]` in logical units) onto a viewport of `size` at the origin. The file's - /// own window records then replace the window; the viewport stays unless the file sets one. - /// Without a window (a plain WMF) only the viewport extent is set: the map mode and viewport - /// origin stay the file's. - pub fn play_into(&mut self, window: Option<[f64; 4]>, size: P) { - if let Some([x, y, w, h]) = window { - self.map.mode = MM_ANISOTROPIC; - self.map.wo = (x, y); - self.map.we = (w, h); - self.map.vo = (0.0, 0.0); - } - self.map.ve = size; - } - - pub fn set_map_mode(&mut self, mode: u32) { - self.map.mode = mode; - } - - /// Device pixels per millimetre (x, y) for the metric map modes. Ignored unless both are positive. - pub fn set_device_mm(&mut self, px_mm: P) { - if px_mm.0.is_finite() && px_mm.1.is_finite() && px_mm.0 > 0.0 && px_mm.1 > 0.0 { - self.map.px_mm = px_mm; - } - } - - pub fn set_polyfill(&mut self, mode: u32) { - self.even_odd = mode != WINDING; - } - - /// SETROP2. R2_NOP draws nothing; R2_XORPEN and R2_MASKPEN are read as the see-through shadow - /// idiom (see [`Canvas::fill_paint`]); the other mix modes draw as copy. - pub fn set_rop2(&mut self, mode: u32) { - self.rop2 = mode; - } - - pub fn set_text_color(&mut self, c: u32) { - self.text.color = colorref(c); - } - - pub fn set_bk_color(&mut self, c: u32) { - self.text.bk_color = colorref(c); - } - - pub fn set_bk_mode(&mut self, mode: u32) { - self.text.bk_opaque = mode == OPAQUE; - } - - pub fn set_text_align(&mut self, ta: u32) { - self.text.align = ta; - } - - /// The character set of the selected font (ANSI without one). - pub fn font_charset(&self) -> u8 { - self.text.font.as_ref().map_or(0, |f| f.font.charset) - } - - /// EMF SETWORLDTRANSFORM (`mode` 4) and MODIFYWORLDTRANSFORM (1 identity, 2 left, 3 right, 4 set). - /// An unknown mode leaves the world transform as it was. - pub fn modify_world(&mut self, x: Xform, mode: u32) { - match mode { - 1 => self.map.world = IDENTITY, - 2 => self.map.world = mul(x, self.map.world), - 3 => self.map.world = mul(self.map.world, x), - 4 => self.map.world = x, - _ => {} - } - } - - /// The current world transform (EMF+ draws through it). - pub fn world(&self) -> Xform { - self.map.world - } - - /// Sets the pen and brush directly (EMF+ records name their own pen and brush). - pub fn set_pen(&mut self, pen: Pen) { - self.pen = pen; - } - - pub fn set_brush(&mut self, brush: Brush) { - self.brush = brush; - } - - /// Sets the EMF+ convex device clip polygon (None: unclipped). - pub fn set_clip(&mut self, clip: Option>) { - self.plus_clip = clip; - } - - /// Logical to device as one affine transform (the mapping is affine). - pub fn device_affine(&self) -> Xform { - let o = self.map.device((0.0, 0.0)); - let x = self.map.device((1.0, 0.0)); - let y = self.map.device((0.0, 1.0)); - [x.0 - o.0, x.1 - o.1, y.0 - o.0, y.1 - o.1, o.0, o.1] - } - - /// Text colour, font and alignment set directly (EMF+ strings name their own font and brush); - /// the background is transparent. - pub fn set_text_style(&mut self, color: [u8; 4], font: LogFont, align: u32) { - self.text.color = color; - self.text.bk_opaque = false; - self.text.align = align; - self.text.font = Some(Arc::new(font)); - } - - /// Pushes an item already in device units (a nested metafile's drawing). - pub fn push_device(&mut self, raw: Raw) { - self.push(raw); - } - - /// Fills logical `ops` with a ready-made tile, laid from the device origin. - pub fn tile_shape(&mut self, ops: &[Op], tile: Arc) { - let dev: Vec = ops.iter().map(|o| self.map_op(*o)).collect(); - if dev.len() >= 2 { - let even_odd = self.even_odd; - self.push(Raw::Pattern { ops: dev, even_odd, tile, region: None }); - } - } - - /// Fills logical `ops` with a colour computed per device point (gradients, textures): a tile - /// of at most [`SHADE_RES`] cells on the longer side, laid out so that its one repeat covers - /// the shape. - pub fn shade_shape(&mut self, ops: &[Op], shade: &dyn Fn(P) -> [u8; 4]) { - // A full canvas or an empty clip keeps nothing more, so do not sample a tile for it. - if self.full || self.clip.as_ref().is_some_and(Vec::is_empty) { - return; - } - let dev: Vec = ops.iter().map(|o| self.map_op(*o)).collect(); - let Some([l, t, r, b]) = crate::cliprect::ops_bbox(&dev) else { return }; - let side = (r - l).max(b - t); - if dev.len() < 2 || !(side.is_finite() && side > 0.0) { - return; - } - let cell = side / SHADE_RES as f64; - let w = (((r - l) / cell).ceil() as usize + 1).clamp(1, SHADE_RES + 1); - let h = (((b - t) / cell).ceil() as usize + 1).clamp(1, SHADE_RES + 1); - let (pw, ph) = (w as f64 * cell, h as f64 * cell); - let mut px = Vec::with_capacity(w * h * 4); - for j in 0..h { - // The device position this tile row stands for inside the shape's box. - let y = t + (j as f64 * cell - t).rem_euclid(ph) + cell / 2.0; - for i in 0..w { - let x = l + (i as f64 * cell - l).rem_euclid(pw) + cell / 2.0; - px.extend_from_slice(&shade((x, y))); - } - } - let even_odd = self.even_odd; - self.push(Raw::Pattern { ops: dev, even_odd, tile: Arc::new(RawTile { w, h, px, cell }), region: None }); - } - - /// Pushes the drawing state. Past [`MAX_STACK`] levels the parse stops, since later restores - /// could no longer match the file. - pub fn save_dc(&mut self) { - if self.saved.len() < MAX_STACK { - self.saved.push(Saved { - map: self.map, - pen: self.pen, - brush: self.brush.clone(), - even_odd: self.even_odd, - rop2: self.rop2, - text: self.text.clone(), - clip: self.clip.clone(), - }); - } else { - self.full = true; - } - } - - /// Negative `n` pops `-n` levels; positive `n` restores absolute level `n`. - pub fn restore_dc(&mut self, n: i32) { - let idx = if n < 0 { self.saved.len().checked_sub(n.unsigned_abs() as usize) } else { (n as usize).checked_sub(1) }; - let Some(idx) = idx else { return }; - let Some(s) = self.saved.get(idx).cloned() else { return }; - self.saved.truncate(idx); - self.map = s.map; - self.pen = s.pen; - self.brush = s.brush; - self.even_odd = s.even_odd; - self.rop2 = s.rop2; - self.text = s.text; - self.clip = s.clip; - } - - /// The logical rectangle as a device rectangle. A scale or translation keeps the rectangle; a - /// rotation stores the bounds of the rotated corners. - fn device_rect(&self, [l, t, r, b]: [f64; 4]) -> Option<[f64; 4]> { - let corners = [(l, t), (r, t), (l, b), (r, b)].map(|p| self.map.device(p)); - if !corners.iter().all(|p| p.0.is_finite() && p.1.is_finite()) { - return None; - } - let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY); - for p in corners { - x0 = x0.min(p.0); - y0 = y0.min(p.1); - x1 = x1.max(p.0); - y1 = y1.max(p.1); - } - clip::normalize([x0, y0, x1, y1]) - } - - fn map_rects(&self, logical: &[[f64; 4]]) -> Vec<[f64; 4]> { - let mut out = Vec::new(); - for r in logical.iter().take(MAX_CLIP_RECTS) { - if let Some(d) = self.device_rect(*r) { - out.push(d); - } - if out.len() >= MAX_CLIP_RECTS { - break; - } - } - out - } - - /// INTERSECTCLIPRECT: later drawing is limited to this logical rectangle as well. - pub fn intersect_clip_rect(&mut self, logical: [f64; 4]) { - self.clip = match self.device_rect(logical) { - Some(rect) => clip::intersect(self.clip.clone(), rect), - None => Some(Vec::new()), - }; - } - - /// EXCLUDECLIPRECT: this logical rectangle is removed from the clip. - pub fn exclude_clip_rect(&mut self, logical: [f64; 4]) { - let Some(rect) = self.device_rect(logical) else { return }; - self.clip = clip::exclude(self.clip.clone(), rect, self.surface); - } - - /// EXTSELECTCLIPRGN: combine these logical rectangles with the clip in a `RGN_*` mode. - pub fn select_clip_rects(&mut self, logical: &[[f64; 4]], mode: u32) { - self.clip = clip::combine(self.clip.clone(), self.map_rects(logical), mode, self.surface); - } - - /// The default clip: later records are not limited by a region. - pub fn reset_clip(&mut self) { - self.clip = None; - } - - /// Stores an object in `slot` (EMF index), or in the first free slot (WMF handle table). - pub fn add_object(&mut self, slot: Option, obj: Obj) { - // A pattern brush's pixels count against the byte budget like drawn items. - if let Obj::Brush(Brush { pattern: Some(p), .. }) = &obj { - let cost = p.px.len(); - if self.used.saturating_add(self.held).saturating_add(cost) > MAX_BYTES { - self.full = true; - return; - } - self.used += cost; - } - match slot { - Some(i) if i < MAX_OBJECTS => { - if self.objects.len() <= i { - self.objects.resize(i + 1, None); - } - if let Some(s) = self.objects.get_mut(i) { - *s = Some(obj); - } - } - Some(_) => {} - None => match self.objects.iter().position(Option::is_none) { - Some(i) => { - if let Some(s) = self.objects.get_mut(i) { - *s = Some(obj); - } - } - None if self.objects.len() < MAX_OBJECTS => self.objects.push(Some(obj)), - None => {} - }, - } - } - - pub fn delete_object(&mut self, i: u32) { - if let Some(s) = self.objects.get_mut(i as usize) { - *s = None; - } - } - - /// Selects an object by index; indices with the high bit set are stock objects. - pub fn select(&mut self, i: u32) { - if i & STOCK != 0 { - return self.select_stock(i & !STOCK); - } - match self.objects.get(i as usize).cloned().flatten() { - Some(Obj::Pen(p)) => self.pen = p, - Some(Obj::Brush(b)) => self.brush = b, - Some(Obj::Font(f)) => self.text.font = Some(f), - Some(Obj::Other) | None => {} - } - } - - fn select_stock(&mut self, i: u32) { - let white = [255, 255, 255, 255]; - let black = [0, 0, 0, 255]; - let gray = |v: u8| Brush::new(BS_SOLID, [v, v, v, 255]); - match i { - 0 | 18 => self.brush = Brush::new(BS_SOLID, white), - 1 => self.brush = gray(192), - 2 => self.brush = gray(128), - 3 => self.brush = gray(64), - 4 => self.brush = Brush::new(BS_SOLID, black), - 5 => self.brush = Brush::new(BS_NULL, black), - 6 => self.pen = Pen { style: 0, width: 0.0, color: white }, - 7 | 19 => self.pen = Pen { style: 0, width: 0.0, color: black }, - 8 => self.pen = Pen { style: PS_NULL, width: 0.0, color: black }, - // The stock fonts (OEM_FIXED_FONT … DEFAULT_GUI_FONT): drawn in the default face. - 10..=17 => self.text.font = None, - _ => {} - } - } - - fn map_op(&self, op: Op) -> Op { - match op { - Op::M(p) => Op::M(self.map.device(p)), - Op::L(p) => Op::L(self.map.device(p)), - Op::C(a, b, c) => Op::C(self.map.device(a), self.map.device(b), self.map.device(c)), - Op::Z => Op::Z, - } - } - - /// The current brush's paint. Hatch and monochrome brushes take the background colour (when - /// the background mode is opaque) and the text colour. Under R2_MASKPEN (destination AND - /// brush) a tile's white pixels keep what is below, and its black ones take the colour of the - /// XOR fill before it: XOR, AND, XOR again leaves that colour where the tile is black. - fn fill_paint(&self) -> Option { - let b = &self.brush; - match b.style { - BS_SOLID => Some(Paint::Solid(b.color)), - BS_HATCHED => { - let bg = self.text.bk_opaque.then_some(self.text.bk_color); - let px = hatch_tile(b.hatch, b.color, bg)?; - Some(Paint::Tile(Arc::new(RawTile { w: 8, h: 8, px, cell: self.pattern_px }))) - } - BS_PATTERN | BS_DIBPATTERN | BS_DIBPATTERNPT => { - let p = b.pattern.as_ref()?; - if !p.mono && self.rop2 != R2_MASKPEN { - let cell = self.pattern_px; - return Some(Paint::Tile(p.plain.get_or_init(|| Arc::new(RawTile { w: p.w, h: p.h, px: p.px.clone(), cell })).clone())); - } - let mut px = p.px.clone(); - for c in px.as_chunks_mut::<4>().0 { - if p.mono { - let light = u16::from(c[0]) + u16::from(c[1]) + u16::from(c[2]) >= 384; - let v = if light { self.text.bk_color } else { self.text.color }; - *c = [v[0], v[1], v[2], 255]; - } - if self.rop2 == R2_MASKPEN { - if c[..3] == [255, 255, 255] { - c[3] = 0; - } else if c[..3] == [0, 0, 0] - && let Some(x) = self.xor_color - { - *c = x; - } - } - } - Some(Paint::Tile(Arc::new(RawTile { w: p.w, h: p.h, px, cell: self.pattern_px }))) - } - _ => None, - } - } - - /// Pushes device ops filled with `fill` and stroked with `stroke`: a tile fill and its outline - /// are two items. - fn push_shape(&mut self, ops: Vec, fill: Option, stroke: Option<([u8; 4], f64)>) { - let even_odd = self.even_odd; - match fill { - Some(Paint::Tile(tile)) => { - if stroke.is_some() { - self.push(Raw::Pattern { ops: ops.clone(), even_odd, tile, region: None }); - self.push(Raw::Path { ops, fill: None, stroke, even_odd, region: None }); - } else { - self.push(Raw::Pattern { ops, even_odd, tile, region: None }); - } - } - Some(Paint::Solid(c)) => self.push(Raw::Path { ops, fill: Some(c), stroke, even_odd, region: None }), - None if stroke.is_some() => self.push(Raw::Path { ops, fill: None, stroke, even_odd, region: None }), - None => {} - } - } - - /// Whether the mix mode suppresses drawing: R2_NOP draws nothing, and R2_XORPEN shapes only - /// remember their fill colour (an XOR pair cancels; the see-through shadow idiom draws its - /// pattern in that colour). - fn mix_skips(&mut self, fill: bool) -> bool { - match self.rop2 { - R2_NOP => true, - R2_XORPEN => { - if fill && self.brush.style == BS_SOLID { - self.xor_color = Some(self.brush.color); - } - true - } - _ => false, - } - } - - fn stroke_pen(&self) -> Option<([u8; 4], f64)> { - if (self.pen.style & PS_STYLE_MASK) == PS_NULL { - return None; - } - // A cosmetic pen (width 0) stays 0: the renderers draw that as a hairline. - let w = if self.pen.width == 0.0 { 0.0 } else { self.pen.width.abs() * self.map.scale() }; - Some((self.pen.color, w)) - } - - fn push_path_ops(&mut self, ops: impl IntoIterator) { - for op in ops { - if self.path.len() >= MAX_PATH_OPS { - return; - } - self.path.push(op); - } - } - - /// Draws logical-space ops with the current pen (`stroke`) and brush (`fill`), or appends them to - /// the open path bracket, where fill and stroke wait for FILLPATH/STROKEPATH. - pub fn shape(&mut self, ops: &[Op], fill: bool, stroke: bool) { - let dev: Vec = ops.iter().map(|o| self.map_op(*o)).collect(); - if self.in_path { - return self.push_path_ops(dev); - } - if self.mix_skips(fill) || dev.len() < 2 { - return; - } - let fill = if fill { self.fill_paint() } else { None }; - let stroke = if stroke { self.stroke_pen() } else { None }; - self.push_shape(dev, fill, stroke); - } - - pub fn move_to(&mut self, p: P) { - if self.in_path { - self.push_path_ops([Op::M(self.map.device(p))]); - } - self.pos = p; - } - - /// LINETO from the current point, then moves the current point to `p`. - pub fn line_to(&mut self, p: P) { - self.line_chain(&[p], false, true); - } - - /// Lines from the current point through `pts` (POLYLINETO), or a polyline from the current point. - pub fn line_chain(&mut self, pts: &[P], fill: bool, stroke: bool) { - // Inside an open bracket the path already ends at the current point, so no MoveTo is needed. - let mut ops = if self.in_path && !self.path.is_empty() { Vec::new() } else { vec![Op::M(self.pos)] }; - ops.extend(pts.iter().map(|p| Op::L(*p))); - if let Some(last) = pts.last() { - self.pos = *last; - } - self.shape(&ops, fill, stroke); - } - - /// POLYBEZIER: cubic segments that start a new figure at `from`. The current point is unchanged. - pub fn bezier(&mut self, from: P, pts: &[P]) { - let mut ops = vec![Op::M(from)]; - ops.extend(cubics(pts)); - self.shape(&ops, false, true); - } - - /// POLYBEZIERTO: cubic segments from the current point through groups of three control or end - /// points; the current point moves to the last point. Inside an open bracket the path already - /// ends at the current point, so no MoveTo is needed. - pub fn bezier_to(&mut self, pts: &[P]) { - let mut ops = if self.in_path && !self.path.is_empty() { Vec::new() } else { vec![Op::M(self.pos)] }; - ops.extend(cubics(pts)); - if let Some(last) = pts.last() { - self.pos = *last; - } - self.shape(&ops, false, true); - } - - pub fn poly(&mut self, pts: &[P], close: bool, fill: bool, stroke: bool) { - let Some(first) = pts.first() else { return }; - let mut ops = vec![Op::M(*first)]; - ops.extend(pts.iter().skip(1).map(|p| Op::L(*p))); - if close { - ops.push(Op::Z); - } - self.shape(&ops, fill, stroke); - } - - pub fn begin_path(&mut self) { - self.in_path = true; - self.path.clear(); - } - - pub fn end_path(&mut self) { - self.in_path = false; - } - - pub fn close_figure(&mut self) { - if self.in_path { - self.push_path_ops([Op::Z]); - } - } - - /// FILLPATH, STROKEPATH and STROKEANDFILLPATH: paints the bracketed path and empties it. - pub fn paint_path(&mut self, fill: bool, stroke: bool) { - let ops = std::mem::take(&mut self.path); - if self.mix_skips(fill) || ops.len() < 2 { - return; - } - let fill = if fill { self.fill_paint() } else { None }; - let stroke = if stroke { self.stroke_pen() } else { None }; - self.push_shape(ops, fill, stroke); - } - - /// Fills a logical rectangle `[l, t, r, b]` with the current brush (PATBLT, PATCOPY blits). - pub fn fill_brush_rect(&mut self, [l, t, r, b]: [f64; 4]) { - if self.mix_skips(true) { - return; - } - let fill = self.fill_paint(); - let dev: Vec = crate::geom::rect(l, t, r, b).iter().map(|o| self.map_op(*o)).collect(); - self.push_shape(dev, fill, None); - } - - /// Fills a logical rectangle `[l, t, r, b]` with a solid colour (ROP fills such as BLACKNESS). - pub fn fill_rect_with(&mut self, [l, t, r, b]: [f64; 4], color: [u8; 4]) { - let dev: Vec = crate::geom::rect(l, t, r, b).iter().map(|o| self.map_op(*o)).collect(); - self.push(Raw::Path { ops: dev, fill: Some(color), stroke: None, even_odd: true, region: None }); - } - - /// Whether a raster operation draws without a source bitmap (PATCOPY, BLACKNESS, WHITENESS). - pub fn sourceless(rop: u32) -> bool { - matches!(rop, PATCOPY | BLACKNESS | WHITENESS) - } - - /// Fills a logical rectangle `[x, y, w, h]` for a source-less raster operation. PATCOPY uses the - /// brush (when solid); the others fill black or white. Other operations are not handled here. - pub fn fill_rop(&mut self, [x, y, w, h]: [f64; 4], rop: u32) { - let rect = [x, y, x + w, y + h]; - match rop { - PATCOPY => self.fill_brush_rect(rect), - BLACKNESS => self.fill_rect_with(rect, [0, 0, 0, 255]), - WHITENESS => self.fill_rect_with(rect, [255, 255, 255, 255]), - _ => {} - } - } - - /// Draws `text` (one `dx` advance per character, or none) with the current font, colours and - /// alignment. The reference point is `at`, or the current position under TA_UPDATECP (which - /// then moves past the string). `rect` (logical `[l, t, r, b]`) is filled with the background - /// colour under ETO_OPAQUE and clips the text under ETO_CLIPPED. Text ignores the mix mode, as - /// GDI's does. - pub fn text(&mut self, at: P, text: String, dx: Vec, options: u32, rect: Option<[f64; 4]>) { - if let (true, Some(r)) = (options & ETO_OPAQUE != 0, rect) { - self.fill_rect_with(r, self.text.bk_color); - } - let lf = self.text.font.as_deref().cloned().unwrap_or_default(); - let update = self.text.align & TA_UPDATECP != 0; - let origin = if update { self.pos } else { at }; - let (h_align, v_align) = align(self.text.align); - let o = self.map.device(origin); - let ux = self.map.device((origin.0 + 1.0, origin.1)); - let uy = self.map.device((origin.0, origin.1 + 1.0)); - let (ux, uy) = ((ux.0 - o.0, ux.1 - o.1), (uy.0 - o.0, uy.1 - o.1)); - if update { - // The current position moves along the baseline past the string (back past it when - // right-aligned). Without a dx array the advance is estimated from the height. - let adv: f64 = if dx.is_empty() { - f64::from(lf.font.height.abs().max(1.0)) * 0.5 * text.chars().count() as f64 - } else { - dx.iter().map(|d| f64::from(*d)).sum() - }; - let adv = match h_align { - crate::HAlign::Left => adv, - crate::HAlign::Right => -adv, - crate::HAlign::Center => 0.0, - }; - let theta = (lf.escapement / 10.0).to_radians(); - let y_sign = if uy.1 < 0.0 { 1.0 } else { -1.0 }; - let p = (origin.0 + adv * theta.cos(), origin.1 + adv * theta.sin() * y_sign); - if p.0.is_finite() && p.1.is_finite() { - self.pos = p; - } - } - let (sx, sy) = (ux.0.hypot(ux.1), uy.0.hypot(uy.1)); - let drawable = !text.is_empty() && [sx, sy, o.0, o.1].iter().all(|v| v.is_finite()) && sx > 0.0 && sy > 0.0; - if !drawable { - return; - } - // The baseline runs along the device direction of the logical x axis (rotated by a world - // transform; a mirroring mapping does not mirror glyphs), turned counter-clockwise by the - // escapement. - let det = ux.0 * uy.1 - ux.1 * uy.0; - let phi = if det > 0.0 { ux.1.atan2(ux.0) } else { ux.1.atan2(ux.0.abs()) }; - let a = phi - (lf.escapement / 10.0).to_radians(); - let (c, s) = (a.cos(), a.sin()); - let clip = match (options & ETO_CLIPPED != 0, rect) { - (true, Some([l, t, r, b])) => { - let (p, q) = (self.map.device((l, t)), self.map.device((r, b))); - Some([p.0.min(q.0) as f32, p.1.min(q.1) as f32, p.0.max(q.0) as f32, p.1.max(q.1) as f32]) - } - _ => None, - }; - self.push(Raw::Text { - text: Box::new(Text { - transform: [sx * c, sx * s, -sy * s, sy * c, o.0, o.1], - text, - dx, - font: lf.font, - color: self.text.color, - background: self.text.bk_opaque.then_some(self.text.bk_color), - h_align, - v_align, - clip, - region: None, - }), - region: None, - }); - } - - /// A bitmap stretched into a logical rectangle `[x, y, w, h]`. A negative extent mirrors the - /// bitmap, as the mapping does. - pub fn bitmap(&mut self, rect: [f64; 4], mut img: Rgba) { - let [x, y, w, h] = rect; - let a = self.map.device((x, y)); - let b = self.map.device((x + w, y + h)); - crate::dib::flip(&mut img, a.0 > b.0, a.1 > b.1); - let dev = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()]; - self.push(Raw::Bitmap { rect: dev, img, region: None }); - } -} - -/// 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. -pub(crate) fn hatch_tile(style: u32, fg: [u8; 4], bg: Option<[u8; 4]>) -> Option> { - if style > 5 { - return None; - } - let mut px = Vec::with_capacity(8 * 8 * 4); - for y in 0..8u32 { - for x in 0..8u32 { - let (h, v, f, b) = (y == 7, x == 7, x == y, x + y == 7); - let on = match style { - 0 => h, - 1 => v, - 2 => f, - 3 => b, - 4 => h || v, - _ => f || b, - }; - px.extend(if on { fg } else { bg.unwrap_or([0, 0, 0, 0]) }); - } - } - Some(px) -} - -/// Cubic segments from groups of three points (control, control, end). A trailing partial group is -/// dropped. -fn cubics(pts: &[P]) -> Vec { - let mut ops = Vec::with_capacity(pts.len() / 3); - let mut it = pts.iter().copied(); - while let (Some(a), Some(b), Some(e)) = (it.next(), it.next(), it.next()) { - ops.push(Op::C(a, b, e)); - } - ops -} diff --git a/crates/metafile/src/clip.rs b/crates/metafile/src/clip.rs deleted file mode 100644 index 2fc88ca44..000000000 --- a/crates/metafile/src/clip.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Clipping regions for EMF and WMF (MS-EMF 2.3.12, MS-WMF 2.3.5). -//! -//! A region is a set of axis-aligned rectangles in device units: `None` is the default clip (the -//! whole surface), `Some([])` draws nothing, and `Some(rects)` is their union. That is how GDI -//! stores a region (`RGNDATA` is a list of rectangles). Counts are capped so a hostile file cannot -//! grow the set without bound; past the cap, extra rectangles are dropped, which can only hide -//! drawing. - -/// Rectangles kept from one region. Past this, further rectangles are dropped. -pub(crate) const MAX_CLIP_RECTS: usize = 64; - -pub(crate) const RGN_AND: u32 = 1; -pub(crate) const RGN_OR: u32 = 2; -pub(crate) const RGN_XOR: u32 = 3; -pub(crate) const RGN_DIFF: u32 = 4; -pub(crate) const RGN_COPY: u32 = 5; - -/// A rectangle `[x0, y0, x1, y1]` with a positive area, or none. -pub(crate) fn normalize(rect: [f64; 4]) -> Option<[f64; 4]> { - let [l, t, r, b] = rect; - if ![l, t, r, b].iter().all(|v| v.is_finite()) { - return None; - } - let (x0, x1) = (l.min(r), l.max(r)); - let (y0, y1) = (t.min(b), t.max(b)); - (x1 - x0 > 1e-6 && y1 - y0 > 1e-6).then_some([x0, y0, x1, y1]) -} - -fn intersection(a: [f64; 4], b: [f64; 4]) -> Option<[f64; 4]> { - let (x0, y0) = (a[0].max(b[0]), a[1].max(b[1])); - let (x1, y1) = (a[2].min(b[2]), a[3].min(b[3])); - (x1 - x0 > 1e-6 && y1 - y0 > 1e-6 && [x0, y0, x1, y1].iter().all(|v| v.is_finite())).then_some([x0, y0, x1, y1]) -} - -/// `a` with `b` removed, as up to four rectangles. -fn subtract_one(a: [f64; 4], b: [f64; 4]) -> Vec<[f64; 4]> { - let Some(i) = intersection(a, b) else { return vec![a] }; - let mut out = Vec::new(); - if i[1] > a[1] - && let Some(r) = normalize([a[0], a[1], a[2], i[1]]) - { - out.push(r); - } - if i[3] < a[3] - && let Some(r) = normalize([a[0], i[3], a[2], a[3]]) - { - out.push(r); - } - if i[0] > a[0] - && let Some(r) = normalize([a[0], i[1], i[0], i[3]]) - { - out.push(r); - } - if i[2] < a[2] - && let Some(r) = normalize([i[2], i[1], a[2], i[3]]) - { - out.push(r); - } - out -} - -fn cap(mut rects: Vec<[f64; 4]>) -> Vec<[f64; 4]> { - rects.truncate(MAX_CLIP_RECTS); - rects -} - -fn subtract_all(subject: Vec<[f64; 4]>, cuts: &[[f64; 4]]) -> Vec<[f64; 4]> { - let mut cur = subject; - for cut in cuts { - let mut next = Vec::new(); - for s in cur { - for piece in subtract_one(s, *cut) { - if next.len() >= MAX_CLIP_RECTS { - return next; - } - next.push(piece); - } - } - cur = next; - } - cur -} - -/// Intersects the current region with one rectangle. -pub(crate) fn intersect(current: Option>, rect: [f64; 4]) -> Option> { - let Some(rect) = normalize(rect) else { return Some(Vec::new()) }; - Some(match current { - None => vec![rect], - Some(rects) => cap(rects.iter().filter_map(|r| intersection(*r, rect)).collect()), - }) -} - -/// Removes one rectangle. `surface` is the device bounds, used when the clip is still the default -/// (the whole surface) so the remainder can be named. -pub(crate) fn exclude(current: Option>, rect: [f64; 4], surface: Option<[f64; 4]>) -> Option> { - let Some(rect) = normalize(rect) else { return current }; - match current { - Some(rects) => Some(subtract_all(rects, &[rect])), - None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &[rect])), - } -} - -/// Combines `incoming` with the current region in a region mode (`RGN_*`). An unknown mode leaves -/// the region as it was. `RGN_COPY` replaces it. An `RGN_OR` or `RGN_XOR` against the default clip -/// stays the default clip: the whole surface already covers the incoming rectangles. -pub(crate) fn combine(current: Option>, incoming: Vec<[f64; 4]>, mode: u32, surface: Option<[f64; 4]>) -> Option> { - let incoming: Vec<[f64; 4]> = cap(incoming.iter().copied().filter_map(normalize).collect()); - match mode { - RGN_COPY => Some(incoming), - RGN_AND => match current { - None => Some(incoming), - Some(rects) => { - let mut out = Vec::new(); - for a in &rects { - for b in &incoming { - if out.len() >= MAX_CLIP_RECTS { - return Some(out); - } - if let Some(r) = intersection(*a, *b) { - out.push(r); - } - } - } - Some(out) - } - }, - RGN_OR => match current { - None => None, - Some(mut rects) => { - rects.extend(incoming); - Some(cap(rects)) - } - }, - RGN_DIFF => match current { - Some(rects) => Some(subtract_all(rects, &incoming)), - None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &incoming)), - }, - RGN_XOR => { - let base = current?; - let mut out = subtract_all(base.clone(), &incoming); - let rest = subtract_all(incoming, &base); - for r in rest { - if out.len() >= MAX_CLIP_RECTS { - break; - } - out.push(r); - } - Some(out) - } - _ => current, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn intersect_shrinks_to_the_overlap() { - let r = intersect(None, [0.0, 0.0, 100.0, 50.0]); - let r = intersect(r, [40.0, 10.0, 80.0, 40.0]); - assert_eq!(r, Some(vec![[40.0, 10.0, 80.0, 40.0]])); - assert_eq!(intersect(r, [0.0, 0.0, 10.0, 10.0]), Some(vec![])); - assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 20.0]]), [40.0, 0.0, 80.0, 20.0]), Some(vec![])); - } - - #[test] - fn modes_copy_and_or_diff_and_xor() { - let a = Some(vec![[0.0, 0.0, 100.0, 40.0]]); - assert_eq!(combine(a.clone(), vec![[10.0, 10.0, 30.0, 20.0]], RGN_COPY, None), Some(vec![[10.0, 10.0, 30.0, 20.0]])); - let and = combine(a.clone(), vec![[50.0, 0.0, 150.0, 40.0]], RGN_AND, None); - assert_eq!(and, Some(vec![[50.0, 0.0, 100.0, 40.0]])); - let or = combine(a.clone(), vec![[200.0, 0.0, 210.0, 10.0]], RGN_OR, None).unwrap(); - assert_eq!(or.len(), 2); - let diff = combine(a.clone(), vec![[20.0, 0.0, 40.0, 40.0]], RGN_DIFF, None).unwrap(); - assert_eq!(diff, vec![[0.0, 0.0, 20.0, 40.0], [40.0, 0.0, 100.0, 40.0]]); - let xor = combine(a, vec![[80.0, 0.0, 120.0, 40.0]], RGN_XOR, None).unwrap(); - assert!(xor.contains(&[0.0, 0.0, 80.0, 40.0])); - assert!(xor.contains(&[100.0, 0.0, 120.0, 40.0])); - } - - #[test] - fn excluding_from_the_default_clip_uses_the_surface() { - let r = exclude(None, [10.0, 0.0, 20.0, 40.0], Some([0.0, 0.0, 40.0, 40.0])).unwrap(); - assert_eq!(r, vec![[0.0, 0.0, 10.0, 40.0], [20.0, 0.0, 40.0, 40.0]]); - assert_eq!(exclude(None, [10.0, 0.0, 20.0, 40.0], None), None); - } - - #[test] - fn a_zero_area_rectangle_clears_an_intersection() { - assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 10.0]]), [5.0, 5.0, 5.0, 8.0]), Some(vec![])); - } -} diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs deleted file mode 100644 index ef6bb7c88..000000000 --- a/crates/metafile/src/cliprect.rs +++ /dev/null @@ -1,357 +0,0 @@ -//! Clipping drawn output to a convex device polygon (EMF+ clip rectangles, and clip paths and -//! regions that are convex; others are reduced to their bounding rectangle). Shapes wholly inside -//! pass unchanged; shapes crossing the edge are flattened and cut: fills with Sutherland–Hodgman -//! per subpath, outlines with Cyrus–Beck per segment. Bitmaps are cropped (and masked outside a -//! non-rectangular polygon) and text gets the polygon's bounding rectangle. - -use crate::canvas::Raw; -use crate::dib::Rgba; -use crate::geom::{Op, P}; - -/// Line segments per Bézier when a crossing shape is flattened. -const CURVE_STEPS: usize = 16; - -/// Point-against-edge tests one clipped item may cost (all its subpaths together); an item over -/// it is dropped, as a single polygon over it already was. -const MAX_CLIP_WORK: usize = 1_000_000; - -/// The same allowance for intersecting two clip regions, a budget of its own: two convex -/// 1,000-gons cost about 1.5 million tests once the cut polygon has grown. -pub(crate) const MAX_AREA_CLIP_WORK: usize = 4 * MAX_CLIP_WORK; - -/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included). -pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> { - let mut b: Option<[f64; 4]> = None; - let mut add = |p: P| { - let v = b.get_or_insert([p.0, p.1, p.0, p.1]); - *v = [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)]; - }; - for op in ops { - match *op { - Op::M(p) | Op::L(p) => add(p), - Op::C(a, c, d) => { - add(a); - add(c); - add(d); - } - Op::Z => {} - } - } - b -} - -pub(crate) fn intersect(a: [f64; 4], b: [f64; 4]) -> [f64; 4] { - let r = [a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]; - // An empty intersection stays empty (zero size) so it clips everything away. - if r[2] < r[0] || r[3] < r[1] { [r[0], r[1], r[0], r[1]] } else { r } -} - -/// A rectangle `[l, t, r, b]` as a polygon. -pub(crate) fn rect_poly([l, t, r, b]: [f64; 4]) -> Vec

{ - vec![(l, t), (r, t), (r, b), (l, b)] -} - -/// The bounding box of a point list. -pub(crate) fn pts_bbox(pts: &[P]) -> Option<[f64; 4]> { - let (&f, rest) = pts.split_first()?; - Some(rest.iter().fold([f.0, f.1, f.0, f.1], |v, p| [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)])) -} - -fn cross(a: P, b: P, p: P) -> f64 { - (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0) -} - -/// A convex clip polygon with positive orientation, or None when `pts` is not convex (or -/// degenerate); duplicate and collinear points are dropped. -pub(crate) fn convex(pts: &[P]) -> Option> { - let mut v: Vec

= Vec::with_capacity(pts.len()); - for &p in pts { - if !(p.0.is_finite() && p.1.is_finite()) { - return None; - } - if v.last().is_none_or(|q: &P| (q.0 - p.0).abs() > 1e-9 || (q.1 - p.1).abs() > 1e-9) { - v.push(p); - } - } - while v.len() > 1 && v.first() == v.last() { - v.pop(); - } - let area: f64 = (0..v.len()).map(|i| cross((0.0, 0.0), v[i], v[(i + 1) % v.len()])).sum(); - if v.len() < 3 || area.abs() < 1e-12 { - return None; - } - if area < 0.0 { - v.reverse(); - } - let n = v.len(); - let mut out = Vec::with_capacity(n); - for i in 0..n { - let c = cross(v[(i + n - 1) % n], v[i], v[(i + 1) % n]); - if c < -1e-9 * (1.0 + area.abs()) { - return None; - } - if c.abs() > 1e-12 { - out.push(v[i]); - } - } - (out.len() >= 3).then_some(out) -} - -fn inside_poly(clip: &[P], p: P) -> bool { - (0..clip.len()).all(|i| cross(clip[i], clip[(i + 1) % clip.len()], p) >= -1e-9) -} - -/// Whether the clip polygon is an axis-aligned rectangle. -fn is_rect(clip: &[P]) -> bool { - pts_bbox(clip).is_some_and(|b| clip.len() == 4 && clip.iter().all(|p| (p.0 == b[0] || p.0 == b[2]) && (p.1 == b[1] || p.1 == b[3]))) -} - -/// Sutherland–Hodgman: `poly` cut by convex `clip`, charging each point visited against each clip -/// edge to `budget`: the polygon -/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None` -/// once the budget is spent. -pub(crate) fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option> { - let mut poly = poly.to_vec(); - for i in 0..clip.len() { - // The work actually done: this edge visits the polygon as it now stands. - if poly.len() > *budget { - return None; - } - *budget -= poly.len(); - let (a, b) = (clip[i], clip[(i + 1) % clip.len()]); - let keep = |p: P| cross(a, b, p) >= 0.0; - let cut = |p: P, q: P| { - let (cp, cq) = (cross(a, b, p), cross(a, b, q)); - let t = cp / (cp - cq); - (p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t) - }; - let mut next = Vec::with_capacity(poly.len() + 4); - for j in 0..poly.len() { - let (p, q) = (poly[(j + poly.len() - 1) % poly.len()], poly[j]); - match (keep(p), keep(q)) { - (true, true) => next.push(q), - (true, false) => next.push(cut(p, q)), - (false, true) => { - next.push(cut(p, q)); - next.push(q); - } - (false, false) => {} - } - } - poly = next; - if poly.is_empty() { - break; - } - } - Some(poly) -} - -fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool { - b[2] < c[0] || b[0] > c[2] || b[3] < c[1] || b[1] > c[3] || c[2] <= c[0] || c[3] <= c[1] -} - -fn bbox_inside(b: [f64; 4], clip: &[P]) -> bool { - rect_poly(b).into_iter().all(|p| inside_poly(clip, p)) -} - -/// Clips one drawn item to a convex device polygon; a shape that is both filled and stroked comes -/// back as two items. An empty polygon clips everything away. -pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { - let Some(cb) = pts_bbox(clip).filter(|_| clip.len() >= 3) else { return Vec::new() }; - match raw { - Raw::Path { ops, fill, stroke, even_odd, region } => { - let Some(b) = ops_bbox(&ops) else { return Vec::new() }; - if bbox_inside(b, clip) { - return vec![Raw::Path { ops, fill, stroke, even_odd, region }]; - } - if disjoint(b, cb) { - return Vec::new(); - } - let subs = flatten(&ops); - // One allowance for the fill and the stroke together; the item goes once it is spent. - let mut budget = MAX_CLIP_WORK; - let mut out = Vec::new(); - if fill.is_some() { - let Some(f) = clip_fill(&subs, clip, &mut budget) else { return Vec::new() }; - if !f.is_empty() { - out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() }); - } - } - if stroke.is_some() { - let Some(s) = clip_stroke(&subs, clip, &mut budget) else { return Vec::new() }; - if !s.is_empty() { - out.push(Raw::Path { ops: s, fill: None, stroke, even_odd, region }); - } - } - out - } - Raw::Pattern { ops, even_odd, tile, region } => { - let Some(b) = ops_bbox(&ops) else { return Vec::new() }; - if bbox_inside(b, clip) { - return vec![Raw::Pattern { ops, even_odd, tile, region }]; - } - if disjoint(b, cb) { - return Vec::new(); - } - let subs = flatten(&ops); - let f = clip_fill(&subs, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default(); - if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] } - } - Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(), - Raw::Text { text: mut t, region } => { - // Text clips to a rectangle: the one around the polygon. - let cur = t.clip.map_or(cb, |[a, b, d, e]| intersect(cb, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)])); - t.clip = Some(cur.map(|v| v as f32)); - vec![Raw::Text { text: t, region }] - } - } -} - -/// Subpaths as point lists with their closed flag. -pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec

, bool)> { - let mut subs: Vec<(Vec

, bool)> = Vec::new(); - for op in ops { - match *op { - Op::M(p) => subs.push((vec![p], false)), - Op::L(p) => match subs.last_mut() { - Some(s) => s.0.push(p), - None => subs.push((vec![p], false)), - }, - Op::C(a, b, d) => { - let Some(s) = subs.last_mut() else { continue }; - let Some(&p0) = s.0.last() else { continue }; - for i in 1..=CURVE_STEPS { - let t = i as f64 / CURVE_STEPS as f64; - let u = 1.0 - t; - let k = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t]; - s.0.push((k[0] * p0.0 + k[1] * a.0 + k[2] * b.0 + k[3] * d.0, k[0] * p0.1 + k[1] * a.1 + k[2] * b.1 + k[3] * d.1)); - } - } - Op::Z => { - if let Some(s) = subs.last_mut() { - s.1 = true; - // Drawing after a close starts from the subpath's first point. - let first = s.0.first().copied(); - if let Some(f) = first { - subs.push((vec![f], false)); - } - } - } - } - } - subs.retain(|s| s.0.len() > 1); - subs -} - -/// The clipped fill of every subpath, its clipping work charged to `budget` with that of the -/// stroke: `None` (the whole item goes) once it is spent. -fn clip_fill(subs: &[(Vec

, bool)], clip: &[P], budget: &mut usize) -> Option> { - let mut ops = Vec::new(); - for (pts, _) in subs { - let poly = clip_polygon_within(pts, clip, budget)?; - if poly.len() >= 3 { - ops.push(Op::M(poly[0])); - ops.extend(poly[1..].iter().map(|p| Op::L(*p))); - ops.push(Op::Z); - } - } - Some(ops) -} - -/// The clipped outline of every subpath; `None` once `budget` is spent. -fn clip_stroke(subs: &[(Vec

, bool)], clip: &[P], budget: &mut usize) -> Option> { - let mut ops = Vec::new(); - for (pts, closed) in subs { - let mut pts = pts.clone(); - if *closed && let Some(&f) = pts.first() { - pts.push(f); - } - let mut last: Option

= None; - for w in pts.windows(2) { - let Some((a, b)) = segment(w[0], w[1], clip, budget)? else { - last = None; - continue; - }; - if last != Some(a) { - ops.push(Op::M(a)); - } - ops.push(Op::L(b)); - last = Some(b); - } - } - Some(ops) -} - -/// Cyrus–Beck: the part of segment `a`–`b` inside convex `clip`, one unit of `budget` per clip edge -/// visited; the outer `None` is a spent budget. -fn segment(a: P, b: P, clip: &[P], budget: &mut usize) -> Option> { - let d = (b.0 - a.0, b.1 - a.1); - let (mut t0, mut t1) = (0.0f64, 1.0f64); - for i in 0..clip.len() { - *budget = budget.checked_sub(1)?; - let (e0, e1) = (clip[i], clip[(i + 1) % clip.len()]); - // Inside is cross(e0, e1, p) >= 0, linear in t along the segment. - let f0 = cross(e0, e1, a); - let df = (e1.0 - e0.0) * d.1 - (e1.1 - e0.1) * d.0; - if df == 0.0 { - if f0 < 0.0 { - return Some(None); - } - } else { - let t = -f0 / df; - if df > 0.0 { - t0 = t0.max(t); - } else { - t1 = t1.min(t); - } - } - } - Some((t0 <= t1).then_some(((a.0 + d.0 * t0, a.1 + d.1 * t0), (a.0 + d.0 * t1, a.1 + d.1 * t1)))) -} - -/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to the clip -/// polygon's box; pixels outside a non-rectangular polygon turn transparent. -fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> { - let [x, y, w, h] = rect; - let b = [x, y, x + w, y + h]; - if bbox_inside(b, clip) { - return Some((rect, img)); - } - let c = pts_bbox(clip)?; - if disjoint(b, c) || w <= 0.0 || h <= 0.0 || img.w == 0 || img.h == 0 { - return None; - } - let i = intersect(b, c); - let (kx, ky) = (img.w as f64 / w, img.h as f64 / h); - let x0 = (((i[0] - x) * kx).floor().max(0.0) as usize).min(img.w); - let x1 = (((i[2] - x) * kx).ceil().max(0.0) as usize).min(img.w); - let y0 = (((i[1] - y) * ky).floor().max(0.0) as usize).min(img.h); - let y1 = (((i[3] - y) * ky).ceil().max(0.0) as usize).min(img.h); - if x1 <= x0 || y1 <= y0 { - return None; - } - let mask = !is_rect(clip); - let total_pixels = (x1 - x0).saturating_mul(y1 - y0); - if mask && total_pixels.saturating_mul(clip.len()) > 5_000_000 { - return None; - } - let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4); - for row in y0..y1 { - // Rows are top-down unless the image says otherwise. - let r = if img.bottom_up { img.h - 1 - row } else { row }; - let s = (r * img.w + x0) * 4; - let start = px.len(); - px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?); - if mask { - let cy = y + (row as f64 + 0.5) / ky; - for (k, p) in px[start..].as_chunks_mut::<4>().0.iter_mut().enumerate() { - let cx = x + ((x0 + k) as f64 + 0.5) / kx; - if !inside_poly(clip, (cx, cy)) { - p[3] = 0; - } - } - } - } - let out = [x + x0 as f64 / kx, y + y0 as f64 / ky, (x1 - x0) as f64 / kx, (y1 - y0) as f64 / ky]; - Some((out, Rgba { w: x1 - x0, h: y1 - y0, bottom_up: false, px })) -} diff --git a/crates/metafile/src/dib.rs b/crates/metafile/src/dib.rs deleted file mode 100644 index 55cf091a0..000000000 --- a/crates/metafile/src/dib.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Device-independent bitmaps. BITMAPINFOHEADER DIBs at 1/4/8/16/24/32 bpp with BI_RGB or BI_BITFIELDS -//! decode to RGBA8 rows, top-down. Compressed (RLE, JPEG, PNG) and V4/V5-only features are not decoded. - -use crate::{MAX_PIXELS, bytes::Bytes}; - -const BI_RGB: u32 = 0; -const BI_BITFIELDS: u32 = 3; -/// Most palette entries a DIB can carry (the colour table never exceeds 2^8). -const MAX_PALETTE: usize = 256; -const ROP_SRCAND: u32 = 0x0088_00C6; -const ROP_SRCPAINT: u32 = 0x00EE_0086; -const ROP_SRCINVERT: u32 = 0x0066_0046; - -/// A decoded picture. `bottom_up` records the DIB's native row order, which source rectangles use. -pub(crate) struct Rgba { - pub w: usize, - pub h: usize, - pub bottom_up: bool, - pub px: Vec, -} - -/// How a decoded bitmap combines with the destination. -#[derive(Clone, Copy)] -pub(crate) enum Blend { - /// A raster operation (BITBLT, STRETCHDIB and friends). - Rop(u32), - /// EMR_ALPHABLEND: a constant alpha (0..=255), times the per-pixel alpha when `per_pixel` is set - /// (AC_SRC_ALPHA: the source colours are premultiplied by it). - Alpha { constant: u8, per_pixel: bool }, -} - -impl Blend { - /// Whether the decoder must keep the 32-bit alpha byte of each pixel. - pub fn per_pixel(self) -> bool { - matches!(self, Blend::Alpha { per_pixel: true, .. }) - } -} - -struct Header { - w: usize, - h: usize, - top_down: bool, - bpp: usize, - masks: [u32; 3], - pal_off: usize, - pal_n: usize, -} - -impl Header { - fn read(info: Bytes) -> Option

{ - let size = info.u32(0)? as usize; - let w = info.i32(4)?; - let h = info.i32(8)?; - let bpp = info.u16(14)? as usize; - let comp = info.u32(16)?; - let clr_used = info.u32(32)? as usize; - if size < 40 || w <= 0 || h == 0 || !matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32) || !matches!(comp, BI_RGB | BI_BITFIELDS) { - return None; - } - let masks = if comp == BI_BITFIELDS { - [info.u32(40)?, info.u32(44)?, info.u32(48)?] - } else if bpp == 16 { - [0x7C00, 0x03E0, 0x001F] - } else { - [0xFF_0000, 0xFF00, 0xFF] - }; - let masks_len = if comp == BI_BITFIELDS && size == 40 { 12 } else { 0 }; - let pal_off = size.checked_add(masks_len)?; - // Indexed formats take 2^bpp entries when clrUsed is 0. Deeper formats carry only the - // entries clrUsed names (WMF DIBs can have them), still capped. - let pal_n = match (bpp <= 8, clr_used) { - (true, 0) => 1usize << bpp, - (true, n) => n.min(1usize << bpp), - (false, n) => n.min(MAX_PALETTE), - }; - Some(Header { w: w as usize, h: h.unsigned_abs() as usize, top_down: h < 0, bpp, masks, pal_off, pal_n }) - } -} - -/// Byte length of the info part (header, masks, palette) of a DIB, so the pixel bits follow it. -pub(crate) fn info_len(dib: Bytes) -> Option { - let h = Header::read(dib)?; - h.pal_off.checked_add(h.pal_n.checked_mul(4)?) -} - -/// Decodes a DIB given as its info part (header plus masks and palette) and its pixel bits. The -/// 32-bit alpha byte is kept only when `keep_alpha` is set (per-pixel alpha blends); otherwise pixels -/// are opaque. -pub(crate) fn decode(info: Bytes, bits: Bytes, keep_alpha: bool) -> Option { - let hd = Header::read(info)?; - let pixels = hd.w.checked_mul(hd.h)?; - if pixels == 0 || pixels > MAX_PIXELS { - return None; - } - let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4; - let data = bits.slice(0, stride.checked_mul(hd.h)?)?; - let pal = info.slice(hd.pal_off, hd.pal_n * 4)?; - let mut px = vec![0u8; pixels * 4]; - for y in 0..hd.h { - let src_row = if hd.top_down { y } else { hd.h - 1 - y }; - let row = src_row * stride; - for x in 0..hd.w { - let p = pixel(data, row, x, &hd, pal, keep_alpha)?; - let o = (y * hd.w + x) * 4; - px.get_mut(o..o + 4)?.copy_from_slice(&p); - } - } - Some(Rgba { w: hd.w, h: hd.h, bottom_up: !hd.top_down, px }) -} - -fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> { - let [m_r, m_g, m_b] = hd.masks; - let [r, g, b] = match hd.bpp { - 1 => { - let b = *data.get(row + x / 8)?; - palette(pal, usize::from((b >> (7 - x % 8)) & 1)) - } - 4 => { - let b = *data.get(row + x / 2)?; - palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F })) - } - 8 => palette(pal, usize::from(*data.get(row + x)?)), - 24 => { - let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?; - [r, g, b] - } - 16 => { - let o = row + x * 2; - let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?)); - [field(v, m_r), field(v, m_g), field(v, m_b)] - } - _ => { - let o = row + x * 4; - let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?); - let a = if keep_alpha { (v >> 24) as u8 } else { 255 }; - return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]); - } - }; - Some([r, g, b, 255]) -} - -/// Palette entries are BGRX quads; an index past the table reads as black rather than failing the image. -fn palette(pal: &[u8], i: usize) -> [u8; 3] { - pal.get(i * 4..i * 4 + 4).and_then(|c| <[u8; 4]>::try_from(c).ok()).map_or([0, 0, 0], |c| [c[2], c[1], c[0]]) -} - -/// Extracts one channel from a masked pixel value and scales it to 8 bits. -fn field(v: u32, mask: u32) -> u8 { - if mask == 0 { - return 0; - } - let shift = mask.trailing_zeros(); - let max = u64::from(mask >> shift); - let val = u64::from((v & mask) >> shift); - ((val * 255 + max / 2) / max) as u8 -} - -/// Clips a source rectangle `src` = `[x, y, w, h]` to the bitmap and returns the cropped pixels (top-down) -/// with the part of the destination `dst` that they fill. `src` is in DIB pixel coordinates (`y` counted -/// from the DIB's origin, so bottom-up images count from the bottom); a non-positive source size means the -/// whole bitmap. Clipping shrinks the destination by the same ratio and shifts it by the clipped-off part, -/// so the rest of the image keeps its place. A negative destination extent is kept (the caller mirrors). -pub(crate) fn clip_blit(img: &Rgba, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> { - let (iw, ih) = (img.w as i64, img.h as i64); - let [sx, sy, sw, sh] = if src[2] > 0 && src[3] > 0 { src } else { [0, 0, iw, ih] }; - // The request's top row in top-down order. - let top = if img.bottom_up { ih.saturating_sub(sy).saturating_sub(sh) } else { sy }; - let (x0, x1) = (sx.max(0), sx.saturating_add(sw).min(iw)); - let (r0, r1) = (top.max(0), top.saturating_add(sh).min(ih)); - if x1 <= x0 || r1 <= r0 { - return None; - } - let (kx, ky) = (dst[2] / sw as f64, dst[3] / sh as f64); - let out = [dst[0] + (x0 - sx) as f64 * kx, dst[1] + (r0 - top) as f64 * ky, (x1 - x0) as f64 * kx, (r1 - r0) as f64 * ky]; - let (x0, x1, r0, r1) = (x0 as usize, x1 as usize, r0 as usize, r1 as usize); - let (ow, oh) = (x1 - x0, r1 - r0); - let mut px = Vec::with_capacity(ow * oh * 4); - for row in r0..r1 { - let s = (row * img.w + x0) * 4; - px.extend_from_slice(img.px.get(s..s + ow * 4)?); - } - Some((Rgba { w: ow, h: oh, bottom_up: false, px }, out)) -} - -/// Applies a blend to decoded pixels before they are drawn. The picture has no destination pixels, so -/// raster operations are drawn as they look over white paper: SRCAND (destination AND source) leaves -/// white source pixels out and draws the rest; SRCPAINT and SRCINVERT leave black ones out. Other -/// operations draw the source as copied. Constant alpha scales every pixel's alpha, after a -/// per-pixel (premultiplied) source is made straight. -pub(crate) fn prepare(img: &mut Rgba, blend: Blend) { - match blend { - Blend::Rop(ROP_SRCAND) => clear_where(img, [255, 255, 255]), - Blend::Rop(ROP_SRCPAINT | ROP_SRCINVERT) => clear_where(img, [0, 0, 0]), - Blend::Rop(_) => {} - Blend::Alpha { constant, per_pixel } => { - for p in img.px.as_chunks_mut::<4>().0 { - if per_pixel { - unpremultiply(p); - } - p[3] = (u16::from(p[3]) * u16::from(constant) / 255) as u8; - } - } - } -} - -/// Makes the pixels of colour `rgb` transparent: where a raster operation keeps the destination. -fn clear_where(img: &mut Rgba, rgb: [u8; 3]) { - for p in img.px.as_chunks_mut::<4>().0 { - if p[..3] == rgb { - p[3] = 0; - } - } -} - -/// AC_SRC_ALPHA sources are premultiplied; the model holds straight RGBA, so divide the colour by the -/// alpha (rounded, capped at 255). A transparent pixel keeps its colour bytes. -fn unpremultiply(p: &mut [u8; 4]) { - let a = u32::from(p[3]); - if a == 0 || a == 255 { - return; - } - for c in &mut p[..3] { - *c = ((u32::from(*c) * 255 + a / 2) / a).min(255) as u8; - } -} - -/// Mirrors a decoded image left-right and/or top-bottom. Rows come from the buffer as chunks, so a -/// short buffer flips only the rows it has. -pub(crate) fn flip(img: &mut Rgba, horizontal: bool, vertical: bool) { - let (w, h) = (img.w, img.h); - if w == 0 || h == 0 { - return; - } - let row = w * 4; - if horizontal { - for line in img.px.chunks_exact_mut(row) { - for x in 0..w / 2 { - for c in 0..4 { - line.swap(x * 4 + c, (w - 1 - x) * 4 + c); - } - } - } - } - if vertical { - // Rows the buffer actually holds: a short buffer flips only those. - let n = h.min(img.px.len() / row); - for y in 0..n / 2 { - swap_rows(&mut img.px, y * row, (n - 1 - y) * row, row); - } - } -} - -/// Swaps the `len` bytes at `a` with those at `b` (`a < b`); does nothing when either is out of range. -fn swap_rows(px: &mut [u8], a: usize, b: usize, len: usize) { - let Some((head, tail)) = px.split_at_mut_checked(b) else { return }; - if let (Some(top), Some(bottom)) = (head.get_mut(a..a.saturating_add(len)), tail.get_mut(..len)) { - top.swap_with_slice(bottom); - } -} diff --git a/crates/metafile/src/emf.rs b/crates/metafile/src/emf.rs deleted file mode 100644 index 94c6d605d..000000000 --- a/crates/metafile/src/emf.rs +++ /dev/null @@ -1,478 +0,0 @@ -//! EMF reader (MS-EMF). Records are (type, size in bytes) followed by a payload; offsets inside a -//! record (bitmap info and bits) are relative to the record start, so each handler reads the record slice. - -use std::sync::Arc; - -use crate::bytes::Bytes; -use crate::canvas::{BS_HATCHED, BS_NULL, Brush, Canvas, LogFont, Obj, PS_NULL, PS_STYLE_MASK, Pen, Xform, colorref}; -use crate::dib::{self, Blend}; -use crate::geom::{self, ArcKind, P}; -use crate::text::{ETO_GLYPH_INDEX, ETO_PDY, Font, MAX_CHARS, decode_bytes, decode_utf16}; -use crate::{Error, Frame, MAX_PATH_OPS}; - -/// ExtTextOut options: no rectangle in the record (EMR_SMALLTEXTOUT, EmrText), and 8-bit -/// characters (EMR_SMALLTEXTOUT). -const ETO_NO_RECT: u32 = 0x0100; -const ETO_SMALL_CHARS: u32 = 0x0200; - -const EMF_SIGNATURE: u32 = 0x464D_4520; -/// Hundredths of a millimetre to points. -const HMM_TO_PT: f64 = 72.0 / 2540.0; -/// SRCCOPY, the plain copy of a bitmap. -const SRCCOPY: u32 = 0x00CC_0020; -/// LogPenEx pen type bits; PS_COSMETIC pens are one device pixel wide. -const PS_TYPE_MASK: u32 = 0x000F_0000; -const PS_COSMETIC: u32 = 0; - -pub(crate) fn is_emf(b: Bytes) -> bool { - b.u32(0) == Some(1) && b.u32(40) == Some(EMF_SIGNATURE) -} - -/// The picture frame: `rclFrame` (0.01 mm) mapped to device units through `szlDevice` and -/// `szlMillimeters`, sized in points. Without a usable frame, the device bounds stand in (1 unit = 1 point). -pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { - if !is_emf(input) { - return Err(Error::BadHeader); - } - let hsize = input.u32(4).ok_or(Error::BadHeader)? as usize; - let bounds = rect32(input, 8).ok_or(Error::BadHeader)?; - let [l, t, r, b] = bounds; - let frame = frame_from_header(input).or_else(|| (r > l && b > t).then_some(Frame { rect: [l, t, r - l, b - t], size: None })); - let mut cv = Canvas::new(); - if r > l && b > t { - cv.set_clip_surface([l, t, r, b]); - } - if let Some(px_mm) = device_px_per_mm(input) { - cv.set_device_mm(px_mm); - } - let mut plus = crate::emfplus::Plus::new(); - plus.frame = frame.as_ref().map(|f| f.rect); - let mut parsed = 0usize; - let mut pos = hsize.max(88); - while !cv.full() { - let (Some(typ), Some(size)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u32(o))) else { break }; - let size = size as usize; - if size < 8 || !size.is_multiple_of(4) { - break; - } - let Some(rec) = input.slice(pos, size) else { break }; - if typ == 14 { - break; - } - if typ == 70 && Bytes(rec).u32(12) == Some(crate::emfplus::EMF_PLUS) { - if let Some(data) = Bytes(rec).tail(16) { - plus.comment(&mut cv, data); - } - } else { - record(&mut cv, typ, Bytes(rec)); - } - parsed += 1; - let Some(next) = pos.checked_add(size) else { break }; - pos = next; - } - if parsed == 0 { - return Err(Error::NoRecords); - } - Ok((cv, frame)) -} - -/// Device pixels per millimetre on each axis, from `szlDevice` (72) and `szlMillimeters` (80). -fn device_px_per_mm(input: Bytes) -> Option<(f64, f64)> { - let (dw, dh) = (input.i32(72)?, input.i32(76)?); - let (mw, mh) = (input.i32(80)?, input.i32(84)?); - (dw > 0 && dh > 0 && mw > 0 && mh > 0).then(|| (f64::from(dw) / f64::from(mw), f64::from(dh) / f64::from(mh))) -} - -/// The frame from `rclFrame` (offset 24), `szlDevice` (72) and `szlMillimeters` (80) in the header. -fn frame_from_header(input: Bytes) -> Option { - let [fl, ft, fr, fb] = rect32(input, 24)?; - if fr <= fl || fb <= ft { - return None; - } - // Device units per hundredth of a millimetre. - let (px_mm_x, px_mm_y) = device_px_per_mm(input)?; - let (sx, sy) = (px_mm_x / 100.0, px_mm_y / 100.0); - let rect = [fl * sx, ft * sy, (fr - fl) * sx, (fb - ft) * sy]; - Some(Frame { rect, size: Some(((fr - fl) * HMM_TO_PT, (fb - ft) * HMM_TO_PT)) }) -} - -fn record(cv: &mut Canvas, typ: u32, r: Bytes) { - if cv.full() { - return; - } - let _ = match typ { - 9 => xy32(r, 8).map(|p| cv.set_window_ext(p)), - 10 => xy32(r, 8).map(|p| cv.set_window_org(p)), - 11 => xy32(r, 8).map(|p| cv.set_viewport_ext(p)), - 12 => xy32(r, 8).map(|p| cv.set_viewport_org(p)), - 17 => r.u32(8).map(|m| cv.set_map_mode(m)), - 19 => r.u32(8).map(|m| cv.set_polyfill(m)), - 20 => r.u32(8).map(|m| cv.set_rop2(m)), - 33 => { - cv.save_dc(); - Some(()) - } - 34 => r.i32(8).map(|n| cv.restore_dc(n)), - 35 => xform(r, 8).map(|x| cv.modify_world(x, 4)), - 36 => xform(r, 8).zip(r.u32(32)).map(|(x, m)| cv.modify_world(x, m)), - 37 => r.u32(8).map(|i| cv.select(i)), - 38 => create_pen(cv, r), - 95 => create_ext_pen(cv, r), - 39 => create_brush(cv, r), - 93 | 94 => create_dib_brush(cv, r, typ == 93), - 82 => create_font(cv, r), - 18 => r.u32(8).map(|m| cv.set_bk_mode(m)), - 22 => r.u32(8).map(|a| cv.set_text_align(a)), - 24 => r.u32(8).map(|c| cv.set_text_color(c)), - 25 => r.u32(8).map(|c| cv.set_bk_color(c)), - 83 | 84 => ext_text_out(cv, r, typ == 84), - 108 => small_text_out(cv, r), - 40 => r.u32(8).map(|i| cv.delete_object(i)), - 29 => rect32(r, 8).map(|rect| cv.exclude_clip_rect(rect)), - 30 => rect32(r, 8).map(|rect| cv.intersect_clip_rect(rect)), - 75 => ext_select_clip(cv, r), - 27 => xy32(r, 8).map(|p| cv.move_to(p)), - 54 => xy32(r, 8).map(|p| cv.line_to(p)), - 42 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)), - 43 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)), - 44 => { - let (cw, ch) = (r.i32(24), r.i32(28)); - let (Some(cw), Some(ch)) = (cw, ch) else { return }; - rect32(r, 8).map(|[l, t, rr, b]| { - cv.shape(&geom::round_rect(l, t, rr, b, f64::from(cw), f64::from(ch)), true, true); - }) - } - 45..=47 => arc32(r).map(|(bounds, s, e)| { - let kind = match typ { - 45 => ArcKind::Arc, - 46 => ArcKind::Chord, - _ => ArcKind::Pie, - }; - let [l, t, rr, b] = bounds; - cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true); - }), - 4 | 87 => counted(r, typ == 4).map(|pts| cv.poly(&pts, false, false, true)), - 3 | 86 => counted(r, typ == 3).map(|pts| cv.poly(&pts, true, true, true)), - 6 | 89 => counted(r, typ == 6).map(|pts| cv.line_chain(&pts, false, true)), - // POLYBEZIER starts its own figure at the first point and leaves the current point alone; - // POLYBEZIERTO continues from the current point. - 2 | 85 => counted(r, typ == 2).and_then(|pts| { - let from = *pts.first()?; - cv.bezier(from, pts.get(1..)?); - Some(()) - }), - 5 | 88 => counted(r, typ == 5).map(|pts| cv.bezier_to(&pts)), - 7 | 90 => poly_groups(r, typ == 7).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, false), false, true)), - 8 | 91 => poly_groups(r, typ == 8).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, true), true, true)), - 59 => { - cv.begin_path(); - Some(()) - } - 60 => { - cv.end_path(); - Some(()) - } - 61 => { - cv.close_figure(); - Some(()) - } - 62 => { - cv.paint_path(true, false); - Some(()) - } - 63 => { - cv.paint_path(true, true); - Some(()) - } - 64 => { - cv.paint_path(false, true); - Some(()) - } - 76 | 77 => bitblt(cv, r, typ == 77), - 80 => set_dibits(cv, r), - 81 => stretch_dibits(cv, r), - 114 => alphablend(cv, r), - _ => Some(()), - }; -} - -/// EMR_EXTSELECTCLIPRGN: `cbRgnData` bytes of `RGNDATA` (rectangles in logical units) and a region -/// mode. No region data selects the default clip. -fn ext_select_clip(cv: &mut Canvas, r: Bytes) -> Option<()> { - let cb = r.u32(8)? as usize; - let mode = r.u32(12)?; - if cb == 0 { - cv.reset_clip(); - return Some(()); - } - let header = 16; - if cb < 32 { - return Some(()); - } - // RDH_RECTANGLES is the only region type GDI writes. - if r.u32(header + 4)? != 1 { - return Some(()); - } - let n = (r.u32(header + 8)? as usize).min(crate::clip::MAX_CLIP_RECTS); - let end = header.checked_add(cb)?; - let mut rects = Vec::new(); - for i in 0..n { - let o = header + 32 + i * 16; - if o + 16 > end { - break; - } - rects.push(rect32(r, o)?); - } - cv.select_clip_rects(&rects, mode); - Some(()) -} - -/// Two little-endian i32 values at `off`, as a point. -fn xy32(r: Bytes, off: usize) -> Option

{ - Some((f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?))) -} - -/// RECTL (left, top, right, bottom) as i32 values widened to f64. -fn rect32(r: Bytes, off: usize) -> Option<[f64; 4]> { - Some([f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?), f64::from(r.i32(off + 8)?), f64::from(r.i32(off + 12)?)]) -} - -/// An i32 field as f64; 0 past the end. -fn i32s(r: Bytes, off: usize) -> f64 { - r.i32(off).map_or(0.0, f64::from) -} - -/// XFORM: six f32 values in M11, M12, M21, M22, Dx, Dy order. -fn xform(r: Bytes, off: usize) -> Option { - let mut x = [0.0f64; 6]; - for (i, v) in x.iter_mut().enumerate() { - *v = f64::from(r.f32(off + i * 4)?); - } - Some(x) -} - -/// ARC, CHORD, PIE: bounds at 8, start point at 24, end point at 32. -fn arc32(r: Bytes) -> Option<([f64; 4], P, P)> { - Some((rect32(r, 8)?, xy32(r, 24)?, xy32(r, 32)?)) -} - -/// A point count from a record, rejected when it exceeds the path budget (before any allocation). -fn budget(n: u32) -> Option { - let n = n as usize; - (n <= MAX_PATH_OPS).then_some(n) -} - -/// Point count at 24, then the points (32-bit or 16-bit) at 28. -fn counted(r: Bytes, wide: bool) -> Option> { - let n = budget(r.u32(24)?)?; - r.tail(28)?.points(0, n, wide) -} - -/// Subpath count at 24, total point count at 28, per-subpath counts at 32, then the points. -fn poly_groups(r: Bytes, wide: bool) -> Option<(Vec, Vec

)> { - let n = budget(r.u32(24)?)?; - // The record must hold the n counts before anything is allocated for them. - let raw = r.slice(32, n.checked_mul(4)?)?; - let counts: Vec = raw.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c) as usize).collect(); - let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?; - let total = budget(u32::try_from(total).ok()?)?; - let pts = r.tail(n.checked_mul(4)?.checked_add(32)?)?.points(0, total, wide)?; - Some((counts, pts)) -} - -/// EMR_CREATEPEN: handle, LogPen (style, width x/y, colour). -fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> { - let slot = r.u32(8)? as usize; - let style = r.u32(12)?; - let width = r.i32(16)?; - let color = r.u32(24)?; - cv.add_object(Some(slot), Obj::Pen(Pen { style, width: f64::from(width), color: colorref(color) })); - Some(()) -} - -/// EMR_EXTCREATEPEN: handle, bitmap offsets (unused), LogPenEx (style, width, brush style, colour). -/// A cosmetic pen's width is in device pixels (always 1), so it is a hairline; a BS_NULL brush draws -/// no line at all. -fn create_ext_pen(cv: &mut Canvas, r: Bytes) -> Option<()> { - let slot = r.u32(8)? as usize; - let (style, width, brush, color) = (r.u32(28)?, r.u32(32)?, r.u32(36)?, r.u32(40)?); - let width = if style & PS_TYPE_MASK == PS_COSMETIC { 0.0 } else { f64::from(width) }; - let style = if brush == BS_NULL { PS_NULL } else { style & PS_STYLE_MASK }; - cv.add_object(Some(slot), Obj::Pen(Pen { style, width, color: colorref(color) })); - Some(()) -} - -/// EMR_CREATEBRUSHINDIRECT: handle, LogBrush32 (style, colour, hatch). -fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> { - let slot = r.u32(8)? as usize; - let style = r.u32(12)?; - let color = r.u32(16)?; - let mut b = Brush::new(style, colorref(color)); - if style == BS_HATCHED { - b.hatch = r.u32(20).unwrap_or(0); - } - cv.add_object(Some(slot), Obj::Brush(b)); - Some(()) -} - -/// EMR_CREATEDIBPATTERNBRUSHPT (94) and EMR_CREATEMONOBRUSH (93): handle, colour usage, then the -/// bitmap info and bits as offset/size pairs. A mono brush (or one whose palette holds indices, -/// DIB_PAL_COLORS) takes the text and background colours. An undecodable bitmap leaves the slot -/// empty. -fn create_dib_brush(cv: &mut Canvas, r: Bytes, mono: bool) -> Option<()> { - let slot = r.u32(8)? as usize; - let usage = r.u32(12)?; - let (bo, bl) = (r.u32(16)? as usize, r.u32(20)? as usize); - let (po, pl) = (r.u32(24)? as usize, r.u32(28)? as usize); - let img = dib::decode(Bytes(r.slice(bo, bl)?), Bytes(r.slice(po, pl)?), false)?; - let b = Brush::pattern(img, mono || usage == 1)?; - cv.add_object(Some(slot), Obj::Brush(b)); - Some(()) -} - -/// EMR_EXTCREATEFONTINDIRECTW: handle, then a LogFont (MS-EMF 2.2.13): height, width, escapement, -/// orientation, weight (i32 each), italic, underline, strike-out, charset and four more bytes, then -/// the face name in 32 UTF-16 units (NUL-terminated). -fn create_font(cv: &mut Canvas, r: Bytes) -> Option<()> { - let slot = r.u32(8)? as usize; - let height = r.i32(12)?; - let width = r.i32(16).unwrap_or(0); - let escapement = r.i32(20).unwrap_or(0); - let weight = r.i32(28).unwrap_or(0); - let byte = |o: usize| r.0.get(o).copied().unwrap_or(0); - let units: Vec = (0..32).map_while(|i| r.u16(40 + 2 * i)).take_while(|u| *u != 0).collect(); - let (family, _) = decode_utf16(&units, &[]); - let font = Font { - family: family.trim().to_string(), - height: height as f32, - width: width as f32, - weight: weight.clamp(0, 1000) as u16, - italic: byte(32) != 0, - underline: byte(33) != 0, - strike: byte(34) != 0, - charset: byte(35), - }; - cv.add_object(Some(slot), Obj::Font(Arc::new(LogFont { font, escapement: f64::from(escapement) }))); - Some(()) -} - -/// The dx array at `off` for `n` characters: i32 advances, or (dx, dy) pairs under ETO_PDY (only -/// dx is used). Empty when absent or short. -fn dx_array(r: Bytes, off: usize, n: usize, opts: u32) -> Vec { - let step = if opts & ETO_PDY != 0 { 8 } else { 4 }; - if off == 0 { - return Vec::new(); - } - let dx: Vec = (0..n).map_while(|i| r.i32(off.checked_add(i.checked_mul(step)?)?)).map(|v| v as f32).collect(); - if dx.len() == n { dx } else { Vec::new() } -} - -/// EMR_EXTTEXTOUTA (83) and EMR_EXTTEXTOUTW (84): bounds, graphics mode and scales, then an EmrText -/// at 36: reference point, character count, string offset, options, the rectangle (absent under -/// ETO_NO_RECT) and the dx offset. Offsets count from the record start. Glyph-index strings name -/// glyphs of a font the reader cannot match, so they are left out. -fn ext_text_out(cv: &mut Canvas, r: Bytes, wide: bool) -> Option<()> { - let at = xy32(r, 36)?; - let n = (r.u32(44)? as usize).min(MAX_CHARS); - let off = r.u32(48)? as usize; - let opts = r.u32(52)?; - let (rect, dx_off) = if opts & ETO_NO_RECT != 0 { (None, r.u32(56)?) } else { (rect32(r, 56), r.u32(72)?) }; - if opts & ETO_GLYPH_INDEX != 0 { - return None; - } - let dx = dx_array(r, dx_off as usize, n, opts); - let (text, dx) = if wide { - let raw = r.slice(off, n.checked_mul(2)?)?; - let units: Vec = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect(); - decode_utf16(&units, &dx) - } else { - decode_bytes(r.slice(off, n)?, cv.font_charset(), &dx) - }; - cv.text(at, text, dx, opts, rect); - Some(()) -} - -/// EMR_SMALLTEXTOUT (MS-EMF 2.3.5.37): x, y, character count, options, graphics mode, scales, the -/// bounds (absent under ETO_NO_RECT), then the characters: 8-bit under ETO_SMALL_CHARS, else UTF-16. -fn small_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> { - let at = xy32(r, 8)?; - let n = (r.u32(16)? as usize).min(MAX_CHARS); - let opts = r.u32(20)?; - if opts & ETO_GLYPH_INDEX != 0 { - return None; - } - let (rect, off) = if opts & ETO_NO_RECT != 0 { (None, 36) } else { (rect32(r, 36), 52) }; - let (text, _) = if opts & ETO_SMALL_CHARS != 0 { - decode_bytes(r.slice(off, n)?, cv.font_charset(), &[]) - } else { - let raw = r.slice(off, n.checked_mul(2)?)?; - let units: Vec = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect(); - decode_utf16(&units, &[]) - }; - cv.text(at, text, Vec::new(), opts, rect); - Some(()) -} - -/// BITBLT (76) and STRETCHBLT (77): dest at 24..40, rop at 40, source x/y at 44, bmi at 84, bits at -/// 92; STRETCHBLT adds the source w/h at 100. A source-less rop fills the destination instead. -fn bitblt(cv: &mut Canvas, r: Bytes, stretch: bool) -> Option<()> { - let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)]; - let rop = r.u32(40)?; - if Canvas::sourceless(rop) { - cv.fill_rop(dst, rop); - return Some(()); - } - let (sw, sh) = if stretch { (i32s(r, 100), i32s(r, 104)) } else { (dst[2], dst[3]) }; - let src = [i32s(r, 44), i32s(r, 48), sw, sh]; - blit(cv, r, dst, src, (84, 92), Blend::Rop(rop)) -} - -/// ALPHABLEND: laid out like STRETCHBLT, with a BLENDFUNCTION at 40. Constant alpha at 42 applies to -/// every pixel; AlphaFormat at 43 with AC_SRC_ALPHA (1) also uses the per-pixel alpha. -fn alphablend(cv: &mut Canvas, r: Bytes) -> Option<()> { - let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)]; - let constant = *r.0.get(42)?; - let per_pixel = (*r.0.get(43)? & 0x01) != 0; - let src = [i32s(r, 44), i32s(r, 48), i32s(r, 100), i32s(r, 104)]; - blit(cv, r, dst, src, (84, 92), Blend::Alpha { constant, per_pixel }) -} - -/// STRETCHDIBITS: dest x/y at 24, src x/y/w/h at 32..48, bmi offset/size at 48, bits at 56, rop at 68, -/// dest w/h at 72. -fn stretch_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> { - let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 72), i32s(r, 76)]; - let src = [i32s(r, 32), i32s(r, 36), i32s(r, 40), i32s(r, 44)]; - let rop = r.u32(68)?; - blit(cv, r, dst, src, (48, 56), Blend::Rop(rop)) -} - -/// SETDIBITSTODEVICE (MS-EMF 2.3.1.5): dest x/y at 24, src x/y at 32, size (cxSrc, cySrc) at 40, bmi -/// at 48, bits at 56, start scan at 68. The image keeps its own size; the bits begin at scan line -/// `iStartScan`, so the source y counts from there. -fn set_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> { - let (cx, cy) = (i32s(r, 40), i32s(r, 44)); - if cx <= 0.0 || cy <= 0.0 { - return None; - } - let dst = [i32s(r, 24), i32s(r, 28), cx, cy]; - let src = [i32s(r, 32), i32s(r, 36) - f64::from(r.u32(68)?), cx, cy]; - blit(cv, r, dst, src, (48, 56), Blend::Rop(SRCCOPY)) -} - -/// Decodes the bitmap at `at` = (offset of the bmi offset/length pair, offset of the bits pair), both -/// inside the record, and stretches the source rectangle `src` into `dst`. Both are `[x, y, w, h]`; a -/// non-positive source size means the whole bitmap. `blend` decides how it combines. -fn blit(cv: &mut Canvas, r: Bytes, dst: [f64; 4], src: [f64; 4], at: (usize, usize), blend: Blend) -> Option<()> { - let (bo, bl) = (r.u32(at.0)? as usize, r.u32(at.0 + 4)? as usize); - let (po, pl) = (r.u32(at.1)? as usize, r.u32(at.1 + 4)? as usize); - if pl == 0 { - return None; - } - let info = Bytes(r.slice(bo, bl)?); - let pixels = Bytes(r.slice(po, pl)?); - let mut img = dib::decode(info, pixels, blend.per_pixel())?; - dib::prepare(&mut img, blend); - let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?; - cv.bitmap(dst, img); - Some(()) -} diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs deleted file mode 100644 index d2d1e2bc0..000000000 --- a/crates/metafile/src/emfplus.rs +++ /dev/null @@ -1,1641 +0,0 @@ -//! EMF+ (MS-EMFPLUS): GDI+ drawing records carried in EMF comment records. -//! -//! Most EMF+ files are *dual*: the same picture is also drawn with plain EMF records, which -//! [`crate::emf`] already plays, so this reader ignores their EMF+ part. An *EMF+ only* file has -//! nothing else to draw; for those this reader plays the records: -//! - brushes (solid, hatch, texture, linear and path gradients), pens, paths, images, fonts, -//! string formats and regions, including objects split across several records; -//! - rectangles, polygons, lines, ellipses, paths and Béziers; -//! - bitmaps (raw pixel formats, and PNG/JPEG/GIF/TIFF/BMP payloads) and strings (DrawString, -//! DrawDriverString with character codes); -//! - world and page transforms, save/restore and containers; -//! - arcs and pies; -//! - clipping to rectangles and convex clip paths and regions (others: their bounding rectangle); -//! - nested metafile images, and glyph-index driver strings in the Windows core fonts; -//! - cardinal splines (DrawCurve, DrawClosedCurve, FillClosedCurve), FillRegion, and Clear -//! (over the picture frame). -//! -//! Every drawing record of MS-EMFPLUS §2.3.4 is played. Not drawn: image attributes, glyph -//! indices of other fonts, and region combinations other than intersect and union (XOR, exclude -//! and complement keep the left area). Deliberately ignored: rendering hints (antialias, text -//! hint, interpolation, pixel offset, compositing; the renderer has its own), SetRenderingOrigin -//! (hatch phase), SerializableObject image effects, the terminal-server records SetTSGraphics and -//! SetTSClip, and StrokeFillPath (0x4037), which the specification lists but gives no layout for -//! and GDI+ doesn't write. - -use std::sync::Arc; - -use crate::bytes::Bytes; -use crate::canvas::Raw; -use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, LogFont, PS_NULL, Pen, RawTile, Xform, hatch_tile, mul}; -use crate::cliprect::{MAX_AREA_CLIP_WORK, clip_polygon_within, convex, flatten, ops_bbox, pts_bbox, rect_poly}; -use crate::dib::Rgba; -use crate::geom::{Op, P}; -use crate::text::{ETO_CLIPPED, Font}; -use crate::{Item, Picture, Seg}; - -/// "EMF+" as the comment identifier of an EMR_COMMENT holding EMF+ records. -pub(crate) const EMF_PLUS: u32 = 0x2B46_4D45; -const IDENTITY: Xform = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; -/// GDI+ allows 64 objects; ids are a byte. -const MAX_OBJECTS: usize = 64; -const MAX_STACK: usize = 64; -/// Points per path or point list read, as the plain EMF reader caps paths. -const MAX_POINTS: usize = 100_000; -/// Gradient stops read from a brush. -const MAX_STOPS: usize = 256; -/// Characters per string. -const MAX_CHARS: usize = 32_768; -/// Bytes gathered for one object split across records. -const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024; -/// Nesting depth of region nodes. -const MAX_REGION_DEPTH: usize = 32; -/// Region nodes and path points parsed together, including nodes in sibling branches. -const MAX_REGION_WORK: usize = MAX_POINTS; - -const TA_RIGHT: u32 = 2; -const TA_CENTER: u32 = 6; -const TA_BOTTOM: u32 = 8; -const TA_BASELINE: u32 = 24; - -/// What a brush paints. Coordinates are in brush space, which `xf` maps to world space. -#[derive(Clone)] -enum Fill { - Solid([u8; 4]), - Hatch { style: u32, fg: [u8; 4], bg: [u8; 4] }, - Linear { rect: [f64; 4], stops: Vec<(f64, [u8; 4])>, wrap: u32, xf: Xform }, - Radial { center: P, radius: P, stops: Vec<(f64, [u8; 4])>, xf: Xform }, - Texture { img: Arc, wrap: u32, xf: Xform }, -} - -impl Fill { - /// One colour standing for the brush (pens and text draw solid). - fn color(&self) -> [u8; 4] { - match self { - Fill::Solid(c) | Fill::Hatch { fg: c, .. } => *c, - Fill::Linear { stops, .. } | Fill::Radial { stops, .. } => ramp(stops, 0.5), - Fill::Texture { img, .. } => img.px.get(..4).and_then(|c| c.try_into().ok()).unwrap_or([0, 0, 0, 255]), - } - } - - /// The colour at a brush-space point. - fn sample(&self, p: P) -> [u8; 4] { - match self { - Fill::Solid(c) | Fill::Hatch { fg: c, .. } => *c, - Fill::Linear { rect, stops, wrap, .. } => { - let t = if rect[2] == 0.0 { 0.0 } else { (p.0 - rect[0]) / rect[2] }; - ramp(stops, wrap_t(t, *wrap)) - } - Fill::Radial { center, radius, stops, .. } => { - let (dx, dy) = ((p.0 - center.0) / radius.0.max(1e-9), (p.1 - center.1) / radius.1.max(1e-9)); - ramp(stops, dx.hypot(dy).min(1.0)) - } - Fill::Texture { img, wrap, .. } => { - let (w, h) = (img.w as f64, img.h as f64); - if w == 0.0 || h == 0.0 { - return [0, 0, 0, 0]; - } - if *wrap == 4 && !((0.0..w).contains(&p.0) && (0.0..h).contains(&p.1)) { - return [0, 0, 0, 0]; - } - let x = (p.0.rem_euclid(w) as usize).min(img.w - 1); - let y = (p.1.rem_euclid(h) as usize).min(img.h - 1); - let i = (y * img.w + x) * 4; - img.px.get(i..i + 4).and_then(|c| c.try_into().ok()).unwrap_or([0, 0, 0, 0]) - } - } - } - - fn xf(&self) -> Xform { - match self { - Fill::Linear { xf, .. } | Fill::Radial { xf, .. } | Fill::Texture { xf, .. } => *xf, - _ => IDENTITY, - } - } -} - -/// Where `t` lands under a wrap mode: tiled, mirrored (flip modes) or clamped. -fn wrap_t(t: f64, wrap: u32) -> f64 { - if !t.is_finite() { - return 0.0; - } - match wrap { - 1 | 3 => { - let m = t.rem_euclid(2.0); - if m > 1.0 { 2.0 - m } else { m } - } - 4 => t.clamp(0.0, 1.0), - _ => t.rem_euclid(1.0), - } -} - -/// The colour at `t` along sorted stops. -fn ramp(stops: &[(f64, [u8; 4])], t: f64) -> [u8; 4] { - let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return [0, 0, 0, 0] }; - if t <= first.0 { - return first.1; - } - for w in stops.windows(2) { - let ((a, ca), (b, cb)) = (w[0], w[1]); - if t <= b { - let k = if b > a { (t - a) / (b - a) } else { 0.0 }; - return lerp(ca, cb, k); - } - } - last.1 -} - -fn lerp(a: [u8; 4], b: [u8; 4], k: f64) -> [u8; 4] { - let mut c = [0u8; 4]; - for i in 0..4 { - c[i] = (f64::from(a[i]) + (f64::from(b[i]) - f64::from(a[i])) * k).round().clamp(0.0, 255.0) as u8; - } - c -} - -#[derive(Clone)] -struct FontObj { - em: f64, - unit: u32, - style: u32, - family: String, -} - -#[derive(Clone, Copy, Debug, Default)] -struct Format { - flags: u32, - align: u32, - line_align: u32, -} - -/// A clip area: a convex polygon (empty clips everything), or None for infinite. -type Area = Option>; - -enum ImageData { - Bitmap(Rgba), - /// A nested WMF or EMF (EMF+) picture. - Meta(Picture), -} - -thread_local! { - /// Nesting depth of metafile images being parsed, so a file of nested metafiles cannot run - /// the stack out. - static NESTING: std::cell::Cell = const { std::cell::Cell::new(0) }; -} -/// Metafile images inside metafile images, at most. -const MAX_NESTING: usize = 3; - -#[derive(Clone)] -enum Obj { - Brush(Fill), - Pen { - color: [u8; 4], - width: f64, - }, - Path(Vec), - Image(Arc), - Font(FontObj), - Format(Format), - /// A region's bounding rectangle `[l, t, r, b]` in world units; None is infinite. - Region(Area), -} - -/// An object arriving in several records: its id, type, total size and the bytes so far. -struct Partial { - id: usize, - kind: u16, - total: usize, - data: Vec, -} - -struct Saved { - id: u32, - world: Xform, - page: f64, - clip: Area, -} - -pub(crate) struct Plus { - /// An EMF+ header said the file is EMF+ only, so these records are the picture. - active: bool, - dpi: f64, - world: Xform, - page: f64, - /// Convex device clip polygon; None is unclipped. - clip: Area, - objects: Vec>, - object_bytes: usize, - stack: Vec, - partial: Option, - /// The picture frame in device units (`[x, y, w, h]`), what Clear paints. - pub frame: Option<[f64; 4]>, -} - -impl Plus { - pub fn new() -> Self { - Plus { - active: false, - dpi: 96.0, - world: IDENTITY, - page: 1.0, - clip: None, - objects: vec![None; MAX_OBJECTS], - object_bytes: 0, - stack: Vec::new(), - partial: None, - frame: None, - } - } - - /// Plays the EMF+ records of one EMR_COMMENT payload (after the identifier). - pub fn comment(&mut self, cv: &mut Canvas, data: Bytes) { - let mut pos = 0usize; - while !cv.full() { - let Some(tail) = data.0.get(pos..) else { break }; - let tail = Bytes(tail); - let (Some(typ), Some(flags), Some(size), Some(dsize)) = (tail.u16(0), tail.u16(2), tail.u32(4), tail.u32(8)) else { - break; - }; - let (size, dsize) = (size as usize, dsize as usize); - if size < 12 || dsize > size - 12 { - break; - } - let Some(rec) = tail.slice(12, dsize) else { break }; - let _ = self.record(cv, typ, flags, Bytes(rec)); - let Some(next) = pos.checked_add(size) else { break }; - pos = next; - } - } - - /// World units to device pixels before the canvas's own mapping. - fn xf(&self) -> Xform { - mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0]) - } - - fn painter(&self) -> Painter { - Painter { xf: self.xf(), clip: self.clip.clone() } - } - - fn record(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { - if typ == 0x4001 { - // Header: dual files also draw with EMF records, which win. - self.active = flags & 1 == 0; - if let Some(d) = r.u32(8).map(f64::from).filter(|d| *d > 0.0 && *d < 100_000.0) { - self.dpi = d; - } - return Some(()); - } - if !self.active { - return None; - } - let done = match typ { - 0x4008 => self.object(flags, r, cv.used().saturating_add(self.clip_bytes())), - 0x4009 | 0x4013 | 0x401A..=0x401C | 0x4036 => self.record_image_text_region(cv, typ, flags, r), - 0x4025..=0x4035 => self.record_state(cv, typ, flags, r), - 0x4010..=0x4012 | 0x4016..=0x4019 => self.record_curve(cv, typ, flags, r), - _ => self.record_shape(cv, typ, flags, r), - }; - // What this player retains (objects, a continued object's pieces, the current and saved - // clips) shares the canvas's byte budget; clip changes are charged as they happen. - cv.hold(self.object_bytes.saturating_add(self.partial.as_ref().map_or(0, |p| p.data.len())).saturating_add(self.clip_bytes())); - done - } - - /// Bytes of the current clip and of every saved one. - fn clip_bytes(&self) -> usize { - let area = |a: &Area| a.as_ref().map_or(0, |v| v.len().saturating_mul(std::mem::size_of::

())); - self.stack.iter().map(|s| area(&s.clip)).fold(area(&self.clip), usize::saturating_add) - } - - /// Rectangle, polygon, ellipse and path records. - fn record_shape(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { - let compressed = flags & 0x4000 != 0; - let id = usize::from(flags & 0xFF); - match typ { - 0x400A => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let p = self.painter(); - for rc in rects(r, 8, r.u32(4)? as usize, compressed)? { - if cv.full() { - break; - } - p.fill(cv, &rect_ops(rc), &fill); - } - Some(()) - } - 0x400B => { - let pen = self.pen(id)?; - let p = self.painter(); - for rc in rects(r, 4, r.u32(0)? as usize, compressed)? { - if cv.full() { - break; - } - p.stroke(cv, &rect_ops(rc), pen); - } - Some(()) - } - 0x400C => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let pts = points(r, 8, r.u32(4)? as usize, compressed)?; - self.painter().fill(cv, &poly_ops(&pts, true), &fill); - Some(()) - } - 0x400D => { - let pen = self.pen(id)?; - let pts = points(r, 4, r.u32(0)? as usize, compressed)?; - self.painter().stroke(cv, &poly_ops(&pts, flags & 0x2000 != 0), pen); - Some(()) - } - 0x400E => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let rect = *rects(r, 4, 1, compressed)?.first()?; - self.painter().fill(cv, &ellipse_ops(rect), &fill); - Some(()) - } - 0x400F => { - let pen = self.pen(id)?; - let rect = *rects(r, 0, 1, compressed)?.first()?; - self.painter().stroke(cv, &ellipse_ops(rect), pen); - Some(()) - } - 0x4014 => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let ops = self.path(id)?; - self.painter().fill(cv, &ops, &fill); - Some(()) - } - 0x4015 => { - let ops = self.path(id)?; - let pen = self.pen(r.u32(0)? as usize)?; - self.painter().stroke(cv, &ops, pen); - Some(()) - } - _ => None, - } - } - - /// Curve, bezier and arc records. - fn record_curve(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { - let compressed = flags & 0x4000 != 0; - let id = usize::from(flags & 0xFF); - match typ { - 0x4019 => { - let pen = self.pen(id)?; - let pts = points(r, 4, r.u32(0)? as usize, compressed)?; - let (first, rest) = pts.split_first()?; - let mut ops = vec![Op::M(*first)]; - ops.extend(rest.as_chunks::<3>().0.iter().map(|c| Op::C(c[0], c[1], c[2]))); - self.painter().stroke(cv, &ops, pen); - Some(()) - } - // Cardinal splines: FillClosedCurve, DrawClosedCurve, DrawCurve. - 0x4016 => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let tension = finite(r.f32(4)?)?; - let pts = points(r, 12, r.u32(8)? as usize, compressed)?; - self.painter().fill(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), &fill); - Some(()) - } - 0x4017 => { - let pen = self.pen(id)?; - let tension = finite(r.f32(0)?)?; - let pts = points(r, 8, r.u32(4)? as usize, compressed)?; - self.painter().stroke(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), pen); - Some(()) - } - 0x4018 => { - let pen = self.pen(id)?; - let tension = finite(r.f32(0)?)?; - let (offset, segments) = (r.u32(4)? as usize, r.u32(8)? as usize); - let pts = points(r, 16, r.u32(12)? as usize, compressed)?; - self.painter().stroke(cv, &curve_ops(&pts, tension, false, offset, segments), pen); - Some(()) - } - 0x4010 => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let [start, sweep] = floats::<2>(r, 4)?; - let rect = *rects(r, 12, 1, compressed)?.first()?; - self.painter().fill(cv, &arc_ops(rect, start, sweep, true), &fill); - Some(()) - } - 0x4011 | 0x4012 => { - let pen = self.pen(id)?; - let [start, sweep] = floats::<2>(r, 0)?; - let rect = *rects(r, 8, 1, compressed)?.first()?; - self.painter().stroke(cv, &arc_ops(rect, start, sweep, typ == 0x4011), pen); - Some(()) - } - _ => None, - } - } - - /// Clear, region fill, image and text records. - fn record_image_text_region(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { - let compressed = flags & 0x4000 != 0; - let id = usize::from(flags & 0xFF); - match typ { - // Clear: the whole picture in one colour, whatever the clip (a transparent clear - // leaves nothing to draw). - 0x4009 => { - let c = argb(r.u32(0)?); - let [x, y, w, h] = self.frame?; - if c[3] == 0 || !(w > 0.0 && h > 0.0) { - return Some(()); - } - let to_device = invert(mul(cv.world(), cv.device_affine()))?; - let p = Painter { xf: to_device, clip: None }; - p.fill(cv, &rect_ops([x, y, x + w, y + h]), &Fill::Solid(c)); - Some(()) - } - // FillRegion: the region's area (as the clip reads it). - 0x4013 => { - let fill = self.brush_of(flags, r.u32(0)?)?; - let Some(Some(Obj::Region(Some(area)))) = self.objects.get(id).cloned() else { return None }; - if area.len() < 3 { - return None; - } - self.painter().fill(cv, &poly_ops(&area, true), &fill); - Some(()) - } - 0x401A => { - let img = self.image(id)?; - let dst = *rects(r, 24, 1, compressed)?.first()?; - self.draw_image(cv, &img, r, [dst[0], dst[1], dst[2] - dst[0], dst[3] - dst[1]]) - } - 0x401B => { - let img = self.image(id)?; - let pts = points(r, 28, 3, compressed)?; - let (p0, p1, p2) = (*pts.first()?, *pts.get(1)?, *pts.get(2)?); - // The destination parallelogram's top-left, top-right and bottom-left corners; a - // sheared or rotated one is drawn into its bounding rectangle's axes. - let dst = [p0.0, p0.1, p1.0 - p0.0 + (p2.0 - p0.0), p2.1 - p0.1 + (p1.1 - p0.1)]; - self.draw_image(cv, &img, r, dst) - } - 0x401C => self.draw_string(cv, flags, r), - 0x4036 => self.draw_driver_string(cv, flags, r), - _ => None, - } - } - - /// Save / restore, transform and clip records. - fn record_state(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { - let id = usize::from(flags & 0xFF); - match typ { - 0x4025 | 0x4028 => r.u32(0).map(|i| self.push(i)), - 0x4027 => r.u32(24).map(|i| self.push(i)), - 0x4026 | 0x4029 => r.u32(0).map(|i| self.pop(i)), - 0x402A => matrix(r, 0).map(|m| self.world = m), - 0x402B => { - self.world = IDENTITY; - Some(()) - } - 0x402C => matrix(r, 0).and_then(|m| self.apply(m, flags)), - 0x402D => self.apply([1.0, 0.0, 0.0, 1.0, f64::from(r.f32(0)?), f64::from(r.f32(4)?)], flags), - 0x402E => self.apply([f64::from(r.f32(0)?), 0.0, 0.0, f64::from(r.f32(4)?), 0.0, 0.0], flags), - 0x402F => { - let a = f64::from(r.f32(0)?).to_radians(); - self.apply([a.cos(), a.sin(), -a.sin(), a.cos(), 0.0, 0.0], flags) - } - 0x4030 => { - let page = f64::from(r.f32(0)?) * unit_px(u32::from(flags & 0xFF), self.dpi); - if page.is_finite() && page > 0.0 && page < 1.0e6 { - self.page = page; - } - Some(()) - } - 0x4031 => { - self.clip = None; - Some(()) - } - 0x4032 => { - let [x, y, w, h] = floats::<4>(r, 0)?; - let area = map_area(Some(rect_poly([x, y, x + w, y + h])), mul(self.xf(), cv.device_affine())); - self.combine(flags, area) - } - 0x4033 => { - let area = map_area(Some(area_of_ops(&self.path(id)?)), mul(self.xf(), cv.device_affine())); - self.combine(flags, area) - } - 0x4034 => { - let Some(Some(Obj::Region(reg))) = self.objects.get(id).cloned() else { return None }; - let area = map_area(reg, mul(self.xf(), cv.device_affine())); - self.combine(flags, area) - } - 0x4035 => { - let [dx, dy] = floats::<2>(r, 0)?; - let a = mul(self.xf(), cv.device_affine()); - let (ox, oy) = (dx * a[0] + dy * a[2], dx * a[1] + dy * a[3]); - if let Some(c) = self.clip.as_mut() { - for p in c.iter_mut() { - *p = (p.0 + ox, p.1 + oy); - } - } - Some(()) - } - _ => None, - } - } - - /// Without the `A` flag a transform is applied before the world transform (prepended). - fn apply(&mut self, m: Xform, flags: u16) -> Option<()> { - self.world = if flags & 0x2000 != 0 { mul(self.world, m) } else { mul(m, self.world) }; - Some(()) - } - - fn push(&mut self, id: u32) { - if self.stack.len() < MAX_STACK { - self.stack.push(Saved { id, world: self.world, page: self.page, clip: self.clip.clone() }); - } - } - - fn pop(&mut self, id: u32) { - if let Some(i) = self.stack.iter().rposition(|s| s.id == id) { - let s = self.stack.swap_remove(i); - (self.world, self.page, self.clip) = (s.world, s.page, s.clip); - self.stack.truncate(i); - } - } - - /// Combines a new clip area with the current one under the record's combine mode: replace, - /// intersect and union (as the rectangle around both). XOR, exclude and complement leave the - /// clip as it was. - fn combine(&mut self, flags: u16, new: Area) -> Option<()> { - self.clip = match (flags >> 8) & 0xF { - 0 => new, - 1 => area_intersect(self.clip.take(), new), - 2 => area_union(self.clip.take(), new), - _ => self.clip.take(), - }; - Some(()) - } - - fn object(&mut self, flags: u16, r: Bytes, canvas_used: usize) -> Option<()> { - let id = usize::from(flags & 0xFF); - let kind = (flags >> 8) & 0x7F; - if flags & 0x8000 != 0 { - // A continued object: the total size, then this record's share of the bytes. - let total = (r.u32(0)? as usize).min(MAX_OBJECT_BYTES); - let chunk = r.tail(4)?.0; - let p = match self.partial.take() { - Some(p) if p.id == id && p.kind == kind && p.total == total => p, - _ => Partial { id, kind, total, data: Vec::new() }, - }; - let mut p = p; - let room = p.total.saturating_sub(p.data.len()); - p.data.extend_from_slice(&chunk[..chunk.len().min(room)]); - if p.data.len() >= p.total { - let obj = parse_object(p.kind, Bytes(&p.data)); - self.store(p.id, obj, canvas_used); - } else { - self.partial = Some(p); - } - return Some(()); - } - // The last piece of a continued object comes without the continue flag. - if let Some(mut p) = self.partial.take() - && p.id == id - && p.kind == kind - { - let room = p.total.saturating_sub(p.data.len()); - p.data.extend_from_slice(&r.0[..r.0.len().min(room)]); - let obj = parse_object(kind, Bytes(&p.data)); - self.store(id, obj, canvas_used); - return Some(()); - } - let obj = parse_object(kind, r); - self.store(id, obj, canvas_used); - Some(()) - } - - /// `canvas_used` is what the canvas already retains: objects and items share one budget. - fn store(&mut self, id: usize, obj: Option, canvas_used: usize) { - let Some(slot) = self.objects.get_mut(id) else { return }; - if let Some(old) = slot.take() { - self.object_bytes = self.object_bytes.saturating_sub(Self::obj_cost(&old)); - } - if let Some(new_obj) = obj { - let cost = Self::obj_cost(&new_obj); - if self.object_bytes.saturating_add(cost).saturating_add(canvas_used) <= crate::MAX_BYTES { - self.object_bytes += cost; - *slot = Some(new_obj); - } - } - } - - fn obj_cost(obj: &Obj) -> usize { - match obj { - Obj::Image(img) => match img.as_ref() { - ImageData::Bitmap(rgba) => rgba.px.len(), - ImageData::Meta(pic) => picture_cost(&pic.items), - }, - Obj::Brush(Fill::Texture { img, .. }) => img.px.len(), - Obj::Path(ops) => ops.len().saturating_mul(std::mem::size_of::()), - _ => 0, - } - } - - fn path(&self, id: usize) -> Option> { - match self.objects.get(id)? { - Some(Obj::Path(ops)) => Some(ops.clone()), - _ => None, - } - } - - fn pen(&self, id: usize) -> Option<([u8; 4], f64)> { - match self.objects.get(id)? { - Some(Obj::Pen { color, width }) => Some((*color, *width)), - _ => None, - } - } - - fn image(&self, id: usize) -> Option> { - match self.objects.get(id)? { - Some(Obj::Image(img)) => Some(img.clone()), - _ => None, - } - } - - /// The brush of a fill record: an ARGB value with the `S` flag, else a brush object. - fn brush_of(&self, flags: u16, v: u32) -> Option { - if flags & 0x8000 != 0 { - return Some(Fill::Solid(argb(v))); - } - match self.objects.get(v as usize)? { - Some(Obj::Brush(f)) => Some(f.clone()), - _ => None, - } - } - - /// DrawImage and DrawImagePoints: the source rectangle (pixels) cut from the image, drawn - /// into world rectangle `dst` = `[x, y, w, h]`. - fn draw_image(&self, cv: &mut Canvas, img: &ImageData, r: Bytes, dst: [f64; 4]) -> Option<()> { - let img = match img { - ImageData::Bitmap(b) => b, - ImageData::Meta(pic) => { - self.painter().picture(cv, dst, pic); - return Some(()); - } - }; - let [sx, sy, sw, sh] = floats::<4>(r, 8)?; - let src = if r.u32(4)? == 2 { [sx, sy, sw, sh].map(|v| v.round().clamp(-1.0e9, 1.0e9) as i64) } else { [0; 4] }; - let (part, dst) = crate::dib::clip_blit(img, src, dst)?; - self.painter().image(cv, dst, part); - Some(()) - } - - fn font(&self, id: usize) -> Option { - match self.objects.get(id)? { - Some(Obj::Font(f)) => Some(f.clone()), - _ => None, - } - } - - /// The font as a LOGFONT whose height is in world units. - fn log_font(&self, f: &FontObj) -> LogFont { - let em = if f.unit == 0 { f.em } else { f.em * unit_px(f.unit, self.dpi) / self.page }; - let font = Font { - family: f.family.clone(), - height: -(em.clamp(0.0, 1.0e6) as f32), - weight: if f.style & 1 != 0 { 700 } else { 400 }, - italic: f.style & 2 != 0, - underline: f.style & 4 != 0, - strike: f.style & 8 != 0, - ..Font::default() - }; - LogFont { font, escapement: 0.0 } - } - - /// DrawString: lines of text laid out in a rectangle by the string format's alignments. - fn draw_string(&self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { - let font = self.font(usize::from(flags & 0xFF))?; - let color = self.brush_of(flags, r.u32(0)?)?.color(); - let fmt = match self.objects.get(r.u32(4)? as usize) { - Some(Some(Obj::Format(f))) => *f, - _ => Format::default(), - }; - let len = (r.u32(8)? as usize).min(MAX_CHARS); - let [x, y, w, h] = floats::<4>(r, 12)?; - let units: Vec = (0..len).map(|i| r.u16(28 + i * 2)).collect::>()?; - let lf = self.log_font(&font); - let em = f64::from(-lf.font.height); - let wrap = w > 0.0 && fmt.flags & 0x1000 == 0; - let lines: Vec> = units - .split(|u| *u == 0x0A) - .flat_map(|l| { - let l: Vec = l.iter().copied().filter(|u| *u != 0x0D).collect(); - if wrap { wrap_line(&l, w, em) } else { vec![l] } - }) - .collect(); - let line_h = em * 1.2; - let (ax, ta_h) = match fmt.align { - 1 => (x + w / 2.0, TA_CENTER), - 2 => (x + w, TA_RIGHT), - _ => (x, 0), - }; - let block = line_h * lines.len() as f64; - let top = match fmt.line_align { - 1 => y + (h - block) / 2.0, - 2 => y + h - block, - _ => y, - }; - let clip = (w > 0.0 && h > 0.0 && fmt.flags & 0x4000 == 0).then_some([x, y, x + w, y + h]); - let p = self.painter(); - for (i, line) in lines.iter().enumerate() { - if line.is_empty() { - continue; - } - let (text, dx) = crate::text::decode_utf16(line, &[]); - let at = (ax, top + line_h * i as f64); - let (at, ta) = if fmt.line_align == 2 && lines.len() == 1 { ((ax, y + h), ta_h | TA_BOTTOM) } else { (at, ta_h) }; - p.text(cv, at, text, dx, TextStyle { color, font: lf.clone(), align: ta, clip }, IDENTITY); - } - Some(()) - } - - /// DrawDriverString with character codes (CmapLookup): positioned glyphs on a baseline. - fn draw_driver_string(&self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { - let font = self.font(usize::from(flags & 0xFF))?; - let color = self.brush_of(flags, r.u32(0)?)?.color(); - let opts = r.u32(4)?; - let n = (r.u32(12)? as usize).min(MAX_CHARS); - let mut units: Vec = (0..n).map(|i| r.u16(16 + i * 2)).collect::>()?; - if opts & 1 == 0 { - // Glyph indices into the font. The crate knows no fonts, but the classic Windows core - // fonts keep the standard Macintosh glyph order for ASCII, so those map back. - units = glyphs_to_chars(&font.family, &units)?; - } - let pos = points(r, 16 + n * 2, n, false)?; - let m = if r.u32(8)? != 0 { matrix(r, 16 + n * 10)? } else { IDENTITY }; - let lf = self.log_font(&font); - let first = *pos.first()?; - let dx: Vec = if opts & 4 != 0 { - Vec::new() - } else { - let mut d: Vec = pos.windows(2).map(|w| (w[1].0 - w[0].0) as f32).collect(); - d.push(d.last().copied().unwrap_or(-lf.font.height * 0.5)); - d - }; - let (text, dx) = crate::text::decode_utf16(&units, &dx); - self.painter().text(cv, first, text, dx, TextStyle { color, font: lf, align: TA_BASELINE, clip: None }, m); - Some(()) - } -} - -/// Page unit to device pixels: world, display and pixel are 1; point, inch, document, mm. -fn unit_px(unit: u32, dpi: f64) -> f64 { - match unit { - 3 => dpi / 72.0, - 4 => dpi, - 5 => dpi / 300.0, - 6 => dpi / 25.4, - _ => 1.0, - } -} - -struct TextStyle { - color: [u8; 4], - font: LogFont, - align: u32, - /// World clip rectangle `[l, t, r, b]`. - clip: Option<[f64; 4]>, -} - -/// Draws through the canvas with EMF+'s transform, clip and its own pen or brush, then puts the -/// canvas's GDI state back. -struct Painter { - xf: Xform, - clip: Area, -} - -impl Painter { - fn begin(&self, cv: &mut Canvas, pre: Xform) { - let world = cv.world(); - cv.save_dc(); - cv.modify_world(mul(mul(pre, self.xf), world), 4); - cv.set_clip(self.clip.clone()); - } - - fn end(&self, cv: &mut Canvas) { - cv.restore_dc(-1); - cv.set_clip(None); - } - - fn no_pen(cv: &mut Canvas) { - cv.set_pen(Pen { style: PS_NULL, width: 0.0, color: [0, 0, 0, 0] }); - } - - fn fill(&self, cv: &mut Canvas, ops: &[Op], fill: &Fill) { - self.begin(cv, IDENTITY); - Self::no_pen(cv); - match fill { - Fill::Solid(c) => { - cv.set_brush(Brush::new(BS_SOLID, *c)); - cv.shape(ops, true, false); - } - Fill::Hatch { style, fg, bg } => match 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. - None => { - cv.set_brush(Brush::new(BS_SOLID, lerp(*fg, *bg, 0.5))); - cv.shape(ops, true, false); - } - }, - _ => { - // Device point to brush space: invert brush space → world → device. - if let Some(inv) = invert(mul(fill.xf(), cv.device_affine())) { - cv.shade_shape(ops, &|p| fill.sample(apply(inv, p))); - } - } - } - self.end(cv); - } - - fn stroke(&self, cv: &mut Canvas, ops: &[Op], (color, width): ([u8; 4], f64)) { - self.begin(cv, IDENTITY); - cv.set_pen(Pen { style: 0, width, color }); - cv.set_brush(Brush::new(BS_NULL, [0, 0, 0, 0])); - cv.shape(ops, false, true); - self.end(cv); - } - - fn image(&self, cv: &mut Canvas, dst: [f64; 4], img: Rgba) { - self.begin(cv, IDENTITY); - cv.bitmap(dst, img); - self.end(cv); - } - - /// A nested metafile's picture stretched into world rectangle `dst` = `[x, y, w, h]`. - fn picture(&self, cv: &mut Canvas, [x, y, w, h]: [f64; 4], pic: &Picture) { - self.begin(cv, IDENTITY); - let (sx, sy) = (w / f64::from(pic.width), h / f64::from(pic.height)); - let m = mul([sx, 0.0, 0.0, sy, x, y], cv.device_affine()); - let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt(); - let pt = |a: f32, b: f32| apply(m, (f64::from(a), f64::from(b))); - let ops = |segs: &[Seg]| -> Vec { - segs.iter() - .map(|s| match *s { - Seg::Move(a, b) => Op::M(pt(a, b)), - Seg::Line(a, b) => Op::L(pt(a, b)), - Seg::Cubic(a, b, c, d, e, f) => Op::C(pt(a, b), pt(c, d), pt(e, f)), - Seg::Close => Op::Z, - }) - .collect() - }; - // The nested picture's own clip regions are not carried over: each item takes the - // canvas's current clip. - for item in &pic.items { - if cv.full() { - break; - } - let raw = match item { - Item::Path { segs, fill, stroke, even_odd, .. } => Raw::Path { - ops: ops(segs), - fill: *fill, - stroke: stroke.map(|(c, w)| (c, f64::from(w) * scale)), - even_odd: *even_odd, - region: None, - }, - Item::Pattern { segs, even_odd, tile, .. } => Raw::Pattern { - ops: ops(segs), - even_odd: *even_odd, - tile: Arc::new(RawTile { - w: tile.width as usize, - h: tile.height as usize, - px: tile.rgba.clone(), - cell: f64::from(tile.cell[0]) * scale, - }), - region: None, - }, - Item::Bitmap { rect, width, height, rgba, .. } => { - let (a, b) = (pt(rect[0], rect[1]), pt(rect[0] + rect[2], rect[1] + rect[3])); - let mut img = Rgba { w: *width as usize, h: *height as usize, bottom_up: false, px: rgba.clone() }; - crate::dib::flip(&mut img, a.0 > b.0, a.1 > b.1); - Raw::Bitmap { rect: [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()], img, region: None } - } - Item::Text(t) => { - let mut t = t.clone(); - t.transform = mul(t.transform, m); - if let Some([x0, y0, x1, y1]) = t.clip { - let (p, q) = (pt(x0, y0), pt(x1, y1)); - t.clip = Some([p.0.min(q.0) as f32, p.1.min(q.1) as f32, p.0.max(q.0) as f32, p.1.max(q.1) as f32]); - } - Raw::Text { text: t, region: None } - } - }; - cv.push_device(raw); - } - self.end(cv); - } - - fn text(&self, cv: &mut Canvas, at: P, text: String, dx: Vec, style: TextStyle, pre: Xform) { - self.begin(cv, pre); - cv.set_text_style(style.color, style.font, style.align); - let (opts, rect) = match style.clip { - Some(c) => (ETO_CLIPPED, Some(c)), - None => (0, None), - }; - cv.text(at, text, dx, opts, rect); - self.end(cv); - } -} - -fn apply(m: Xform, p: P) -> P { - (p.0 * m[0] + p.1 * m[2] + m[4], p.0 * m[1] + p.1 * m[3] + m[5]) -} - -fn invert(m: Xform) -> Option { - let det = m[0] * m[3] - m[1] * m[2]; - if !det.is_finite() || det.abs() < 1e-12 { - return None; - } - let (a, b, c, d) = (m[3] / det, -m[1] / det, -m[2] / det, m[0] / det); - Some([a, b, c, d, -(m[4] * a + m[5] * c), -(m[4] * b + m[5] * d)]) -} - -/// ARGB (0xAARRGGBB) to RGBA. -fn argb(v: u32) -> [u8; 4] { - [(v >> 16) as u8, (v >> 8) as u8, v as u8, (v >> 24) as u8] -} - -fn floats(r: Bytes, off: usize) -> Option<[f64; N]> { - let mut v = [0.0; N]; - for (i, x) in v.iter_mut().enumerate() { - *x = f64::from(r.f32(off + i * 4)?); - } - v.iter().all(|x| x.is_finite()).then_some(v) -} - -fn matrix(r: Bytes, off: usize) -> Option { - floats::<6>(r, off) -} - -fn parse_object(kind: u16, r: Bytes) -> Option { - match kind { - 1 => brush(r, 0).map(Obj::Brush), - 2 => pen(r), - 3 => path(r).map(Obj::Path), - 4 => region(r), - 5 => image(r).map(|i| Obj::Image(Arc::new(i))), - 6 => font(r), - 7 => Some(Obj::Format(Format { flags: r.u32(4)?, align: r.u32(12)?, line_align: r.u32(16)? })), - _ => None, - } -} - -/// A brush object at `off`: version, type, then the type's data. -fn brush(r: Bytes, off: usize) -> Option { - let d = r.tail(off + 8)?; - match r.u32(off + 4)? { - 0 => Some(Fill::Solid(argb(d.u32(0)?))), - 1 => Some(Fill::Hatch { style: d.u32(0)?, fg: argb(d.u32(4)?), bg: argb(d.u32(8)?) }), - 2 => { - let bflags = d.u32(0)?; - let wrap = d.u32(4)?; - let (xf, off) = if bflags & 0x2 != 0 { (matrix(d, 8)?, 32) } else { (IDENTITY, 8) }; - let ImageData::Bitmap(img) = image(d.tail(off)?)? else { return None }; - Some(Fill::Texture { img: Arc::new(img), wrap, xf }) - } - 3 => path_gradient(d), - 4 => linear_gradient(d), - _ => None, - } -} - -fn linear_gradient(d: Bytes) -> Option { - let bflags = d.u32(0)?; - let wrap = d.u32(4)?; - let rect = floats::<4>(d, 8)?; - let (c0, c1) = (argb(d.u32(24)?), argb(d.u32(28)?)); - let mut off = 40; - let mut xf = IDENTITY; - if bflags & 0x2 != 0 { - xf = matrix(d, off)?; - off += 24; - } - let mut stops = vec![(0.0, c0), (1.0, c1)]; - if bflags & 0x4 != 0 { - // Preset colours: positions, then one colour each. - let n = (d.u32(off)? as usize).min(MAX_STOPS); - let pos: Vec = (0..n).map(|i| d.f32(off + 4 + i * 4).map(f64::from)).collect::>()?; - let cols: Vec<[u8; 4]> = (0..n).map(|i| d.u32(off + 4 + n * 4 + i * 4).map(argb)).collect::>()?; - stops = pos.into_iter().zip(cols).collect(); - } else if bflags & 0x8 != 0 { - // Blend factors: positions, then how far from the start colour to the end colour. - let n = (d.u32(off)? as usize).min(MAX_STOPS); - let pos: Vec = (0..n).map(|i| d.f32(off + 4 + i * 4).map(f64::from)).collect::>()?; - let fac: Vec = (0..n).map(|i| d.f32(off + 4 + n * 4 + i * 4).map(f64::from)).collect::>()?; - stops = pos.into_iter().zip(fac).map(|(p, k)| (p, lerp(c0, c1, k.clamp(0.0, 1.0)))).collect(); - } - stops.retain(|s| s.0.is_finite()); - stops.sort_by(|a, b| a.0.total_cmp(&b.0)); - (!stops.is_empty()).then_some(Fill::Linear { rect, stops, wrap, xf }) -} - -/// A path gradient: from the centre colour to the (first) surrounding colour at the boundary, -/// taken as the ellipse in the boundary's bounding rectangle. -fn path_gradient(d: Bytes) -> Option { - let bflags = d.u32(0)?; - let center_c = argb(d.u32(8)?); - let [cx, cy] = floats::<2>(d, 12)?; - let nc = (d.u32(20)? as usize).min(MAX_STOPS); - let surround = if nc > 0 { argb(d.u32(24)?) } else { center_c }; - let mut off = 24 + nc * 4; - let bbox = if bflags & 0x1 != 0 { - let size = d.u32(off)? as usize; - let b = ops_bbox(&path(d.tail(off + 4)?)?)?; - off = off.checked_add(4 + size)?; - b - } else { - let n = d.u32(off)? as usize; - let pts = points(d, off + 4, n, false)?; - off = off.checked_add(4 + 8 * n.min(MAX_POINTS))?; - ops_bbox(&poly_ops(&pts, true))? - }; - let xf = if bflags & 0x2 != 0 { matrix(d, off)? } else { IDENTITY }; - let radius = ((bbox[2] - bbox[0]) / 2.0, (bbox[3] - bbox[1]) / 2.0); - Some(Fill::Radial { center: (cx, cy), radius, stops: vec![(0.0, center_c), (1.0, surround)], xf }) -} - -/// A pen object: version, type, then pen data (flags, unit, width, optional fields), then a brush. -fn pen(r: Bytes) -> Option { - let pflags = r.u32(8)?; - let width = f64::from(r.f32(16)?); - let mut off = 20usize; - for (bit, len) in [(0x1, 24), (0x2, 4), (0x4, 4), (0x8, 4), (0x10, 4), (0x20, 4), (0x40, 4), (0x80, 4)] { - if pflags & bit != 0 { - off += len; - } - } - for bit in [0x100, 0x200, 0x400] { - if pflags & bit != 0 { - off += if bit == 0x200 { 4 } else { 4 + 4 * (r.u32(off)? as usize).min(MAX_POINTS) }; - } - } - for bit in [0x800, 0x1000] { - if pflags & bit != 0 { - off += 4 + (r.u32(off)? as usize).min(1 << 20); - } - } - let color = brush(r, off)?.color(); - Some(Obj::Pen { color, width: if width.is_finite() { width.clamp(0.0, 1.0e5) } else { 0.0 } }) -} - -/// A path object: points, then one type byte per point (start, line, Bézier; 0x80 closes). -fn path(r: Bytes) -> Option> { - let n = (r.u32(4)? as usize).min(MAX_POINTS); - let pflags = r.u32(8)?; - if pflags & 0x1800 != 0 { - // Relative (0x0800) and run-length encoded (0x1000) points: not read yet. - return None; - } - let compressed = pflags & 0x4000 != 0; - let pts = points(r, 12, n, compressed)?; - let toff = 12 + n * if compressed { 4 } else { 8 }; - let types = r.slice(toff, n)?; - let mut ops = Vec::with_capacity(n + 4); - let mut i = 0; - while i < n { - let (t, p) = (types[i], pts[i]); - match t & 0x07 { - 0 => ops.push(Op::M(p)), - 3 if i + 2 < n => { - ops.push(Op::C(p, pts[i + 1], pts[i + 2])); - if types[i + 2] & 0x80 != 0 { - ops.push(Op::Z); - } - i += 3; - continue; - } - _ => ops.push(Op::L(p)), - } - if t & 0x80 != 0 { - ops.push(Op::Z); - } - i += 1; - } - Some(ops) -} - -/// A region object as a convex area in world units (non-convex parts become their bounding -/// rectangle). -fn region(r: Bytes) -> Option { - let mut work = 0; - region_node(r, 8, 0, &mut work).map(|(a, _)| Obj::Region(a)) -} - -/// One region node at `off`: its area and its length in bytes. -fn region_node(r: Bytes, off: usize, depth: usize, work: &mut usize) -> Option<(Area, usize)> { - *work = work.checked_add(1)?; - if depth > MAX_REGION_DEPTH || *work > MAX_REGION_WORK { - return None; - } - match r.u32(off)? { - 0x1000_0000 => { - let [x, y, w, h] = floats::<4>(r, off + 4)?; - Some((Some(rect_poly([x.min(x + w), y.min(y + h), x.max(x + w), y.max(y + h)])), 20)) - } - 0x1000_0001 => { - let size = r.u32(off + 4)? as usize; - if size < 12 { - return None; - } - let data = Bytes(r.slice(off + 8, size)?); - let points = (data.u32(4)? as usize).min(MAX_POINTS); - *work = work.checked_add(points)?; - if *work > MAX_REGION_WORK { - return None; - } - let area = area_of_ops(&path(data)?); - Some((Some(area), 8usize.checked_add(size)?)) - } - 0x1000_0002 => Some((Some(Vec::new()), 4)), - 0x1000_0003 => Some((None, 4)), - mode @ 0..=5 => { - let (a, la) = region_node(r, off + 4, depth + 1, work)?; - let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1, work)?; - let out = match mode { - 0 => b, - 1 => area_intersect(a, b), - 2 => area_union(a, b), - // XOR, exclude, complement: the left area bounds the result closely enough. - _ => a, - }; - Some((out, (4 + la).checked_add(lb)?)) - } - _ => None, - } -} - -/// The area a path encloses: its outline when that is one convex figure, else the rectangle -/// around it. -fn area_of_ops(ops: &[Op]) -> Vec

{ - let subs = flatten(ops); - if let [(pts, _)] = subs.as_slice() - && let Some(c) = convex(pts) - { - return c; - } - ops_bbox(ops).map(rect_poly).unwrap_or_default() -} - -fn area_intersect(a: Area, b: Area) -> Area { - match (a, b) { - (None, x) | (x, None) => x, - (Some(a), Some(b)) if a.len() >= 3 && b.len() >= 3 => { - // Out of work: keep the area so far rather than call the region empty. - match clip_polygon_within(&a, &b, &mut { MAX_AREA_CLIP_WORK }) { - Some(p) => Some(convex(&p).unwrap_or_default()), - None => Some(a), - } - } - _ => Some(Vec::new()), - } -} - -fn area_union(a: Area, b: Area) -> Area { - match (a, b) { - (Some(a), Some(b)) => { - if a.len() < 3 { - return Some(b); - } - if b.len() < 3 { - return Some(a); - } - let all: Vec

= a.into_iter().chain(b).collect(); - Some(pts_bbox(&all).map(rect_poly).unwrap_or_default()) - } - _ => None, - } -} - -/// An area through an affine map (which keeps it convex; the orientation is put right again). -fn map_area(a: Area, m: Xform) -> Area { - a.map(|pts| { - let mapped: Vec

= pts.iter().map(|p| apply(m, *p)).collect(); - if mapped.len() < 3 { Vec::new() } else { convex(&mapped).unwrap_or_default() } - }) -} - -/// An elliptical arc in `rect` from `start` sweeping `sweep` degrees (clockwise on screen), as -/// cubics; a pie adds the lines to and from the centre. -fn arc_ops([l, t, r, b]: [f64; 4], start: f64, sweep: f64, pie: bool) -> Vec { - let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0); - let sweep = sweep.clamp(-360.0, 360.0); - // GDI+ angles are along rays from the centre; turn them into the ellipse's parameter. - let param = |deg: f64| { - let a = deg.to_radians(); - (rx * a.sin()).atan2(ry * a.cos()) - }; - let t0 = param(start); - let mut dt = if sweep.abs() >= 360.0 { sweep.signum() * std::f64::consts::TAU } else { param(start + sweep) - t0 }; - if sweep.abs() < 360.0 { - while sweep > 0.0 && dt < 0.0 { - dt += std::f64::consts::TAU; - } - while sweep < 0.0 && dt > 0.0 { - dt -= std::f64::consts::TAU; - } - } - let at = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin()); - let mut ops = Vec::with_capacity(8); - if pie { - ops.push(Op::M((cx, cy))); - ops.push(Op::L(at(t0))); - } else { - ops.push(Op::M(at(t0))); - } - let n = ((dt.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 4); - let step = dt / n as f64; - let k = 4.0 / 3.0 * (step / 4.0).tan(); - for i in 0..n { - let (a, e) = (t0 + step * i as f64, t0 + step * (i + 1) as f64); - let (p0, p3) = (at(a), at(e)); - let c1 = (p0.0 - k * rx * a.sin(), p0.1 + k * ry * a.cos()); - let c2 = (p3.0 + k * rx * e.sin(), p3.1 - k * ry * e.cos()); - ops.push(Op::C(c1, c2, p3)); - } - if pie { - ops.push(Op::Z); - } - ops -} - -/// A font object: em size, unit, style flags, then the family name in UTF-16. -fn font(r: Bytes) -> Option { - let em = f64::from(r.f32(4)?); - let n = (r.u32(20)? as usize).min(64); - let units: Vec = (0..n).map(|i| r.u16(24 + i * 2)).collect::>()?; - let family = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string(); - em.is_finite().then_some(Obj::Font(FontObj { em, unit: r.u32(8)?, style: r.u32(12)?, family })) -} - -/// An image object: a bitmap of raw pixels or a compressed (PNG, JPEG, GIF…) file, or a nested -/// metafile. -fn image(r: Bytes) -> Option { - match r.u32(4)? { - 1 => bitmap(r).map(ImageData::Bitmap), - 2 => metafile(r).map(ImageData::Meta), - _ => None, - } -} - -/// A metafile image: type, size, then a WMF (placeable or not) or EMF file, parsed as a picture. -fn metafile(r: Bytes) -> Option { - let size = r.u32(12)? as usize; - let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?; - let depth = NESTING.with(std::cell::Cell::get); - if depth >= MAX_NESTING { - return None; - } - NESTING.with(|n| n.set(depth + 1)); - let pic = crate::parse(data).ok(); - NESTING.with(|n| n.set(depth)); - pic.filter(|p| p.width > 0.0 && p.height > 0.0) -} - -/// Bytes the items of a nested picture own: path segments, clip rectangles, pixels and text, with -/// each shared pattern tile counted once. -fn picture_cost(items: &[crate::Item]) -> usize { - use crate::Item; - let region = |r: &Option>| r.as_ref().map_or(0, |v| v.len().saturating_mul(16)); - let mut tiles = std::collections::HashSet::new(); - let mut total = 0usize; - for item in items { - let own = match item { - Item::Path { segs, region: r, .. } => segs.len().saturating_mul(std::mem::size_of::()).saturating_add(region(r)), - Item::Bitmap { rgba, region: r, .. } => rgba.len().saturating_add(region(r)), - Item::Pattern { segs, tile, region: r, .. } => { - let tile_bytes = if tiles.insert(Arc::as_ptr(tile)) { tile.rgba.len() } else { 0 }; - segs.len().saturating_mul(std::mem::size_of::()).saturating_add(tile_bytes).saturating_add(region(r)) - } - Item::Text(t) => t - .text - .len() - .saturating_add(t.dx.len().saturating_mul(std::mem::size_of::())) - .saturating_add(t.font.family.len()) - .saturating_add(region(&t.region)), - }; - total = total.saturating_add(128).saturating_add(own); - } - total -} - -fn bitmap(r: Bytes) -> Option { - let (w, h, stride) = (r.u32(8)? as i32, r.u32(12)? as i32, r.u32(16)? as i32); - let format = r.u32(20)?; - let data = r.tail(28)?; - if r.u32(24)? == 1 { - return decode_compressed(data.0); - } - let (w, h) = (usize::try_from(w).ok()?, usize::try_from(h).ok()?); - if w == 0 || h == 0 || w.checked_mul(h)? > crate::MAX_PIXELS { - return None; - } - let bpp = ((format >> 8) & 0xFF) as usize; - let (pal, pixels) = if format & 0x0001_0000 != 0 { - // Indexed formats carry a palette first: flags, count, ARGB entries. - let n = (data.u32(4)? as usize).min(256); - let pal: Vec<[u8; 4]> = (0..n).map(|i| data.u32(8 + i * 4).map(argb)).collect::>()?; - (pal, data.tail(8 + n * 4)?) - } else { - (Vec::new(), data) - }; - let row = (w.checked_mul(bpp)?).div_ceil(8); - let stride_abs = (stride.unsigned_abs() as usize).max(row); - let mut px = Vec::with_capacity(w * h * 4); - for y in 0..h { - let line = pixels.slice(y.checked_mul(stride_abs)?, row)?; - for x in 0..w { - let c = match (format, bpp) { - (_, 1 | 4 | 8) if !pal.is_empty() => { - let bit = x * bpp; - let byte = *line.get(bit / 8)?; - let i = (byte >> (8 - bpp - bit % 8)) & ((1u16 << bpp) - 1) as u8; - pal.get(usize::from(i)).copied().unwrap_or([0, 0, 0, 255]) - } - (_, 24) => { - let p = line.get(x * 3..x * 3 + 3)?; - [p[2], p[1], p[0], 255] - } - (0x0002_2009, 32) => { - let p = line.get(x * 4..x * 4 + 4)?; - [p[2], p[1], p[0], 255] - } - (0x000E_200B, 32) => { - let p = line.get(x * 4..x * 4 + 4)?; - let a = u32::from(p[3]); - let un = |v: u8| (u32::from(v) * 255 + a / 2).checked_div(a).map_or(v, |c| c.min(255) as u8); - [un(p[2]), un(p[1]), un(p[0]), p[3]] - } - (_, 32) => { - let p = line.get(x * 4..x * 4 + 4)?; - [p[2], p[1], p[0], p[3]] - } - (0x0002_1006, 16) => { - let v = u16::from_le_bytes([*line.get(x * 2)?, *line.get(x * 2 + 1)?]); - let s = |v: u16, bits: u32| ((u32::from(v) * 255) / ((1 << bits) - 1)) as u8; - [s(v >> 11, 5), s((v >> 5) & 0x3F, 6), s(v & 0x1F, 5), 255] - } - (_, 16) => { - let v = u16::from_le_bytes([*line.get(x * 2)?, *line.get(x * 2 + 1)?]); - let s = |v: u16| ((u32::from(v & 0x1F) * 255) / 31) as u8; - [s(v >> 10), s(v >> 5), s(v), 255] - } - _ => return None, - }; - px.extend_from_slice(&c); - } - } - // A negative stride stores the rows bottom-up. - Some(top_down(Rgba { w, h, bottom_up: stride < 0, px })) -} - -fn top_down(mut img: Rgba) -> Rgba { - if img.bottom_up { - crate::dib::flip(&mut img, false, true); - img.bottom_up = false; - } - img -} - -fn decode_compressed(bytes: &[u8]) -> Option { - let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?; - let mut limits = image::Limits::default(); - limits.max_image_width = Some(1 << 15); - limits.max_image_height = Some(1 << 15); - limits.max_alloc = Some((crate::MAX_PIXELS * 4) as u64); - reader.limits(limits); - let (w, h) = reader.into_dimensions().ok()?; - let (w, h) = (w as usize, h as usize); - if w == 0 || h == 0 || w.checked_mul(h)? > crate::MAX_PIXELS { - return None; - } - let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?; - let mut limits = image::Limits::default(); - limits.max_image_width = Some(1 << 15); - limits.max_image_height = Some(1 << 15); - limits.max_alloc = Some((crate::MAX_PIXELS * 4) as u64); - reader.limits(limits); - let img = reader.decode().ok()?.to_rgba8(); - Some(Rgba { w, h, bottom_up: false, px: img.into_raw() }) -} - -/// `n` points at `off`: 16-bit integers when compressed, else 32-bit floats. -fn points(r: Bytes, off: usize, n: usize, compressed: bool) -> Option> { - let n = n.min(MAX_POINTS); - let mut v = Vec::with_capacity(n); - for i in 0..n { - v.push(if compressed { - (f64::from(r.i16(off + i * 4)?), f64::from(r.i16(off + i * 4 + 2)?)) - } else { - let (x, y) = (f64::from(r.f32(off + i * 8)?), f64::from(r.f32(off + i * 8 + 4)?)); - if !(x.is_finite() && y.is_finite()) { - return None; - } - (x, y) - }); - } - Some(v) -} - -/// `n` rectangles (x, y, w, h) at `off`, as `[l, t, r, b]`. -fn rects(r: Bytes, off: usize, n: usize, compressed: bool) -> Option> { - let pts = points(r, off, n.min(MAX_POINTS / 2).checked_mul(2)?, compressed)?; - Some(pts.as_chunks::<2>().0.iter().map(|c| [c[0].0, c[0].1, c[0].0 + c[1].0, c[0].1 + c[1].1]).collect()) -} - -fn rect_ops([l, t, r, b]: [f64; 4]) -> Vec { - crate::geom::rect(l, t, r, b) -} - -fn finite(v: f32) -> Option { - v.is_finite().then_some(f64::from(v)) -} - -/// A GDI+ cardinal spline through `pts` as cubic Béziers: each segment's control points lie a -/// third of `tension` (GDI+'s default 0.5) along the neighbouring chords. `closed` joins the -/// last point back to the first; an open curve draws `segments` segments from point `offset`. -fn curve_ops(pts: &[P], tension: f64, closed: bool, offset: usize, segments: usize) -> Vec { - let n = pts.len(); - if n < 2 { - return Vec::new(); - } - let k = tension.clamp(0.0, 10.0) / 3.0; - let at = |i: isize| -> P { - let i = if closed { i.rem_euclid(n as isize) as usize } else { i.clamp(0, n as isize - 1) as usize }; - pts.get(i).copied().unwrap_or((0.0, 0.0)) - }; - let (first, count) = if closed { (0, n) } else { (offset.min(n - 1), segments.min(n - 1 - offset.min(n - 1))) }; - let mut ops = vec![Op::M(at(first as isize))]; - for s in first..first + count { - let i = s as isize; - let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2)); - let c1 = (p1.0 + (p2.0 - p0.0) * k, p1.1 + (p2.1 - p0.1) * k); - let c2 = (p2.0 - (p3.0 - p1.0) * k, p2.1 - (p3.1 - p1.1) * k); - ops.push(Op::C(c1, c2, p2)); - } - if closed { - ops.push(Op::Z); - } - ops -} - -fn poly_ops(pts: &[P], close: bool) -> Vec { - let mut ops: Vec = pts.iter().enumerate().map(|(i, p)| if i == 0 { Op::M(*p) } else { Op::L(*p) }).collect(); - if close { - ops.push(Op::Z); - } - ops -} - -/// An ellipse in its bounding box, as four cubic arcs. -fn ellipse_ops([l, t, r, b]: [f64; 4]) -> Vec { - let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0); - let k = 0.552_284_75; - let (ox, oy) = (rx * k, ry * k); - vec![ - Op::M((cx + rx, cy)), - Op::C((cx + rx, cy + oy), (cx + ox, cy + ry), (cx, cy + ry)), - Op::C((cx - ox, cy + ry), (cx - rx, cy + oy), (cx - rx, cy)), - Op::C((cx - rx, cy - oy), (cx - ox, cy - ry), (cx, cy - ry)), - Op::C((cx + ox, cy - ry), (cx + rx, cy - oy), (cx + rx, cy)), - Op::Z, - ] -} - -/// Core fonts whose first glyphs follow the standard Macintosh order (glyph 3 is the space, -/// glyphs 3..=97 are U+0020..=U+007E). -const MAC_ORDER_FONTS: &[&str] = - &["arial", "arial black", "times new roman", "courier new", "verdana", "georgia", "tahoma", "trebuchet ms", "comic sans ms", "impact"]; - -/// Glyph indices of a known font back to ASCII; None when the font is unknown or a glyph is -/// outside the ASCII range. -fn glyphs_to_chars(family: &str, glyphs: &[u16]) -> Option> { - let family = family.trim().to_ascii_lowercase(); - if !MAC_ORDER_FONTS.contains(&family.as_str()) { - return None; - } - glyphs.iter().map(|g| (3..=97).contains(g).then_some(g + 29)).collect() -} - -/// A rough advance for a character in ems: the metafile crate has no fonts, so line breaks for -/// wrapped strings use typical proportional widths. -fn char_em(u: u16) -> f64 { - match char::from_u32(u32::from(u)).unwrap_or('x') { - ' ' | 'i' | 'l' | 'j' | '.' | ',' | ':' | ';' | '\'' | '!' | '|' | 'I' | 'f' | 't' | 'r' => 0.3, - 'm' | 'w' | 'M' | 'W' => 0.85, - c if c.is_ascii_uppercase() || c.is_ascii_digit() => 0.62, - c if (c as u32) >= 0x2E80 => 1.0, - _ => 0.52, - } -} - -/// Greedy word wrap of one line to `width` world units at em size `em`; a word longer than the -/// line breaks between characters. -fn wrap_line(line: &[u16], width: f64, em: f64) -> Vec> { - if em <= 0.0 || !width.is_finite() { - return vec![line.to_vec()]; - } - let trimmed = |v: &[u16]| v.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); - let measure = |v: &[u16]| v.iter().map(|u| char_em(*u) * em).sum::(); - - let mut out: Vec> = Vec::new(); - let mut cur: Vec = Vec::new(); - let mut cur_total_w = 0.0; - - for word in line.split_inclusive(|u| *u == 0x20) { - let n_non_space = trimmed(word); - let word_non_space_w = measure(&word[..n_non_space]); - let word_total_w = measure(word); - - if cur.is_empty() { - if word_non_space_w <= width { - cur.extend_from_slice(word); - cur_total_w = word_total_w; - continue; - } - } else if cur_total_w + word_non_space_w <= width { - cur.extend_from_slice(word); - cur_total_w += word_total_w; - continue; - } - - if !cur.is_empty() { - let n = trimmed(&cur); - cur.truncate(n); - out.push(std::mem::take(&mut cur)); - } - - if word_non_space_w <= width { - cur.extend_from_slice(word); - cur_total_w = word_total_w; - continue; - } - - // A word wider than the line on its own: break it between characters. - let mut cur_w = 0.0; - for &u in word { - let u_w = char_em(u) * em; - if cur.is_empty() { - cur.push(u); - cur_w = u_w; - } else if cur_w + u_w <= width { - cur.push(u); - cur_w += u_w; - } else { - let n = trimmed(&cur); - cur.truncate(n); - out.push(std::mem::take(&mut cur)); - cur.push(u); - cur_w = u_w; - } - } - cur_total_w = cur_w; - } - let n = cur.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); - cur.truncate(n); - out.push(cur); - out -} - -#[cfg(test)] -mod cost_tests { - use super::*; - use crate::{Item, Seg, Tile}; - - #[test] - fn a_shared_pattern_tile_is_charged_once() { - let tile = Arc::new(Tile { width: 512, height: 512, rgba: vec![0; 1 << 20], cell: [1.0, 1.0], origin: [0.0, 0.0] }); - let item = || Item::Pattern { segs: vec![Seg::Move(0.0, 0.0), Seg::Line(1.0, 1.0)], even_odd: false, tile: tile.clone(), region: None }; - let items: Vec = (0..257).map(|_| item()).collect(); - assert!(picture_cost(&items) < 2 << 20, "{}", picture_cost(&items)); - } - - #[test] - fn pieces_of_a_continued_object_count_against_the_shared_budget() { - let (mut plus, mut cv) = (Plus::new(), Canvas::new()); - plus.active = true; - // Image object (kind 5), id 0, continued: 1000 bytes announced, 100 delivered. - let mut rec = 1000u32.to_le_bytes().to_vec(); - rec.extend([0u8; 100]); - plus.record(&mut cv, 0x4008, 0x8000 | (5 << 8), Bytes(&rec)); - assert_eq!(cv.held(), 100); - } - - #[test] - fn current_and_saved_clips_count_against_the_shared_budget() { - let (mut plus, mut cv) = (Plus::new(), Canvas::new()); - plus.active = true; - let pts = vec![(0.0, 0.0); 1000]; - plus.clip = Some(pts.clone()); - plus.stack.push(Saved { id: 1, world: IDENTITY, page: 1.0, clip: Some(pts) }); - // Any record refreshes the held count (an unknown record draws nothing). - plus.record(&mut cv, 0x40FF, 0, Bytes(&[])); - assert_eq!(cv.held(), 2 * 1000 * std::mem::size_of::

()); - // Restoring the saved state releases the charge of what it dropped. - plus.stack.clear(); - plus.clip = None; - plus.record(&mut cv, 0x40FF, 0, Bytes(&[])); - assert_eq!(cv.held(), 0); - } -} diff --git a/crates/metafile/src/geom.rs b/crates/metafile/src/geom.rs deleted file mode 100644 index 8063c730a..000000000 --- a/crates/metafile/src/geom.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Path operations in `f64` and the shape builders that turn WMF/EMF rectangles, ellipses and arcs -//! into cubic Béziers (the classic 4/3·tan(Δ/4) approximation, at most 90° per piece). - -use std::f64::consts::{FRAC_PI_2, PI, TAU}; - -pub(crate) type P = (f64, f64); - -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) enum Op { - M(P), - L(P), - C(P, P, P), - Z, -} - -pub(crate) fn rect(l: f64, t: f64, r: f64, b: f64) -> Vec { - vec![Op::M((l, t)), Op::L((r, t)), Op::L((r, b)), Op::L((l, b)), Op::Z] -} - -/// Corner radii are half the EMF/WMF corner ellipse size, clamped so the corners never overlap. -pub(crate) fn round_rect(l: f64, t: f64, r: f64, b: f64, ew: f64, eh: f64) -> Vec { - // Reversed corners (right < left, bottom < top) describe the same rectangle. - let (l, r) = (l.min(r), l.max(r)); - let (t, b) = (t.min(b), t.max(b)); - let rx = (ew / 2.0).abs().min((r - l) / 2.0); - let ry = (eh / 2.0).abs().min((b - t) / 2.0); - let mut ops = vec![Op::M((l + rx, t)), Op::L((r - rx, t))]; - ops.extend(arc(r - rx, t + ry, rx, ry, -FRAC_PI_2, FRAC_PI_2)); - ops.push(Op::L((r, b - ry))); - ops.extend(arc(r - rx, b - ry, rx, ry, 0.0, FRAC_PI_2)); - ops.push(Op::L((l + rx, b))); - ops.extend(arc(l + rx, b - ry, rx, ry, FRAC_PI_2, FRAC_PI_2)); - ops.push(Op::L((l, t + ry))); - ops.extend(arc(l + rx, t + ry, rx, ry, PI, FRAC_PI_2)); - ops.push(Op::Z); - ops -} - -pub(crate) fn ellipse(l: f64, t: f64, r: f64, b: f64) -> Vec { - let (cx, cy, rx, ry) = center(l, t, r, b); - let mut ops = vec![Op::M((cx + rx, cy))]; - ops.extend(arc(cx, cy, rx, ry, 0.0, TAU)); - ops.push(Op::Z); - ops -} - -/// Which closed or open shape an ARC/CHORD/PIE record draws. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum ArcKind { - Arc, - Chord, - Pie, -} - -/// ARC, CHORD and PIE: the arc runs counter-clockwise on screen from the start point to the end point. -pub(crate) fn arc_shape(l: f64, t: f64, r: f64, b: f64, s: P, e: P, kind: ArcKind) -> Vec { - let (cx, cy, rx, ry) = center(l, t, r, b); - let ang = |p: P| ((p.1 - cy) / ry.max(f64::MIN_POSITIVE)).atan2((p.0 - cx) / rx.max(f64::MIN_POSITIVE)); - let (a0, a1) = (ang(s), ang(e)); - let mut d = (a0 - a1).rem_euclid(TAU); - if d < 1e-12 { - d = TAU; - } - let sweep = -d; - let start = (cx + rx * a0.cos(), cy + ry * a0.sin()); - let mut ops = if kind == ArcKind::Pie { vec![Op::M((cx, cy)), Op::L(start)] } else { vec![Op::M(start)] }; - ops.extend(arc(cx, cy, rx, ry, a0, sweep)); - match kind { - ArcKind::Arc => {} - ArcKind::Chord => ops.push(Op::Z), - ArcKind::Pie => ops.extend([Op::L((cx, cy)), Op::Z]), - } - ops -} - -fn center(l: f64, t: f64, r: f64, b: f64) -> (f64, f64, f64, f64) { - ((l + r) / 2.0, (t + b) / 2.0, (r - l).abs() / 2.0, (b - t).abs() / 2.0) -} - -/// Cubic pieces along an ellipse from parameter `a0` over signed `sweep` radians. The current point -/// must already be at `(cx + rx cos a0, cy + ry sin a0)`. -fn arc(cx: f64, cy: f64, rx: f64, ry: f64, a0: f64, sweep: f64) -> Vec { - let n = ((sweep.abs() / FRAC_PI_2).ceil() as usize).clamp(1, 8); - let step = sweep / n as f64; - let k = 4.0 / 3.0 * (step / 4.0).tan(); - let pt = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin()); - let mut ops = Vec::with_capacity(n); - for i in 0..n { - let t0 = a0 + step * i as f64; - let t1 = t0 + step; - let (p0, p3) = (pt(t0), pt(t1)); - let c1 = (p0.0 - k * rx * t0.sin(), p0.1 + k * ry * t0.cos()); - let c2 = (p3.0 + k * rx * t1.sin(), p3.1 - k * ry * t1.cos()); - ops.push(Op::C(c1, c2, p3)); - } - ops -} - -/// Consecutive subpaths: `counts[i]` points each, taken in order from `pts`. Counts that run past the -/// point list are cut short rather than failing the whole record. -pub(crate) fn subpaths(pts: &[P], counts: &[usize], close: bool) -> Vec { - let mut ops = Vec::with_capacity(pts.len() + counts.len()); - let mut at = 0usize; - for &c in counts { - let end = at.saturating_add(c).min(pts.len()); - let Some((first, rest)) = pts.get(at..end).and_then(|s| s.split_first()) else { - at = end; - continue; - }; - ops.push(Op::M(*first)); - ops.extend(rest.iter().map(|p| Op::L(*p))); - if close { - ops.push(Op::Z); - } - at = end; - } - ops -} diff --git a/crates/metafile/src/lib.rs b/crates/metafile/src/lib.rs deleted file mode 100644 index 8e10cdf3c..000000000 --- a/crates/metafile/src/lib.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! DeckCraft metafiles (ported from WordCraft's `crates/metafile`): reads Windows Metafiles (WMF) -//! and Enhanced Metafiles (EMF) into a neutral vector model of filled and stroked paths, pattern -//! fills, RGBA bitmaps and text strings, in a picture frame with y down. -//! -//! Text stays font-agnostic: a [`Text`] item names the font the file asks for and carries the -//! string, its advances and alignment; the renderer resolves the font and draws the glyphs. -//! -//! The reader is pure Rust over byte slices (no file system), so it builds for wasm. Every read is -//! bounds-checked and every count is capped; hostile input yields a shorter picture or an [`Error`], -//! never a panic. -//! -//! Records follow MS-WMF and MS-EMF. Rectangle clip regions (`INTERSECTCLIPRECT`, -//! `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit what later records draw. Records outside the -//! supported set are skipped silently. -#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -#![forbid(unsafe_code)] - -mod bytes; -mod canvas; -mod clip; -mod cliprect; -mod dib; -mod emf; -mod emfplus; -mod geom; -mod place; -mod text; -mod wmf; - -#[cfg(test)] -mod tests; - -use std::collections::HashMap; -use std::sync::Arc; - -use bytes::Bytes; -use canvas::Raw; -use geom::{Op, P}; -pub use place::{MAX_BITMAP_SIDE, PlacedItem}; -pub use text::{Font, HAlign, SYMBOL_CHARSET, Text, VAlign}; - -/// Items kept from one file. Past the cap the parse stops and keeps what it has. -pub(crate) const MAX_ITEMS: usize = 200_000; -/// Object table slots (WMF handles, EMF object indices). -pub(crate) const MAX_OBJECTS: usize = 4096; -/// Largest bitmap in pixels (about 40 megapixels, 160 MB of RGBA). -pub(crate) const MAX_PIXELS: usize = 40_000_000; -/// DC save-stack depth. -pub(crate) const MAX_STACK: usize = 1024; -/// Points in one open path bracket. -pub(crate) const MAX_PATH_OPS: usize = 2_000_000; -/// Bytes of path ops and pixels kept from one file (about 256 MB). -pub(crate) const MAX_BYTES: usize = 256 * 1024 * 1024; - -/// A parsed metafile. Items use coordinates in `0..width` and `0..height`, y down. -#[derive(Debug, Clone, PartialEq)] -pub struct Picture { - /// Size of the drawing in its own units: points for placeable WMF and EMF (from the frame), device - /// or logical units otherwise. - pub width: f32, - pub height: f32, - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Item { - /// A filled and/or stroked path, in picture coordinates. - Path { - segs: Vec, - fill: Option<[u8; 4]>, - /// Colour and width in picture units. - stroke: Option<([u8; 4], f32)>, - even_odd: bool, - /// Clip region in picture units, each rectangle `[x0, y0, x1, y1]`. None is the default clip. - region: Option>, - }, - /// A bitmap stretched into `rect` = `[x, y, w, h]` (picture units), RGBA8 rows top-down. - Bitmap { - rect: [f32; 4], - width: u32, - height: u32, - rgba: Vec, - /// Clip region in picture units. None is the default clip. - region: Option>, - }, - /// A path filled with a repeating tile (pattern and hatch brushes). Items filled with the same - /// brush share its tile. - Pattern { - segs: Vec, - even_odd: bool, - tile: Arc, - /// Clip region in picture units. None is the default clip. - region: Option>, - }, - /// A text string. - Text(Box), -} - -/// A brush tile: RGBA8 rows top-down (transparent where the brush leaves the background), each -/// pixel `cell` picture units, with a tile's top-left corner at `origin`. -#[derive(Debug, Clone, PartialEq)] -pub struct Tile { - pub width: u32, - pub height: u32, - pub rgba: Vec, - pub cell: [f32; 2], - pub origin: [f32; 2], -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Seg { - Move(f32, f32), - Line(f32, f32), - Cubic(f32, f32, f32, f32, f32, f32), - Close, -} - -#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] -pub enum Error { - #[error("not a WMF or EMF file")] - NotMetafile, - #[error("metafile header is truncated or invalid")] - BadHeader, - #[error("metafile has no readable records")] - NoRecords, -} - -/// The picture frame in device units (`rect` = `[x, y, w, h]`) and, when the file states one, its -/// size in picture units. -pub(crate) struct Frame { - pub rect: [f64; 4], - pub size: Option<(f64, f64)>, -} - -/// Parses a WMF (placeable or plain) or EMF file. -pub fn parse(bytes: &[u8]) -> Result { - let b = Bytes(bytes); - let (cv, frame) = if emf::is_emf(b) { - emf::parse(b)? - } else if wmf::is_wmf(b) { - // A WMF converted from an EMF can carry the original in MFCOMMENT escapes; PowerPoint draws - // the WMF's own records (its dx rounding shows), so the copy is not used. - wmf::parse(b)? - } else { - return Err(Error::NotMetafile); - }; - Ok(build(cv.items, frame)) -} - -/// True when the bytes start like a WMF (placeable or plain header) or an EMF. -pub fn is_metafile(bytes: &[u8]) -> bool { - let b = Bytes(bytes); - emf::is_emf(b) || wmf::is_wmf(b) -} - -/// Maps device-space output into the picture frame. Without a usable frame the frame is the bounding -/// box of the drawing, so the origin is still the top-left of what was drawn. -fn build(raw: Vec, frame: Option) -> Picture { - let frame = frame.filter(|f| f.rect.iter().all(|v| v.is_finite()) && f.rect[2] > 0.0 && f.rect[3] > 0.0); - let (rect, size) = match frame { - Some(f) => (f.rect, f.size.unwrap_or((f.rect[2], f.rect[3]))), - None => match bbox(&raw) { - Some([x0, y0, x1, y1]) => ([x0, y0, x1 - x0, y1 - y0], (x1 - x0, y1 - y0)), - None => return Picture { width: 0.0, height: 0.0, items: Vec::new() }, - }, - }; - let sx = if rect[2] > 0.0 { size.0 / rect[2] } else { 1.0 }; - let sy = if rect[3] > 0.0 { size.1 / rect[3] } else { 1.0 }; - let tf = move |p: P| ((p.0 - rect[0]) * sx, (p.1 - rect[1]) * sy); - let stroke_scale = (sx + sy) / 2.0; - // No stroke is wider than the picture's diagonal: anything wider is a mapping artefact. - let diag = size.0.hypot(size.1); - // Tiles shared by several fills stay shared. - let mut tiles: HashMap<*const canvas::RawTile, (Arc, Arc)> = HashMap::new(); - let items = raw.into_iter().filter_map(|r| match r { - Raw::Path { ops, fill, stroke, even_odd, region } => { - let segs = ops.iter().map(|op| seg(op, tf)).collect::>>()?; - if segs.len() < 2 { - return None; - } - let stroke = stroke.and_then(|(c, w)| Some((c, f((w * stroke_scale).min(diag))?))); - Some(Item::Path { segs, fill, stroke, even_odd, region: map_region(region, &tf)? }) - } - Raw::Bitmap { rect: r, img, region } => { - let (x, y) = tf((r[0], r[1])); - let rect = [f(x)?, f(y)?, f(r[2] * sx)?, f(r[3] * sy)?]; - Some(Item::Bitmap { rect, width: img.w as u32, height: img.h as u32, rgba: img.px, region: map_region(region, &tf)? }) - } - Raw::Pattern { ops, even_odd, tile, region } => { - let segs = ops.iter().map(|op| seg(op, tf)).collect::>>()?; - if segs.len() < 2 { - return None; - } - let key = Arc::as_ptr(&tile); - let tile = match tiles.get(&key) { - Some((_, t)) => t.clone(), - None => { - let (ox, oy) = tf((0.0, 0.0)); - let t = Arc::new(Tile { - width: u32::try_from(tile.w).ok()?, - height: u32::try_from(tile.h).ok()?, - rgba: tile.px.clone(), - cell: [f(tile.cell * sx)?, f(tile.cell * sy)?], - origin: [f(ox)?, f(oy)?], - }); - // The raw tile is kept with its key, so the address is never reused by another. - tiles.insert(key, (tile.clone(), t.clone())); - t - } - }; - Some(Item::Pattern { segs, even_odd, tile, region: map_region(region, &tf)? }) - } - Raw::Text { mut text, region } => { - // Device to picture: scale by (sx, sy) after moving the frame's corner to the origin. - let [a, b, c, d, e, g] = text.transform; - let (e, g) = tf((e, g)); - text.transform = [a * sx, b * sy, c * sx, d * sy, e, g]; - if !text.transform.iter().all(|v| v.is_finite()) { - return None; - } - if let Some([x0, y0, x1, y1]) = text.clip { - let (p, q) = (tf((f64::from(x0), f64::from(y0))), tf((f64::from(x1), f64::from(y1)))); - text.clip = Some([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]); - } - text.region = map_region(region, &tf)?; - Some(Item::Text(text)) - } - }); - let items = items.collect(); - Picture { width: f(size.0).unwrap_or(0.0), height: f(size.1).unwrap_or(0.0), items } -} - -fn map_region(region: Option>, tf: &impl Fn(P) -> P) -> Option>> { - let Some(rects) = region else { return Some(None) }; - let mut out = Vec::with_capacity(rects.len()); - for [x0, y0, x1, y1] in rects { - let (p, q) = (tf((x0, y0)), tf((x1, y1))); - out.push([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]); - } - Some(Some(out)) -} - -fn f(v: f64) -> Option { - let v = v as f32; - v.is_finite().then_some(v) -} - -fn seg(op: &Op, tf: impl Fn(P) -> P) -> Option { - let pt = |p: P| { - let (x, y) = tf(p); - Some((f(x)?, f(y)?)) - }; - Some(match *op { - Op::M(p) => { - let (x, y) = pt(p)?; - Seg::Move(x, y) - } - Op::L(p) => { - let (x, y) = pt(p)?; - Seg::Line(x, y) - } - Op::C(a, b, c) => { - let (x1, y1) = pt(a)?; - let (x2, y2) = pt(b)?; - let (x3, y3) = pt(c)?; - Seg::Cubic(x1, y1, x2, y2, x3, y3) - } - Op::Z => Seg::Close, - }) -} - -/// Bounding box `[x0, y0, x1, y1]` over the finite points of the output. -fn bbox(raw: &[Raw]) -> Option<[f64; 4]> { - let mut b: Option<[f64; 4]> = None; - let mut grow = |p: P| { - if p.0.is_finite() && p.1.is_finite() { - b = Some(match b { - None => [p.0, p.1, p.0, p.1], - Some([x0, y0, x1, y1]) => [x0.min(p.0), y0.min(p.1), x1.max(p.0), y1.max(p.1)], - }); - } - }; - for r in raw { - match r { - Raw::Path { ops, .. } | Raw::Pattern { ops, .. } => { - for op in ops { - match *op { - Op::M(p) | Op::L(p) => grow(p), - Op::C(a, c, d) => { - grow(a); - grow(c); - grow(d); - } - Op::Z => {} - } - } - } - Raw::Bitmap { rect, .. } => { - grow((rect[0], rect[1])); - grow((rect[0] + rect[2], rect[1] + rect[3])); - } - Raw::Text { text: t, .. } => grow((t.transform[4], t.transform[5])), - } - } - b -} diff --git a/crates/metafile/src/place.rs b/crates/metafile/src/place.rs deleted file mode 100644 index 78ffc4b0a..000000000 --- a/crates/metafile/src/place.rs +++ /dev/null @@ -1,206 +0,0 @@ -//! Placing a picture on a target (a page or a device): the fitted transform, paths as kurbo paths in -//! target units and bitmaps as target rectangles. The rasteriser and the PDF writer only paint the result. - -use std::sync::Arc; - -use kurbo::{Affine, BezPath, Rect, Shape}; - -use crate::{Item, Picture, Seg}; - -/// Largest bitmap side the renderers draw: within the PDF image limit and the rasteriser's 16-bit sides. -pub const MAX_BITMAP_SIDE: u32 = 30_000; -/// Placed coordinates past this (target units) are far off any page; such items are left out. -const MAX_COORD: f64 = 1e6; -/// Smallest placed bitmap side (target units), so the stretch onto it is never singular. -const MIN_BITMAP_SIDE: f64 = 1e-6; -/// Maximum aggregate path elements retained in placed output. -const MAX_PLACED_ELEMENTS: usize = 500_000; - -/// One item of a placed picture, in target units. -pub enum PlacedItem { - /// A filled and/or stroked path. The stroke width is in target units; 0 is a cosmetic hairline that - /// the renderer draws at its minimum width. - Path { path: BezPath, fill: Option<[u8; 4]>, stroke: Option<([u8; 4], f64)>, even_odd: bool, region: Option> }, - /// `Picture::items[index]`, a bitmap, stretched into `rect`. - Bitmap { index: usize, rect: Rect, region: Option> }, - /// `Picture::items[index]`, a pattern fill: `path` (target units) filled with the item's tile - /// repeated; `tile` maps tile pixels to target units. - Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option> }, - /// `Picture::items[index]`, a text string: `transform` maps its text space to target units; - /// `clip` (target units) bounds what it draws. `region` is the device-context clip. - Text { index: usize, transform: Affine, clip: Option, region: Option> }, -} - -impl Picture { - /// Whether the picture has a finite, positive size, so it can be fitted into a frame. - pub fn has_size(&self) -> bool { - self.width.is_finite() && self.height.is_finite() && self.width > 0.0 && self.height > 0.0 - } - - /// The transform that fits the picture into `r` with the crop fractions `crop` (left, top, right, - /// bottom) removed first: the visible part fills `r`. None for a degenerate size. - pub fn fit(&self, r: Rect, crop: [f32; 4]) -> Option { - let [cl, ct, cr, cb] = crop.map(|v| if v.is_finite() { f64::from(v.clamp(0.0, 0.95)) } else { 0.0 }); - let (w, h) = (f64::from(self.width), f64::from(self.height)); - let sx = r.width() / (w * (1.0 - cl - cr).max(0.05)); - let sy = r.height() / (h * (1.0 - ct - cb).max(0.05)); - (sx.is_finite() && sy.is_finite() && sx > 0.0 && sy > 0.0) - .then(|| Affine::translate((r.x0 - cl * w * sx, r.y0 - ct * h * sy)) * Affine::scale_non_uniform(sx, sy)) - } - - /// The items placed into `r` with `crop` applied. Items that are not finite or reach past ±1e6 - /// target units are left out (they are far off any page), as are bitmaps with no area. Stroke - /// widths are scaled to target units (along the mean axis scale, so a non-uniform fit keeps - /// strokes even). - pub fn place(&self, r: Rect, crop: [f32; 4]) -> Vec { - let Some(tf) = self.fit(r, crop) else { return Vec::new() }; - let c = tf.as_coeffs(); - let k = (c[0].abs() + c[3].abs()) / 2.0; - let mut out = Vec::with_capacity(self.items.len()); - let mut total_elements = 0usize; - let region_elements = std::cell::Cell::new(0usize); - let mut cached_region: Option<(*const [f32; 4], usize, Option>>)> = None; - let mut get_region = |reg: &Option>| -> Option>> { - let (ptr, len) = match reg { - None => (std::ptr::null(), 0), - Some(v) => (v.as_ptr(), v.len()), - }; - if let Some((c_ptr, c_len, ref res)) = cached_region - && c_ptr == ptr - && c_len == len - { - return res.clone(); - } - let res = placed_region(&tf, reg); - // Each distinct clip region is expanded once; its elements count against the placement budget. - let n = res.as_ref().and_then(Option::as_ref).map_or(0, |p| p.elements().len()); - region_elements.set(region_elements.get().saturating_add(n)); - cached_region = Some((ptr, len, res.clone())); - res - }; - - for (index, item) in self.items.iter().enumerate() { - if total_elements.saturating_add(region_elements.get()) >= MAX_PLACED_ELEMENTS { - break; - } - match item { - Item::Path { segs, fill, stroke, even_odd, region } => { - let path = tf * bez(segs); - let Some(region) = get_region(region) else { continue }; - if in_range(path.bounding_box()) { - // This item's own region is expanded by now: it counts before it is kept. - if total_elements.saturating_add(path.elements().len()).saturating_add(region_elements.get()) > MAX_PLACED_ELEMENTS { - break; - } - total_elements = total_elements.saturating_add(path.elements().len()); - let stroke = stroke.map(|(c, w)| (c, f64::from(w) * k)); - out.push(PlacedItem::Path { path, fill: *fill, stroke, even_odd: *even_odd, region }); - } - } - Item::Pattern { segs, even_odd, tile: t, region } => { - let path = tf * bez(segs); - let Some(region) = get_region(region) else { continue }; - let tile = tf - * Affine::translate((f64::from(t.origin[0]), f64::from(t.origin[1]))) - * Affine::scale_non_uniform(f64::from(t.cell[0]), f64::from(t.cell[1])); - let det = tile.determinant(); - if in_range(path.bounding_box()) && det.is_finite() && det.abs() > 1e-12 { - if total_elements.saturating_add(path.elements().len()).saturating_add(region_elements.get()) > MAX_PLACED_ELEMENTS { - break; - } - total_elements = total_elements.saturating_add(path.elements().len()); - out.push(PlacedItem::Pattern { index, path, even_odd: *even_odd, tile, region }); - } - } - Item::Text(t) => { - let transform = tf * Affine::new(t.transform); - let Some(region) = get_region(&t.region) else { continue }; - let clip = - t.clip.map(|[x0, y0, x1, y1]| tf.transform_rect_bbox(Rect::new(f64::from(x0), f64::from(y0), f64::from(x1), f64::from(y1)))); - let c = transform.as_coeffs(); - let origin = Rect::new(c[4], c[5], c[4], c[5]); - if in_range(origin) && c.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) && clip.is_none_or(in_range) { - out.push(PlacedItem::Text { index, transform, clip, region }); - } - } - Item::Bitmap { rect: [x, y, w, h], region, .. } => { - let (x0, y0) = (f64::from(*x), f64::from(*y)); - let rect = tf.transform_rect_bbox(Rect::new(x0, y0, x0 + f64::from(*w), y0 + f64::from(*h))); - let Some(region) = get_region(region) else { continue }; - if in_range(rect) && rect.width() > MIN_BITMAP_SIDE && rect.height() > MIN_BITMAP_SIDE { - out.push(PlacedItem::Bitmap { index, rect, region }); - } - } - } - } - out - } -} - -impl Item { - /// Takes a bitmap's pixels out of the item (its geometry stays) and returns them as (width, height, - /// RGBA) when a renderer can draw them: both sides in 1..=[`MAX_BITMAP_SIDE`] and exactly that many - /// pixels. None for paths and for bitmaps that cannot be drawn. - pub fn take_pixels(&mut self) -> Option<(u32, u32, Vec)> { - let Item::Bitmap { width, height, rgba, .. } = self else { return None }; - let (w, h, px) = (*width, *height, std::mem::take(rgba)); - let sides = (1..=MAX_BITMAP_SIDE).contains(&w) && (1..=MAX_BITMAP_SIDE).contains(&h); - (sides && px.len() == (w as usize).saturating_mul(h as usize).saturating_mul(4)).then_some((w, h, px)) - } -} - -/// The device-context clip in target units. `Some(None)` is the default clip. `None` means the -/// region does not meet the page, so the item draws nothing. -fn placed_region(tf: &Affine, region: &Option>) -> Option>> { - let Some(rects) = region else { return Some(None) }; - let mut path = BezPath::new(); - for [x0, y0, x1, y1] in rects { - let r = tf.transform_rect_bbox(Rect::new(f64::from(*x0), f64::from(*y0), f64::from(*x1), f64::from(*y1))); - if in_range(r) && r.width() > 1e-9 && r.height() > 1e-9 { - path.extend(r.to_path(0.1)); - } - } - (!path.is_empty()).then_some(Some(Arc::new(path))) -} - -/// Segments as a kurbo path. A segment after a close opens a new subpath at the last start point. -fn bez(segs: &[Seg]) -> BezPath { - let mut p = BezPath::new(); - let mut start = (0.0, 0.0); - let mut open = false; - for s in segs { - match *s { - Seg::Move(x, y) => { - start = (f64::from(x), f64::from(y)); - p.move_to(start); - open = true; - } - Seg::Line(x, y) => { - if !open { - p.move_to(start); - } - p.line_to((f64::from(x), f64::from(y))); - open = true; - } - Seg::Cubic(a, b, c, d, e, f) => { - if !open { - p.move_to(start); - } - p.curve_to((f64::from(a), f64::from(b)), (f64::from(c), f64::from(d)), (f64::from(e), f64::from(f))); - open = true; - } - Seg::Close => { - if open { - p.close_path(); - } - open = false; - } - } - } - p -} - -/// Whether a placed box is finite and within ±[`MAX_COORD`]. -fn in_range(b: Rect) -> bool { - [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() < MAX_COORD) -} diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs deleted file mode 100644 index 8655a11b9..000000000 --- a/crates/metafile/src/tests.rs +++ /dev/null @@ -1,2196 +0,0 @@ -//! Hand-built WMF and EMF buffers: each test writes the records it needs and checks the parsed model. - -use crate::canvas::Canvas; -use crate::dib::Rgba; -use crate::{Error, Item, MAX_BYTES, MAX_ITEMS, Seg, is_metafile, parse}; - -fn h16(v: i32) -> Vec { - (v as i16).to_le_bytes().to_vec() -} - -fn h32(v: u32) -> Vec { - v.to_le_bytes().to_vec() -} - -fn e32(v: i32) -> Vec { - v.to_le_bytes().to_vec() -} - -fn cat(parts: Vec>) -> Vec { - parts.concat() -} - -/// One WMF record: size in words, function, parameters (padded to a word boundary). -fn wrec(func: u16, params: &[u8]) -> Vec { - let mut p = params.to_vec(); - if !p.len().is_multiple_of(2) { - p.push(0); - } - let words = ((6 + p.len()) / 2) as u32; - let mut v = words.to_le_bytes().to_vec(); - v.extend(func.to_le_bytes()); - v.extend(p); - v -} - -/// A WMF file: optional placeable header (bbox left, top, right, bottom in logical units, 1440 per inch), -/// the standard header, the records and an end-of-file record. -fn wmf(records: &[Vec], placeable: Option<[i32; 4]>) -> Vec { - let body: Vec = records.concat(); - let eof = wrec(0, &[]); - let mut f = Vec::new(); - if let Some(bb) = placeable { - f.extend(0x9AC6_CDD7u32.to_le_bytes()); - f.extend(0u16.to_le_bytes()); - for v in bb { - f.extend((v as i16).to_le_bytes()); - } - f.extend(1440u16.to_le_bytes()); - f.extend(0u32.to_le_bytes()); - f.extend(0u16.to_le_bytes()); - } - let total = (f.len() + 18 + body.len() + eof.len()) / 2; - f.extend(1u16.to_le_bytes()); - f.extend(9u16.to_le_bytes()); - f.extend(0x300u16.to_le_bytes()); - f.extend((total as u32).to_le_bytes()); - f.extend(16u16.to_le_bytes()); - f.extend(0u32.to_le_bytes()); - f.extend(0u16.to_le_bytes()); - f.extend(body); - f.extend(eof); - f -} - -/// One EMF record: type, size in bytes (payload padded to 4), payload. -fn erec(typ: u32, payload: &[u8]) -> Vec { - let mut p = payload.to_vec(); - while !p.len().is_multiple_of(4) { - p.push(0); - } - let mut v = typ.to_le_bytes().to_vec(); - v.extend(((8 + p.len()) as u32).to_le_bytes()); - v.extend(p); - v -} - -/// An EMF file with an 88-byte header (bounds and frame in 0.01 mm), the records and an EOF record. -fn emf(records: &[Vec], bounds: [i32; 4], frame: [i32; 4]) -> Vec { - let body: Vec = records.concat(); - let eof = erec(14, &[0; 12]); - let total = 88 + body.len() + eof.len(); - let mut h = vec![0u8; 88]; - h[0..4].copy_from_slice(&1u32.to_le_bytes()); - h[4..8].copy_from_slice(&88u32.to_le_bytes()); - h[8..24].copy_from_slice(&bounds.iter().flat_map(|v| v.to_le_bytes()).collect::>()); - h[24..40].copy_from_slice(&frame.iter().flat_map(|v| v.to_le_bytes()).collect::>()); - h[40..44].copy_from_slice(&0x464D_4520u32.to_le_bytes()); - h[44..48].copy_from_slice(&0x0001_0000u32.to_le_bytes()); - h[48..52].copy_from_slice(&(total as u32).to_le_bytes()); - h[52..56].copy_from_slice(&((records.len() + 1) as u32).to_le_bytes()); - h[56..58].copy_from_slice(&1u16.to_le_bytes()); - // 1000 x 500 device pixels for 250 x 125 mm: 0.04 device units per hundredth of a millimetre. - h[72..76].copy_from_slice(&1000i32.to_le_bytes()); - h[76..80].copy_from_slice(&500i32.to_le_bytes()); - h[80..84].copy_from_slice(&250i32.to_le_bytes()); - h[84..88].copy_from_slice(&125i32.to_le_bytes()); - let mut out = h; - out.extend(body); - out.extend(eof); - out -} - -/// A BITMAPINFOHEADER (40 bytes). -fn info_header(w: i32, h: i32, bpp: u16, size_image: u32) -> Vec { - cat(vec![h32(40), e32(w), e32(h), h16(1), h16(i32::from(bpp)), h32(0), h32(size_image), e32(0), e32(0), h32(0), h32(0)]) -} - -/// A 2x2 24-bit bottom-up DIB. File rows are stored bottom first: the bottom image row is blue, green; -/// the top row is red, white. Rows are padded to 4 bytes (8 bytes per row). -fn dib24_bottom_up() -> Vec { - let mut v = info_header(2, 2, 24, 16); - v.extend([255, 0, 0, 0, 255, 0, 0, 0]); - v.extend([0, 0, 255, 255, 255, 255, 0, 0]); - v -} - -/// Top-down RGBA of [`dib24_bottom_up`]: top-left red, top-right white, bottom-left blue, bottom-right green. -const DIB24_RGBA: [u8; 16] = [255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 255, 0, 255]; - -fn path_segs(item: &Item) -> (&Vec, Option<[u8; 4]>, Option<([u8; 4], f32)>, bool) { - match item { - Item::Path { segs, fill, stroke, even_odd, .. } => (segs, *fill, *stroke, *even_odd), - other => panic!("expected a path: {other:?}"), - } -} - -fn close(a: f32, b: f32) -> bool { - (a - b).abs() < 1e-3 -} - -#[test] -fn wmf_rectangle_with_solid_brush() { - let recs = [ - wrec(0x02FC, &cat(vec![h16(0), h32(0x0000_00FF), h16(0)])), - wrec(0x012D, &h16(0)), - wrec(0x041B, &cat(vec![h16(70), h16(110), h16(20), h16(10)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (100.0, 50.0)); - assert_eq!(p.items.len(), 1); - let (segs, fill, stroke, even_odd) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(100.0, 0.0), Seg::Line(100.0, 50.0), Seg::Line(0.0, 50.0), Seg::Close]); - assert_eq!(fill, Some([255, 0, 0, 255])); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); - assert!(even_odd); -} - -#[test] -fn wmf_polyline_with_pen() { - let recs = [ - wrec(0x02FA, &cat(vec![h16(0), h16(4), h16(0), h32(0x0000_FF00)])), - wrec(0x012D, &h16(0)), - wrec(0x0325, &cat(vec![h16(3), h16(0), h16(0), h16(50), h16(0), h16(50), h16(40)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (50.0, 40.0)); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(50.0, 0.0), Seg::Line(50.0, 40.0)]); - assert_eq!(fill, None); - assert_eq!(stroke, Some(([0, 255, 0, 255], 4.0))); -} - -#[test] -fn wmf_polygon_fill_rule_follows_polyfillmode() { - let square = cat(vec![h16(4), h16(0), h16(0), h16(10), h16(0), h16(10), h16(10), h16(0), h16(10)]); - let winding = [wrec(0x0106, &h16(2)), wrec(0x0324, &square)]; - let p = parse(&wmf(&winding, None)).unwrap(); - assert!(!path_segs(&p.items[0]).3, "WINDING fills use the nonzero rule"); - let alternate = [wrec(0x0324, &square)]; - let p = parse(&wmf(&alternate, None)).unwrap(); - let (segs, fill, _, even_odd) = path_segs(&p.items[0]); - assert!(even_odd, "default ALTERNATE is even-odd"); - assert_eq!(fill, Some([255, 255, 255, 255])); - assert_eq!(segs.last(), Some(&Seg::Close)); -} - -#[test] -fn wmf_stretchdib_flips_bottom_up_rows() { - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(0), h16(0), dib24_bottom_up()]); - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], None)).unwrap(); - assert_eq!((p.width, p.height), (20.0, 10.0)); - let Item::Bitmap { rect, width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[0.0, 0.0, 20.0, 10.0]); - assert_eq!((*width, *height), (2, 2)); - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn wmf_mapping_window_and_viewport() { - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020B, &cat(vec![h16(100), h16(100)])), - wrec(0x020C, &cat(vec![h16(50), h16(50)])), - wrec(0x020E, &cat(vec![h16(100), h16(100)])), - wrec(0x0214, &cat(vec![h16(100), h16(100)])), - wrec(0x0213, &cat(vec![h16(150), h16(150)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (100.0, 100.0)); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(100.0, 100.0)]); - assert_eq!(fill, None); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); -} - -#[test] -fn wmf_placeable_header_sets_frame_in_points() { - let p = parse(&wmf(&[wrec(0x041B, &cat(vec![h16(1440), h16(2880), h16(0), h16(0)]))], Some([0, 0, 2880, 1440]))).unwrap(); - assert_eq!((p.width, p.height), (144.0, 72.0)); -} - -#[test] -fn emf_stretchdibits_flips_and_fits_frame() { - let mut payload = vec![0u8; 16]; - payload.extend(cat(vec![ - e32(0), - e32(0), - e32(0), - e32(0), - e32(2), - e32(2), - e32(80), - e32(40), - e32(120), - e32(16), - e32(0), - e32(0x00CC_0020), - e32(100), - e32(50), - ])); - payload.extend(info_header(2, 2, 24, 16)); - payload.extend(dib24_bottom_up().get(40..).unwrap_or_default()); - let rec = erec(81, &payload); - // A 25 x 12.5 mm frame is 70.87 x 35.43 points; the bitmap fills the whole frame. - let file = emf(&[rec], [0, 0, 100, 50], [0, 0, 2500, 1250]); - let p = parse(&file).unwrap(); - let (w, h) = (2500.0 * 72.0 / 2540.0, 1250.0 * 72.0 / 2540.0); - assert!(close(p.width, w as f32) && close(p.height, h as f32)); - let Item::Bitmap { rect, width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert!(rect.iter().zip([0.0, 0.0, w, h]).all(|(a, b)| close(*a, b as f32))); - assert_eq!((*width, *height), (2, 2)); - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn emf_world_transform_scales_geometry() { - let xf = cat([2.0f32, 0.0, 0.0, 2.0, 0.0, 0.0].iter().map(|v| v.to_le_bytes().to_vec()).collect()); - let recs = [erec(35, &xf), erec(27, &cat(vec![e32(0), e32(0)])), erec(54, &cat(vec![e32(10), e32(5)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - assert_eq!((p.width, p.height), (20.0, 10.0)); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(20.0, 10.0)]); - assert_eq!(fill, None); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); -} - -#[test] -fn emf_ellipse_is_four_cubics_back_to_start() { - let rect = cat(vec![e32(0), e32(0), e32(40), e32(20)]); - let p = parse(&emf(&[erec(42, &rect)], [0; 4], [0; 4])).unwrap(); - let (segs, fill, _, _) = path_segs(&p.items[0]); - assert_eq!(segs.len(), 6); - assert_eq!(segs[0], Seg::Move(40.0, 10.0)); - assert!(matches!(segs[4], Seg::Cubic(_, _, _, _, x, y) if close(x, 40.0) && close(y, 10.0))); - assert_eq!(segs[5], Seg::Close); - assert_eq!(fill, Some([255, 255, 255, 255])); -} - -#[test] -fn emf_path_bracket_strokeandfill_uses_stock_black_brush() { - let bounds = vec![0u8; 16]; - let recs = [ - erec(59, &[]), - erec(27, &cat(vec![e32(0), e32(0)])), - erec(54, &cat(vec![e32(10), e32(0)])), - erec(54, &cat(vec![e32(10), e32(10)])), - erec(61, &[]), - erec(60, &[]), - erec(37, &h32(0x8000_0004)), - erec(63, &bounds), - ]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(10.0, 10.0), Seg::Close]); - assert_eq!(fill, Some([0, 0, 0, 255])); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); -} - -#[test] -fn item_cap_stops_the_parse_and_keeps_the_rest() { - let rect = wrec(0x041B, &cat(vec![h16(10), h16(10), h16(0), h16(0)])); - let recs: Vec> = (0..MAX_ITEMS + 10).map(|_| rect.clone()).collect(); - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!(p.items.len(), MAX_ITEMS); -} - -#[test] -fn detection_and_garbage() { - let w = wmf(&[wrec(0x041B, &cat(vec![h16(1), h16(1), h16(0), h16(0)]))], None); - let e = emf(&[erec(42, &cat(vec![e32(0), e32(0), e32(4), e32(4)]))], [0; 4], [0; 4]); - assert!(is_metafile(&w)); - assert!(is_metafile(&e)); - assert!(!is_metafile(b"hello world, not a metafile")); - assert_eq!(parse(b"hello world, not a metafile").unwrap_err(), Error::NotMetafile); - assert!(parse(&[]).is_err()); -} - -/// Fixed-seed LCG (Knuth's constants): the fuzz inputs are reproducible. -struct Lcg(u64); - -impl Lcg { - fn rnd(&mut self) -> u32 { - self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); - (self.0 >> 33) as u32 - } -} - -#[test] -fn truncated_random_and_mutated_inputs_never_panic() { - let rect = wrec(0x041B, &cat(vec![h16(10), h16(10), h16(0), h16(0)])); - let wfile = wmf( - &[ - wrec(0x02FC, &cat(vec![h16(0), h32(0xFF), h16(0)])), - wrec(0x012D, &h16(0)), - rect, - wrec(0x0325, &cat(vec![h16(2), h16(0), h16(0), h16(5), h16(5)])), - ], - Some([0, 0, 20, 10]), - ); - let mut dib_rec = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(0), h16(0)]); - dib_rec.extend(dib24_bottom_up()); - let mut payload = vec![0u8; 16]; - payload.extend(cat(vec![e32(0), e32(0), e32(0), e32(0), e32(2), e32(2), e32(80), e32(40), e32(120), e32(16), e32(0), e32(0), e32(20), e32(10)])); - payload.extend(info_header(2, 2, 24, 16)); - payload.extend(dib24_bottom_up().get(40..).unwrap_or_default()); - let efile = emf( - &[ - erec(59, &[]), - erec(27, &cat(vec![e32(0), e32(0)])), - erec(54, &cat(vec![e32(9), e32(9)])), - erec(61, &[]), - erec(60, &[]), - erec(63, &[0; 16]), - erec(81, &payload), - erec(86, &cat(vec![h32(4), h16(0), h16(0), h16(9), h16(9), h16(0), h16(9)])), - ], - [0, 0, 20, 10], - [0, 0, 2540, 1270], - ); - for file in [&wfile, &efile] { - for n in 0..=file.len() { - let prefix = &file[..n]; - let _ = parse(prefix); - let _ = is_metafile(prefix); - } - } - let mut rng = Lcg(0x2545_F491_4F6C_DD1D); - for _ in 0..3000 { - let len = (rng.rnd() % 400) as usize; - let buf: Vec = (0..len).map(|_| rng.rnd() as u8).collect(); - let _ = parse(&buf); - } - for i in 0..1500usize { - let mut buf = if i.is_multiple_of(2) { efile.clone() } else { wfile.clone() }; - for _ in 0..8 { - let at = rng.rnd() as usize % buf.len(); - buf[at] = rng.rnd() as u8; - } - let _ = parse(&buf); - } - for i in 0..500usize { - let mut buf = if i.is_multiple_of(2) { efile.clone() } else { wfile.clone() }; - let at = rng.rnd() as usize % buf.len(); - buf[at..].iter_mut().for_each(|b| *b = 0xFF); - let _ = parse(&buf); - } -} - -#[test] -fn wmf_stock_null_brush_by_16_bit_handle_has_no_fill() { - // SELECTOBJECT with 0x8005 is the NULL_BRUSH stock object (0x8000 | 5), so the rectangle is outline only. - let recs = [wrec(0x012D, &h16(0x8005u16 as i32)), wrec(0x041B, &cat(vec![h16(70), h16(110), h16(20), h16(10)]))]; - let p = parse(&wmf(&recs, None)).unwrap(); - let (_, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(fill, None); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); -} - -#[test] -fn emf_frame_comes_from_rclframe_not_device_bounds() { - // Bounds cover only the left half of the 25 x 12.5 mm frame (device units are 0.04 per 0.01 mm), so a - // rectangle from x = 0 to 50 px ends at half the picture width. - let rec = erec(43, &cat(vec![e32(0), e32(0), e32(50), e32(25)])); - let p = parse(&emf(&[rec], [0, 0, 50, 25], [0, 0, 2500, 1250])).unwrap(); - let w = 2500.0 * 72.0 / 2540.0; - assert!(close(p.width, w as f32) && close(p.height, (1250.0 * 72.0 / 2540.0) as f32)); - let (segs, ..) = path_segs(&p.items[0]); - assert!(matches!(segs[1], Seg::Line(x, _) if close(x, (w / 2.0) as f32)), "{segs:?}"); -} - -#[test] -fn emf_bounds_stand_in_when_the_frame_is_empty() { - let rec = erec(43, &cat(vec![e32(0), e32(0), e32(50), e32(25)])); - let p = parse(&emf(&[rec], [0, 0, 50, 25], [0; 4])).unwrap(); - assert_eq!((p.width, p.height), (50.0, 25.0)); -} - -#[test] -fn wmf_placeable_bounds_go_through_the_window_mapping() { - // Logical bounds 200 x 100 at 1440 per inch are 10 x 5 points; the window is 2:1 onto the viewport, so - // the frame is 100 x 50 device units and the rectangle fills it. - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020C, &cat(vec![h16(100), h16(200)])), - wrec(0x020E, &cat(vec![h16(50), h16(100)])), - wrec(0x041B, &cat(vec![h16(100), h16(200), h16(0), h16(0)])), - ]; - let p = parse(&wmf(&recs, Some([0, 0, 200, 100]))).unwrap(); - assert_eq!((p.width, p.height), (10.0, 5.0)); - let (segs, ..) = path_segs(&p.items[0]); - assert!(segs.iter().any(|s| matches!(s, Seg::Line(x, y) if close(*x, 10.0) && close(*y, 5.0))), "{segs:?}"); -} - -#[test] -fn wmf_stretchdib_clipped_source_keeps_its_place() { - // Source x from -1 for 2 columns: only column 0 is in the bitmap, so it fills the right half of the - // destination and the picture keeps the original 20-unit width scale. - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(-1), h16(10), h16(20), h16(0), h16(0), dib24_bottom_up()]); - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], Some([0, 0, 20, 10]))).unwrap(); - let Item::Bitmap { rect, width, height, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - // Placeable units are points: 20 units at 1440 per inch are 1 point, so the right column is 0.5 pt wide. - assert_eq!(rect, &[0.5, 0.0, 0.5, 0.5]); - assert_eq!((*width, *height), (1, 2)); - assert_eq!(rgba.as_slice(), [255, 0, 0, 255, 0, 0, 255, 255]); -} - -#[test] -fn wmf_negative_destination_width_mirrors_the_bitmap() { - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(-20), h16(0), h16(0), dib24_bottom_up()]); - // Frame from the placeable bounds (-20, 0)-(0, 10): the mirrored bitmap fills it. - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], Some([-20, 0, 0, 10]))).unwrap(); - let Item::Bitmap { rect, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[0.0, 0.0, 1.0, 0.5]); - assert_eq!(&rgba[..8], [255, 255, 255, 255, 255, 0, 0, 255]); -} - -#[test] -fn emf_blackness_bitblt_fills_black_without_a_source() { - let mut payload = vec![0u8; 16]; - payload.extend(cat(vec![e32(10), e32(20), e32(30), e32(40), e32(0x0000_0042)])); - let p = parse(&emf(&[erec(76, &payload)], [0; 4], [0; 4])).unwrap(); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(fill, Some([0, 0, 0, 255])); - assert_eq!(stroke, None); - assert_eq!(segs.len(), 5); -} - -#[test] -fn emf_savedc_past_the_stack_cap_stops_the_parse() { - let mut recs: Vec> = (0..=crate::MAX_STACK).map(|_| erec(33, &[])).collect(); - recs.push(erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))); - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - assert!(p.items.is_empty(), "records after the cap must not be drawn"); -} - -#[test] -fn canvas_stops_at_the_byte_budget() { - // Two 128 MB bitmaps fit the budget; the third would exceed it and stops the canvas. - let mut cv = Canvas::new(); - let big = || Rgba { w: 1, h: 1, bottom_up: false, px: vec![0u8; MAX_BYTES / 2] }; - cv.bitmap([0.0, 0.0, 1.0, 1.0], big()); - cv.bitmap([0.0, 0.0, 1.0, 1.0], big()); - assert!(!cv.full()); - cv.bitmap([0.0, 0.0, 1.0, 1.0], big()); - assert!(cv.full()); - assert_eq!(cv.items.len(), 2); -} - -#[test] -fn oversized_dib_header_size_is_rejected_without_overflow() { - let mut info = info_header(2, 2, 24, 16); - info[0..4].copy_from_slice(&0xFFFF_FFF0u32.to_le_bytes()); - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(0), h16(0), info]); - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], None)).unwrap(); - assert!(p.items.is_empty()); -} - -/// An EMF record of `len` bytes (header included) with u32 fields and byte blobs written at absolute -/// record offsets, so each test places a field where the spec puts it. -fn erec_at(typ: u32, len: usize, fields: &[(usize, u32)], blobs: &[(usize, &[u8])]) -> Vec { - let mut v = vec![0u8; len]; - v[0..4].copy_from_slice(&typ.to_le_bytes()); - v[4..8].copy_from_slice(&(len as u32).to_le_bytes()); - for (off, val) in fields { - v[*off..*off + 4].copy_from_slice(&val.to_le_bytes()); - } - for (off, b) in blobs { - v[*off..*off + b.len()].copy_from_slice(b); - } - v -} - -/// A BITBLT-style record (BITBLT, STRETCHBLT, ALPHABLEND layout) with its DIB at 112: the bmi and bits -/// offsets and lengths go at 84 and 92. `fields` and `blobs` add the other fields. -fn dib_rec(typ: u32, fields: &[(usize, u32)], blobs: &[(usize, &[u8])], info: &[u8], bits: &[u8]) -> Vec { - let at = 112; - let bits_at = at + info.len(); - let mut f = fields.to_vec(); - f.extend([(84, at as u32), (88, info.len() as u32), (92, bits_at as u32), (96, bits.len() as u32)]); - let mut b: Vec<(usize, &[u8])> = blobs.to_vec(); - b.extend([(at, info), (bits_at, bits)]); - erec_at(typ, bits_at + bits.len(), &f, &b) -} - -/// BITBLT (76) with destination `dst` = [x, y, w, h] and a 40-byte header DIB. -fn bitblt_rec(rop: u32, dst: [u32; 4], info: &[u8], bits: &[u8]) -> Vec { - let fields = [(24, dst[0]), (28, dst[1]), (32, dst[2]), (36, dst[3]), (40, rop)]; - dib_rec(76, &fields, &[], info, bits) -} - -#[test] -fn wmf_font_takes_an_object_slot_so_handles_line_up() { - // The font is handle 0, so the pen created after it is handle 1. - let recs = [ - wrec(0x02FB, &[0u8; 18]), - wrec(0x02FA, &cat(vec![h16(0), h16(4), h16(0), h32(0x0000_FF00)])), - wrec(0x012D, &h16(1)), - wrec(0x0325, &cat(vec![h16(2), h16(0), h16(0), h16(50), h16(0), h16(50)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - let (_, _, stroke, _) = path_segs(&p.items[0]); - assert_eq!(stroke, Some(([0, 255, 0, 255], 4.0))); -} - -#[test] -fn emf_polybezierto16_continues_the_open_path() { - // BEGINPATH, MOVETO a, LINETO b, POLYBEZIERTO16, CLOSEFIGURE, FILLPATH: one closed figure from a. - let mut bezier = vec![0u8; 16]; - bezier.extend(cat(vec![e32(3), h16(10), h16(10), h16(20), h16(10), h16(20), h16(0)])); - let recs = [ - erec(59, &[]), - erec(27, &cat(vec![e32(0), e32(0)])), - erec(54, &cat(vec![e32(10), e32(0)])), - erec(88, &bezier), - erec(61, &[]), - erec(62, &[0; 16]), - ]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); - let (segs, fill, _, _) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Cubic(10.0, 10.0, 20.0, 10.0, 20.0, 0.0), Seg::Close]); - assert_eq!(fill, Some([255, 255, 255, 255])); -} - -#[test] -fn emf_polybezier_starts_its_own_figure_and_keeps_the_current_point() { - // POLYBEZIER (2) starts at its first point; the LINETO after it still starts at the MOVETO point. - let mut bezier = vec![0u8; 16]; - bezier.extend(cat(vec![e32(4), e32(0), e32(0), e32(5), e32(5), e32(10), e32(5), e32(10), e32(0)])); - let recs = [erec(27, &cat(vec![e32(3), e32(3)])), erec(2, &bezier), erec(54, &cat(vec![e32(50), e32(50)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 2); - let (segs, ..) = path_segs(&p.items[0]); - assert_eq!(segs[0], Seg::Move(0.0, 0.0)); - let (segs, ..) = path_segs(&p.items[1]); - assert_eq!(segs, &vec![Seg::Move(3.0, 3.0), Seg::Line(50.0, 50.0)]); -} - -#[test] -fn hairline_pen_stays_zero_under_an_anisotropic_window() { - // Window 2000 x 2000 onto a 1 x 1 viewport: a cosmetic pen must not become one device unit wide. - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020C, &cat(vec![h16(2000), h16(2000)])), - wrec(0x020E, &cat(vec![h16(1), h16(1)])), - wrec(0x0214, &cat(vec![h16(0), h16(0)])), - wrec(0x0213, &cat(vec![h16(2000), h16(2000)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (1.0, 1.0)); - let (segs, _, stroke, _) = path_segs(&p.items[0]); - assert_eq!(segs.last(), Some(&Seg::Line(1.0, 1.0))); - assert_eq!(stroke, Some(([0, 0, 0, 255], 0.0))); -} - -#[test] -fn null_pen_is_recognised_under_extra_style_bits() { - // Pen style 0x105: PS_NULL (5) with a flag above the style nibble still draws no outline. - let pen = erec_at(38, 28, &[(8, 0), (12, 0x105), (16, 1), (24, 0)], &[]); - let recs = [pen, erec(37, &h32(0)), erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - let (_, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(fill, Some([255, 255, 255, 255])); - assert_eq!(stroke, None); -} - -#[test] -fn metric_map_mode_flips_y_and_scales_to_device_pixels_emf() { - // LOMETRIC is 0.1 mm per unit; the header maps 4 device pixels to 1 mm. Points go y-up, so the - // drawing lies below its origin and the picture frame moves up to its top. - let recs = [erec(17, &h32(2)), erec(27, &cat(vec![e32(0), e32(0)])), erec(54, &cat(vec![e32(100), e32(50)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - let (segs, ..) = path_segs(&p.items[0]); - assert!(close_seg(&segs[0], Seg::Move(0.0, 20.0)) && close_seg(&segs[1], Seg::Line(40.0, 0.0)), "{segs:?}"); -} - -#[test] -fn metric_map_mode_uses_96_dpi_for_wmf() { - let recs = [wrec(0x0103, &h16(2)), wrec(0x0214, &cat(vec![h16(0), h16(0)])), wrec(0x0213, &cat(vec![h16(50), h16(100)]))]; - let p = parse(&wmf(&recs, None)).unwrap(); - let px_mm = 96.0 / 25.4; - let (segs, ..) = path_segs(&p.items[0]); - let (x, y) = (100.0 * 0.1 * px_mm, 50.0 * 0.1 * px_mm); - assert!(close_seg(&segs[0], Seg::Move(0.0, y as f32)) && close_seg(&segs[1], Seg::Line(x as f32, 0.0)), "{segs:?}"); -} - -/// Segment equality within [`close`] tolerance. -fn close_seg(a: &Seg, b: Seg) -> bool { - match (*a, b) { - (Seg::Move(x, y), Seg::Move(u, v)) | (Seg::Line(x, y), Seg::Line(u, v)) => close(x, u) && close(y, v), - _ => false, - } -} - -#[test] -fn isotropic_mode_uses_one_scale_and_keeps_axis_signs() { - // Window 100 x 50; viewport 400 x -100: the scale is min(4, 2) = 2 on both axes, y flipped. - let recs = [ - wrec(0x0103, &h16(7)), - wrec(0x020C, &cat(vec![h16(50), h16(100)])), - wrec(0x020E, &cat(vec![h16(-100), h16(400)])), - wrec(0x0214, &cat(vec![h16(0), h16(0)])), - wrec(0x0213, &cat(vec![h16(25), h16(50)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - let (segs, ..) = path_segs(&p.items[0]); - assert!(close_seg(&segs[0], Seg::Move(0.0, 100.0)) && close_seg(&segs[1], Seg::Line(100.0, 50.0)), "{segs:?}"); -} - -#[test] -fn modify_world_with_an_unknown_mode_keeps_the_transform() { - let scale2 = cat([2.0f32, 0.0, 0.0, 2.0, 0.0, 0.0].iter().map(|v| v.to_le_bytes().to_vec()).collect()); - let identity = cat([1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0].iter().map(|v| v.to_le_bytes().to_vec()).collect()); - let recs = - [erec(35, &scale2), erec(36, &cat(vec![identity, h32(99)])), erec(27, &cat(vec![e32(0), e32(0)])), erec(54, &cat(vec![e32(10), e32(5)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - let (segs, ..) = path_segs(&p.items[0]); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(20.0, 10.0)]); -} - -#[test] -fn round_rect_accepts_reversed_corners() { - let normal = crate::geom::round_rect(0.0, 0.0, 10.0, 20.0, 4.0, 4.0); - assert_eq!(crate::geom::round_rect(10.0, 20.0, 0.0, 0.0, 4.0, 4.0), normal); - assert_eq!(crate::geom::round_rect(10.0, 0.0, 0.0, 20.0, 4.0, 4.0), normal); -} - -#[test] -fn stroke_width_is_clamped_to_the_picture_diagonal() { - // A 1440-unit placeable picture is 72 points square (diagonal 101.8); a 5000-unit pen would be 250. - let recs = [ - wrec(0x02FA, &cat(vec![h16(0), h16(5000), h16(0), h32(0)])), - wrec(0x012D, &h16(0)), - wrec(0x041B, &cat(vec![h16(1440), h16(1440), h16(0), h16(0)])), - ]; - let p = parse(&wmf(&recs, Some([0, 0, 1440, 1440]))).unwrap(); - assert_eq!((p.width, p.height), (72.0, 72.0)); - let (_, _, stroke, _) = path_segs(&p.items[0]); - assert!(close(stroke.unwrap_or(([0; 4], 0.0)).1, 72.0 * 2.0f32.sqrt()), "{stroke:?}"); -} - -/// A WMF rectangle from (0, 0) to (1, 1), so a picture's frame starts at the origin. -fn origin_rect() -> Vec { - wrec(0x041B, &cat(vec![h16(1), h16(1), h16(0), h16(0)])) -} - -#[test] -fn wmf_dibbitblt_without_a_dib_is_a_pattern_fill() { - // The 12-word form: rop(4), ySrc, xSrc, reserved, height, width, yDest, xDest; PATCOPY fills with the brush. - let params = cat(vec![h32(0x00F0_0021), h16(0), h16(0), h16(0), h16(10), h16(20), h16(5), h16(7)]); - let p = parse(&wmf(&[origin_rect(), wrec(0x0940, ¶ms)], None)).unwrap(); - let (segs, fill, _, _) = path_segs(&p.items[1]); - assert_eq!(fill, Some([255, 255, 255, 255])); - assert_eq!(segs, &vec![Seg::Move(7.0, 5.0), Seg::Line(27.0, 5.0), Seg::Line(27.0, 15.0), Seg::Line(7.0, 15.0), Seg::Close]); -} - -#[test] -fn wmf_dibstretchblt_without_a_dib_is_a_rop_fill() { - // The 14-word form: rop(4), srcH, srcW, ySrc, xSrc, reserved, destH, destW, yDest, xDest; BLACKNESS fills black. - let params = cat(vec![h32(0x0000_0042), h16(2), h16(2), h16(0), h16(0), h16(0), h16(10), h16(20), h16(5), h16(7)]); - let p = parse(&wmf(&[origin_rect(), wrec(0x0B41, ¶ms)], None)).unwrap(); - let (segs, fill, stroke, _) = path_segs(&p.items[1]); - assert_eq!((fill, stroke), (Some([0, 0, 0, 255]), None)); - assert_eq!(segs, &vec![Seg::Move(7.0, 5.0), Seg::Line(27.0, 5.0), Seg::Line(27.0, 15.0), Seg::Line(7.0, 15.0), Seg::Close]); -} - -#[test] -fn wmf_dibstretchblt_stretches_the_dib_into_its_destination() { - // rop(4), srcH, srcW, ySrc, xSrc, destH, destW, yDest, xDest, then the DIB (no reserved word). - let mut params = cat(vec![h32(0x00CC_0020), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(6), h16(5)]); - params.extend(dib24_bottom_up()); - let p = parse(&wmf(&[origin_rect(), wrec(0x0B41, ¶ms)], None)).unwrap(); - let Item::Bitmap { rect, rgba, .. } = &p.items[1] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[5.0, 6.0, 20.0, 10.0]); - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn wmf_negative_destination_height_mirrors_the_rows() { - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(-10), h16(20), h16(0), h16(0), dib24_bottom_up()]); - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], None)).unwrap(); - let Item::Bitmap { rect, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[0.0, 0.0, 20.0, 10.0]); - // Blue, green on top; red, white below. - assert_eq!(rgba.as_slice(), [0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255]); -} - -#[test] -fn wmf_dibbitblt_copies_the_dib_at_its_destination() { - // The rectangle fixes the picture origin at (0, 0); the DIB lands at x = 5, y = 6. - let mut params = cat(vec![h32(0x00CC_0020), h16(0), h16(0), h16(2), h16(2), h16(6), h16(5)]); - params.extend(dib24_bottom_up()); - let recs = [wrec(0x041B, &cat(vec![h16(10), h16(10), h16(0), h16(0)])), wrec(0x0940, ¶ms)]; - let p = parse(&wmf(&recs, None)).unwrap(); - let Item::Bitmap { rect, width, height, rgba, .. } = &p.items[1] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[5.0, 6.0, 2.0, 2.0]); - assert_eq!((*width, *height), (2, 2)); - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn emf_setdibitstodevice_draws_the_image_at_its_own_size() { - let dib = dib24_bottom_up(); - let (info, bits) = (&dib[..40], &dib[40..]); - // Destination x/y at 24, src x/y at 32, size at 40, bmi at 48 (offset, length), bits at 56, scan start - // 68 and scan count 72. - let fields = [(24, 10), (28, 20), (40, 2), (44, 2), (48, 112), (52, 40), (56, 152), (60, 16), (72, 2)]; - let rec = erec_at(80, 168, &fields, &[(112, info), (152, bits)]); - let p = parse(&emf(&[rec], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rect, rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[0.0, 0.0, 2.0, 2.0]); - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn emf_setdibitstodevice_source_counts_from_the_start_scan() { - // ySrc 1 with the bits starting at scan 1 is source row 0 of the bits: the bottom row (blue, green), - // drawn cxSrc x cySrc = 2 x 1 at the destination. - let dib = dib24_bottom_up(); - let (info, bits) = (&dib[..40], &dib[40..]); - let fields = [(24, 10), (28, 20), (36, 1), (40, 2), (44, 1), (48, 112), (52, 40), (56, 152), (60, 16), (68, 1), (72, 2)]; - let rec = erec_at(80, 168, &fields, &[(112, info), (152, bits)]); - let r = erec(43, &cat(vec![e32(0), e32(0), e32(1), e32(1)])); - let p = parse(&emf(&[r, rec], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rect, rgba, .. } = &p.items[1] else { panic!("expected a bitmap") }; - assert_eq!(rect, &[10.0, 20.0, 2.0, 1.0]); - assert_eq!(rgba.as_slice(), [0, 0, 255, 255, 0, 255, 0, 255]); -} - -#[test] -fn emf_alphablend_uses_constant_and_per_pixel_alpha() { - // Constant alpha 128 (BLENDFUNCTION bytes at 40: op, flags, constant, format) over a 24-bit DIB. - let blend = [0u8, 0, 128, 0]; - let dib = dib24_bottom_up(); - let rec = dib_rec(114, &[(24, 0), (28, 0), (32, 2), (36, 2), (44, 0), (48, 0), (100, 2), (104, 2)], &[(40, &blend[..])], &dib[..40], &dib[40..]); - let p = parse(&emf(&[rec], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - let alpha: Vec = rgba.as_chunks::<4>().0.iter().map(|px| px[3]).collect(); - assert_eq!(alpha, vec![128; 4]); - // Per-pixel alpha (AC_SRC_ALPHA) over a 32-bit premultiplied DIB: BGRA 0, 0, 128, 128 is straight red at - // half alpha, and the constant 128 halves the alpha again. - let one_px = |constant: u8, bgra: [u8; 4]| { - let blend = [0u8, 0, constant, 1]; - let info = info_header(1, 1, 32, 4); - let rec = dib_rec(114, &[(24, 0), (28, 0), (32, 1), (36, 1), (44, 0), (48, 0), (100, 1), (104, 1)], &[(40, &blend[..])], &info, &bgra); - let p = parse(&emf(&[rec], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - rgba.clone() - }; - assert_eq!(one_px(255, [0, 0, 128, 128]), [255, 0, 0, 128]); - assert_eq!(one_px(128, [0, 0, 128, 128]), [255, 0, 0, 64]); - assert_eq!(one_px(255, [10, 20, 64, 128]), [128, 40, 20, 128], "rounded to the nearest value"); -} - -#[test] -fn emf_srcand_mask_draws_all_but_its_white_pixels() { - // SRCAND over white paper is the source itself; white keeps the destination, so it is transparent. - let dib = dib24_bottom_up(); - let rec = bitblt_rec(0x0088_00C6, [0, 0, 2, 2], &dib[..40], &dib[40..]); - let p = parse(&emf(&[rec], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rgba.as_slice(), [255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 0, 255]); -} - -#[test] -fn emf_srcpaint_makes_black_pixels_transparent() { - // A 2 x 1 DIB: black, then red. SRCPAINT leaves black transparent; SRCCOPY keeps it opaque. - let info = info_header(2, 1, 24, 8); - let bits = [0u8, 0, 0, 0, 0, 255, 0, 0]; - let p = parse(&emf(&[bitblt_rec(0x00EE_0086, [0, 0, 2, 1], &info, &bits)], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rgba.as_slice(), [0, 0, 0, 0, 255, 0, 0, 255]); - let p = parse(&emf(&[bitblt_rec(0x00CC_0020, [0, 0, 2, 1], &info, &bits)], [0; 4], [0; 4])).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rgba.as_slice(), [0, 0, 0, 255, 255, 0, 0, 255]); -} - -#[test] -fn wmf_dib_palette_entries_are_skipped_before_the_bits() { - // A 24-bit DIB with biClrUsed = 2 carries two RGBQUADs (8 bytes) before its pixel bits. - let mut info = info_header(2, 2, 24, 16); - info[32..36].copy_from_slice(&2u32.to_le_bytes()); - let mut dib = info; - dib.extend([9u8; 8]); - dib.extend(&dib24_bottom_up()[40..]); - let params = cat(vec![h32(0x00CC_0020), h16(0), h16(2), h16(2), h16(0), h16(0), h16(10), h16(20), h16(0), h16(0), dib]); - let p = parse(&wmf(&[wrec(0x0F43, ¶ms)], None)).unwrap(); - let Item::Bitmap { rgba, .. } = &p.items[0] else { panic!("expected a bitmap") }; - assert_eq!(rgba.as_slice(), DIB24_RGBA); -} - -#[test] -fn emf_point_counts_past_the_budget_are_rejected() { - // A count of 4 billion points in a record that cannot hold them: nothing is drawn and nothing is allocated. - let mut poly = vec![0u8; 16]; - poly.extend(cat(vec![h32(u32::MAX), h32(0), h32(0)])); - let mut groups = vec![0u8; 16]; - groups.extend(cat(vec![h32(u32::MAX), h32(u32::MAX), h32(1)])); - let p = parse(&emf(&[erec(4, &poly), erec(7, &groups)], [0; 4], [0; 4])).unwrap(); - assert!(p.items.is_empty()); -} - -#[test] -fn emf_polypolygon_counts_must_fit_the_record() { - // A polygon count within the point budget in a record too short to hold the counts draws nothing. - let mut short = vec![0u8; 16]; - short.extend(cat(vec![h32(crate::MAX_PATH_OPS as u32), h32(3), h32(3)])); - // POLYPOLYGON16: two triangles of three points each. - let mut two = vec![0u8; 16]; - two.extend(cat(vec![h32(2), h32(6), h32(3), h32(3)])); - two.extend(cat([0, 0, 10, 0, 0, 10, 20, 0, 30, 0, 20, 10].iter().map(|v| h16(*v)).collect())); - let p = parse(&emf(&[erec(91, &short), erec(91, &two)], [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); - let (segs, ..) = path_segs(&p.items[0]); - assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Move(..))).count(), 2, "{segs:?}"); -} - -/// EMR_EXTCREATEPEN into slot 1 with LogPenEx style, width and brush style, then a selected rectangle. -fn ext_pen_rect(style: u32, width: u32, brush: u32) -> Option<([u8; 4], f32)> { - let pen = erec_at(95, 52, &[(8, 1), (28, style), (32, width), (36, brush), (40, 0x0000_FF00)], &[]); - let recs = [pen, erec(37, &h32(1)), erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))]; - let p = parse(&emf(&recs, [0; 4], [0; 4])).unwrap(); - path_segs(&p.items[0]).2 -} - -#[test] -fn emf_ext_pen_cosmetic_is_a_hairline_and_null_brush_draws_nothing() { - // PS_COSMETIC widths are device pixels: a hairline, not one logical unit. - assert_eq!(ext_pen_rect(0, 1, 0), Some(([0, 255, 0, 255], 0.0))); - // PS_GEOMETRIC (0x10000) keeps its logical width. - assert_eq!(ext_pen_rect(0x0001_0000, 4, 0), Some(([0, 255, 0, 255], 4.0))); - // A BS_NULL brush draws no line. - assert_eq!(ext_pen_rect(0x0001_0000, 4, 1), None); -} - -#[test] -fn flip_tolerates_a_short_pixel_buffer() { - let mut img = Rgba { w: 2, h: 2, bottom_up: false, px: vec![1u8; 7] }; - crate::dib::flip(&mut img, true, true); - assert_eq!(img.px.len(), 7); - // Three rows held of four: only the rows present swap (0 with 2). - let mut img = Rgba { w: 1, h: 4, bottom_up: false, px: (0u8..12).collect() }; - crate::dib::flip(&mut img, false, true); - assert_eq!(img.px, [8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3]); -} - -#[test] -fn place_fits_the_frame_scales_strokes_and_drops_far_paths() { - let pic = crate::Picture { - width: 100.0, - height: 50.0, - items: vec![ - Item::Path { - segs: vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 10.0)], - fill: None, - stroke: Some(([0, 0, 0, 255], 4.0)), - even_odd: true, - region: None, - }, - Item::Path { segs: vec![Seg::Move(0.0, 0.0), Seg::Line(2e6, 0.0)], fill: None, stroke: None, even_odd: true, region: None }, - ], - }; - let placed = pic.place(kurbo::Rect::new(0.0, 0.0, 200.0, 100.0), [0.0; 4]); - assert_eq!(placed.len(), 1); - let crate::PlacedItem::Path { stroke, .. } = &placed[0] else { panic!("expected a path") }; - assert_eq!(stroke.map(|s| s.1), Some(8.0)); -} - -#[test] -fn place_counts_the_items_own_clip_region_before_keeping_it() { - // A path of 499,998 elements nearly fills the 500,000 budget; a second one whose new clip - // region adds elements goes past it and is left out. - let mut segs = vec![Seg::Move(0.0, 0.0)]; - segs.extend((0..499_997).map(|i| Seg::Line(1.0 + (i % 50) as f32, 1.0))); - let big = Item::Path { segs, fill: None, stroke: None, even_odd: false, region: None }; - let small = Item::Path { - segs: vec![Seg::Move(0.0, 0.0), Seg::Line(1.0, 1.0)], - fill: None, - stroke: None, - even_odd: false, - region: Some(vec![[0.0, 0.0, 50.0, 25.0], [10.0, 10.0, 20.0, 20.0]]), - }; - let pic = crate::Picture { width: 100.0, height: 50.0, items: vec![big, small] }; - assert_eq!(pic.place(kurbo::Rect::new(0.0, 0.0, 100.0, 50.0), [0.0; 4]).len(), 1); -} - -#[test] -fn place_keeps_only_finite_bounded_bitmaps_with_area() { - let bitmap = |rect: [f32; 4]| Item::Bitmap { rect, width: 1, height: 1, rgba: vec![0; 4], region: None }; - let pic = crate::Picture { - width: 100.0, - height: 50.0, - items: vec![ - bitmap([f32::NAN, 0.0, 10.0, 10.0]), - bitmap([0.0, 0.0, 0.0, 10.0]), - bitmap([0.0, 0.0, 2e6, 10.0]), - bitmap([0.0, 0.0, f32::INFINITY, 10.0]), - bitmap([10.0, 5.0, 10.0, 10.0]), - ], - }; - let placed = pic.place(kurbo::Rect::new(0.0, 0.0, 200.0, 100.0), [0.0; 4]); - assert_eq!(placed.len(), 1); - let crate::PlacedItem::Bitmap { index, rect, .. } = &placed[0] else { panic!("expected a bitmap") }; - assert_eq!((*index, *rect), (4, kurbo::Rect::new(20.0, 10.0, 40.0, 30.0))); -} - -#[test] -fn take_pixels_returns_only_drawable_bitmaps() { - let mut ok = Item::Bitmap { rect: [0.0; 4], width: 2, height: 1, rgba: vec![7; 8], region: None }; - assert_eq!(ok.take_pixels(), Some((2, 1, vec![7; 8]))); - assert!(matches!(&ok, Item::Bitmap { rgba, .. } if rgba.is_empty()), "the pixels move out"); - let mut short = Item::Bitmap { rect: [0.0; 4], width: 2, height: 2, rgba: vec![0; 8], region: None }; - assert_eq!(short.take_pixels(), None); - let side = crate::MAX_BITMAP_SIDE + 1; - let mut wide = Item::Bitmap { rect: [0.0; 4], width: side, height: 1, rgba: vec![0; side as usize * 4], region: None }; - assert_eq!(wide.take_pixels(), None); - let mut path = Item::Path { segs: Vec::new(), fill: None, stroke: None, even_odd: false, region: None }; - assert_eq!(path.take_pixels(), None); - let pic = |width: f32, height: f32| crate::Picture { width, height, items: Vec::new() }; - assert!(pic(1.0, 2.0).has_size()); - assert!(!pic(0.0, 2.0).has_size() && !pic(f32::NAN, 2.0).has_size() && !pic(1.0, f32::INFINITY).has_size()); -} - -/// The rectangle a WMF RECTANGLE record covers, from its path's corners. -fn rect_extent(item: &Item) -> (f32, f32, f32, f32) { - let (segs, ..) = path_segs(item); - let pts: Vec<(f32, f32)> = segs - .iter() - .filter_map(|s| match s { - Seg::Move(x, y) | Seg::Line(x, y) => Some((*x, *y)), - _ => None, - }) - .collect(); - let (xs, ys): (Vec, Vec) = pts.into_iter().unzip(); - let lo = |v: &[f32]| v.iter().copied().fold(f32::MAX, f32::min); - let hi = |v: &[f32]| v.iter().copied().fold(f32::MIN, f32::max); - (lo(&xs), lo(&ys), hi(&xs), hi(&ys)) -} - -#[test] -fn plain_anisotropic_wmf_without_a_viewport_fills_its_window() { - // Anisotropic, window 200 x 100, no viewport (a logo strip saved without a placeable header): the - // window is the target, not a 1 x 1 viewport that squeezes the picture to a point. - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020B, &cat(vec![h16(0), h16(0)])), - wrec(0x020C, &cat(vec![h16(100), h16(200)])), - wrec(0x041B, &cat(vec![h16(100), h16(200), h16(0), h16(0)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (200.0, 100.0)); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 200.0) && close(y1, 100.0), "{x0} {y0} {x1} {y1}"); -} - -#[test] -fn placeable_wmf_window_records_replace_the_bounds() { - // The placeable bounds say 1440 square (72 points); the file then draws in a window centred on the - // origin, as a coat of arms saved by a drawing program does. The window maps onto the 72-point - // target, so the drawing fills it instead of being blown up around the bounds. - let recs = [ - wrec(0x020C, &cat(vec![h16(2000), h16(2000)])), - wrec(0x020B, &cat(vec![h16(-1000), h16(-1000)])), - wrec(0x041B, &cat(vec![h16(1000), h16(1000), h16(-1000), h16(-1000)])), - ]; - let p = parse(&wmf(&recs, Some([238, 539, 1678, 1979]))).unwrap(); - assert_eq!((p.width, p.height), (72.0, 72.0)); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 72.0) && close(y1, 72.0), "{x0} {y0} {x1} {y1}"); -} - -#[test] -fn placeable_wmf_keeps_its_target_when_the_window_changes_shape() { - // Bounds 600 x 300 (30 x 15 points), then a portrait window 100 x 200 for a bitmap-sized drawing: - // the drawing still fills the target (Word stretches it to the picture's box). - let recs = [wrec(0x0103, &h16(8)), wrec(0x020C, &cat(vec![h16(200), h16(100)])), wrec(0x041B, &cat(vec![h16(200), h16(100), h16(0), h16(0)]))]; - let p = parse(&wmf(&recs, Some([0, 0, 600, 300]))).unwrap(); - assert_eq!((p.width, p.height), (30.0, 15.0)); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 30.0) && close(y1, 15.0), "{x0} {y0} {x1} {y1}"); -} - -#[test] -fn rop2_nop_draws_nothing_until_the_mode_changes() { - // A coat of arms that first draws its bounding rectangle with the R2_NOP mix mode (no output), - // then its shapes with R2_COPYPEN. Restoring a saved state brings R2_NOP back. - let rect = |n: i32| wrec(0x041B, &cat(vec![h16(n), h16(n), h16(0), h16(0)])); - let recs = [wrec(0x0104, &h16(11)), rect(100), wrec(0x001E, &[]), wrec(0x0104, &h16(13)), rect(50), wrec(0x0127, &h16(-1)), rect(20)]; - let p = parse(&wmf(&recs, Some([0, 0, 100, 100]))).unwrap(); - assert_eq!(p.items.len(), 1, "{:?}", p.items); - let (_, _, x1, y1) = rect_extent(&p.items[0]); - // 50 of 100 units in a 5-point picture. - assert!(close(x1, 2.5) && close(y1, 2.5), "{x1} {y1}"); -} - -#[test] -fn plain_wmf_target_comes_from_a_window_that_was_read() { - // A window record too short to read, then the real one: the target is the real window, not the - // 1 x 1 default the short record left in place. A viewport origin set before it is kept. - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020D, &cat(vec![h16(0), h16(0)])), - wrec(0x020C, &[]), - wrec(0x020C, &cat(vec![h16(100), h16(200)])), - wrec(0x041B, &cat(vec![h16(100), h16(200), h16(0), h16(0)])), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - assert_eq!((p.width, p.height), (200.0, 100.0)); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 200.0) && close(y1, 100.0), "{x0} {y0} {x1} {y1}"); -} - -// ---- Text, pattern and hatch brushes, mix-mode idioms ---- - -use crate::{HAlign, Picture, Text, VAlign}; - -/// META_CREATEFONTINDIRECT: height, width, escapement (and orientation), weight, the italic, -/// underline, strike-out and charset bytes, four precision bytes, then the NUL-terminated face. -fn wfont(height: i32, weight: i32, flags: [u8; 3], charset: u8, esc: i32, face: &[u8]) -> Vec { - let mut v = cat(vec![h16(height), h16(0), h16(esc), h16(esc), h16(weight)]); - v.extend(flags); - v.extend([charset, 0, 0, 0, 0]); - v.extend(face); - v.push(0); - wrec(0x02FB, &v) -} - -/// META_EXTTEXTOUT at (x, y): the rectangle when given, the string padded to a word, then `dx`. -fn wtext(x: i32, y: i32, s: &[u8], opts: i32, rect: Option<[i32; 4]>, dx: &[i32]) -> Vec { - let mut v = cat(vec![h16(y), h16(x), h16(s.len() as i32), h16(opts)]); - for c in rect.into_iter().flatten() { - v.extend(h16(c)); - } - v.extend(s); - if s.len() % 2 == 1 { - v.push(0); - } - for d in dx { - v.extend(h16(*d)); - } - wrec(0x0A32, &v) -} - -/// A plain WMF whose 200 × 100 window maps one logical unit to one picture unit. -fn wmf200(records: &[Vec]) -> Picture { - let mut recs = vec![wrec(0x020C, &cat(vec![h16(100), h16(200)]))]; - recs.extend(records.iter().cloned()); - parse(&wmf(&recs, None)).unwrap() -} - -fn texts(p: &Picture) -> Vec<&Text> { - p.items - .iter() - .filter_map(|i| match i { - Item::Text(t) => Some(t.as_ref()), - _ => None, - }) - .collect() -} - -fn close6(a: [f64; 6], b: [f64; 6]) -> bool { - a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) -} - -#[test] -fn wmf_text_reads_the_font_string_dx_colour_and_alignment() { - let recs = [ - wfont(-20, 700, [1, 1, 1], 0, 0, b"Arial"), - wrec(0x012D, &h16(0)), - wrec(0x0209, &h32(0x0000_00FF)), - wrec(0x0102, &h16(1)), - wrec(0x012E, &h16(24 | 6)), - wtext(50, 60, b"Hi!", 0, None, &[10, 8, 6]), - ]; - let p = wmf200(&recs); - let t = texts(&p); - assert_eq!(t.len(), 1, "{:?}", p.items); - let t = t[0]; - assert_eq!(t.text, "Hi!"); - assert_eq!(t.dx, vec![10.0, 8.0, 6.0], "the dx array follows the word-padded odd string"); - assert_eq!(t.font.family, "Arial"); - assert_eq!((t.font.height, t.font.weight, t.font.italic, t.font.underline, t.font.strike), (-20.0, 700, true, true, true)); - assert_eq!(t.color, [255, 0, 0, 255]); - assert_eq!(t.background, None, "TRANSPARENT background mode"); - assert_eq!((t.h_align, t.v_align), (HAlign::Center, VAlign::Baseline)); - assert!(close6(t.transform, [1.0, 0.0, 0.0, 1.0, 50.0, 60.0]), "{:?}", t.transform); - assert_eq!(t.clip, None); -} - -#[test] -fn font_height_sign_and_default_state_are_kept() { - // A positive height is the cell height; it stays positive for the renderer to resolve. Without - // SETTEXTALIGN the reference point is the top left; without SETBKMODE the cell is filled white. - let recs = [wfont(30, 400, [0, 0, 0], 0, 0, b"Times New Roman"), wrec(0x012D, &h16(0)), wtext(5, 5, b"x", 0, None, &[])]; - let p = wmf200(&recs); - let t = texts(&p)[0]; - assert_eq!(t.font.height, 30.0); - assert_eq!(t.font.family, "Times New Roman"); - assert_eq!((t.h_align, t.v_align), (HAlign::Left, VAlign::Top)); - assert_eq!(t.background, Some([255, 255, 255, 255])); - assert!(t.dx.is_empty(), "no dx array in the record"); - // Right and bottom. - let p = wmf200(&[wrec(0x012E, &h16(2 | 8)), wtext(5, 5, b"x", 0, None, &[])]); - let t = texts(&p)[0]; - assert_eq!((t.h_align, t.v_align), (HAlign::Right, VAlign::Bottom)); - assert_eq!(t.font, crate::Font::default(), "no font selected"); -} - -#[test] -fn escapement_turns_the_baseline_counter_clockwise() { - let recs = [wfont(-20, 400, [0, 0, 0], 0, 900, b"Arial"), wrec(0x012D, &h16(0)), wtext(100, 90, b"Up", 0, None, &[])]; - let p = wmf200(&recs); - let t = texts(&p)[0]; - // Text x runs up the picture (0, -1); text y (down the glyphs) runs right (1, 0). - assert!(close6(t.transform, [0.0, -1.0, 1.0, 0.0, 100.0, 90.0]), "{:?}", t.transform); -} - -#[test] -fn a_y_up_mapping_moves_text_but_does_not_mirror_it() { - // Window 200 × -100 onto a 200 × 100 viewport: y goes up. - let recs = [ - wrec(0x0103, &h16(8)), - wrec(0x020C, &cat(vec![h16(-100), h16(200)])), - wrec(0x020E, &cat(vec![h16(100), h16(200)])), - wtext(40, -30, b"A", 0, None, &[]), - ]; - let p = parse(&wmf(&recs, None)).unwrap(); - let t = texts(&p)[0]; - assert!(close6(t.transform, [1.0, 0.0, 0.0, 1.0, 40.0, 30.0]), "{:?}", t.transform); -} - -#[test] -fn update_cp_starts_at_the_current_position_and_moves_past_the_string() { - let recs = - [wrec(0x012E, &h16(1)), wrec(0x0214, &cat(vec![h16(20), h16(10)])), wtext(0, 0, b"ab", 0, None, &[5, 7]), wtext(0, 0, b"c", 0, None, &[3])]; - let p = wmf200(&recs); - let t = texts(&p); - assert_eq!(t.len(), 2); - assert!(close6(t[0].transform, [1.0, 0.0, 0.0, 1.0, 10.0, 20.0]), "{:?}", t[0].transform); - assert!(close6(t[1].transform, [1.0, 0.0, 0.0, 1.0, 22.0, 20.0]), "{:?}", t[1].transform); -} - -#[test] -fn eto_opaque_fills_the_rectangle_and_eto_clipped_clips() { - let recs = [wrec(0x0201, &h32(0x0000_FF00)), wtext(12, 30, b"ok", 2 | 4, Some([10, 10, 60, 40]), &[])]; - let p = wmf200(&recs); - assert_eq!(p.items.len(), 2, "{:?}", p.items); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 10.0) && close(y0, 10.0) && close(x1, 60.0) && close(y1, 40.0)); - assert_eq!(path_segs(&p.items[0]).1, Some([0, 255, 0, 255])); - let t = texts(&p)[0]; - assert_eq!(t.clip, Some([10.0, 10.0, 60.0, 40.0])); - assert_eq!(t.background, Some([0, 255, 0, 255])); -} - -#[test] -fn character_sets_decode_bytes() { - let one = |charset: u8, s: &[u8]| { - let p = wmf200(&[wfont(-10, 400, [0, 0, 0], charset, 0, b"F"), wrec(0x012D, &h16(0)), wtext(0, 0, s, 0, None, &[])]); - texts(&p)[0].text.clone() - }; - assert_eq!(one(0, &[b'A', 0x80, 0x93, 0x94, 0xE9]), "A€“”é"); - assert_eq!(one(238, &[0xB9, 0xE8, 0x9E, 0xA3]), "ąčžŁ"); - assert_eq!(one(204, &[0xC0, 0xFF, 0xA8]), "АяЁ"); - assert_eq!(one(2, b"n\xA7"), "\u{F06E}\u{F0A7}", "symbol fonts keep their codes in the private-use area"); - assert_eq!(one(0, &[0x81]), "\u{FFFD}", "an undefined byte"); - // A double-byte character set: a lead and trail byte become one blank with both advances. - let p = - wmf200(&[wfont(-10, 400, [0, 0, 0], 128, 0, b"F"), wrec(0x012D, &h16(0)), wtext(0, 0, &[0x41, 0x82, 0xA0, 0x42], 0, None, &[1, 2, 3, 4])]); - let t = texts(&p)[0]; - assert_eq!((t.text.as_str(), t.dx.as_slice()), ("A B", [1.0, 5.0, 4.0].as_slice())); -} - -#[test] -fn wmf_textout_reads_its_string_and_position() { - let mut v = cat(vec![h16(3)]); - v.extend(b"abc\0"); - v.extend(cat(vec![h16(40), h16(30)])); - let p = wmf200(&[wrec(0x0521, &v)]); - let t = texts(&p)[0]; - assert_eq!(t.text, "abc"); - assert!(close6(t.transform, [1.0, 0.0, 0.0, 1.0, 30.0, 40.0]), "{:?}", t.transform); -} - -#[test] -fn savedc_restores_text_colour_and_font() { - let recs = [ - wfont(-10, 400, [0, 0, 0], 0, 0, b"One"), - wrec(0x012D, &h16(0)), - wrec(0x0209, &h32(0x0000_00FF)), - wrec(0x001E, &[]), - wfont(-10, 400, [0, 0, 0], 0, 0, b"Two"), - wrec(0x012D, &h16(1)), - wrec(0x0209, &h32(0x00FF_0000)), - wrec(0x0127, &h16(-1)), - wtext(0, 0, b"x", 0, None, &[]), - ]; - let p = wmf200(&recs); - let t = texts(&p)[0]; - assert_eq!((t.font.family.as_str(), t.color), ("One", [255, 0, 0, 255])); -} - -/// EMR_EXTCREATEFONTINDIRECTW in slot 1: LogFont at 12, face at 40. -fn efont(height: i32, esc: i32, weight: i32, italic: u8, charset: u8, face: &str) -> Vec { - let mut v = cat(vec![h32(1), e32(height), e32(0), e32(esc), e32(esc), e32(weight)]); - v.extend([italic, 0, 0, charset, 0, 0, 0, 0]); - let mut name: Vec = face.encode_utf16().flat_map(u16::to_le_bytes).collect(); - name.resize(64, 0); - v.extend(name); - erec(82, &v) -} - -/// EMR_EXTTEXTOUTW (or A) at (x, y) with options, the rectangle (absent under ETO_NO_RECT), the -/// string at 76 and the dx array after it. -fn etext(wide: bool, x: i32, y: i32, units: &[u16], opts: u32, dx: &[i32]) -> Vec { - let no_rect = opts & 0x100 != 0; - let fixed = if no_rect { 60 } else { 76 }; - let s: Vec = if wide { units.iter().flat_map(|u| u.to_le_bytes()).collect() } else { units.iter().map(|u| *u as u8).collect() }; - let s_off = fixed; - let dx_off = (s_off + s.len()).next_multiple_of(4); - let len = dx_off + dx.len() * 4; - let mut f = vec![(24, 1u32), (36, x as u32), (40, y as u32), (44, units.len() as u32), (48, s_off as u32), (52, opts)]; - f.push((if no_rect { 56 } else { 72 }, if dx.is_empty() { 0 } else { dx_off as u32 })); - let dxb: Vec = dx.iter().flat_map(|d| d.to_le_bytes()).collect(); - erec_at(if wide { 84 } else { 83 }, len.next_multiple_of(4), &f, &[(s_off, &s), (dx_off, &dxb)]) -} - -#[test] -fn emf_exttextoutw_reads_utf16_dx_and_the_font() { - let recs = [ - efont(24, 0, 400, 1, 0, "Segoe UI"), - erec(37, &h32(1)), - erec(22, &h32(0x18 | 2)), - erec(24, &h32(0x0000_FF00)), - erec(18, &h32(1)), - etext(true, 100, 50, &[0x41, 0xD83D, 0xDE00], 0, &[10, 6, 0]), - ]; - let p = parse(&emf(&recs, [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - let t = texts(&p); - assert_eq!(t.len(), 1, "{:?}", p.items); - let t = t[0]; - assert_eq!(t.text, "A\u{1F600}"); - assert_eq!(t.dx, vec![10.0, 6.0], "a surrogate pair takes both advances"); - assert_eq!((t.font.family.as_str(), t.font.height, t.font.italic), ("Segoe UI", 24.0, true)); - assert_eq!((t.h_align, t.v_align, t.color, t.background), (HAlign::Right, VAlign::Baseline, [0, 255, 0, 255], None)); - // Upright, uniformly scaled. - let [a, b, c, d, ..] = t.transform; - assert!(a > 0.0 && d > 0.0 && b.abs() < 1e-9 && c.abs() < 1e-9 && (a - d).abs() < 1e-9, "{:?}", t.transform); -} - -#[test] -fn emf_text_options_pdy_no_rect_ansi_and_glyph_indices() { - // ETO_PDY: (dx, dy) pairs, of which dx is used. ETO_NO_RECT moves the dx offset to 56. - let recs = [etext(true, 0, 0, &[0x61, 0x62], 0x2000 | 0x100, &[7, 1, 9, 2])]; - let p = parse(&emf(&recs, [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - assert_eq!(texts(&p)[0].dx, vec![7.0, 9.0]); - // EXTTEXTOUTA decodes through the font's charset. - let recs = [efont(-10, 0, 400, 0, 238, "F"), erec(37, &h32(1)), etext(false, 0, 0, &[0xB9], 0, &[])]; - let p = parse(&emf(&recs, [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - assert_eq!(texts(&p)[0].text, "ą"); - // Glyph indices name glyphs of a font we cannot match: left out. - let recs = [etext(true, 0, 0, &[5, 6], 0x10, &[])]; - let p = parse(&emf(&recs, [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - assert!(texts(&p).is_empty()); -} - -#[test] -fn emf_smalltextout_reads_8_bit_and_16_bit_strings() { - // x, y, count, options, mode, scales, (no bounds under ETO_NO_RECT), characters. - let small = |opts: u32, chars: &[u8]| { - let mut v = cat(vec![e32(3), e32(4), h32(2), h32(opts), h32(1), h32(0), h32(0)]); - v.extend(chars); - erec(108, &v) - }; - let p = parse(&emf(&[small(0x100 | 0x200, b"hi")], [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - assert_eq!(texts(&p)[0].text, "hi"); - let p = parse(&emf(&[small(0x100, &[b'o', 0, b'k', 0])], [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - assert_eq!(texts(&p)[0].text, "ok"); -} - -/// An 8 × 8 one-bit DIB (palette white, black; stored bottom-up): `rows` top to bottom. -fn mono_dib(rows: [u8; 8]) -> Vec { - let mut v = info_header(8, 8, 1, 32); - v.extend([255, 255, 255, 0, 0, 0, 0, 0]); - for r in rows.iter().rev() { - v.extend([*r, 0, 0, 0]); - } - v -} - -fn pattern_of(item: &Item) -> &crate::Tile { - match item { - Item::Pattern { tile, .. } => tile, - other => panic!("expected a pattern fill: {other:?}"), - } -} - -/// Pixel (x, y) of a tile. -fn tile_px(t: &crate::Tile, x: usize, y: usize) -> [u8; 4] { - let o = (y * t.width as usize + x) * 4; - t.rgba[o..o + 4].try_into().unwrap() -} - -#[test] -fn wmf_dib_pattern_brush_paints_its_rows_top_down() { - let brush = wrec(0x0142, &cat(vec![h16(5), h16(0), mono_dib([0xFF, 0x80, 0, 0, 0, 0, 0, 0x01])])); - let recs = [ - brush, - // A pen after the brush: the brush took handle 0. - wrec(0x02FA, &cat(vec![h16(0), h16(1), h16(0), h32(0x0000_00FF)])), - wrec(0x012D, &h16(0)), - wrec(0x012D, &h16(1)), - wrec(0x041B, &cat(vec![h16(720), h16(1440), h16(0), h16(0)])), - ]; - let p = parse(&wmf(&recs, Some([0, 0, 1440, 720]))).unwrap(); - assert_eq!(p.items.len(), 2, "the tile fill and the outline: {:?}", p.items); - let t = pattern_of(&p.items[0]); - assert_eq!((t.width, t.height), (8, 8)); - for x in 0..8 { - assert_eq!(tile_px(t, x, 0), [0, 0, 0, 255], "top row is set: black"); - } - assert_eq!(tile_px(t, 0, 1), [0, 0, 0, 255]); - assert_eq!(tile_px(t, 1, 1), [255, 255, 255, 255]); - assert_eq!(tile_px(t, 7, 7), [0, 0, 0, 255]); - assert_eq!(tile_px(t, 0, 7), [255, 255, 255, 255]); - // 96-dpi pixels of a picture in points, laid from the picture's corner. - assert!(close(t.cell[0], 0.75) && close(t.cell[1], 0.75) && t.origin == [0.0, 0.0], "{:?} {:?}", t.cell, t.origin); - assert_eq!(path_segs(&p.items[1]).2.map(|s| s.0), Some([255, 0, 0, 255])); -} - -#[test] -fn hatch_brushes_draw_lines_over_the_background_mode() { - let hatch = |style: i32| wrec(0x02FC, &cat(vec![h16(2), h32(0x00FF_0000), h16(style)])); - let rect = wrec(0x041B, &cat(vec![h16(50), h16(50), h16(0), h16(0)])); - // HS_CROSS over a transparent background. - let p = wmf200(&[hatch(4), wrec(0x012D, &h16(0)), wrec(0x0102, &h16(1)), rect.clone()]); - let t = pattern_of(&p.items[0]); - let blue = [0, 0, 255, 255]; - assert_eq!((tile_px(t, 3, 7), tile_px(t, 7, 3)), (blue, blue)); - assert_eq!(tile_px(t, 3, 3)[3], 0, "gaps are transparent"); - // HS_BDIAGONAL over the opaque white background. - let p = wmf200(&[hatch(3), wrec(0x012D, &h16(0)), rect.clone()]); - let t = pattern_of(&p.items[0]); - assert_eq!((tile_px(t, 0, 7), tile_px(t, 7, 0)), (blue, blue)); - assert_eq!(tile_px(t, 0, 0), [255, 255, 255, 255]); - // An unknown hatch style draws no fill. - let p = wmf200(&[hatch(9), wrec(0x012D, &h16(0)), rect]); - assert!(p.items.iter().all(|i| !matches!(i, Item::Pattern { .. })), "{:?}", p.items); -} - -#[test] -fn xor_mask_xor_draws_the_pattern_in_the_xor_colour() { - // The see-through shadow: XOR a grey polygon, AND a checker pattern, XOR the polygon again. - let square = wrec(0x0324, &cat(vec![h16(4), h16(0), h16(0), h16(40), h16(0), h16(40), h16(40), h16(0), h16(40)])); - let recs = [ - wrec(0x0104, &h16(7)), - wrec(0x02FC, &cat(vec![h16(0), h32(0x0080_8080), h16(0)])), - wrec(0x012D, &h16(0)), - square.clone(), - wrec(0x0104, &h16(9)), - wrec(0x0142, &cat(vec![h16(5), h16(0), mono_dib([0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA])])), - wrec(0x012D, &h16(1)), - wrec(0x041B, &cat(vec![h16(40), h16(40), h16(0), h16(0)])), - wrec(0x0104, &h16(7)), - wrec(0x012D, &h16(0)), - square, - ]; - let p = wmf200(&recs); - assert_eq!(p.items.len(), 2, "only the AND pattern (and its outline) draw: {:?}", p.items); - let t = pattern_of(&p.items[0]); - // 0x55 = 0101 0101: white, black, … - assert_eq!(tile_px(t, 0, 0)[3], 0, "white keeps what is below"); - assert_eq!(tile_px(t, 1, 0), [128, 128, 128, 255], "black takes the XOR colour"); -} - -#[test] -fn patblt_fills_with_the_brush() { - let patblt = |rop: u32| wrec(0x061D, &cat(vec![h32(rop), h16(30), h16(40), h16(10), h16(20)])); - let recs = [wrec(0x02FC, &cat(vec![h16(0), h32(0x00FF_0000), h16(0)])), wrec(0x012D, &h16(0)), patblt(0x00F0_0021), patblt(0x0055_0009)]; - let p = wmf200(&recs); - assert_eq!(p.items.len(), 1, "DSTINVERT draws nothing: {:?}", p.items); - let (x0, y0, x1, y1) = rect_extent(&p.items[0]); - assert!(close(x0, 20.0) && close(y0, 10.0) && close(x1, 60.0) && close(y1, 40.0), "{x0} {y0} {x1} {y1}"); - assert_eq!(path_segs(&p.items[0]).1, Some([0, 0, 255, 255])); -} - -#[test] -fn emf_dib_pattern_and_mono_brushes() { - let dib = mono_dib([0xF0; 8]); - let (info, bits) = dib.split_at(48); - let brush = |typ: u32| { - let f = [(8, 1u32), (12, 0), (16, 32), (20, info.len() as u32), (24, 32 + info.len() as u32), (28, bits.len() as u32)]; - erec_at(typ, 32 + dib.len(), &f, &[(32, info), (32 + info.len(), bits)]) - }; - let rect = erec(43, &cat(vec![e32(0), e32(0), e32(40), e32(40)])); - let null_pen = erec(37, &h32(0x8000_0008)); - let p = parse(&emf(&[brush(94), erec(37, &h32(1)), null_pen.clone(), rect.clone()], [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - let t = pattern_of(&p.items[0]); - assert_eq!((tile_px(t, 0, 0), tile_px(t, 7, 0)), ([0, 0, 0, 255], [255, 255, 255, 255])); - // A mono brush paints in the text colour (red) and the background colour (green). - let recs = [erec(24, &h32(0xFF)), erec(25, &h32(0xFF00)), brush(93), erec(37, &h32(1)), null_pen, rect]; - let p = parse(&emf(&recs, [0, 0, 200, 100], [0, 0, 5000, 2500])).unwrap(); - let t = pattern_of(&p.items[0]); - assert_eq!((tile_px(t, 0, 0), tile_px(t, 7, 0)), ([255, 0, 0, 255], [0, 255, 0, 255])); -} - -#[test] -fn hostile_text_and_brush_records_never_panic() { - // A string longer than its record is cut to what the record holds. - let mut v = cat(vec![h16(0), h16(0), h16(0x7FFF), h16(0)]); - v.extend(b"abcd"); - let p = wmf200(&[wrec(0x0A32, &v)]); - assert_eq!(texts(&p)[0].text, "abcd"); - // The rectangle flag without room for the rectangle draws nothing. - let p = wmf200(&[wrec(0x0A32, &cat(vec![h16(0), h16(0), h16(1), h16(6)]))]); - assert!(texts(&p).is_empty()); - // A face name with no terminator stops at 32 bytes. - let p = wmf200(&[wfont(-10, 400, [0, 0, 0], 0, 0, &[b'A'; 40]), wrec(0x012D, &h16(0)), wtext(0, 0, b"x", 0, None, &[])]); - assert_eq!(texts(&p)[0].font.family.len(), 32); - // A pattern brush whose DIB is cut short, or too big for a tile, still takes its handle slot - // and draws no fill. - let short = wrec(0x0142, &cat(vec![h16(5), h16(0), info_header(8, 8, 1, 32)])); - let mut huge = info_header(300, 300, 1, 0); - huge.extend([255, 255, 255, 0, 0, 0, 0, 0]); - huge.extend(vec![0u8; 40 * 300]); - let huge = wrec(0x0142, &cat(vec![h16(5), h16(0), huge])); - for b in [short, huge] { - let recs = [ - b, - wrec(0x02FA, &cat(vec![h16(0), h16(1), h16(0), h32(0xFF)])), - wrec(0x012D, &h16(0)), - wrec(0x012D, &h16(1)), - wrec(0x041B, &cat(vec![h16(9), h16(9), h16(0), h16(0)])), - ]; - let p = wmf200(&recs); - assert_eq!(p.items.len(), 1, "{:?}", p.items); - let (_, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!((fill, stroke.map(|s| s.0)), (Some([255, 255, 255, 255]), Some([255, 0, 0, 255]))); - } - // EMF: the string offset past the record, a huge count, a dx offset past the record. - let bad = [ - erec_at(84, 80, &[(44, 2), (48, 4000), (52, 0)], &[]), - erec_at(84, 84, &[(44, u32::MAX), (48, 76), (52, 0)], &[(76, &[b'a', 0, b'b', 0])]), - erec_at(84, 80, &[(44, 2), (48, 76), (52, 0), (72, 0xFFFF_FFF0)], &[(76, &[b'a', 0, b'b', 0])]), - erec_at(82, 20, &[(8, 1)], &[]), - erec_at(94, 32, &[(8, 1), (16, 0xFFFF_FFFF), (20, 40), (24, 0), (28, 0xFFFF_FFFF)], &[]), - erec_at(108, 40, &[(16, u32::MAX), (20, 0x300)], &[]), - ]; - for r in bad { - let _ = parse(&emf(&[r], [0, 0, 200, 100], [0, 0, 5000, 2500])); - } - let p = parse(&emf( - &[erec_at(84, 84, &[(44, 2), (48, 76), (52, 0), (72, 0xFFFF_FFF0)], &[(76, &[b'a', 0, b'b', 0])])], - [0, 0, 200, 100], - [0, 0, 5000, 2500], - )) - .unwrap(); - assert!(texts(&p)[0].dx.is_empty(), "a dx array past the record is dropped"); - // Mutated text and brush files. - let wfile = wmf( - &[ - wfont(-20, 700, [1, 1, 1], 238, 450, b"Arial"), - wrec(0x012D, &h16(0)), - wrec(0x0142, &cat(vec![h16(5), h16(0), mono_dib([0x55; 8])])), - wrec(0x012D, &h16(1)), - wrec(0x012E, &h16(1)), - wtext(5, 5, b"Hello", 6, Some([0, 0, 50, 50]), &[1, 2, 3, 4, 5]), - wrec(0x041B, &cat(vec![h16(9), h16(9), h16(0), h16(0)])), - ], - Some([0, 0, 100, 100]), - ); - let efile = emf( - &[efont(24, 900, 400, 1, 0, "Segoe UI"), erec(37, &h32(1)), etext(true, 1, 2, &[0x41, 0xD83D, 0xDE00], 0x2000, &[1, 2, 3, 4, 5, 6])], - [0, 0, 200, 100], - [0, 0, 5000, 2500], - ); - let mut rng = Lcg(0x9E37_79B9_7F4A_7C15); - for file in [&wfile, &efile] { - for n in 0..=file.len() { - let _ = parse(&file[..n]); - } - for _ in 0..1500 { - let mut buf = file.clone(); - for _ in 0..6 { - let at = rng.rnd() as usize % buf.len(); - buf[at] = rng.rnd() as u8; - } - let _ = parse(&buf); - } - } -} - -#[test] -fn text_items_are_capped_like_any_other() { - let t = wtext(0, 0, b"x", 0, None, &[]); - let recs: Vec> = (0..MAX_ITEMS + 5).map(|_| t.clone()).collect(); - let p = wmf200(&recs); - assert_eq!(p.items.len(), MAX_ITEMS); -} - -#[test] -fn pattern_brush_pixels_count_against_the_byte_budget() { - // 256 × 256 one-bit pattern brushes (256 KiB of RGBA each): far fewer than the object table - // holds fit the budget, and the parse stops there. - let mut dib = info_header(256, 256, 1, 0); - dib.extend([255, 255, 255, 0, 0, 0, 0, 0]); - dib.extend(vec![0x55u8; 32 * 256]); - let brush = wrec(0x0142, &cat(vec![h16(5), h16(0), dib])); - let mut recs: Vec> = (0..1100).map(|_| brush.clone()).collect(); - recs.push(wrec(0x041B, &cat(vec![h16(9), h16(9), h16(0), h16(0)]))); - let p = wmf200(&recs); - assert!(p.items.is_empty(), "the parse stopped at the budget: {:?}", p.items.len()); -} - -fn item_region(item: &Item) -> Option<&[[f32; 4]]> { - match item { - Item::Path { region, .. } | Item::Bitmap { region, .. } | Item::Pattern { region, .. } => region.as_deref(), - Item::Text(t) => t.region.as_deref(), - } -} - -#[test] -fn emf_intersect_clip_rect_limits_later_records_only() { - let full = cat(vec![e32(0), e32(0), e32(80), e32(40)]); - let clip = cat(vec![e32(10), e32(5), e32(40), e32(30)]); - let p = parse(&emf(&[erec(43, &full), erec(30, &clip), erec(43, &full)], [0, 0, 80, 40], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 2); - assert_eq!(item_region(&p.items[0]), None); - assert_eq!(item_region(&p.items[1]), Some(&[[10.0, 5.0, 40.0, 30.0]][..])); -} - -#[test] -fn emf_clip_follows_the_world_transform_and_save_restores_it() { - let xf = cat([2.0f32, 0.0, 0.0, 2.0, 0.0, 0.0].iter().map(|v| v.to_le_bytes().to_vec()).collect()); - let rect = cat(vec![e32(0), e32(0), e32(10), e32(10)]); - let clip = cat(vec![e32(0), e32(0), e32(5), e32(5)]); - let p = parse(&emf(&[erec(35, &xf), erec(30, &clip), erec(43, &rect)], [0, 0, 20, 20], [0; 4])).unwrap(); - assert_eq!(item_region(&p.items[0]), Some(&[[0.0, 0.0, 10.0, 10.0]][..])); - - let full = cat(vec![e32(0), e32(0), e32(40), e32(20)]); - let p = - parse(&emf(&[erec(43, &full), erec(33, &[]), erec(30, &clip), erec(43, &full), erec(34, &e32(-1)), erec(43, &full)], [0, 0, 40, 20], [0; 4])) - .unwrap(); - assert_eq!(p.items.len(), 3); - assert_eq!(item_region(&p.items[0]), None); - assert_eq!(item_region(&p.items[1]), Some(&[[0.0, 0.0, 5.0, 5.0]][..])); - assert_eq!(item_region(&p.items[2]), None); -} - -#[test] -fn emf_ext_select_clip_rgn_copies_and_intersects_rectangles() { - let mut data = cat(vec![h32(32), h32(1), h32(2), h32(32), e32(0), e32(0), e32(50), e32(20)]); - data.extend(cat(vec![e32(0), e32(0), e32(10), e32(20), e32(30), e32(0), e32(50), e32(20)])); - let payload = cat(vec![h32(data.len() as u32), h32(5), data]); - let rect = cat(vec![e32(0), e32(0), e32(50), e32(20)]); - let p = parse(&emf(&[erec(75, &payload), erec(43, &rect)], [0, 0, 50, 20], [0; 4])).unwrap(); - assert_eq!(item_region(&p.items[0]), Some(&[[0.0, 0.0, 10.0, 20.0], [30.0, 0.0, 50.0, 20.0]][..])); - - let and = cat(vec![h32(0), h32(1)]); - // A zero-length region selects the default clip, so the next rectangle is not clipped. - let p = parse(&emf(&[erec(75, &payload), erec(75, &and), erec(43, &rect)], [0, 0, 50, 20], [0; 4])).unwrap(); - assert_eq!(item_region(&p.items[0]), None); - - let overlap = cat(vec![e32(40), e32(0), e32(80), e32(20)]); - let p = parse(&emf(&[erec(75, &payload), erec(30, &overlap), erec(43, &rect)], [0, 0, 80, 20], [0; 4])).unwrap(); - assert_eq!(item_region(&p.items[0]), Some(&[[40.0, 0.0, 50.0, 20.0]][..])); -} - -#[test] -fn emf_exclude_clip_rect_cuts_the_surface() { - let hole = cat(vec![e32(20), e32(0), e32(40), e32(40)]); - let rect = cat(vec![e32(0), e32(0), e32(80), e32(40)]); - let p = parse(&emf(&[erec(29, &hole), erec(43, &rect)], [0, 0, 80, 40], [0; 4])).unwrap(); - assert_eq!(item_region(&p.items[0]), Some(&[[0.0, 0.0, 20.0, 40.0], [40.0, 0.0, 80.0, 40.0]][..])); -} - -#[test] -fn an_empty_clip_drops_later_drawing() { - let empty = cat(vec![e32(5), e32(5), e32(5), e32(8)]); - let rect = cat(vec![e32(0), e32(0), e32(20), e32(10)]); - let p = parse(&emf(&[erec(43, &rect), erec(30, &empty), erec(43, &rect)], [0, 0, 20, 10], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); -} - -#[test] -fn wmf_intersect_clip_rect_limits_later_records() { - let clip = cat(vec![h16(30), h16(40), h16(5), h16(10)]); - let rect = cat(vec![h16(40), h16(80), h16(0), h16(0)]); - let p = parse(&wmf(&[wrec(0x0416, &clip), wrec(0x041B, &rect)], None)).unwrap(); - assert_eq!(item_region(&p.items[0]), Some(&[[10.0, 5.0, 40.0, 30.0]][..])); -} - -// ---- EMF+ ---- - -fn f32s(v: &[f32]) -> Vec { - v.iter().flat_map(|x| x.to_le_bytes()).collect() -} - -/// One EMF+ record: type, flags, size, data size, data (padded to 4). -fn prec(typ: u16, flags: u16, data: &[u8]) -> Vec { - let mut d = data.to_vec(); - while !d.len().is_multiple_of(4) { - d.push(0); - } - let mut v = typ.to_le_bytes().to_vec(); - v.extend(flags.to_le_bytes()); - v.extend(((12 + d.len()) as u32).to_le_bytes()); - v.extend((d.len() as u32).to_le_bytes()); - v.extend(d); - v -} - -/// An EMR_COMMENT carrying EMF+ records. -fn plus_comment(recs: &[Vec]) -> Vec { - let body = recs.concat(); - let mut p = ((body.len() + 4) as u32).to_le_bytes().to_vec(); - p.extend(0x2B46_4D45u32.to_le_bytes()); - p.extend(body); - erec(70, &p) -} - -/// EMF+ header: dual (bit 0) or EMF+ only, 96 dpi. -fn plus_header(dual: bool) -> Vec { - prec(0x4001, u16::from(dual), &cat(vec![h32(0xDBC0_1002), h32(0), h32(96), h32(96)])) -} - -fn solid_pen(argb: u32, width: f32) -> Vec { - cat(vec![h32(0xDBC0_1002), h32(0), h32(0), h32(0), f32s(&[width]), h32(0xDBC0_1002), h32(0), h32(argb)]) -} - -#[test] -fn emfplus_only_fill_rects_with_argb_colour() { - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_80FF), h32(1), f32s(&[10.0, 20.0, 30.0, 40.0])])); - let p = parse(&emf(&[plus_comment(&[plus_header(false), fill])], [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - assert_eq!(fill, Some([0x00, 0x80, 0xFF, 0xFF])); - assert_eq!(stroke, None); - // Geometry is placed relative to the drawing's bounding box (no frame given). - assert_eq!((p.width, p.height), (30.0, 40.0)); - assert_eq!(segs[0], Seg::Move(0.0, 0.0)); - assert!(segs.contains(&Seg::Line(30.0, 40.0))); -} - -#[test] -fn emfplus_dual_files_draw_only_the_emf_records() { - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_0000), h32(1), f32s(&[0.0, 0.0, 5.0, 5.0])])); - let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)])); - let p = parse(&emf(&[plus_comment(&[plus_header(true), fill]), gdi], [0; 4], [0; 4])).unwrap(); - assert_eq!(p.items.len(), 1); - assert_eq!(path_segs(&p.items[0]).1, Some([255, 255, 255, 255])); -} - -#[test] -fn emfplus_pen_object_draws_lines_under_a_scale_transform() { - let pen = prec(0x4008, 0x0200 | 3, &solid_pen(0xFF00_FF00, 2.0)); - let scale = prec(0x402E, 0, &f32s(&[2.0, 3.0])); - let lines = prec(0x400D, 3, &cat(vec![h32(2), f32s(&[1.0, 1.0, 4.0, 2.0])])); - let p = parse(&emf(&[plus_comment(&[plus_header(false), pen, scale, lines])], [0; 4], [0; 4])).unwrap(); - let (segs, fill, stroke, _) = path_segs(&p.items[0]); - // (1,1)-(4,2) scaled by (2,3) is (2,3)-(8,6), placed relative to its bounding box. - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(6.0, 3.0)]); - assert_eq!(fill, None); - let (c, w) = stroke.unwrap(); - assert_eq!(c, [0, 255, 0, 255]); - assert!(w > 0.0); -} - -#[test] -fn emfplus_path_object_fills_with_brush_object_and_restore_undoes_transform() { - let brush = prec(0x4008, 0x0100 | 1, &cat(vec![h32(0xDBC0_1002), h32(0), h32(0x8012_3456)])); - // Triangle: start, line, line|close. - let path = prec(0x4008, 0x0300 | 2, &cat(vec![h32(0xDBC0_1002), h32(3), h32(0), f32s(&[0.0, 0.0, 10.0, 0.0, 0.0, 10.0]), vec![0, 1, 0x81]])); - let save = prec(0x4025, 0, &h32(7)); - let mv = prec(0x402D, 0, &f32s(&[100.0, 0.0])); - let restore = prec(0x4026, 0, &h32(7)); - let fill = prec(0x4014, 2, &h32(1)); - let p = parse(&emf(&[plus_comment(&[plus_header(false), brush, path, save, mv, restore, fill])], [0; 4], [0; 4])).unwrap(); - let (segs, fill, _, _) = path_segs(&p.items[0]); - assert_eq!(fill, Some([0x12, 0x34, 0x56, 0x80])); - assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(0.0, 10.0), Seg::Close]); -} - -#[test] -fn emfplus_garbage_records_never_panic() { - let mut s = 0x1234_5678u32; - for _ in 0..300 { - let mut recs = vec![plus_header(false)]; - for _ in 0..6 { - s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345); - let typ = 0x4000 + (s >> 16) as u16 % 0x40; - let data: Vec = (0..(s % 64)).map(|i| (s.rotate_left(i)) as u8).collect(); - recs.push(prec(typ, (s >> 8) as u16, &data)); - } - let _ = parse(&emf(&[plus_comment(&recs)], [0; 4], [0; 4])); - } -} - -fn plus_pic(recs: Vec>) -> Picture { - let mut all = vec![plus_header(false)]; - all.extend(recs); - parse(&emf(&[plus_comment(&all)], [0; 4], [0; 4])).unwrap() -} - -fn u16s(s: &str) -> Vec { - s.encode_utf16().flat_map(u16::to_le_bytes).collect() -} - -/// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white. -fn bitmap_2x2() -> Vec { - let px = [0xFFFF_0000u32, 0xFF00_FF00, 0xFF00_00FF, 0xFFFF_FFFF]; - cat(vec![h32(0xDBC0_1002), h32(1), h32(2), h32(2), h32(8), h32(0x0026_200A), h32(0), px.iter().flat_map(|v| v.to_le_bytes()).collect()]) -} - -fn draw_image(id: u16, dst: [f32; 4]) -> Vec { - prec(0x401A, id, &cat(vec![h32(0), h32(2), f32s(&[0.0, 0.0, 2.0, 2.0]), f32s(&dst)])) -} - -#[test] -fn emfplus_draw_image_places_the_bitmap() { - let p = plus_pic(vec![prec(0x4008, 0x0500 | 4, &bitmap_2x2()), draw_image(4, [10.0, 10.0, 20.0, 40.0])]); - match &p.items[0] { - Item::Bitmap { rect, width, height, rgba, .. } => { - assert_eq!((*width, *height), (2, 2)); - assert_eq!(rect, &[0.0, 0.0, 20.0, 40.0]); - assert_eq!(&rgba[..8], &[255, 0, 0, 255, 0, 255, 0, 255]); - assert_eq!(&rgba[8..12], &[0, 0, 255, 255]); - } - other => panic!("expected a bitmap: {other:?}"), - } -} - -#[test] -fn emfplus_object_split_across_records_is_joined() { - let obj = bitmap_2x2(); - let (a, b) = obj.split_at(20); - let total = h32(obj.len() as u32); - let first = prec(0x4008, 0x8000 | 0x0500 | 1, &cat(vec![total.clone(), a.to_vec()])); - let second = prec(0x4008, 0x0500 | 1, b); - let p = plus_pic(vec![first, second, draw_image(1, [0.0, 0.0, 4.0, 4.0])]); - assert!(matches!(&p.items[0], Item::Bitmap { width: 2, height: 2, .. })); -} - -#[test] -fn emfplus_compressed_png_image_decodes() { - let mut png = Vec::new(); - let img = image::RgbaImage::from_pixel(3, 1, image::Rgba([1, 2, 3, 255])); - image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); - let obj = cat(vec![h32(0xDBC0_1002), h32(1), h32(0), h32(0), h32(0), h32(0), h32(1), png]); - let draw = prec(0x401A, 2, &cat(vec![h32(0), h32(2), f32s(&[0.0, 0.0, 3.0, 1.0]), f32s(&[0.0, 0.0, 3.0, 1.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0500 | 2, &obj), draw]); - assert!(matches!(&p.items[0], Item::Bitmap { width: 3, height: 1, rgba, .. } if rgba[..4] == [1, 2, 3, 255])); -} - -fn font_obj(family: &str, em: f32, unit: u32, style: u32) -> Vec { - cat(vec![h32(0xDBC0_1002), f32s(&[em]), h32(unit), h32(style), h32(0), h32(family.encode_utf16().count() as u32), u16s(family)]) -} - -#[test] -fn emfplus_draw_string_with_font_and_centred_format() { - let font = prec(0x4008, 0x0600 | 3, &font_obj("Calibri", 12.0, 2, 1)); - // String format: centre alignment, no clip. - let fmt = prec(0x4008, 0x0700 | 4, &cat(vec![h32(0xDBC0_1002), h32(0x4000), h32(0), h32(1), h32(0)])); - let s = prec(0x401C, 0x8000 | 3, &cat(vec![h32(0xFF11_2233), h32(4), h32(2), f32s(&[0.0, 0.0, 100.0, 20.0]), u16s("Hi")])); - let p = plus_pic(vec![font, fmt, prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_FFFF), h32(1), f32s(&[0.0, 0.0, 100.0, 20.0])])), s]); - let t = p.items.iter().find_map(|i| if let Item::Text(t) = i { Some(t) } else { None }).expect("a text item"); - assert_eq!(t.text, "Hi"); - assert_eq!(t.font.family, "Calibri"); - assert_eq!(t.font.weight, 700); - assert!(close(t.font.height, -12.0)); - assert_eq!(t.color, [0x11, 0x22, 0x33, 0xFF]); - assert_eq!(t.h_align, HAlign::Center); - assert!(close(t.transform[4] as f32, 50.0)); - assert_eq!(t.background, None); - assert_eq!(t.clip, None); -} - -#[test] -fn emfplus_driver_string_uses_character_codes_and_positions() { - let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)); - let pos = f32s(&[5.0, 30.0, 11.0, 30.0]); - let s = prec(0x4036, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), h32(0), h32(2), u16s("Ab"), pos])); - let p = plus_pic(vec![prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_FFFF), h32(1), f32s(&[0.0, 0.0, 50.0, 50.0])])), font, s]); - let Some(Item::Text(t)) = p.items.get(1) else { panic!("expected text: {:?}", p.items) }; - assert_eq!(t.text, "Ab"); - assert_eq!(t.dx, vec![6.0, 6.0]); - assert_eq!(t.v_align, VAlign::Baseline); - assert!(close(t.transform[4] as f32, 5.0) && close(t.transform[5] as f32, 30.0)); -} - -#[test] -fn emfplus_glyph_index_driver_strings_map_back_in_core_fonts_only() { - // Glyph indices (no CmapLookup): 36, 69 are "Ab" in Arial's standard glyph order. - let gi = || prec(0x4036, 0x8000, &cat(vec![h32(0xFF00_0000), h32(0), h32(0), h32(2), h16(36), h16(69), f32s(&[0.0, 0.0, 6.0, 0.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)), gi()]); - assert!(matches!(&p.items[0], Item::Text(t) if t.text == "Ab"), "{:?}", p.items); - // An unknown font's glyph indices cannot be mapped: nothing is drawn. - assert!(plus_pic(vec![prec(0x4008, 0x0600, &font_obj("Calibri", 10.0, 0, 0)), gi()]).items.is_empty()); -} - -#[test] -fn emfplus_linear_gradient_fills_with_a_shaded_tile() { - // Red to blue across x = 0..100. - let brush = - cat(vec![h32(0xDBC0_1002), h32(4), h32(0), h32(0), f32s(&[0.0, 0.0, 100.0, 10.0]), h32(0xFFFF_0000), h32(0xFF00_00FF), h32(0), h32(0)]); - let fill = prec(0x400A, 0, &cat(vec![h32(5), h32(1), f32s(&[0.0, 0.0, 100.0, 10.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0100 | 5, &brush), fill]); - let Item::Pattern { tile, .. } = &p.items[0] else { panic!("expected a pattern: {:?}", p.items) }; - let px = |x: usize| &tile.rgba[x * 4..x * 4 + 4]; - assert!(px(0)[0] > 240 && px(0)[2] < 15, "left is red: {:?}", px(0)); - let right = (tile.width as usize - 2).min(127); - assert!(px(right)[2] > 230 && px(right)[0] < 25, "right is blue: {:?}", px(right)); -} - -#[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)]); - 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)); -} - -#[test] -fn emfplus_clip_rect_cuts_fills_strokes_and_images() { - let clip = prec(0x4032, 0, &f32s(&[0.0, 0.0, 50.0, 50.0])); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[25.0, 25.0, 50.0, 50.0])])); - let outside = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[60.0, 60.0, 5.0, 5.0])])); - let pen = prec(0x4008, 0x0200 | 1, &solid_pen(0xFF00_FF00, 1.0)); - let line = prec(0x400D, 1, &cat(vec![h32(2), f32s(&[0.0, 10.0, 100.0, 10.0])])); - let img = prec(0x4008, 0x0500 | 2, &bitmap_2x2()); - let p = plus_pic(vec![clip, fill, outside, pen, line, img, draw_image(2, [40.0, 0.0, 20.0, 20.0])]); - assert_eq!(p.items.len(), 3, "{:?}", p.items); - let (segs, ..) = path_segs(&p.items[0]); - let xs: Vec = segs.iter().filter_map(|s| if let Seg::Move(x, _) | Seg::Line(x, _) = s { Some(*x) } else { None }).collect(); - // Picture coordinates start at the drawing's left edge (0): the square is cut at x = 50. - assert!(xs.iter().all(|x| *x <= 50.0 + 1e-3), "{segs:?}"); - let (segs, ..) = path_segs(&p.items[1]); - assert!(matches!(segs[1], Seg::Line(x, _) if close(x, 50.0)), "{segs:?}"); - let Item::Bitmap { rect, width, .. } = &p.items[2] else { panic!() }; - assert_eq!(*width, 1); - assert!(close(rect[2], 10.0)); -} - -#[test] -fn emfplus_reset_clip_and_restore_bring_drawing_back() { - let clip = prec(0x4032, 0, &f32s(&[0.0, 0.0, 1.0, 1.0])); - let fill = || prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[10.0, 10.0, 5.0, 5.0])])); - let p = plus_pic(vec![prec(0x4025, 0, &h32(1)), clip, fill(), prec(0x4026, 0, &h32(1)), fill()]); - assert_eq!(p.items.len(), 1); - let clip = prec(0x4032, 0, &f32s(&[0.0, 0.0, 1.0, 1.0])); - let p = plus_pic(vec![clip, fill(), prec(0x4031, 0, &[]), fill()]); - assert_eq!(p.items.len(), 1); -} - -#[test] -fn emfplus_region_clip_intersects_rectangles() { - // Region: intersect(rect 0,0,30,30 ; rect 10,10,100,100) = 10..30. - let rect = |r: [f32; 4]| cat(vec![h32(0x1000_0000), f32s(&r)]); - let region = cat(vec![h32(0xDBC0_1002), h32(2), h32(1), rect([0.0, 0.0, 30.0, 30.0]), rect([10.0, 10.0, 90.0, 90.0])]); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); - assert_eq!((p.width, p.height), (20.0, 20.0)); -} - -#[test] -fn emfplus_region_path_cannot_read_into_its_sibling() { - let rect = |r: [f32; 4]| cat(vec![h32(0x1000_0000), f32s(&r)]); - // The path declares only its 12-byte header, but claims three points. The following - // rectangle must not supply those points or be parsed twice as the next node. - let path = cat(vec![h32(0x1000_0001), h32(12), h32(0), h32(3), h32(0)]); - let region = cat(vec![h32(0xDBC0_1002), h32(2), h32(1), path, rect([10.0, 10.0, 20.0, 20.0])]); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); - assert_eq!(p.items.len(), 1); - assert_eq!((p.width, p.height), (100.0, 100.0)); -} - -#[test] -fn emfplus_region_path_keeps_its_shape() { - let mut path_data = cat(vec![h32(0), h32(4), h32(0x4000)]); - for (x, y) in [(10i16, 10i16), (30, 10), (30, 30), (10, 30)] { - path_data.extend(x.to_le_bytes()); - path_data.extend(y.to_le_bytes()); - } - path_data.extend([0, 1, 1, 0x81]); - let path_node = cat(vec![h32(0x1000_0001), h32(path_data.len() as u32), path_data]); - let region = cat(vec![h32(0xDBC0_1002), h32(1), path_node]); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); - assert_eq!((p.width, p.height), (20.0, 20.0)); -} - -#[test] -fn emfplus_pie_and_arc() { - // A quarter pie from 0 to 90 degrees in a 0..100 circle: centre, then (100, 50) round to (50, 100). - let pie = prec(0x4010, 0x8000, &cat(vec![h32(0xFF00_0000), f32s(&[0.0, 90.0]), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![pie]); - let (segs, fill, ..) = path_segs(&p.items[0]); - assert_eq!(fill, Some([0, 0, 0, 255])); - assert_eq!(segs[0], Seg::Move(0.0, 0.0)); - assert_eq!(segs[1], Seg::Line(50.0, 0.0)); - assert!(matches!(segs[2], Seg::Cubic(.., x, y) if close(x, 0.0) && close(y, 50.0)), "{segs:?}"); - assert_eq!(segs[3], Seg::Close); - // A half-circle arc stroke is two cubics. - let pen = prec(0x4008, 0x0200, &solid_pen(0xFF00_0000, 1.0)); - let arc = prec(0x4012, 0, &cat(vec![f32s(&[180.0, -180.0]), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![pen, arc]); - let (segs, ..) = path_segs(&p.items[0]); - assert_eq!(segs.len(), 3, "{segs:?}"); -} - -#[test] -fn emfplus_convex_clip_path_cuts_to_the_shape_not_its_box() { - // A diamond clip path (0,50) (50,0) (100,50) (50,100): the corner of a full square fill is cut away. - let diamond = cat(vec![h32(0xDBC0_1002), h32(4), h32(0), f32s(&[0.0, 50.0, 50.0, 0.0, 100.0, 50.0, 50.0, 100.0]), vec![0, 1, 1, 0x81]]); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); - let p = plus_pic(vec![prec(0x4008, 0x0300 | 9, &diamond), prec(0x4033, 9, &[]), fill]); - let (segs, ..) = path_segs(&p.items[0]); - let pts: Vec<(f32, f32)> = segs.iter().filter_map(|s| if let Seg::Move(x, y) | Seg::Line(x, y) = s { Some((*x, *y)) } else { None }).collect(); - assert!(!pts.iter().any(|(x, y)| close(*x, 0.0) && close(*y, 0.0)), "corner kept: {pts:?}"); - assert!(pts.iter().all(|(x, y)| (x - 50.0).abs() + (y - 50.0).abs() <= 50.0 + 1e-3), "{pts:?}"); -} - -#[test] -fn emfplus_draw_string_wraps_to_the_layout_width() { - let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)); - let text = "alpha beta gamma delta"; - let s = prec( - 0x401C, - 0x8000, - &cat(vec![h32(0xFF00_0000), h32(9), h32(text.encode_utf16().count() as u32), f32s(&[0.0, 0.0, 60.0, 100.0]), u16s(text)]), - ); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_FFFF), h32(1), f32s(&[0.0, 0.0, 60.0, 100.0])])); - let p = plus_pic(vec![fill, font, s]); - let lines: Vec<&str> = p.items.iter().filter_map(|i| if let Item::Text(t) = i { Some(t.text.as_str()) } else { None }).collect(); - assert_eq!(lines, vec!["alpha beta", "gamma delta"]); -} - -#[test] -fn emfplus_nested_metafile_image_draws_its_items_into_the_destination() { - // The nested picture: a plain EMF with a 10 x 10 rectangle (white brush, black pen). - let inner = emf(&[erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))], [0; 4], [0; 4]); - let obj = cat(vec![h32(0xDBC0_1002), h32(2), h32(3), h32(inner.len() as u32), inner]); - let p = plus_pic(vec![prec(0x4008, 0x0500 | 3, &obj), draw_image(3, [0.0, 0.0, 40.0, 20.0])]); - assert_eq!(p.items.len(), 1); - assert_eq!((p.width, p.height), (40.0, 20.0)); - let (segs, fill, ..) = path_segs(&p.items[0]); - assert_eq!(fill, Some([255, 255, 255, 255])); - assert!(segs.contains(&Seg::Line(40.0, 20.0)), "{segs:?}"); -} - -/// Curves and a region fill, as an EMF+ only file (also written to `$DECKCRAFT_EMF_OUT` to look at). -fn curves_file() -> Vec { - let pen = prec(0x4008, 0x0200 | 1, &solid_pen(0xFF20_60C0, 3.0)); - // A closed curve through a diamond's corners, filled orange. - let diamond = f32s(&[100.0, 10.0, 190.0, 100.0, 100.0, 190.0, 10.0, 100.0]); - let fill = prec(0x4016, 0x8000, &cat(vec![h32(0xFFE0_8020), f32s(&[0.5]), h32(4), diamond.clone()])); - let outline = prec(0x4017, 1, &cat(vec![f32s(&[0.5]), h32(4), diamond])); - // An open curve (a smoothed line chart): all four segments of five points. - let line = - prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(0), h32(4), h32(5), f32s(&[210.0, 150.0, 260.0, 60.0, 310.0, 120.0, 360.0, 30.0, 410.0, 90.0])])); - // A green rectangle region, filled. - let region = cat(vec![h32(0xDBC0_1002), h32(0), h32(0x1000_0000), f32s(&[220.0, 160.0, 180.0, 30.0])]); - let fill_region = prec(0x4013, 0x8000 | 2, &h32(0xFF30_A040)); - // A cream background first. - let clear = prec(0x4009, 0, &h32(0xFFF4_EED8)); - let mut all = vec![plus_header(false), clear, pen, fill, outline, line, prec(0x4008, 0x0400 | 2, ®ion), fill_region]; - all.push(prec(0x4002, 0, &[])); - emf(&[plus_comment(&all)], [0, 0, 420, 200], [0; 4]) -} - -#[test] -fn emfplus_cardinal_curves_and_region_fill() { - let bytes = curves_file(); - if let Ok(path) = std::env::var("DECKCRAFT_EMF_OUT") { - let _ = std::fs::write(path, &bytes); - } - let p = parse(&bytes).unwrap(); - assert_eq!(p.items.len(), 5, "{:?}", p.items.len()); - assert_eq!(path_segs(&p.items[0]).1, Some([0xF4, 0xEE, 0xD8, 0xFF]), "the clear comes first"); - // The closed curve: four cubics through the diamond's corners, closed. - let (segs, fill, ..) = path_segs(&p.items[1]); - assert_eq!(fill, Some([0xE0, 0x80, 0x20, 0xFF])); - let cubics: Vec<(f32, f32)> = segs.iter().filter_map(|s| if let Seg::Cubic(.., x, y) = s { Some((*x, *y)) } else { None }).collect(); - assert_eq!(cubics.len(), 4, "{segs:?}"); - assert_eq!(segs.last(), Some(&Seg::Close)); - // The open curve: four cubics and no close; its controls bulge past the straight chords. - let (segs, fill, stroke, _) = path_segs(&p.items[3]); - assert_eq!((fill, stroke.map(|s| s.0)), (None, Some([0x20, 0x60, 0xC0, 0xFF]))); - assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Cubic(..))).count(), 4); - assert!(!segs.contains(&Seg::Close)); - // The region fill is its rectangle. - let (segs, fill, ..) = path_segs(&p.items[4]); - assert_eq!(fill, Some([0x30, 0xA0, 0x40, 0xFF])); - assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Line(..))).count(), 3, "{segs:?}"); -} - -#[test] -fn emfplus_curve_offsets_and_degenerate_input_are_safe() { - let pen = prec(0x4008, 0x0200 | 1, &solid_pen(0xFF00_0000, 1.0)); - // Segments from point 2 of 5, asking for more than there are: two segments. - let line = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(2), h32(99), h32(5), f32s(&[0.0, 0.0, 10.0, 5.0, 20.0, 0.0, 30.0, 5.0, 40.0, 0.0])])); - let p = plus_pic(vec![pen.clone(), line]); - let (segs, ..) = path_segs(&p.items[0]); - assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Cubic(..))).count(), 2, "{segs:?}"); - // One point, an offset past the end, a NaN tension, a missing region: nothing, no panic. - let one = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(0), h32(1), h32(1), f32s(&[1.0, 1.0])])); - let past = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(9), h32(9), h32(2), f32s(&[0.0, 0.0, 9.0, 9.0])])); - let nan = prec(0x4017, 1, &cat(vec![f32s(&[f32::NAN]), h32(3), f32s(&[0.0, 0.0, 9.0, 9.0, 0.0, 9.0])])); - let no_region = prec(0x4013, 0x8000 | 7, &h32(0xFF00_0000)); - let p = plus_pic(vec![pen, one, past, nan, no_region]); - assert!(p.items.iter().all(|i| path_segs(i).0.iter().filter(|s| matches!(s, Seg::Cubic(..))).count() == 0)); -} - -#[test] -fn emfplus_clear_paints_the_whole_frame_and_transparent_clears_nothing() { - let dot = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[40.0, 40.0, 10.0, 10.0])])); - let clear = |argb: u32| prec(0x4009, 0, &h32(argb)); - // 100 × 50 device units (0..2500 × 0..1250 hundredths of a millimetre at 0.04 units each). - let file = |recs: Vec>| { - let mut all = vec![plus_header(false)]; - all.extend(recs); - parse(&emf(&[plus_comment(&all)], [0, 0, 99, 49], [0, 0, 2500, 1250])).unwrap() - }; - // A translated, clipped world doesn't move or cut the clear. - let shift = prec(0x402D, 0, &f32s(&[30.0, 30.0])); - let clip = prec(0x4032, 0, &f32s(&[0.0, 0.0, 5.0, 5.0])); - let p = file(vec![shift, clip, clear(0xFFFF_FFFF), prec(0x4031, 0, &[]), dot.clone()]); - assert_eq!(p.items.len(), 2); - let (segs, fill, ..) = path_segs(&p.items[0]); - assert_eq!(fill, Some([255, 255, 255, 255])); - let xs: Vec = segs.iter().filter_map(|s| if let Seg::Move(x, _) | Seg::Line(x, _) = s { Some(*x) } else { None }).collect(); - let span = xs.iter().cloned().fold(f32::MIN, f32::max) - xs.iter().cloned().fold(f32::MAX, f32::min); - assert!((span - p.width).abs() < 0.5, "clear spans {span} of {}", p.width); - // Transparent: only the dot. - assert_eq!(file(vec![clear(0x00FF_FFFF), dot.clone()]).items.len(), 1); - // No frame: nothing to clear. - assert_eq!(plus_pic(vec![clear(0xFFFF_FFFF), dot]).items.len(), 1); -} - -#[test] -fn emfplus_wrap_line_linear_performance() { - let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)); - // Repeated words that wrap across lines - let text = "word ".repeat(500); - let s = prec( - 0x401C, - 0x8000, - &cat(vec![h32(0xFF00_0000), h32(9), h32(text.encode_utf16().count() as u32), f32s(&[0.0, 0.0, 100.0, 1000.0]), u16s(&text)]), - ); - let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_FFFF), h32(1), f32s(&[0.0, 0.0, 100.0, 1000.0])])); - let p = plus_pic(vec![fill, font, s]); - let text_items = p.items.iter().filter(|i| matches!(i, Item::Text(_))).count(); - assert!(text_items > 10, "wrapped into multiple lines: {text_items}"); -} - -#[test] -fn clip_polygon_work_bound() { - let poly: Vec<(f64, f64)> = (0..2000).map(|i| (i as f64, i as f64)).collect(); - let clip: Vec<(f64, f64)> = (0..2000).map(|i| (i as f64, i as f64)).collect(); - assert!(crate::cliprect::clip_polygon_within(&poly, &clip, &mut 1_000_000).is_none(), "excessive polygon clip product rejected"); -} - -#[test] -fn clip_polygon_charges_actual_work_not_projected_growth() { - // Two convex 1,000-gons: the product is exactly the budget, and a cut that adds a vertex must - // not make a later step look over budget. - let gon = |cx: f64| -> Vec<(f64, f64)> { - (0..1000).map(|i| (i as f64 / 1000.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (cx + 100.0 * c, 100.0 * s)).collect() - }; - let clipped = crate::cliprect::clip_polygon_within(&gon(0.0), &gon(30.0), &mut 4_000_000).unwrap(); - assert!(clipped.len() > 100, "the intersection is kept: {}", clipped.len()); -} - -#[test] -fn clip_work_is_shared_by_fill_and_stroke() { - use crate::canvas::Raw; - use crate::geom::Op; - // One zigzag subpath crossing a 512-gon's edge: about 510k point-edge tests per pass, so a - // fill alone fits the allowance but a fill plus a stroke would spend it twice. - let clip: Vec<(f64, f64)> = - (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect(); - let mut ops = vec![Op::M((0.0, 95.0))]; - ops.extend((1..1000).map(|i| Op::L((i as f64 * 0.19, if i % 2 == 0 { 95.0 } else { 300.0 })))); - ops.push(Op::Z); - let raw = |fill, stroke| Raw::Path { ops: ops.clone(), fill, stroke, even_odd: false, region: None }; - let black = [0, 0, 0, 255]; - assert!(!crate::cliprect::clip_raw(raw(Some(black), None), &clip).is_empty(), "a fill alone is within the allowance"); - assert!(crate::cliprect::clip_raw(raw(Some(black), Some((black, 1.0))), &clip).is_empty(), "fill and stroke share one allowance"); -} - -#[test] -fn clip_work_is_bounded_across_subpaths() { - use crate::canvas::Raw; - use crate::geom::Op; - // 512-gon clip; 4,000 small triangles crossing its edge: each under the per-polygon cap, together - // far over it, for both the fill and the stroke. - let clip: Vec<(f64, f64)> = - (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect(); - let tri = |x: f64| [Op::M((x, 95.0)), Op::L((x + 1.0, 95.0)), Op::L((x, 300.0)), Op::Z]; - let many: Vec = (0..4000).flat_map(|i| tri(i as f64 % 200.0)).collect(); - let raw = |ops| Raw::Path { ops, fill: Some([0, 0, 0, 255]), stroke: Some(([0, 0, 0, 255], 1.0)), even_odd: false, region: None }; - assert!(crate::cliprect::clip_raw(raw(many), &clip).is_empty(), "the whole item's clipping work is bounded"); - assert_eq!(crate::cliprect::clip_raw(raw(tri(100.0).to_vec()), &clip).len(), 2, "a small crossing shape is still clipped"); -} - -/// Clipping a fill can add a vertex at every clip edge, so a polygon's work isn't its input size: -/// shapes that pass the up-front estimate but grow while clipped are charged as they grow, against -/// one budget for the whole item. -#[test] -fn growing_fill_clips_share_one_budget() { - use crate::canvas::Raw; - use crate::geom::Op; - let clip: Vec<(f64, f64)> = - (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect(); - // A triangle around the whole circle: clipped, it becomes the 512-gon, about 130,000 steps. - let tri = [Op::M((-1000.0, -1000.0)), Op::L((1000.0, -1000.0)), Op::L((100.0, 1000.0)), Op::Z]; - let raw = |n: usize| Raw::Path { ops: tri.repeat(n), fill: Some([0, 0, 0, 255]), stroke: None, even_odd: false, region: None }; - // 400 of them pass the estimate (400 x 4 x 512 is under a million) but cost about 52 million. - assert!(crate::cliprect::clip_raw(raw(400), &clip).is_empty(), "the growing work is charged"); - assert_eq!(crate::cliprect::clip_raw(raw(2), &clip).len(), 1, "a couple of them are still clipped"); -} - -#[test] -fn stroke_work_is_charged_and_outside_items_are_cheap() { - use crate::canvas::Raw; - use crate::geom::Op; - let clip: Vec<(f64, f64)> = - (0..512).map(|i| (i as f64 / 512.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (100.0 + 100.0 * c, 100.0 + 100.0 * s)).collect(); - // A stroke-only zigzag of 4,000 segments across the edge: 4,000 x 512 tests, over the allowance. - let mut ops = vec![Op::M((0.0, 95.0))]; - ops.extend((1..4000).map(|i| Op::L((i as f64 * 0.05, if i % 2 == 0 { 95.0 } else { 300.0 })))); - let raw = Raw::Path { ops, fill: None, stroke: Some(([0, 0, 0, 255], 1.0)), even_odd: false, region: None }; - assert!(crate::cliprect::clip_raw(raw, &clip).is_empty(), "stroke work is bounded"); -} - -#[test] -fn region_intersection_over_budget_keeps_the_previous_area() { - let gon = |cx: f64| -> Vec<(f64, f64)> { - (0..1000).map(|i| (i as f64 / 1000.0 * std::f64::consts::TAU).sin_cos()).map(|(s, c)| (cx + 100.0 * c, 100.0 * s)).collect() - }; - assert!(crate::cliprect::clip_polygon_within(&gon(0.0), &gon(30.0), &mut 10).is_none()); - assert!(crate::cliprect::clip_polygon_within(&gon(0.0), &gon(30.0), &mut crate::cliprect::MAX_AREA_CLIP_WORK.clone()).is_some()); -} - -#[test] -fn full_canvas_does_not_sample_gradient_tiles() { - let mut cv = Canvas::new(); - let big = || Rgba { w: 1, h: 1, bottom_up: false, px: vec![0u8; MAX_BYTES / 2] }; - for _ in 0..3 { - cv.bitmap([0.0, 0.0, 1.0, 1.0], big()); - } - assert!(cv.full()); - cv.shade_shape(&crate::geom::rect(0.0, 0.0, 10.0, 10.0), &|_| panic!("sampled a full canvas")); - assert_eq!(cv.items.len(), 2); -} - -#[test] -fn canvas_budget_counts_bytes_held_elsewhere() { - let mut cv = Canvas::new(); - cv.hold(MAX_BYTES - 100); - cv.bitmap([0.0, 0.0, 1.0, 1.0], Rgba { w: 1, h: 1, bottom_up: false, px: vec![0u8; 200] }); - assert!(cv.full()); - assert!(cv.items.is_empty()); -} diff --git a/crates/metafile/src/text.rs b/crates/metafile/src/text.rs deleted file mode 100644 index 0125e9c5d..000000000 --- a/crates/metafile/src/text.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Text records: the font (LOGFONT), the text string decoded from its character set, and the -//! neutral [`Text`] item. The metafile crate knows no fonts, so measuring, alignment against the -//! font's metrics and glyph drawing are left to the renderer. - -/// A font chosen by a CREATEFONTINDIRECT record (MS-WMF 2.2.1.2 Font, MS-EMF 2.2.13 LogFont). -#[derive(Debug, Clone, PartialEq, Default)] -pub struct Font { - /// The face name the file asks for (`"Arial"`, `"Wingdings"`); may be empty. - pub family: String, - /// LOGFONT height in text units: negative is the character (em) height, positive the cell - /// height (ascent plus descent), 0 the default size. - pub height: f32, - /// Average character width in text units; 0 keeps the font's own proportions. - pub width: f32, - /// 100..=900 (400 regular, 700 bold); 0 is the default weight. - pub weight: u16, - pub italic: bool, - pub underline: bool, - pub strike: bool, - /// LOGFONT character set (0 ANSI, 2 SYMBOL, 238 EASTEUROPE…). - pub charset: u8, -} - -/// SYMBOL_CHARSET: byte codes are the font's own symbol codes. -pub const SYMBOL_CHARSET: u8 = 2; - -/// Where the reference point sits on the string horizontally (TA_LEFT, TA_CENTER, TA_RIGHT). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum HAlign { - #[default] - Left, - Center, - Right, -} - -/// Where the reference point sits vertically (TA_TOP, TA_BASELINE, TA_BOTTOM): the top of the -/// character cell, the baseline or the bottom of the cell. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum VAlign { - #[default] - Top, - Baseline, - Bottom, -} - -/// One string drawn by EXTTEXTOUT, TEXTOUT or SMALLTEXTOUT. -#[derive(Debug, Clone, PartialEq)] -pub struct Text { - /// Text space to picture space as affine coefficients `[a, b, c, d, e, f]` (`x' = a·x + c·y + e`, - /// `y' = b·x + d·y + f`, kurbo's order). Text space has x along the baseline and y down, in the - /// metafile's logical units, with the reference point at the origin. Escapement and the - /// mapping are folded in; glyphs are never mirrored. - pub transform: [f64; 6], - pub text: String, - /// Advance after each character of `text`, in text units (the dx array). Empty when the file - /// gives none: the font's own advances apply. - pub dx: Vec, - pub font: Font, - pub color: [u8; 4], - /// Colour filled behind the character cells (OPAQUE background mode); None when transparent. - pub background: Option<[u8; 4]>, - pub h_align: HAlign, - pub v_align: VAlign, - /// Clip rectangle `[x0, y0, x1, y1]` in picture units (ETO_CLIPPED). - pub clip: Option<[f32; 4]>, - /// Clip region from the device context, in picture units. None is the default clip. Drawn - /// together with [`Text::clip`] when both are set. - pub region: Option>, -} - -/// TA_UPDATECP: the string starts at the current position, which moves past it. -pub(crate) const TA_UPDATECP: u32 = 0x0001; -/// ETO_OPAQUE fills the rectangle with the background colour; ETO_CLIPPED clips the text to it. -pub(crate) const ETO_OPAQUE: u32 = 0x0002; -pub(crate) const ETO_CLIPPED: u32 = 0x0004; -/// The string holds glyph indices, not characters (drawn only when the font matches). -pub(crate) const ETO_GLYPH_INDEX: u32 = 0x0010; -/// The dx array holds (dx, dy) pairs. -pub(crate) const ETO_PDY: u32 = 0x2000; -/// Longest string kept from one record. -pub(crate) const MAX_CHARS: usize = 16_384; - -/// Horizontal and vertical alignment from a TA_* word (the vertical bits are 0 top, 8 bottom, -/// 24 baseline; the horizontal ones 0 left, 2 right, 6 center). -pub(crate) fn align(ta: u32) -> (HAlign, VAlign) { - let h = match ta & 0x06 { - 6 => HAlign::Center, - 2 => HAlign::Right, - _ => HAlign::Left, - }; - let v = match ta & 0x18 { - 0x18 => VAlign::Baseline, - 0x08 => VAlign::Bottom, - _ => VAlign::Top, - }; - (h, v) -} - -/// Decodes 8-bit text in `charset` to one character per byte, and merges `dx` to match: a -/// double-byte character set's lead and trail bytes become one blank (its glyphs are not -/// decoded) whose advance is the sum of both. `dx` may be empty. -pub(crate) fn decode_bytes(bytes: &[u8], charset: u8, dx: &[f32]) -> (String, Vec) { - let mut s = String::with_capacity(bytes.len()); - let mut out_dx = Vec::with_capacity(dx.len().min(bytes.len())); - let dbcs = matches!(charset, 128 | 129 | 130 | 134 | 136); - let mut i = 0; - while let Some(&b) = bytes.get(i) { - let n = if dbcs && b >= 0x81 && i + 1 < bytes.len() { 2 } else { 1 }; - let c = if n == 2 { ' ' } else { byte_char(b, charset) }; - s.push(c); - if !dx.is_empty() { - out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum()); - } - i += n; - } - (s, out_dx) -} - -/// UTF-16 text to a string with one dx entry per character: a surrogate pair takes the sum of -/// its two advances. Unpaired surrogates become U+FFFD. -pub(crate) fn decode_utf16(units: &[u16], dx: &[f32]) -> (String, Vec) { - let mut s = String::with_capacity(units.len()); - let mut out_dx = Vec::with_capacity(dx.len().min(units.len())); - let mut i = 0; - for r in char::decode_utf16(units.iter().copied()) { - let c = r.unwrap_or(char::REPLACEMENT_CHARACTER); - let n = c.len_utf16(); - s.push(c); - if !dx.is_empty() { - out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum()); - } - i += n; - } - (s, out_dx) -} - -/// One byte in a single-byte Windows code page. Symbol fonts keep their codes in the private-use -/// range (U+F000 + code), where symbol fonts map them. -fn byte_char(b: u8, charset: u8) -> char { - if b < 0x80 && charset != SYMBOL_CHARSET { - return char::from(b); - } - let cp = match charset { - SYMBOL_CHARSET => 0xF000 | u32::from(b), - 238 => high(b, &CP1250), - 204 => high(b, &CP1251), - _ => high(b, &CP1252), - }; - char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER) -} - -/// The code point of byte `b` (0x80..=0xFF) from a table of the upper half; 0 entries are -/// undefined bytes, drawn as U+FFFD. -fn high(b: u8, table: &[u16; 128]) -> u32 { - match table.get(usize::from(b.wrapping_sub(0x80))) { - Some(0) | None => 0xFFFD, - Some(v) => u32::from(*v), - } -} - -/// Windows-1252 (Western) 0x80..=0xFF: the upper half of Latin-1 with typographic marks in 0x80..=0x9F. -const CP1252: [u16; 128] = { - let mut t = [0u16; 128]; - let low: [u16; 32] = [ - 0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0, 0, 0x2018, 0x2019, 0x201C, - 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178, - ]; - let mut i = 0; - while i < 128 { - t[i] = if i < 32 { low[i] } else { 0x80 + i as u16 }; - i += 1; - } - t -}; - -/// Windows-1250 (Central European) 0x80..=0xFF. -const CP1250: [u16; 128] = [ - 0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, // - 0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, // - 0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, // - 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, // - 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, // - 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, // - 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, // - 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, -]; - -/// Windows-1251 (Cyrillic) 0x80..=0xFF; 0xC0..=0xFF are А..я in order. -const CP1251: [u16; 128] = { - let low: [u16; 64] = [ - 0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, // - 0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, // - 0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, // - 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, - ]; - let mut t = [0u16; 128]; - let mut i = 0; - while i < 128 { - t[i] = if i < 64 { low[i] } else { 0x0410 + (i as u16 - 64) }; - i += 1; - } - t -}; diff --git a/crates/metafile/src/wmf.rs b/crates/metafile/src/wmf.rs deleted file mode 100644 index 3e02c10bd..000000000 --- a/crates/metafile/src/wmf.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! WMF reader (MS-WMF). A file is an optional 22-byte placeable header, a 9-word standard header, then -//! records of (size in 16-bit words, function, parameters). Parameters are read at byte 6 of each record. - -use std::sync::Arc; - -use crate::bytes::Bytes; -use crate::canvas::{BS_HATCHED, Brush, Canvas, LogFont, Obj, Pen, colorref}; -use crate::dib; -use crate::geom::{self, ArcKind, P}; -use crate::text::{ETO_CLIPPED, ETO_OPAQUE, Font, MAX_CHARS, decode_bytes}; -use crate::{Error, Frame, MAX_PATH_OPS}; - -const PLACEABLE_KEY: u32 = 0x9AC6_CDD7; -const DEFAULT_INCH: f64 = 1440.0; -/// A placeable WMF plays into a target sized in points; brushes are laid out in 96-dpi pixels. -const PT_PER_PX: f64 = 72.0 / 96.0; - -pub(crate) fn is_wmf(b: Bytes) -> bool { - if b.u32(0) == Some(PLACEABLE_KEY) { - return true; - } - matches!(b.u16(0), Some(1 | 2)) && b.u16(2) == Some(9) && matches!(b.u16(4), Some(0x100 | 0x300)) -} - -/// Reads all records. Returns the drawing state and the picture frame, or an error when no record -/// could be read at all. -pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { - let placeable = input.u32(0) == Some(PLACEABLE_KEY); - // Placeable bounds: logical corners (left, top, right, bottom) and their size in points. - let (hdr, bounds) = if placeable { - let [l, t, r, b] = [6, 8, 10, 12].map(|o| input.i16(o).map(f64::from)); - let (Some(l), Some(t), Some(r), Some(b)) = (l, t, r, b) else { return Err(Error::BadHeader) }; - let inch = input.u16(14).filter(|v| *v > 0).map_or(DEFAULT_INCH, f64::from); - let (w, h) = (r - l, b - t); - let bounds = (w > 0.0 && h > 0.0).then_some(([l, t, r, b], (w * 72.0 / inch, h * 72.0 / inch))); - (22, bounds) - } else { - (0, None) - }; - let hsize = input.u16(hdr + 2).ok_or(Error::BadHeader)? as usize; - if hsize < 9 { - return Err(Error::BadHeader); - } - let mut pos = hdr.checked_add(hsize.checked_mul(2).ok_or(Error::BadHeader)?).ok_or(Error::BadHeader)?; - // Played into a target like Word does: the placeable bounds are the first window, the target (the - // picture size) the viewport. A plain WMF's target is its first window. - let mut cv = Canvas::new(); - if let Some((c, size)) = bounds { - cv.play_into(Some([c[0], c[1], c[2] - c[0], c[3] - c[1]]), size); - cv.set_clip_surface([0.0, 0.0, size.0, size.1]); - cv.pattern_px = PT_PER_PX; - } - let mut player_viewport = bounds.is_some(); - let mut parsed = 0usize; - while !cv.full() { - let (Some(words), Some(func)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u16(o))) else { break }; - let Some(len) = (words as usize).checked_mul(2).filter(|l| *l >= 6) else { break }; - let Some(rec) = input.slice(pos, len) else { break }; - if func == 0 { - break; - } - record(&mut cv, func, Bytes(rec)); - if func == 0x020C && cv.window_set && !player_viewport && !cv.viewport_set { - let we = cv.map.window_ext(); - cv.play_into(None, (we.0.abs(), we.1.abs())); - player_viewport = true; - } - parsed += 1; - let Some(next) = pos.checked_add(len) else { break }; - pos = next; - } - if parsed == 0 { - return Err(Error::NoRecords); - } - let frame = match bounds { - // Played into its target: the frame is the target. - Some((_, size)) if !cv.viewport_set && cv.map.uses_extents() => Some(Frame { rect: [0.0, 0.0, size.0, size.1], size: Some(size) }), - // The file set its own viewport, so the drawing is in its device units and the logical bounds - // go through the final mapping. - Some((c, size)) => { - let (a, b) = (cv.map.device((c[0], c[1])), cv.map.device((c[2], c[3]))); - let rect = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()]; - (rect[2] > 0.0 && rect[3] > 0.0).then_some(Frame { rect, size: Some(size) }) - } - None if player_viewport && !cv.viewport_set && cv.map.uses_extents() => { - let ve = cv.map.viewport_ext(); - Some(Frame { rect: [0.0, 0.0, ve.0, ve.1], size: None }) - } - None if cv.window_set => Some(Frame { rect: cv.map.window_rect(), size: None }), - None => None, - }; - Ok((cv, frame)) -} - -fn record(cv: &mut Canvas, func: u16, r: Bytes) { - if cv.full() { - return; - } - let _ = match func { - 0x0103 => r.u16(6).map(|m| cv.set_map_mode(u32::from(m))), - 0x0106 => r.u16(6).map(|m| cv.set_polyfill(u32::from(m))), - 0x0104 => r.u16(6).map(|m| cv.set_rop2(u32::from(m))), - // Y is stored before X in these records. - 0x020B => xy16(r, 6).map(|p| cv.set_window_org(p)), - 0x020C => xy16(r, 6).map(|p| cv.set_window_ext(p)), - 0x020D => xy16(r, 6).map(|p| cv.set_viewport_org(p)), - 0x020E => xy16(r, 6).map(|p| cv.set_viewport_ext(p)), - 0x001E => { - cv.save_dc(); - Some(()) - } - 0x0127 => r.i16(6).map(|n| cv.restore_dc(i32::from(n))), - 0x0214 => xy16(r, 6).map(|p| cv.move_to(p)), - 0x0213 => xy16(r, 6).map(|p| cv.line_to(p)), - 0x0325 => counted_points(r).map(|pts| cv.poly(&pts, false, false, true)), - 0x0324 => counted_points(r).map(|pts| cv.poly(&pts, true, true, true)), - 0x0538 => polypolygon(r).map(|ops| cv.shape(&ops, true, true)), - 0x041B => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)), - 0x061C => { - let [ew, eh] = [r.i16(8), r.i16(6)]; - let (Some(ew), Some(eh)) = (ew, eh) else { return }; - rect16(r, 10).map(|[l, t, rr, b]| { - cv.shape(&geom::round_rect(l, t, rr, b, f64::from(ew), f64::from(eh)), true, true); - }) - } - 0x0415 => rect16(r, 6).map(|rect| cv.exclude_clip_rect(rect)), - 0x0416 => rect16(r, 6).map(|rect| cv.intersect_clip_rect(rect)), - 0x0418 => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)), - 0x0817 | 0x081A | 0x0830 => arc16(r).map(|(bounds, s, e)| { - let kind = match func { - 0x0817 => ArcKind::Arc, - 0x081A => ArcKind::Pie, - _ => ArcKind::Chord, - }; - let [l, t, rr, b] = bounds; - cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true); - }), - 0x02FA => create_pen(cv, r), - 0x02FC => create_brush(cv, r), - 0x02FB => { - let f = font(r).unwrap_or_default(); - cv.add_object(None, Obj::Font(Arc::new(f))); - Some(()) - } - 0x0142 => { - // A pattern the reader cannot decode still takes its handle slot. - let b = dib_pattern_brush(r).map_or(Obj::Other, Obj::Brush); - cv.add_object(None, b); - Some(()) - } - // Palettes, old-style pattern brushes and regions take a handle slot too, so later - // SELECTOBJECT handles count them; selecting one draws nothing. - 0x00F7 | 0x01F9 | 0x06FF => { - cv.add_object(None, Obj::Other); - Some(()) - } - 0x0209 => r.u32(6).map(|c| cv.set_text_color(c)), - 0x0201 => r.u32(6).map(|c| cv.set_bk_color(c)), - 0x0102 => r.u16(6).map(|m| cv.set_bk_mode(u32::from(m))), - 0x012E => r.u16(6).map(|a| cv.set_text_align(u32::from(a))), - 0x0A32 => ext_text_out(cv, r), - 0x0521 => text_out(cv, r), - // PATBLT: rop(4), height, width, y, x. - 0x061D => r.u32(6).map(|rop| cv.fill_rop([i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)], rop)), - 0x012D => r.u16(6).map(|h| cv.select(wmf_handle(h))), - 0x01F0 => r.u16(6).map(|h| cv.delete_object(u32::from(h))), - // STRETCHDIB: rop(4) usage(2) srcH srcW ySrc xSrc destH destW yDest xDest, then the DIB. - 0x0F43 => { - let rop = r.u32(6).unwrap_or(0); - dib_blit(cv, r, 28, rop, [i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], [i16s(r, 18), i16s(r, 16), i16s(r, 14), i16s(r, 12)]) - } - 0x0B41 => dib_stretchblt(cv, r), - 0x0940 => dib_bitblt(cv, r), - _ => Some(()), - }; -} - -/// A 16-bit WMF handle as a canvas object index: stock objects are `0x8000 | index`, which the -/// canvas reads with the 32-bit stock flag. -fn wmf_handle(h: u16) -> u32 { - let h = u32::from(h); - if h & 0x8000 != 0 { (h & 0x7FFF) | 0x8000_0000 } else { h } -} - -/// An i16 field as f64; 0 past the end. -fn i16s(r: Bytes, off: usize) -> f64 { - r.i16(off).map_or(0.0, f64::from) -} - -/// CREATEPENINDIRECT: style, width (x, y), colour. Only the width's x component is used. -fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> { - let style = r.u16(6)?; - let width = r.i16(8)?; - let color = r.u32(12)?; - cv.add_object(None, Obj::Pen(Pen { style: u32::from(style), width: f64::from(width), color: colorref(color) })); - Some(()) -} - -/// CREATEBRUSHINDIRECT: style, colour, hatch. -fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> { - let style = u32::from(r.u16(6)?); - let color = r.u32(8)?; - let mut b = Brush::new(style, colorref(color)); - if style == BS_HATCHED { - b.hatch = u32::from(r.u16(12).unwrap_or(0)); - } - cv.add_object(None, Obj::Brush(b)); - Some(()) -} - -/// DIBCREATEPATTERNBRUSH: style, colour usage, then a DIB (MS-WMF 2.3.4.8). The DIB's own palette -/// gives the colours; with DIB_PAL_COLORS (indices into a palette the reader does not keep) a -/// monochrome DIB takes the text and background colours. -fn dib_pattern_brush(r: Bytes) -> Option { - let usage = r.u16(8)?; - let dib = r.tail(10)?; - let n = dib::info_len(dib)?; - let img = dib::decode(Bytes(dib.slice(0, n)?), dib.tail(n)?, false)?; - Brush::pattern(img, usage == 1) -} - -/// Font object (MS-WMF 2.2.1.2): height, width, escapement, orientation, weight (i16 each), italic, -/// underline, strike-out and charset bytes, three precision/quality bytes, pitch and family, then the -/// face name (up to 32 bytes, NUL-terminated, in the font's character set). -fn font(r: Bytes) -> Option { - let height = r.i16(6)?; - let width = r.i16(8).unwrap_or(0); - let escapement = r.i16(10).unwrap_or(0); - let weight = r.i16(14).unwrap_or(0); - let byte = |o: usize| r.0.get(o).copied().unwrap_or(0); - let charset = byte(19); - let name = r.0.get(24..).unwrap_or(&[]); - let name = name.get(..32).unwrap_or(name); - let name = name.split(|b| *b == 0).next().unwrap_or(&[]); - // Face names are plain ASCII in practice; other bytes go through the Western code page. - let (family, _) = decode_bytes(name, 0, &[]); - Some(LogFont { - font: Font { - family: family.trim().to_string(), - height: f32::from(height), - width: f32::from(width), - weight: weight.clamp(0, 1000) as u16, - italic: byte(16) != 0, - underline: byte(17) != 0, - strike: byte(18) != 0, - charset, - }, - escapement: f64::from(escapement), - }) -} - -/// The charset of the selected font, for decoding 8-bit text. -fn charset(cv: &Canvas) -> u8 { - cv.font_charset() -} - -/// EXTTEXTOUT (MS-WMF 2.3.3.5): y, x, string length, options, the rectangle (left, top, right, -/// bottom) when ETO_OPAQUE or ETO_CLIPPED is set, the string padded to a word, then an optional dx -/// array of i16 advances. -fn ext_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> { - let (y, x) = (r.i16(6)?, r.i16(8)?); - let n = usize::from(r.u16(10)?).min(MAX_CHARS); - let opts = u32::from(r.u16(12)?); - let has_rect = opts & (ETO_OPAQUE | ETO_CLIPPED) != 0; - let rect = if has_rect { Some([i16s(r, 14), i16s(r, 16), i16s(r, 18), i16s(r, 20)]) } else { None }; - let at = if has_rect { 22 } else { 14 }; - // A string running past the record is cut to what the record holds. - let avail = r.0.len().saturating_sub(at); - let n = n.min(avail); - let bytes = r.slice(at, n)?; - let dx_at = at + n.next_multiple_of(2); - let dx: Vec = if r.0.len() >= dx_at + 2 * n { (0..n).filter_map(|i| r.i16(dx_at + 2 * i)).map(f32::from).collect() } else { Vec::new() }; - let (text, dx) = decode_bytes(bytes, charset(cv), &dx); - cv.text((f64::from(x), f64::from(y)), text, dx, opts, rect); - Some(()) -} - -/// TEXTOUT (MS-WMF 2.3.3.20): string length, the string padded to a word, then y and x. -fn text_out(cv: &mut Canvas, r: Bytes) -> Option<()> { - let n = usize::from(r.u16(6)?).min(MAX_CHARS); - let bytes = r.slice(8, n)?; - let at = 8 + n.next_multiple_of(2); - let (y, x) = (r.i16(at)?, r.i16(at + 2)?); - let (text, _) = decode_bytes(bytes, charset(cv), &[]); - cv.text((f64::from(x), f64::from(y)), text, Vec::new(), 0, None); - Some(()) -} - -/// (x, y) from a Y-then-X pair stored at `off`. -fn xy16(r: Bytes, off: usize) -> Option

{ - Some((f64::from(r.i16(off + 2)?), f64::from(r.i16(off)?))) -} - -/// Rectangle stored as bottom, right, top, left (the order MS-WMF uses for RECTANGLE and ELLIPSE). -fn rect16(r: Bytes, off: usize) -> Option<[f64; 4]> { - let (b, rr, t, l) = (r.i16(off)?, r.i16(off + 2)?, r.i16(off + 4)?, r.i16(off + 6)?); - Some([f64::from(l), f64::from(t), f64::from(rr), f64::from(b)]) -} - -/// ARC, PIE and CHORD: end Y, end X, start Y, start X, then the bounding rectangle (bottom, right, top, left). -fn arc16(r: Bytes) -> Option<([f64; 4], P, P)> { - let end = xy16(r, 6)?; - let start = xy16(r, 10)?; - let bounds = rect16(r, 14)?; - Some((bounds, start, end)) -} - -/// A count of 16-bit points, then the points (POLYLINE, POLYGON). -fn counted_points(r: Bytes) -> Option> { - let n = r.u16(6)? as usize; - r.tail(8)?.points(0, n, false) -} - -/// POLYPOLYGON: polygon count, one 16-bit point count per polygon, then all points. -fn polypolygon(r: Bytes) -> Option> { - let n = r.u16(6)? as usize; - let mut counts = Vec::with_capacity(n); - for i in 0..n { - counts.push(r.u16(i.checked_mul(2)?.checked_add(8)?)? as usize); - } - let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?; - if total > MAX_PATH_OPS { - return None; - } - let pts = r.tail(n.checked_mul(2)?.checked_add(8)?)?.points(0, total, false)?; - Some(geom::subpaths(&pts, &counts, true)) -} - -/// Whether a DIBBITBLT or DIBSTRETCHBLT record carries a bitmap: without one it is exactly -/// `(function >> 8) + 3` words long (MS-WMF 2.3.1.2, 2.3.1.3). -fn has_bitmap(func: u16, r: Bytes) -> bool { - r.0.len() / 2 > usize::from(func >> 8) + 3 -} - -/// META_DIBBITBLT: rop(4), ySrc, xSrc, then height, width, yDest, xDest and the DIB at 22. The -/// bitmap-less form has a reserved word before the height, so its fields sit 2 bytes later; it, or -/// a source-less raster operation, fills the destination. -fn dib_bitblt(cv: &mut Canvas, r: Bytes) -> Option<()> { - let rop = r.u32(6)?; - if !has_bitmap(0x0940, r) { - cv.fill_rop([i16s(r, 22), i16s(r, 20), i16s(r, 18), i16s(r, 16)], rop); - return Some(()); - } - let dst = [i16s(r, 20), i16s(r, 18), i16s(r, 16), i16s(r, 14)]; - if Canvas::sourceless(rop) { - cv.fill_rop(dst, rop); - return Some(()); - } - let src = [i16s(r, 12), i16s(r, 10), dst[2], dst[3]]; - dib_blit(cv, r, 22, rop, dst, src) -} - -/// META_DIBSTRETCHBLT: rop(4), srcH, srcW, ySrc, xSrc, then destH, destW, yDest, xDest and the DIB at -/// 26. The bitmap-less form has a reserved word after xSrc, so the destination sits 2 bytes later; -/// it, or a source-less raster operation, fills the destination. -fn dib_stretchblt(cv: &mut Canvas, r: Bytes) -> Option<()> { - let rop = r.u32(6)?; - if !has_bitmap(0x0B41, r) { - cv.fill_rop([i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], rop); - return Some(()); - } - let dst = [i16s(r, 24), i16s(r, 22), i16s(r, 20), i16s(r, 18)]; - if Canvas::sourceless(rop) { - cv.fill_rop(dst, rop); - return Some(()); - } - let src = [i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)]; - dib_blit(cv, r, 26, rop, dst, src) -} - -/// A DIB at `at`, stretched from source rectangle `src` into `dst` (both in logical units, pixels for `src`). -/// The raster operation `rop` decides how the pixels combine (see [`dib::prepare`]). -fn dib_blit(cv: &mut Canvas, r: Bytes, at: usize, rop: u32, dst: [f64; 4], src: [f64; 4]) -> Option<()> { - let dib = r.tail(at)?; - let n = dib::info_len(dib)?; - let info = dib.slice(0, n)?; - let bits = dib.tail(n)?; - let mut img = dib::decode(Bytes(info), bits, false)?; - dib::prepare(&mut img, dib::Blend::Rop(rop)); - let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?; - cv.bitmap(dst, img); - Some(()) -} diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 27042312d..2b04b93c7 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] deckcraft-geom = { workspace = true } deckcraft-color = { workspace = true } -deckcraft-metafile = { workspace = true } +craft-metafile = { workspace = true } deckcraft-model = { workspace = true } deckcraft-anim = { workspace = true } deckcraft-fonts = { workspace = true } diff --git a/crates/render/src/metafile.rs b/crates/render/src/metafile.rs index 91fa01389..a6e55c4fb 100644 --- a/crates/render/src/metafile.rs +++ b/crates/render/src/metafile.rs @@ -1,5 +1,5 @@ //! Windows Metafile and Enhanced Metafile pictures (`image/x-wmf`, `image/x-emf`): parsed by -//! `deckcraft-metafile` and drawn as vectors into a bitmap of the size the slide needs, so they go +//! `craft-metafile` and drawn as vectors into a bitmap of the size the slide needs, so they go //! through the same crop, tile and recolour path as any other picture. //! //! Text items name the font the file asks for; they are drawn with the face slide text would use @@ -10,8 +10,8 @@ use std::collections::HashMap; use std::sync::Arc; +use craft_metafile::{Cap, HAlign, Item, Join, LineStyle, PlacedClip, PlacedItem, Text, Tile, VAlign}; use deckcraft_fonts::{FontDb, FontFace}; -use deckcraft_metafile::{HAlign, Item, PlacedItem, Text, Tile, VAlign}; use kurbo::{Affine, BezPath, Rect, Shape as _}; use skrifa::MetadataProvider as _; use vello_cpu::{Pixmap, RenderContext, Resources, peniko}; @@ -28,13 +28,13 @@ const DEFAULT_EM: f64 = 12.0; /// Is `bytes` a WMF or EMF file? pub fn is_metafile(bytes: &[u8]) -> bool { - deckcraft_metafile::is_metafile(bytes) + craft_metafile::is_metafile(bytes) } /// Draw the metafile `bytes` into straight RGBA with its longer side `side` px (transparent where /// it draws nothing). `None` when it doesn't parse or has no size. pub fn rasterize(bytes: &[u8], side: u32) -> Option { - let mut pic = deckcraft_metafile::parse(bytes).ok()?; + let mut pic = craft_metafile::parse(bytes).ok()?; if !pic.has_size() { return None; } @@ -51,31 +51,39 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { ctx.push_clip_layer(&r.to_path(0.1)); for it in pic.place(r, [0.0; 4]) { match it { - PlacedItem::Path { path, fill, stroke, even_odd, region } => clipped(&mut ctx, region.as_deref(), |ctx| { + PlacedItem::Path { path, fill, stroke, line, even_odd, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| { if let Some(f) = fill { ctx.set_paint(rgba(f)); ctx.set_fill_rule(if even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero }); ctx.fill_path(&path); } - // GDI pens: round caps and joins; a cosmetic (zero-width) pen is one pixel. + // The pen's caps, joins and dashes (GDI's default: round, solid); a cosmetic + // (zero-width) pen is one pixel. if let Some((s, sw)) = stroke { ctx.set_paint(rgba(s)); - ctx.set_stroke(kurbo::Stroke::new(sw.max(1.0)).with_caps(kurbo::Cap::Round).with_join(kurbo::Join::Round)); + ctx.set_stroke(pen(sw.max(1.0), &line)); ctx.stroke_path(&path); } }), - PlacedItem::Bitmap { index, rect, region } => clipped(&mut ctx, region.as_deref(), |ctx| { + PlacedItem::Bitmap { index, rect, transform, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| { let Some(Some(pm)) = pixmaps.get(index) else { return }; let (bw, bh) = (pm.width().max(1) as f64, pm.height().max(1) as f64); ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm.clone()), sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium), }); - ctx.set_paint_transform(Affine::translate((rect.x0, rect.y0)) * Affine::scale_non_uniform(rect.width() / bw, rect.height() / bh)); - ctx.fill_rect(&rect); + let c = transform.as_coeffs(); + if c[1] == 0.0 && c[2] == 0.0 { + ctx.set_paint_transform(Affine::translate((rect.x0, rect.y0)) * Affine::scale_non_uniform(rect.width() / bw, rect.height() / bh)); + ctx.fill_rect(&rect); + } else { + // A turned or skewed bitmap: `transform` maps the unit square onto its place. + ctx.set_paint_transform(transform * Affine::scale_non_uniform(1.0 / bw, 1.0 / bh)); + ctx.fill_path(&(transform * Rect::new(0.0, 0.0, 1.0, 1.0).to_path(0.1))); + } ctx.reset_paint_transform(); }), - PlacedItem::Pattern { index, path, even_odd, tile, region } => clipped(&mut ctx, region.as_deref(), |ctx| { + PlacedItem::Pattern { index, path, even_odd, tile, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| { let Some(Item::Pattern { tile: t, .. }) = pic.items.get(index) else { return }; let key = Arc::as_ptr(t); let (pm, avg) = tiles.entry(key).or_insert_with(|| (premul(t.width, t.height, &t.rgba), average(&t.rgba))).clone(); @@ -102,7 +110,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { } } }), - PlacedItem::Text { index, transform, clip, region } => clipped(&mut ctx, region.as_deref(), |ctx| { + PlacedItem::Text { index, transform, clip, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| { let Some(Item::Text(t)) = pic.items.get(index) else { return }; if let Some(c) = clip { ctx.push_clip_layer(&c.to_path(0.1)); @@ -133,15 +141,40 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { image::RgbaImage::from_raw(w as u32, h as u32, px) } -/// Draws `draw` inside the device-context clip. No region is the default clip. -fn clipped(ctx: &mut RenderContext, region: Option<&BezPath>, draw: impl FnOnce(&mut RenderContext)) { +/// Draws `draw` inside the device-context clip and every clip path. No region is the default clip. +fn clipped(ctx: &mut RenderContext, region: Option<&BezPath>, clips: &[Arc], draw: impl FnOnce(&mut RenderContext)) { if let Some(region) = region { ctx.push_clip_layer(region); - draw(ctx); - ctx.pop_layer(); - } else { - draw(ctx); } + for c in clips { + ctx.set_fill_rule(if c.even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero }); + ctx.push_clip_layer(&c.path); + } + if !clips.is_empty() { + ctx.set_fill_rule(peniko::Fill::NonZero); + } + draw(ctx); + for _ in 0..clips.len() + usize::from(region.is_some()) { + ctx.pop_layer(); + } +} + +/// A GDI pen as a stroke `width` wide (target units): its caps, joins, miter limit and dashes. +fn pen(width: f64, line: &LineStyle) -> kurbo::Stroke { + let cap = match line.cap { + Cap::Round => kurbo::Cap::Round, + Cap::Square => kurbo::Cap::Square, + Cap::Flat => kurbo::Cap::Butt, + }; + let join = match line.join { + Join::Round => kurbo::Join::Round, + Join::Bevel => kurbo::Join::Bevel, + Join::Miter => kurbo::Join::Miter, + }; + let stroke = kurbo::Stroke::new(width).with_caps(cap).with_join(join).with_miter_limit(f64::from(line.miter_limit)); + // A dash pattern with no length would never advance: such a pen draws solid. + let dash: Vec = line.dash.iter().map(|d| f64::from(*d)).collect(); + if dash.iter().all(|d| d.is_finite() && *d >= 0.0) && dash.iter().sum::() > 0.0 { stroke.with_dashes(0.0, dash) } else { stroke } } /// One glyph placed in text space (x along the baseline from the string's start, font units at diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 540a8500a..eeade59d0 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1962,6 +1962,19 @@ fn mf_ink(img: &image::RgbaImage) -> Option<(u32, u32, u32, u32)> { b } +#[test] +fn metafile_dashed_pen_leaves_gaps() { + // CREATEPENINDIRECT PS_DASH | PS_ENDCAP_FLAT, 40 units (4 px) wide, black, then a line across + // the picture at y = 72 px: 12 px dashes with 4 px gaps. + let mut pen = i16s(&[0x0201, 40, 0]); + pen.extend(0u32.to_le_bytes()); + let recs = vec![(0x02FA, pen), (0x012D, i16s(&[0])), (0x0214, i16s(&[720, 100])), (0x0213, i16s(&[720, 2780]))]; + let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster"); + let ink: Vec = (20..260).map(|x| img.get_pixel(x, 72)[3] > 128).collect(); + assert!(ink.iter().filter(|b| **b).count() > 120, "dashes are drawn"); + assert!(ink.iter().filter(|b| !**b).count() > 30, "with gaps between them"); +} + #[test] fn metafile_text_ink_lands_at_its_reference_point() { // 2 × 1 inch (144 × 72 pt) drawn 288 px wide: 2 px per point. A 36-point "HH", top-left diff --git a/docs/architecture.md b/docs/architecture.md index 2d27e707c..358c3ca5d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,7 +15,7 @@ nothing below L6 uses egui, eframe, winit or rfd. | Layer | Crates | What they do | |---|---|---| -| L0 | `geom`, `color`, `metafile`; standalone `bitstream` | EMU/pt units, transforms, ~180 preset shape generators re-derived from ECMA-376, adjust handles, hit testing; RGB/HSL and DrawingML colour transforms; bit-level I/O and the video decoders' memory budget (`bitstream`); EMF/WMF read into a neutral vector model (`metafile`) | +| L0 | `geom`, `color`; standalone `bitstream` | EMU/pt units, transforms, ~180 preset shape generators re-derived from ECMA-376, adjust handles, hit testing; RGB/HSL and DrawingML colour transforms; bit-level I/O and the video decoders' memory budget (`bitstream`) | | L1 | `model`, `fonts`; `h264`, `hevc`, `vp9`, `av1` | Presentation, masters, layouts, slides, shapes, text, themes, tables, charts, transitions, animations, placeholder inheritance (`resolve`); font database (craft-fonts + system) with metric-compatible substitutes for Office fonts (Carlito for Calibri, Caladea for Cambria, Liberation for Arial/Times); video decoders | | L2 | `text`, `anim`, `format`, `media` | Shaping (HarfRust), UAX #9 bidi, UAX #14 line breaking, autofit, caret hit testing; per-shape animation state over time, the show state machine, transition frame layouts, Morph matching; `.deckcraft` (zip + JSON + media) and outline text; media probing, decoding, mixer and clock (MP4/MOV, MKV/WebM and Ogg demuxers from craft-media and the Opus decoder from craft-audio, both in craft-libs) | | L3 | `render`, `pptx` | `vello_cpu` raster of slides, masters and layouts (fills, outlines, effects, pictures, tables, charts, Morph frames); PPTX import/export from ECMA-376 | @@ -81,6 +81,7 @@ macOS, Windows, Linux, FreeBSD and web artifacts. | Date | Change | Summary | |---|---|---| +| 2026-11-12 | trivial | The `metafile` crate is gone: `render` reads EMF/WMF with craft-metafile from craft-libs | | 2026-11-11 | trivial | The `opus`, `ogg`, `isobmff` and `matroska` crates are gone: `media` uses craft-audio and craft-media from craft-libs | | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged | | 2026-10-14 | minor | Community PR wave folded: one L2/L5/L6 row each; command registry 320 (`cmd!` registrations in `cmd/*`), 41 dialogs (titled in `dialogs.rs`), 202k lines, 24 crates (`metafile` added at L0), 1,641 tests. The per-PR rows below give the counts each PR saw on its own branch | diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index d8edf371f..be75438ad 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -44,8 +44,6 @@ pub const TABLE: &[(&str, Class)] = &[ ("av1", Class::Layer(1)), ("media", Class::Layer(2)), ("color", Class::Layer(0)), - // WMF/EMF reader (ported from WordCraft). - ("metafile", Class::Layer(0)), ("model", Class::Layer(1)), ("fonts", Class::Layer(1)), ("text", Class::Layer(2)),