mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Metafile: EMF+ images, text, gradient brushes and clipping
Builds on the EMF+ only reader: bitmap images (raw pixel formats and compressed PNG/JPEG payloads) through DrawImage and DrawImagePoints, DrawString and character-code DrawDriverString, hatch, texture, linear and path gradient brushes, clip rectangles, paths and regions, and objects split across several records. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Generated
+1
@@ -1050,6 +1050,7 @@ dependencies = [
|
|||||||
name = "deckcraft-metafile"
|
name = "deckcraft-metafile"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"image",
|
||||||
"kurbo",
|
"kurbo",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
kurbo = { workspace = true }
|
kurbo = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ pub(crate) const PS_NULL: u32 = 5;
|
|||||||
/// Pen style bits; the rest of the style word holds end-cap, join and type flags.
|
/// 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 PS_STYLE_MASK: u32 = 0x0F;
|
||||||
pub(crate) const BS_SOLID: u32 = 0;
|
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_NULL: u32 = 1;
|
||||||
pub(crate) const BS_HATCHED: u32 = 2;
|
pub(crate) const BS_HATCHED: u32 = 2;
|
||||||
/// BS_PATTERN, BS_DIBPATTERN and BS_DIBPATTERNPT: the brush paints a bitmap tile.
|
/// BS_PATTERN, BS_DIBPATTERN and BS_DIBPATTERNPT: the brush paints a bitmap tile.
|
||||||
@@ -304,6 +306,8 @@ pub(crate) struct Canvas {
|
|||||||
in_path: bool,
|
in_path: bool,
|
||||||
path: Vec<Op>,
|
path: Vec<Op>,
|
||||||
full: bool,
|
full: bool,
|
||||||
|
/// Device clip rectangle `[l, t, r, b]` applied to everything drawn (EMF+ sets it).
|
||||||
|
clip: Option<[f64; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Canvas {
|
impl Canvas {
|
||||||
@@ -327,6 +331,7 @@ impl Canvas {
|
|||||||
in_path: false,
|
in_path: false,
|
||||||
path: Vec::new(),
|
path: Vec::new(),
|
||||||
full: false,
|
full: false,
|
||||||
|
clip: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +341,17 @@ impl Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, raw: Raw) {
|
fn push(&mut self, raw: Raw) {
|
||||||
|
match self.clip {
|
||||||
|
None => self.push_raw(raw),
|
||||||
|
Some(c) => {
|
||||||
|
for r in crate::clip::clip_raw(raw, c) {
|
||||||
|
self.push_raw(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_raw(&mut self, raw: Raw) {
|
||||||
let cost = match &raw {
|
let cost = match &raw {
|
||||||
Raw::Path { ops, .. } => ops.len() * std::mem::size_of::<Op>(),
|
Raw::Path { ops, .. } => ops.len() * std::mem::size_of::<Op>(),
|
||||||
// A tile shared with its brush is counted once (by the brush); one made for this fill
|
// A tile shared with its brush is counted once (by the brush); one made for this fill
|
||||||
@@ -456,6 +472,64 @@ impl Canvas {
|
|||||||
self.brush = brush;
|
self.brush = brush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the device clip rectangle `[l, t, r, b]` (None: unclipped).
|
||||||
|
pub fn set_clip(&mut self, clip: Option<[f64; 4]>) {
|
||||||
|
self.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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills logical `ops` with a ready-made tile, laid from the device origin.
|
||||||
|
pub fn tile_shape(&mut self, ops: &[Op], tile: Arc<RawTile>) {
|
||||||
|
let dev: Vec<Op> = 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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]) {
|
||||||
|
let dev: Vec<Op> = ops.iter().map(|o| self.map_op(*o)).collect();
|
||||||
|
let Some([l, t, r, b]) = crate::clip::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 }) });
|
||||||
|
}
|
||||||
|
|
||||||
/// Pushes the drawing state. Past [`MAX_STACK`] levels the parse stops, since later restores
|
/// Pushes the drawing state. Past [`MAX_STACK`] levels the parse stops, since later restores
|
||||||
/// could no longer match the file.
|
/// could no longer match the file.
|
||||||
pub fn save_dc(&mut self) {
|
pub fn save_dc(&mut self) {
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
//! Clipping drawn output to a device rectangle (EMF+ clip rectangles, paths and regions 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
|
||||||
|
//! Liang–Barsky per segment. Bitmaps are cropped and text gets a clip 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;
|
||||||
|
|
||||||
|
/// 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inside(b: [f64; 4], c: [f64; 4]) -> bool {
|
||||||
|
b[0] >= c[0] && b[1] >= c[1] && b[2] <= c[2] && b[3] <= c[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clips one drawn item; a shape that is both filled and stroked comes back as two items.
|
||||||
|
pub(crate) fn clip_raw(raw: Raw, c: [f64; 4]) -> Vec<Raw> {
|
||||||
|
match raw {
|
||||||
|
Raw::Path { ops, fill, stroke, even_odd } => {
|
||||||
|
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
|
||||||
|
if inside(b, c) {
|
||||||
|
return vec![Raw::Path { ops, fill, stroke, even_odd }];
|
||||||
|
}
|
||||||
|
if disjoint(b, c) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let subs = flatten(&ops);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if fill.is_some() {
|
||||||
|
let f = clip_fill(&subs, c);
|
||||||
|
if !f.is_empty() {
|
||||||
|
out.push(Raw::Path { ops: f, fill, stroke: None, even_odd });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stroke.is_some() {
|
||||||
|
let s = clip_stroke(&subs, c);
|
||||||
|
if !s.is_empty() {
|
||||||
|
out.push(Raw::Path { ops: s, fill: None, stroke, even_odd });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
Raw::Pattern { ops, even_odd, tile } => {
|
||||||
|
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
|
||||||
|
if inside(b, c) {
|
||||||
|
return vec![Raw::Pattern { ops, even_odd, tile }];
|
||||||
|
}
|
||||||
|
if disjoint(b, c) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let f = clip_fill(&flatten(&ops), c);
|
||||||
|
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile }] }
|
||||||
|
}
|
||||||
|
Raw::Bitmap { rect, img } => crop(rect, img, c).map(|(rect, img)| vec![Raw::Bitmap { rect, img }]).unwrap_or_default(),
|
||||||
|
Raw::Text(mut t) => {
|
||||||
|
let cur = t.clip.map_or(c, |[a, b, d, e]| intersect(c, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)]));
|
||||||
|
t.clip = Some(cur.map(|v| v as f32));
|
||||||
|
vec![Raw::Text(t)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subpaths as point lists with their closed flag.
|
||||||
|
fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
|
||||||
|
let mut subs: Vec<(Vec<P>, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_fill(subs: &[(Vec<P>, bool)], c: [f64; 4]) -> Vec<Op> {
|
||||||
|
let mut ops = Vec::new();
|
||||||
|
for (pts, _) in subs {
|
||||||
|
let mut poly = pts.clone();
|
||||||
|
// Left, top, right, bottom edges in turn.
|
||||||
|
for edge in 0..4 {
|
||||||
|
let keep = |p: P| match edge {
|
||||||
|
0 => p.0 >= c[0],
|
||||||
|
1 => p.1 >= c[1],
|
||||||
|
2 => p.0 <= c[2],
|
||||||
|
_ => p.1 <= c[3],
|
||||||
|
};
|
||||||
|
let cut = |a: P, b: P| {
|
||||||
|
let t = match edge {
|
||||||
|
0 => (c[0] - a.0) / (b.0 - a.0),
|
||||||
|
1 => (c[1] - a.1) / (b.1 - a.1),
|
||||||
|
2 => (c[2] - a.0) / (b.0 - a.0),
|
||||||
|
_ => (c[3] - a.1) / (b.1 - a.1),
|
||||||
|
};
|
||||||
|
(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t)
|
||||||
|
};
|
||||||
|
let mut next = Vec::with_capacity(poly.len() + 4);
|
||||||
|
for i in 0..poly.len() {
|
||||||
|
let (a, b) = (poly[(i + poly.len() - 1) % poly.len()], poly[i]);
|
||||||
|
match (keep(a), keep(b)) {
|
||||||
|
(true, true) => next.push(b),
|
||||||
|
(true, false) => next.push(cut(a, b)),
|
||||||
|
(false, true) => {
|
||||||
|
next.push(cut(a, b));
|
||||||
|
next.push(b);
|
||||||
|
}
|
||||||
|
(false, false) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poly = next;
|
||||||
|
if poly.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if poly.len() >= 3 {
|
||||||
|
ops.push(Op::M(poly[0]));
|
||||||
|
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
|
||||||
|
ops.push(Op::Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_stroke(subs: &[(Vec<P>, bool)], c: [f64; 4]) -> Vec<Op> {
|
||||||
|
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<P> = None;
|
||||||
|
for w in pts.windows(2) {
|
||||||
|
let Some((a, b)) = segment(w[0], w[1], c) else {
|
||||||
|
last = None;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if last != Some(a) {
|
||||||
|
ops.push(Op::M(a));
|
||||||
|
}
|
||||||
|
ops.push(Op::L(b));
|
||||||
|
last = Some(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liang–Barsky: the part of segment `a`–`b` inside `c`.
|
||||||
|
fn segment(a: P, b: P, c: [f64; 4]) -> Option<(P, P)> {
|
||||||
|
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
|
||||||
|
let (mut t0, mut t1) = (0.0f64, 1.0f64);
|
||||||
|
for (p, q) in [(-dx, a.0 - c[0]), (dx, c[2] - a.0), (-dy, a.1 - c[1]), (dy, c[3] - a.1)] {
|
||||||
|
if p == 0.0 {
|
||||||
|
if q < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let r = q / p;
|
||||||
|
if p < 0.0 {
|
||||||
|
t0 = t0.max(r);
|
||||||
|
} else {
|
||||||
|
t1 = t1.min(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(t0 <= t1).then_some(((a.0 + dx * t0, a.1 + dy * t0), (a.0 + dx * t1, a.1 + dy * t1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to `c`.
|
||||||
|
fn crop(rect: [f64; 4], img: Rgba, c: [f64; 4]) -> Option<([f64; 4], Rgba)> {
|
||||||
|
let [x, y, w, h] = rect;
|
||||||
|
let b = [x, y, x + w, y + h];
|
||||||
|
if inside(b, c) {
|
||||||
|
return Some((rect, img));
|
||||||
|
}
|
||||||
|
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 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;
|
||||||
|
px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?);
|
||||||
|
}
|
||||||
|
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 }))
|
||||||
|
}
|
||||||
+811
-131
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
mod bytes;
|
mod bytes;
|
||||||
mod canvas;
|
mod canvas;
|
||||||
|
mod clip;
|
||||||
mod dib;
|
mod dib;
|
||||||
mod emf;
|
mod emf;
|
||||||
mod emfplus;
|
mod emfplus;
|
||||||
|
|||||||
@@ -1624,3 +1624,163 @@ fn emfplus_garbage_records_never_panic() {
|
|||||||
let _ = parse(&emf(&[plus_comment(&recs)], [0; 4], [0; 4]));
|
let _ = parse(&emf(&[plus_comment(&recs)], [0; 4], [0; 4]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn plus_pic(recs: Vec<Vec<u8>>) -> 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<u8> {
|
||||||
|
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white.
|
||||||
|
fn bitmap_2x2() -> Vec<u8> {
|
||||||
|
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<u8> {
|
||||||
|
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<u8> {
|
||||||
|
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));
|
||||||
|
// Glyph indices (no CmapLookup) are skipped.
|
||||||
|
let gi = prec(0x4036, 0x8000, &cat(vec![h32(0xFF00_0000), h32(0), h32(0), h32(1), h16(36), f32s(&[0.0, 0.0])]));
|
||||||
|
let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0));
|
||||||
|
assert!(plus_pic(vec![font, 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<f32> = 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));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user