mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -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"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
@@ -11,3 +11,4 @@ workspace = true
|
||||
[dependencies]
|
||||
kurbo = { 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.
|
||||
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.
|
||||
@@ -304,6 +306,8 @@ pub(crate) struct Canvas {
|
||||
in_path: bool,
|
||||
path: Vec<Op>,
|
||||
full: bool,
|
||||
/// Device clip rectangle `[l, t, r, b]` applied to everything drawn (EMF+ sets it).
|
||||
clip: Option<[f64; 4]>,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
@@ -327,6 +331,7 @@ impl Canvas {
|
||||
in_path: false,
|
||||
path: Vec::new(),
|
||||
full: false,
|
||||
clip: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +341,17 @@ impl Canvas {
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -456,6 +472,64 @@ impl Canvas {
|
||||
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
|
||||
/// could no longer match the file.
|
||||
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 canvas;
|
||||
mod clip;
|
||||
mod dib;
|
||||
mod emf;
|
||||
mod emfplus;
|
||||
|
||||
@@ -1624,3 +1624,163 @@ fn emfplus_garbage_records_never_panic() {
|
||||
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