1521 lines
67 KiB
Rust
1521 lines
67 KiB
Rust
//! 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<u8> {
|
||
(v as i16).to_le_bytes().to_vec()
|
||
}
|
||
|
||
fn h32(v: u32) -> Vec<u8> {
|
||
v.to_le_bytes().to_vec()
|
||
}
|
||
|
||
fn e32(v: i32) -> Vec<u8> {
|
||
v.to_le_bytes().to_vec()
|
||
}
|
||
|
||
fn cat(parts: Vec<Vec<u8>>) -> Vec<u8> {
|
||
parts.concat()
|
||
}
|
||
|
||
/// One WMF record: size in words, function, parameters (padded to a word boundary).
|
||
fn wrec(func: u16, params: &[u8]) -> Vec<u8> {
|
||
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<u8>], placeable: Option<[i32; 4]>) -> Vec<u8> {
|
||
let body: Vec<u8> = 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<u8> {
|
||
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<u8>], bounds: [i32; 4], frame: [i32; 4]) -> Vec<u8> {
|
||
let body: Vec<u8> = 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::<Vec<u8>>());
|
||
h[24..40].copy_from_slice(&frame.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>());
|
||
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<u8> {
|
||
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<u8> {
|
||
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<Seg>, 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<Vec<u8>> = (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<u8> = (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<Vec<u8>> = (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<u8> {
|
||
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<u8> {
|
||
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<u8> {
|
||
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<u8> {
|
||
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<u8> = 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 },
|
||
Item::Path { segs: vec![Seg::Move(0.0, 0.0), Seg::Line(2e6, 0.0)], fill: None, stroke: None, even_odd: true },
|
||
],
|
||
};
|
||
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_keeps_only_finite_bounded_bitmaps_with_area() {
|
||
let bitmap = |rect: [f32; 4]| Item::Bitmap { rect, width: 1, height: 1, rgba: vec![0; 4] };
|
||
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] };
|
||
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] };
|
||
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] };
|
||
assert_eq!(wide.take_pixels(), None);
|
||
let mut path = Item::Path { segs: Vec::new(), fill: None, stroke: None, even_odd: false };
|
||
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<f32>, Vec<f32>) = 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<u8> {
|
||
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<u8> {
|
||
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<u8>]) -> 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<u8> {
|
||
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<u8> = face.encode_utf16().flat_map(|u| u.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<u8> {
|
||
let no_rect = opts & 0x100 != 0;
|
||
let fixed = if no_rect { 60 } else { 76 };
|
||
let s: Vec<u8> = 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<u8> = 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<u8> {
|
||
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<Vec<u8>> = (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<Vec<u8>> = (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());
|
||
}
|