feat(ole): render EMF/WMF OLE presentations

OLE2FRAME entities whose compound file carries a metafile presentation
(Excel/Word previews, scanned WMF pages) drew only their border frame —
the old decoder scanned the raw blob for a contiguous BMP, which the
CFB sector layout almost never provides. acadrust now extracts the
picture bytes tagged by kind; the new scene/model/gdi player rasterizes
EMF and WMF record streams on the CPU (pens/brushes, polygons, paths,
DIB blits, clipping, transforms) with text drawn through the existing
system-font glyph cache, and image_model feeds the result into the
raster pipeline. Rasters (BMP/PNG/JPEG/GIF, CF_DIB) decode as before
via the image crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 01:23:31 +03:00
1 parent 1cfb856224
commit e8411c1b6d
10 files changed
+2088 -42

No files matched your search

Generated
+1 -1
View File
@@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#6ddff17beca4413df1bad0d441eff9d6aaad298b"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#733d6f5d9c876d881baa3badc5c8297928cb923c"
dependencies = [
"ahash 0.8.12",
"anyhow",
+89
View File
@@ -0,0 +1,89 @@
// Device-independent bitmap (BITMAPINFO + pixel bits) → RGBA.
//
// Metafile blit records carry a headerless DIB: a BITMAPINFOHEADER, an
// optional palette / bitfield masks, then the pixel data. Rather than decode
// the many bpp/compression variants by hand, synthesize the 14-byte
// BITMAPFILEHEADER in front and let the `image` crate's BMP decoder do it.
/// Decode a metafile-embedded DIB. `bmi` is the BITMAPINFO (header +
/// palette), `bits` the pixel data. Returns RGBA plus dimensions.
pub fn decode(bmi: &[u8], bits: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
if bmi.len() < 12 {
return None;
}
let file_size = 14 + bmi.len() + bits.len();
let pixel_offset = 14 + bmi.len();
let mut bmp = Vec::with_capacity(file_size);
bmp.extend_from_slice(b"BM");
bmp.extend_from_slice(&(file_size as u32).to_le_bytes());
bmp.extend_from_slice(&0u32.to_le_bytes());
bmp.extend_from_slice(&(pixel_offset as u32).to_le_bytes());
bmp.extend_from_slice(bmi);
bmp.extend_from_slice(bits);
let img = image::load_from_memory_with_format(&bmp, image::ImageFormat::Bmp).ok()?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
if w == 0 || h == 0 {
return None;
}
Some((rgba.into_raw(), w, h))
}
/// Decode a *packed* DIB (BITMAPINFO immediately followed by the bits, no
/// explicit split) — the layout WMF blit records embed. The pixel-data offset
/// is derived from the header size, palette length, and bitfield masks.
pub fn decode_packed(dibdata: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
if dibdata.len() < 12 {
return None;
}
let bi_size = u32::from_le_bytes(dibdata[0..4].try_into().ok()?) as usize;
if bi_size < 12 || bi_size > dibdata.len() {
return None;
}
let (bpp, compression, clr_used) = if bi_size == 12 {
// BITMAPCOREHEADER: u16 bitcount at 10, palette entries are 3 bytes.
let bpp = u16::from_le_bytes(dibdata[10..12].try_into().ok()?) as usize;
(bpp, 0usize, 0usize)
} else {
let bpp = u16::from_le_bytes(dibdata[14..16].try_into().ok()?) as usize;
let comp = u32::from_le_bytes(dibdata[16..20].try_into().ok()?) as usize;
let used = u32::from_le_bytes(dibdata[32..36].try_into().ok()?) as usize;
(bpp, comp, used)
};
let pal_entry = if bi_size == 12 { 3 } else { 4 };
let pal_count = if bpp <= 8 {
if clr_used > 0 {
clr_used.min(1 << bpp)
} else {
1 << bpp
}
} else {
clr_used
};
// BI_BITFIELDS (3) on a 40-byte header carries three u32 masks.
let masks = if compression == 3 && bi_size == 40 {
12
} else {
0
};
let offset = bi_size + masks + pal_count * pal_entry;
if offset > dibdata.len() {
return None;
}
decode(&dibdata[..offset], &dibdata[offset..])
}
/// Split a record's trailing `BITMAPINFO + bits` region where the two ranges
/// are given as offsets from the record start. Returns `(bmi, bits)` slices,
/// or `None` when either range falls outside the record.
pub fn ranges<'a>(
record: &'a [u8],
off_bmi: usize,
cb_bmi: usize,
off_bits: usize,
cb_bits: usize,
) -> Option<(&'a [u8], &'a [u8])> {
let bmi = record.get(off_bmi..off_bmi.checked_add(cb_bmi)?)?;
let bits = record.get(off_bits..off_bits.checked_add(cb_bits)?)?;
Some((bmi, bits))
}
+626
View File
@@ -0,0 +1,626 @@
// EMF (enhanced metafile) record player.
//
// Walks the record stream and replays the drawing subset onto a `Canvas`
// through the `Dc` transform stack. Unknown records are skipped by their
// self-declared size, so partially supported files still draw everything
// this player understands.
use super::raster::{Canvas, Rgba};
use super::{canvas_for_box, colorref, dib, stock_obj, text, Brush, Dc, Font, Obj, Pen, Xform};
#[inline]
fn u32_at(d: &[u8], o: usize) -> u32 {
u32::from_le_bytes(d[o..o + 4].try_into().unwrap())
}
#[inline]
fn i32_at(d: &[u8], o: usize) -> i32 {
i32::from_le_bytes(d[o..o + 4].try_into().unwrap())
}
#[inline]
fn f32_at(d: &[u8], o: usize) -> f32 {
f32::from_le_bytes(d[o..o + 4].try_into().unwrap())
}
#[inline]
fn i16_at(d: &[u8], o: usize) -> i16 {
i16::from_le_bytes(d[o..o + 2].try_into().unwrap())
}
fn xform_at(d: &[u8], o: usize) -> Xform {
Xform {
m11: f32_at(d, o),
m12: f32_at(d, o + 4),
m21: f32_at(d, o + 8),
m22: f32_at(d, o + 12),
dx: f32_at(d, o + 16),
dy: f32_at(d, o + 20),
}
}
/// Flatten a cubic Bézier run (`pts[0]` current point, then control triples).
fn flatten_beziers(pts: &[[f32; 2]]) -> Vec<[f32; 2]> {
const STEPS: usize = 12;
let mut out = Vec::new();
if pts.is_empty() {
return out;
}
out.push(pts[0]);
let mut p0 = pts[0];
let mut i = 1;
while i + 2 < pts.len() {
let (c1, c2, p3) = (pts[i], pts[i + 1], pts[i + 2]);
for k in 1..=STEPS {
let t = k as f32 / STEPS as f32;
let u = 1.0 - t;
out.push([
u * u * u * p0[0]
+ 3.0 * u * u * t * c1[0]
+ 3.0 * u * t * t * c2[0]
+ t * t * t * p3[0],
u * u * u * p0[1]
+ 3.0 * u * u * t * c1[1]
+ 3.0 * u * t * t * c2[1]
+ t * t * t * p3[1],
]);
}
p0 = p3;
i += 3;
}
out
}
struct Player {
dc: Dc,
objects: Vec<Option<Obj>>,
/// BEGINPATH..ENDPATH contour accumulator (logical coords).
path: Option<Vec<Vec<[f32; 2]>>>,
}
impl Player {
fn select(&mut self, ih: u32) {
let obj = if ih & 0x8000_0000 != 0 {
stock_obj(ih & 0x7FFF_FFFF)
} else {
self.objects.get(ih as usize).and_then(|o| o.clone())
};
match obj {
Some(Obj::Pen(p)) => self.dc.pen = p,
Some(Obj::Brush(b)) => self.dc.brush = b,
Some(Obj::Font(f)) => self.dc.font = f,
_ => {}
}
}
fn store(&mut self, ih: u32, obj: Obj) {
let i = ih as usize;
if i >= self.objects.len() {
if i > 4096 {
return; // corrupt index — don't balloon the table
}
self.objects.resize(i + 1, None);
}
self.objects[i] = Some(obj);
}
/// Convert logical points to canvas space.
fn map(&self, pts: &[[f32; 2]]) -> Vec<[f32; 2]> {
pts.iter().map(|p| self.dc.to_canvas(p[0], p[1])).collect()
}
fn fill_and_stroke(&mut self, canvas: &mut Canvas, rings: &[Vec<[f32; 2]>], close: bool) {
let mapped: Vec<Vec<[f32; 2]>> = rings.iter().map(|r| self.map(r)).collect();
if !self.dc.brush.null && close {
canvas.fill_polys(&mapped, self.dc.brush.color);
}
if !self.dc.pen.null {
let w = self.dc.pen_px();
for r in &mapped {
if close && r.len() >= 2 {
let mut closed = r.clone();
closed.push(r[0]);
canvas.stroke_polyline(&closed, w, self.dc.pen.color);
} else {
canvas.stroke_polyline(r, w, self.dc.pen.color);
}
}
}
}
/// Points of a 16-bit poly record at `off`, `n` entries.
fn pts16(rec: &[u8], off: usize, n: usize) -> Vec<[f32; 2]> {
let mut v = Vec::with_capacity(n);
for i in 0..n {
let o = off + i * 4;
if o + 4 > rec.len() {
break;
}
v.push([i16_at(rec, o) as f32, i16_at(rec, o + 2) as f32]);
}
v
}
fn pts32(rec: &[u8], off: usize, n: usize) -> Vec<[f32; 2]> {
let mut v = Vec::with_capacity(n);
for i in 0..n {
let o = off + i * 8;
if o + 8 > rec.len() {
break;
}
v.push([i32_at(rec, o) as f32, i32_at(rec, o + 4) as f32]);
}
v
}
}
pub fn render(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
if data.len() < 88 || u32_at(data, 0) != 1 || &data[40..44] != b" EMF" {
return None;
}
// Header bounds: the device-space box the picture occupies.
let (bl, bt, br, bb) = (
i32_at(data, 8) as f32,
i32_at(data, 12) as f32,
i32_at(data, 16) as f32,
i32_at(data, 20) as f32,
);
let (mut canvas, dev_scale) = canvas_for_box(br - bl, bb - bt)?;
let mut p = Player {
dc: Dc::new(),
objects: Vec::new(),
path: None,
};
p.dc.dev_org = (bl, bt);
p.dc.dev_scale = dev_scale;
let mut off = 0usize;
let mut guard = 0usize;
while off + 8 <= data.len() {
guard += 1;
if guard > 4_000_000 {
break;
}
let ty = u32_at(data, off);
let size = u32_at(data, off + 4) as usize;
if size < 8 || size % 4 != 0 || off + size > data.len() {
break;
}
let rec = &data[off..off + size];
off += size;
match ty {
14 => break, // EOF
9 if rec.len() >= 16 => {
let (cx, cy) = (i32_at(rec, 8) as f32, i32_at(rec, 12) as f32);
if cx != 0.0 && cy != 0.0 {
p.dc.win_ext = (cx, cy);
}
}
10 if rec.len() >= 16 => p.dc.win_org = (i32_at(rec, 8) as f32, i32_at(rec, 12) as f32),
11 if rec.len() >= 16 => {
let (cx, cy) = (i32_at(rec, 8) as f32, i32_at(rec, 12) as f32);
if cx != 0.0 && cy != 0.0 {
p.dc.vp_ext = (cx, cy);
}
}
12 if rec.len() >= 16 => p.dc.vp_org = (i32_at(rec, 8) as f32, i32_at(rec, 12) as f32),
17 if rec.len() >= 12 => {
// MM_TEXT resets to the identity logical→device mapping.
if u32_at(rec, 8) == 1 {
p.dc.win_org = (0.0, 0.0);
p.dc.win_ext = (1.0, 1.0);
p.dc.vp_org = (0.0, 0.0);
p.dc.vp_ext = (1.0, 1.0);
}
}
18 if rec.len() >= 12 => p.dc.bk_mode = u32_at(rec, 8),
22 if rec.len() >= 12 => p.dc.text_align = u32_at(rec, 8),
24 if rec.len() >= 12 => p.dc.text_color = colorref(u32_at(rec, 8)),
25 if rec.len() >= 12 => p.dc.bk_color = colorref(u32_at(rec, 8)),
27 if rec.len() >= 16 => {
p.dc.pos = (i32_at(rec, 8) as f32, i32_at(rec, 12) as f32);
if let Some(path) = &mut p.path {
path.push(vec![[p.dc.pos.0, p.dc.pos.1]]);
}
}
30 if rec.len() >= 24 => {
let a =
p.dc.to_canvas(i32_at(rec, 8) as f32, i32_at(rec, 12) as f32);
let b =
p.dc.to_canvas(i32_at(rec, 16) as f32, i32_at(rec, 20) as f32);
let c = &mut canvas.clip;
c.0 = c.0.max(a[0].min(b[0]));
c.1 = c.1.max(a[1].min(b[1]));
c.2 = c.2.min(a[0].max(b[0]));
c.3 = c.3.min(a[1].max(b[1]));
}
33 => p.dc.save(&canvas),
34 => {
let n = if rec.len() >= 12 {
i32_at(rec, 8).unsigned_abs().max(1)
} else {
1
};
for _ in 0..n {
p.dc.restore(&mut canvas);
}
}
35 if rec.len() >= 32 => p.dc.world = xform_at(rec, 8),
36 if rec.len() >= 36 => {
let x = xform_at(rec, 8);
match u32_at(rec, 32) {
1 => p.dc.world = Xform::IDENTITY,
2 => p.dc.world = p.dc.world.mul(&x),
3 => p.dc.world = x.mul(&p.dc.world),
_ => p.dc.world = x,
}
}
37 if rec.len() >= 12 => p.select(u32_at(rec, 8)),
38 if rec.len() >= 28 => {
let style = u32_at(rec, 12);
p.store(
u32_at(rec, 8),
Obj::Pen(Pen {
color: colorref(u32_at(rec, 24)),
width: i32_at(rec, 16) as f32,
null: style & 0xF == 5,
}),
);
}
39 if rec.len() >= 24 => {
let style = u32_at(rec, 12);
p.store(
u32_at(rec, 8),
Obj::Brush(Brush {
color: colorref(u32_at(rec, 16)),
null: style == 1,
}),
);
}
40 if rec.len() >= 12 => {
let ih = u32_at(rec, 8) as usize;
if ih < p.objects.len() {
p.objects[ih] = None;
}
}
42 | 43 if rec.len() >= 24 => {
let (l, t, r, b) = (
i32_at(rec, 8) as f32,
i32_at(rec, 12) as f32,
i32_at(rec, 16) as f32,
i32_at(rec, 20) as f32,
);
let ring = if ty == 43 {
vec![[l, t], [r, t], [r, b], [l, b]]
} else {
let (cx, cy) = ((l + r) * 0.5, (t + b) * 0.5);
let (rx, ry) = ((r - l) * 0.5, (b - t) * 0.5);
(0..64)
.map(|i| {
let a = i as f32 / 64.0 * std::f32::consts::TAU;
[cx + rx * a.cos(), cy + ry * a.sin()]
})
.collect()
};
p.fill_and_stroke(&mut canvas, std::slice::from_ref(&ring), true);
}
44 if rec.len() >= 24 => {
// ROUNDRECT — drawn square-cornered.
let (l, t, r, b) = (
i32_at(rec, 8) as f32,
i32_at(rec, 12) as f32,
i32_at(rec, 16) as f32,
i32_at(rec, 20) as f32,
);
let ring = vec![[l, t], [r, t], [r, b], [l, b]];
p.fill_and_stroke(&mut canvas, std::slice::from_ref(&ring), true);
}
54 if rec.len() >= 16 => {
let to = [i32_at(rec, 8) as f32, i32_at(rec, 12) as f32];
if let Some(path) = &mut p.path {
match path.last_mut() {
Some(c) => c.push(to),
None => path.push(vec![[p.dc.pos.0, p.dc.pos.1], to]),
}
} else if !p.dc.pen.null {
let seg = p.map(&[[p.dc.pos.0, p.dc.pos.1], to]);
let w = p.dc.pen_px();
canvas.stroke_polyline(&seg, w, p.dc.pen.color);
}
p.dc.pos = (to[0], to[1]);
}
59 => p.path = Some(Vec::new()),
60 => {}
61 => {
if let Some(path) = &mut p.path {
if let Some(c) = path.last_mut() {
if let Some(&first) = c.first() {
c.push(first);
}
}
}
}
62 | 63 | 64 => {
if let Some(path) = p.path.take() {
let close = ty != 64;
let stroke = ty != 62;
let saved_pen = p.dc.pen;
let saved_brush = p.dc.brush;
if !stroke {
p.dc.pen.null = true;
}
if ty == 64 {
p.dc.brush.null = true;
}
p.fill_and_stroke(&mut canvas, &path, close);
p.dc.pen = saved_pen;
p.dc.brush = saved_brush;
}
}
76 | 77 if rec.len() >= 100 => {
let (xd, yd, cxd, cyd) = (
i32_at(rec, 24) as f32,
i32_at(rec, 28) as f32,
i32_at(rec, 32) as f32,
i32_at(rec, 36) as f32,
);
let rop = u32_at(rec, 40);
let cb_bmi = u32_at(rec, 88) as usize;
let cb_bits = u32_at(rec, 96) as usize;
if cb_bits > 0 && cb_bmi > 0 {
let off_bmi = u32_at(rec, 84) as usize;
let off_bits = u32_at(rec, 92) as usize;
if let Some((bmi, bits)) = dib::ranges(rec, off_bmi, cb_bmi, off_bits, cb_bits)
{
if let Some((px, w, h)) = dib::decode(bmi, bits) {
let (sx, sy) = (i32_at(rec, 44) as f32, i32_at(rec, 48) as f32);
let (scx, scy) = if ty == 77 && rec.len() >= 108 {
(i32_at(rec, 100) as f32, i32_at(rec, 104) as f32)
} else {
(cxd, cyd)
};
let a = p.dc.to_canvas(xd, yd);
let b = p.dc.to_canvas(xd + cxd, yd + cyd);
canvas.blit(
&px, w as usize, h as usize, sx, sy, scx, scy, a[0], a[1], b[0],
b[1],
);
}
}
} else {
let a = p.dc.to_canvas(xd, yd);
let b = p.dc.to_canvas(xd + cxd, yd + cyd);
let color = match rop {
0x00F0_0021 | 0x005A_0049 => Some(p.dc.brush.color), // PATCOPY / PATINVERT
0x0000_0042 => Some(Rgba([0, 0, 0, 255])), // BLACKNESS
0x00FF_0062 => Some(Rgba([255, 255, 255, 255])), // WHITENESS
_ => None,
};
if let Some(c) = color {
if !p.dc.brush.null || !matches!(rop, 0x00F0_0021 | 0x005A_0049) {
canvas.fill_rect(
a[0].min(b[0]),
a[1].min(b[1]),
a[0].max(b[0]),
a[1].max(b[1]),
c,
);
}
}
}
}
80 if rec.len() >= 76 => {
let (xd, yd) = (i32_at(rec, 24) as f32, i32_at(rec, 28) as f32);
let (sx, sy, scx, scy) = (
i32_at(rec, 32) as f32,
i32_at(rec, 36) as f32,
i32_at(rec, 40) as f32,
i32_at(rec, 44) as f32,
);
let (off_bmi, cb_bmi, off_bits, cb_bits) = (
u32_at(rec, 48) as usize,
u32_at(rec, 52) as usize,
u32_at(rec, 56) as usize,
u32_at(rec, 60) as usize,
);
if let Some((bmi, bits)) = dib::ranges(rec, off_bmi, cb_bmi, off_bits, cb_bits) {
if let Some((px, w, h)) = dib::decode(bmi, bits) {
let a = p.dc.to_canvas(xd, yd);
let b = p.dc.to_canvas(xd + scx, yd + scy);
canvas.blit(
&px, w as usize, h as usize, sx, sy, scx, scy, a[0], a[1], b[0], b[1],
);
}
}
}
81 if rec.len() >= 80 => {
let (xd, yd) = (i32_at(rec, 24) as f32, i32_at(rec, 28) as f32);
let (sx, sy, scx, scy) = (
i32_at(rec, 32) as f32,
i32_at(rec, 36) as f32,
i32_at(rec, 40) as f32,
i32_at(rec, 44) as f32,
);
let (off_bmi, cb_bmi, off_bits, cb_bits) = (
u32_at(rec, 48) as usize,
u32_at(rec, 52) as usize,
u32_at(rec, 56) as usize,
u32_at(rec, 60) as usize,
);
let (cxd, cyd) = (i32_at(rec, 72) as f32, i32_at(rec, 76) as f32);
if let Some((bmi, bits)) = dib::ranges(rec, off_bmi, cb_bmi, off_bits, cb_bits) {
if let Some((px, w, h)) = dib::decode(bmi, bits) {
let a = p.dc.to_canvas(xd, yd);
let b = p.dc.to_canvas(xd + cxd, yd + cyd);
canvas.blit(
&px, w as usize, h as usize, sx, sy, scx, scy, a[0], a[1], b[0], b[1],
);
}
}
}
82 if rec.len() >= 104 => {
let mut name = String::new();
for i in 0..32 {
let o = 40 + i * 2;
let c = u16::from_le_bytes(rec[o..o + 2].try_into().unwrap());
if c == 0 {
break;
}
name.push(char::from_u32(c as u32).unwrap_or('?'));
}
p.store(
u32_at(rec, 8),
Obj::Font(Font {
height: i32_at(rec, 12) as f32,
escapement: i32_at(rec, 20) as f32,
facename: name,
charset: rec[35], // lfCharSet: LOGFONT offset 23 → record 35
}),
);
}
83 | 84 if rec.len() >= 76 => {
let is_wide = ty == 84;
let (rx, ry) = (i32_at(rec, 36) as f32, i32_at(rec, 40) as f32);
let n = u32_at(rec, 44) as usize;
let off_str = u32_at(rec, 48) as usize;
let opts = u32_at(rec, 52);
let rcl = [
i32_at(rec, 56) as f32,
i32_at(rec, 60) as f32,
i32_at(rec, 64) as f32,
i32_at(rec, 68) as f32,
];
let off_dx = u32_at(rec, 72) as usize;
if n == 0 || n > 4096 {
continue;
}
let s = if is_wide {
let end = off_str + n * 2;
if end > rec.len() {
continue;
}
let units: Vec<u16> = (0..n)
.map(|i| {
u16::from_le_bytes(
rec[off_str + i * 2..off_str + i * 2 + 2]
.try_into()
.unwrap(),
)
})
.collect();
String::from_utf16_lossy(&units)
} else {
let end = off_str + n;
if end > rec.len() {
continue;
}
text::decode_ansi(&rec[off_str..end], &p.dc.font)
};
let dx: Option<Vec<f32>> = if off_dx != 0 && off_dx + n * 4 <= rec.len() {
Some((0..n).map(|i| u32_at(rec, off_dx + i * 4) as f32).collect())
} else {
None
};
let opaque = if opts & 2 != 0 { Some(rcl) } else { None };
text::draw_text(&mut canvas, &mut p.dc, rx, ry, &s, dx.as_deref(), opaque);
}
2..=8 | 85..=91 if rec.len() >= 28 => {
let wide = ty <= 8;
let (base, kind) = if wide { (0, ty) } else { (0, ty - 83) };
let _ = base;
match kind {
// POLYBEZIER(2)/POLYGON(3)/POLYLINE(4)/POLYBEZIERTO(5)/POLYLINETO(6)
2..=6 => {
let n = u32_at(rec, 24) as usize;
if n == 0 || n > 200_000 {
continue;
}
let mut pts = if wide {
Player::pts32(rec, 28, n)
} else {
Player::pts16(rec, 28, n)
};
let to_variant = kind == 5 || kind == 6;
if to_variant {
pts.insert(0, [p.dc.pos.0, p.dc.pos.1]);
}
if let Some(&last) = pts.last() {
if to_variant {
p.dc.pos = (last[0], last[1]);
}
}
let flat = if kind == 2 || kind == 5 {
flatten_beziers(&pts)
} else {
pts
};
if let Some(path) = &mut p.path {
if to_variant {
match path.last_mut() {
Some(c) => c.extend_from_slice(&flat[1..]),
None => path.push(flat),
}
} else {
path.push(flat);
}
} else if kind == 3 {
p.fill_and_stroke(&mut canvas, std::slice::from_ref(&flat), true);
} else if !p.dc.pen.null {
let mapped = p.map(&flat);
let w = p.dc.pen_px();
canvas.stroke_polyline(&mapped, w, p.dc.pen.color);
}
}
// POLYPOLYLINE(7)/POLYPOLYGON(8)
7 | 8 => {
let n_polys = u32_at(rec, 24) as usize;
let total = u32_at(rec, 28) as usize;
if n_polys == 0 || n_polys > 100_000 || total > 400_000 {
continue;
}
let counts_off = 32;
let pts_off = counts_off + n_polys * 4;
let mut rings = Vec::with_capacity(n_polys);
let mut cursor = 0usize;
for i in 0..n_polys {
let co = counts_off + i * 4;
if co + 4 > rec.len() {
break;
}
let c = u32_at(rec, co) as usize;
let ring = if wide {
Player::pts32(rec, pts_off + cursor * 8, c)
} else {
Player::pts16(rec, pts_off + cursor * 4, c)
};
cursor += c;
rings.push(ring);
}
if kind == 8 {
p.fill_and_stroke(&mut canvas, &rings, true);
} else if !p.dc.pen.null {
let w = p.dc.pen_px();
for r in rings {
let mapped = p.map(&r);
canvas.stroke_polyline(&mapped, w, p.dc.pen.color);
}
}
}
_ => {}
}
}
95 if rec.len() >= 48 => {
let style = u32_at(rec, 28);
p.store(
u32_at(rec, 8),
Obj::Pen(Pen {
color: colorref(u32_at(rec, 40)),
width: u32_at(rec, 32) as f32,
null: style & 0xF == 5,
}),
);
}
_ => {}
}
}
let (px, w, h) = canvas.downsample(super::SS);
Some((px, w, h))
}
+344
View File
@@ -0,0 +1,344 @@
// Minimal GDI metafile playback: rasterize the EMF / WMF presentation
// pictures embedded in OLE2FRAME entities into an RGBA image.
//
// This is not a general GDI implementation — it covers the record set that
// Office-generated OLE previews actually use: window/viewport + world
// transforms, pens/brushes/fonts, polygon & polyline drawing, rectangle and
// ellipse primitives, paths, DIB blits (including brush-only PATCOPY fills),
// and text (EXTTEXTOUTW / TEXTOUT with per-char advances). Text glyphs come
// from the system-font outline cache the CAD text renderer already maintains,
// so scripts and fallback behave exactly like drawing text in the editor.
pub mod dib;
mod emf;
mod raster;
mod text;
mod wmf;
pub use raster::{Canvas, Rgba};
/// Output long-side target in pixels for a rasterized metafile.
const TARGET_DIM: usize = 1600;
/// Supersampling factor; the canvas renders at TARGET_DIM×SS and is
/// box-downsampled once at the end for uniform anti-aliasing.
const SS: usize = 2;
/// 2-D affine transform (GDI XFORM layout).
#[derive(Clone, Copy, Debug)]
pub struct Xform {
pub m11: f32,
pub m12: f32,
pub m21: f32,
pub m22: f32,
pub dx: f32,
pub dy: f32,
}
impl Xform {
pub const IDENTITY: Xform = Xform {
m11: 1.0,
m12: 0.0,
m21: 0.0,
m22: 1.0,
dx: 0.0,
dy: 0.0,
};
pub fn apply(&self, x: f32, y: f32) -> (f32, f32) {
(
self.m11 * x + self.m21 * y + self.dx,
self.m12 * x + self.m22 * y + self.dy,
)
}
/// self ∘ other (apply `other` first, then `self`).
pub fn mul(&self, o: &Xform) -> Xform {
Xform {
m11: o.m11 * self.m11 + o.m12 * self.m21,
m12: o.m11 * self.m12 + o.m12 * self.m22,
m21: o.m21 * self.m11 + o.m22 * self.m21,
m22: o.m21 * self.m12 + o.m22 * self.m22,
dx: o.dx * self.m11 + o.dy * self.m21 + self.dx,
dy: o.dx * self.m12 + o.dy * self.m22 + self.dy,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Pen {
pub color: Rgba,
/// Logical-unit width; 0 = cosmetic (1 device pixel).
pub width: f32,
/// PS_NULL — draws nothing.
pub null: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Brush {
pub color: Rgba,
/// BS_NULL / HOLLOW — fills nothing.
pub null: bool,
}
#[derive(Clone, Debug)]
pub struct Font {
/// Logical-unit height; negative = em height, positive = cell height.
pub height: f32,
/// Escapement angle in 0.1°, counter-clockwise.
pub escapement: f32,
pub facename: String,
/// LOGFONT charset (WMF ANSI strings decode through it).
pub charset: u8,
}
impl Default for Font {
fn default() -> Self {
Font {
height: -12.0,
escapement: 0.0,
facename: String::new(),
charset: 0,
}
}
}
/// One GDI object slot (metafiles address objects by table index).
#[derive(Clone, Debug)]
pub enum Obj {
Pen(Pen),
Brush(Brush),
Font(Font),
/// Created-but-unsupported object kind, occupies its slot.
Other,
}
/// Playback device context.
pub struct Dc {
pub world: Xform,
pub win_org: (f32, f32),
pub win_ext: (f32, f32),
pub vp_org: (f32, f32),
pub vp_ext: (f32, f32),
/// device → canvas mapping.
pub dev_org: (f32, f32),
pub dev_scale: (f32, f32),
pub pen: Pen,
pub brush: Brush,
pub font: Font,
pub text_color: Rgba,
pub bk_color: Rgba,
/// 1 = TRANSPARENT, 2 = OPAQUE.
pub bk_mode: u32,
pub text_align: u32,
/// Current position (logical), for MOVETO/LINETO chains.
pub pos: (f32, f32),
saved: Vec<DcSnapshot>,
}
struct DcSnapshot {
world: Xform,
win_org: (f32, f32),
win_ext: (f32, f32),
vp_org: (f32, f32),
vp_ext: (f32, f32),
pen: Pen,
brush: Brush,
font: Font,
text_color: Rgba,
bk_color: Rgba,
bk_mode: u32,
text_align: u32,
pos: (f32, f32),
clip: (f32, f32, f32, f32),
}
impl Dc {
pub fn new() -> Self {
Dc {
world: Xform::IDENTITY,
win_org: (0.0, 0.0),
win_ext: (1.0, 1.0),
vp_org: (0.0, 0.0),
vp_ext: (1.0, 1.0),
dev_org: (0.0, 0.0),
dev_scale: (1.0, 1.0),
pen: Pen {
color: Rgba([0, 0, 0, 255]),
width: 0.0,
null: false,
},
brush: Brush {
color: Rgba([255, 255, 255, 255]),
null: false,
},
font: Font::default(),
text_color: Rgba([0, 0, 0, 255]),
bk_color: Rgba([255, 255, 255, 255]),
bk_mode: 2,
text_align: 0,
pos: (0.0, 0.0),
saved: Vec::new(),
}
}
/// Logical point → canvas pixel.
pub fn to_canvas(&self, x: f32, y: f32) -> [f32; 2] {
let (px, py) = self.world.apply(x, y);
let dx = (px - self.win_org.0) * (self.vp_ext.0 / self.win_ext.0) + self.vp_org.0;
let dy = (py - self.win_org.1) * (self.vp_ext.1 / self.win_ext.1) + self.vp_org.1;
[
(dx - self.dev_org.0) * self.dev_scale.0,
(dy - self.dev_org.1) * self.dev_scale.1,
]
}
/// Average |logical → canvas| scale factor (pen widths, font sizes).
pub fn scale_avg(&self) -> f32 {
let wsx = (self.world.m11 * self.world.m11 + self.world.m12 * self.world.m12).sqrt();
let wsy = (self.world.m21 * self.world.m21 + self.world.m22 * self.world.m22).sqrt();
let sx = (self.vp_ext.0 / self.win_ext.0 * self.dev_scale.0).abs() * wsx.max(1e-12);
let sy = (self.vp_ext.1 / self.win_ext.1 * self.dev_scale.1).abs() * wsy.max(1e-12);
((sx + sy) * 0.5).max(1e-12)
}
/// Vertical |logical → canvas| scale (font heights).
pub fn scale_y(&self) -> f32 {
let wsy = (self.world.m21 * self.world.m21 + self.world.m22 * self.world.m22)
.sqrt()
.max(1e-12);
(self.vp_ext.1 / self.win_ext.1 * self.dev_scale.1).abs() * wsy
}
pub fn pen_px(&self) -> f32 {
if self.pen.width <= 0.0 {
// Cosmetic pen: one device pixel regardless of transforms — at the
// supersampled canvas resolution that's SS pixels.
SS as f32
} else {
(self.pen.width * self.scale_avg()).max(SS as f32)
}
}
pub fn save(&mut self, canvas: &Canvas) {
self.saved.push(DcSnapshot {
world: self.world,
win_org: self.win_org,
win_ext: self.win_ext,
vp_org: self.vp_org,
vp_ext: self.vp_ext,
pen: self.pen,
brush: self.brush,
font: self.font.clone(),
text_color: self.text_color,
bk_color: self.bk_color,
bk_mode: self.bk_mode,
text_align: self.text_align,
pos: self.pos,
clip: canvas.clip,
});
}
pub fn restore(&mut self, canvas: &mut Canvas) {
if let Some(s) = self.saved.pop() {
self.world = s.world;
self.win_org = s.win_org;
self.win_ext = s.win_ext;
self.vp_org = s.vp_org;
self.vp_ext = s.vp_ext;
self.pen = s.pen;
self.brush = s.brush;
self.font = s.font;
self.text_color = s.text_color;
self.bk_color = s.bk_color;
self.bk_mode = s.bk_mode;
self.text_align = s.text_align;
self.pos = s.pos;
canvas.clip = s.clip;
}
}
}
/// COLORREF (0x00BBGGRR) → RGBA.
pub fn colorref(v: u32) -> Rgba {
Rgba([
(v & 0xFF) as u8,
((v >> 8) & 0xFF) as u8,
((v >> 16) & 0xFF) as u8,
255,
])
}
/// Stock object index (SELECTOBJECT with the 0x8000_0000 flag) → object.
pub fn stock_obj(idx: u32) -> Option<Obj> {
let white = Rgba([255, 255, 255, 255]);
let black = Rgba([0, 0, 0, 255]);
let grey = |v: u8| Rgba([v, v, v, 255]);
Some(match idx {
0 => Obj::Brush(Brush {
color: white,
null: false,
}), // WHITE_BRUSH
1 => Obj::Brush(Brush {
color: grey(0xC0),
null: false,
}), // LTGRAY
2 => Obj::Brush(Brush {
color: grey(0x80),
null: false,
}), // GRAY
3 => Obj::Brush(Brush {
color: grey(0x40),
null: false,
}), // DKGRAY
4 => Obj::Brush(Brush {
color: black,
null: false,
}), // BLACK_BRUSH
5 => Obj::Brush(Brush {
color: white,
null: true,
}), // NULL_BRUSH
6 => Obj::Pen(Pen {
color: white,
width: 0.0,
null: false,
}), // WHITE_PEN
7 => Obj::Pen(Pen {
color: black,
width: 0.0,
null: false,
}), // BLACK_PEN
8 => Obj::Pen(Pen {
color: black,
width: 0.0,
null: true,
}), // NULL_PEN
10..=17 => Obj::Font(Font::default()),
_ => return None,
})
}
/// Size the supersampled canvas for a device-space content box, and yield the
/// device→canvas mapping. Returns `(canvas, dev_org, dev_scale)`.
fn canvas_for_box(w: f32, h: f32) -> Option<(Canvas, (f32, f32))> {
if !(w.is_finite() && h.is_finite()) || w.abs() < 1e-9 || h.abs() < 1e-9 {
return None;
}
let long = w.abs().max(h.abs());
let k = (TARGET_DIM * SS) as f32 / long;
let cw = ((w.abs() * k).round() as usize).clamp(SS, TARGET_DIM * SS);
let ch = ((h.abs() * k).round() as usize).clamp(SS, TARGET_DIM * SS);
Some((Canvas::new(cw, ch), (k * w.signum(), k * h.signum())))
}
/// Rasterize an EMF picture. Returns RGBA pixels + dimensions.
pub fn render_emf(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
emf::render(data)
}
/// Rasterize a WMF picture, placeable or standard. WMFC-wrapped enhanced
/// metafiles never reach here — acadrust's presentation extraction already
/// reassembles those to EMF.
pub fn render_wmf(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
wmf::render(data)
}
+250
View File
@@ -0,0 +1,250 @@
// CPU raster canvas for metafile playback.
//
// An RGBA8 pixel buffer with the primitive set the EMF/WMF players need:
// even-odd polygon fill, polyline stroking (expanded to quads), triangle
// fill (glyph outlines arrive pre-triangulated), axis-aligned rect fill,
// stretched RGBA blits, and a rectangular clip. Rendering happens at a
// supersampled resolution and is box-downsampled once at the end, which
// anti-aliases every primitive uniformly.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rgba(pub [u8; 4]);
pub struct Canvas {
pub w: usize,
pub h: usize,
pub px: Vec<u8>,
/// Current clip rectangle in canvas pixels (x0, y0, x1, y1), half-open.
pub clip: (f32, f32, f32, f32),
}
impl Canvas {
pub fn new(w: usize, h: usize) -> Self {
// White background — OLE presentations are recorded against a white
// page, and AutoCAD draws OLE frames on white as well.
Canvas {
w,
h,
px: vec![255u8; w * h * 4],
clip: (0.0, 0.0, w as f32, h as f32),
}
}
#[inline]
fn put(&mut self, x: usize, y: usize, c: Rgba) {
let i = (y * self.w + x) * 4;
self.px[i..i + 4].copy_from_slice(&c.0);
}
/// Fill a polygon set with the even-odd rule. `polys` are closed rings in
/// canvas pixel coordinates; all rings participate in one parity test, so
/// holes work the way GDI's ALTERNATE fill mode does.
pub fn fill_polys(&mut self, polys: &[Vec<[f32; 2]>], c: Rgba) {
let mut ymin = f32::MAX;
let mut ymax = f32::MIN;
for p in polys {
for v in p {
ymin = ymin.min(v[1]);
ymax = ymax.max(v[1]);
}
}
if !ymin.is_finite() || !ymax.is_finite() {
return;
}
let y0 = (ymin.max(self.clip.1).floor() as isize).max(0) as usize;
let y1 = (ymax.min(self.clip.3).ceil() as isize).max(0) as usize;
let y1 = y1.min(self.h);
let mut xs: Vec<f32> = Vec::new();
for y in y0..y1 {
let yc = y as f32 + 0.5;
xs.clear();
for p in polys {
let n = p.len();
if n < 3 {
continue;
}
for i in 0..n {
let a = p[i];
let b = p[(i + 1) % n];
if (a[1] <= yc && b[1] > yc) || (b[1] <= yc && a[1] > yc) {
let t = (yc - a[1]) / (b[1] - a[1]);
xs.push(a[0] + t * (b[0] - a[0]));
}
}
}
if xs.len() < 2 {
continue;
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for pair in xs.chunks_exact(2) {
let xa = pair[0].max(self.clip.0).max(0.0).round() as usize;
let xb = pair[1].min(self.clip.2).min(self.w as f32).round() as usize;
for x in xa..xb.min(self.w) {
self.put(x, y, c);
}
}
}
}
/// Fill a flat triangle list (groups of 3 vertices) — used for glyphs.
pub fn fill_tris(&mut self, tris: &[[f32; 2]], c: Rgba) {
for t in tris.chunks_exact(3) {
let (a, b, d) = (t[0], t[1], t[2]);
let ymin = a[1].min(b[1]).min(d[1]).max(self.clip.1).max(0.0);
let ymax = a[1].max(b[1]).max(d[1]).min(self.clip.3).min(self.h as f32);
let det = (b[0] - a[0]) * (d[1] - a[1]) - (d[0] - a[0]) * (b[1] - a[1]);
if det.abs() < 1e-12 {
continue;
}
let y0 = ymin.floor() as usize;
let y1 = ymax.ceil() as usize;
for y in y0..y1.min(self.h) {
let yc = y as f32 + 0.5;
// Row span via edge intersections — cheaper than barycentric
// over the bbox for the thin triangles glyphs produce.
let mut xmin = f32::MAX;
let mut xmax = f32::MIN;
let edges = [(a, b), (b, d), (d, a)];
for (p, q) in edges {
if (p[1] <= yc && q[1] > yc) || (q[1] <= yc && p[1] > yc) {
let t = (yc - p[1]) / (q[1] - p[1]);
let x = p[0] + t * (q[0] - p[0]);
xmin = xmin.min(x);
xmax = xmax.max(x);
}
}
if xmin > xmax {
continue;
}
let xa = xmin.max(self.clip.0).max(0.0).round() as usize;
let xb = xmax.min(self.clip.2).min(self.w as f32).round() as usize;
for x in xa..xb {
self.put(x, y, c);
}
}
}
}
/// Stroke an open polyline with the given width by expanding each segment
/// into a quad (plus square joints at interior vertices).
pub fn stroke_polyline(&mut self, pts: &[[f32; 2]], width: f32, c: Rgba) {
let w = width.max(1.0) * 0.5;
for seg in pts.windows(2) {
let (a, b) = (seg[0], seg[1]);
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-6 {
continue;
}
let nx = -dy / len * w;
let ny = dx / len * w;
// extend caps by half-width so joints don't leave pinholes
let ex = dx / len * w;
let ey = dy / len * w;
let quad = vec![
[a[0] + nx - ex, a[1] + ny - ey],
[b[0] + nx + ex, b[1] + ny + ey],
[b[0] - nx + ex, b[1] - ny + ey],
[a[0] - nx - ex, a[1] - ny - ey],
];
self.fill_polys(std::slice::from_ref(&quad), c);
}
}
pub fn fill_rect(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, c: Rgba) {
let r = vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
self.fill_polys(std::slice::from_ref(&r), c);
}
/// Blit `src` (RGBA, sw×sh) stretched into the destination rectangle,
/// nearest-neighbour sampled from the `sx..sx+scx` × `sy..sy+scy` source
/// window. Negative destination extents flip the image. Source alpha
/// composites over the canvas.
#[allow(clippy::too_many_arguments)]
pub fn blit(
&mut self,
src: &[u8],
sw: usize,
sh: usize,
sx: f32,
sy: f32,
scx: f32,
scy: f32,
dx0: f32,
dy0: f32,
dx1: f32,
dy1: f32,
) {
if sw == 0 || sh == 0 || (dx1 - dx0).abs() < 0.5 || (dy1 - dy0).abs() < 0.5 {
return;
}
let (xa, xb) = (dx0.min(dx1), dx0.max(dx1));
let (ya, yb) = (dy0.min(dy1), dy0.max(dy1));
let x0 = xa.max(self.clip.0).max(0.0).floor() as usize;
let x1 = (xb.min(self.clip.2).min(self.w as f32).ceil() as usize).min(self.w);
let y0 = ya.max(self.clip.1).max(0.0).floor() as usize;
let y1 = (yb.min(self.clip.3).min(self.h as f32).ceil() as usize).min(self.h);
for y in y0..y1 {
let fy = ((y as f32 + 0.5) - dy0) / (dy1 - dy0);
let syf = sy + fy * scy;
let syi = syf.floor() as isize;
if syi < 0 || syi >= sh as isize {
continue;
}
for x in x0..x1 {
let fx = ((x as f32 + 0.5) - dx0) / (dx1 - dx0);
let sxf = sx + fx * scx;
let sxi = sxf.floor() as isize;
if sxi < 0 || sxi >= sw as isize {
continue;
}
let si = (syi as usize * sw + sxi as usize) * 4;
let sa = src[si + 3] as u32;
if sa == 0 {
continue;
}
let di = (y * self.w + x) * 4;
if sa == 255 {
self.px[di..di + 4].copy_from_slice(&src[si..si + 4]);
} else {
for k in 0..3 {
let d = self.px[di + k] as u32;
let s = src[si + k] as u32;
self.px[di + k] = ((s * sa + d * (255 - sa)) / 255) as u8;
}
self.px[di + 3] = 255;
}
}
}
}
/// Box-downsample by an integer factor into a fresh RGBA buffer.
pub fn downsample(&self, factor: usize) -> (Vec<u8>, u32, u32) {
if factor <= 1 {
return (self.px.clone(), self.w as u32, self.h as u32);
}
let ow = (self.w / factor).max(1);
let oh = (self.h / factor).max(1);
let mut out = vec![0u8; ow * oh * 4];
let n = (factor * factor) as u32;
for oy in 0..oh {
for ox in 0..ow {
let mut acc = [0u32; 4];
for fy in 0..factor {
for fx in 0..factor {
let i = ((oy * factor + fy) * self.w + ox * factor + fx) * 4;
for k in 0..4 {
acc[k] += self.px[i + k] as u32;
}
}
}
let o = (oy * ow + ox) * 4;
for k in 0..4 {
out[o + k] = (acc[k] / n) as u8;
}
}
}
(out, ow as u32, oh as u32)
}
}
+241
View File
@@ -0,0 +1,241 @@
// Metafile text → canvas, using the CAD text renderer's glyph outline cache.
//
// Glyph outlines arrive from `ttf_glyph` pre-triangulated in a 9-unit
// cap-height space with the baseline at y = 0 (y-up); here they are scaled by
// the font's device pixel size, flipped to the canvas's y-down space, rotated
// by the font escapement, and filled.
use super::raster::Canvas;
use super::{Dc, Font};
use crate::scene::text::{sysfont, ttf_glyph};
/// Per-face metrics needed to place metafile text: cap-height and ascent as
/// fractions of the em. Falls back to typical Latin ratios when the face (or
/// the whole family) is unavailable.
fn face_ratios(family: &str) -> (f32, f32) {
sysfont::with_face_data(family, |data, index| {
let face = ttf_parser::Face::parse(data, index).ok()?;
let upem = face.units_per_em() as f32;
let cap = face
.capital_height()
.filter(|&c| c > 0)
.map(|c| c as f32 / upem)
.unwrap_or(0.7);
let asc = face.ascender() as f32 / upem;
Some((cap, asc.max(0.5)))
})
.flatten()
.unwrap_or((0.7, 0.9))
}
/// Resolve a LOGFONT facename to an installed family, with sane fallbacks.
fn resolve_family(facename: &str) -> Option<String> {
if !facename.is_empty() {
if let Some(f) = sysfont::canonical_family_name(facename) {
return Some(f);
}
}
for cand in ["Arial", "Liberation Sans", "DejaVu Sans", "Noto Sans"] {
if let Some(f) = sysfont::canonical_family_name(cand) {
return Some(f);
}
}
sysfont::families().first().cloned()
}
/// TA_* horizontal / vertical alignment split.
struct Align {
/// 0 = left, 1 = center, 2 = right.
h: u8,
/// 0 = top, 1 = baseline, 2 = bottom.
v: u8,
update_cp: bool,
}
fn split_align(ta: u32) -> Align {
let h = if ta & 6 == 6 {
1
} else if ta & 2 != 0 {
2
} else {
0
};
let v = if ta & 24 == 24 {
1
} else if ta & 8 != 0 {
2
} else {
0
};
Align {
h,
v,
update_cp: ta & 1 != 0,
}
}
/// Draw one metafile text run.
///
/// `xr`, `yr` — the logical-space reference point. `dx` — optional per-char
/// logical advances (metafiles record them so playback matches the recorder's
/// font metrics exactly; without them the run is shaped fresh).
#[allow(clippy::too_many_arguments)]
pub fn draw_text(
canvas: &mut Canvas,
dc: &mut Dc,
xr: f32,
yr: f32,
text: &str,
dx: Option<&[f32]>,
opaque_rect: Option<[f32; 4]>,
) {
if text.is_empty() {
return;
}
let font = dc.font.clone();
let Some(family) = resolve_family(&font.facename) else {
return;
};
let (cap_ratio, ascent_ratio) = face_ratios(&family);
// Font pixel size on the canvas. Negative LOGFONT height = em size,
// positive = cell height (em + internal leading ≈ em × 1.15).
let em_log = if font.height < 0.0 {
-font.height
} else if font.height > 0.0 {
font.height / 1.15
} else {
12.0
};
let em_px = em_log * dc.scale_y();
if em_px < 0.5 {
return;
}
// Glyph 9-unit cap space → canvas pixels.
let s = em_px * cap_ratio / 9.0;
let ascent_px = em_px * ascent_ratio;
let align = split_align(dc.text_align);
let (rx, ry) = if align.update_cp {
(dc.pos.0, dc.pos.1)
} else {
(xr, yr)
};
// Fill the opaque background rectangle first (logical coords).
if let Some([x0, y0, x1, y1]) = opaque_rect {
let a = dc.to_canvas(x0, y0);
let b = dc.to_canvas(x1, y1);
let bk = dc.bk_color;
canvas.fill_rect(
a[0].min(b[0]),
a[1].min(b[1]),
a[0].max(b[0]),
a[1].max(b[1]),
bk,
);
}
// Collect positioned glyph triangles in run-local pixel space (x right,
// y down, origin at the run's left baseline start).
let mut tris: Vec<[f32; 2]> = Vec::new();
let run_advance_px: f32;
let log_to_px = dc.scale_avg();
if let Some(dx) = dx {
// Recorded per-char advances: place each char's glyph independently.
let mut pen_x = 0.0_f32;
for (i, ch) in text.chars().enumerate() {
let g = ttf_glyph::glyph(&family, ch).or_else(|| ttf_glyph::fallback_glyph(ch));
if let Some(g) = g {
for v in &g.fill_tris {
tris.push([pen_x + v[0] * s, -v[1] * s]);
}
}
pen_x += dx.get(i).copied().unwrap_or(0.0) * log_to_px;
}
run_advance_px = pen_x;
} else if let Some(run) = ttf_glyph::shape_run(&family, text) {
for g in &run.glyphs {
for v in &g.fill_tris {
tris.push([v[0] * s, -v[1] * s]);
}
}
run_advance_px = run.advance * s;
} else {
// No shaping available (e.g. web build): naive per-char advances.
let mut pen_x = 0.0_f32;
for ch in text.chars() {
let g = ttf_glyph::glyph(&family, ch).or_else(|| ttf_glyph::fallback_glyph(ch));
if let Some(g) = g {
for v in &g.fill_tris {
tris.push([pen_x + v[0] * s, -v[1] * s]);
}
pen_x += g.advance * s;
} else {
pen_x += em_px * 0.5;
}
}
run_advance_px = pen_x;
}
// Alignment offsets in run-local space.
let x_off = match align.h {
1 => -run_advance_px * 0.5,
2 => -run_advance_px,
_ => 0.0,
};
let y_off = match align.v {
1 => 0.0, // reference is the baseline
2 => -em_px * 0.25, // bottom ≈ baseline + descent
_ => ascent_px, // top: baseline sits one ascent below
};
// Rotate by escapement (0.1° CCW in logical space = CW in y-down canvas),
// then translate to the canvas-space reference point.
let ref_px = dc.to_canvas(rx, ry);
let ang = font.escapement / 10.0 * std::f32::consts::PI / 180.0;
let (sin, cos) = (-ang).sin_cos();
let color = dc.text_color;
for v in &mut tris {
let x = v[0] + x_off;
let y = v[1] + y_off;
*v = [ref_px[0] + x * cos - y * sin, ref_px[1] + x * sin + y * cos];
}
canvas.fill_tris(&tris, color);
if align.update_cp {
// Advance the current position along the run direction (logical).
let adv_log = run_advance_px / log_to_px.max(1e-12);
dc.pos.0 += adv_log * (font.escapement / 10.0).to_radians().cos();
dc.pos.1 -= adv_log * (font.escapement / 10.0).to_radians().sin();
}
}
/// Decode a WMF ANSI string per the current font's charset. Only the Turkish
/// code page (162) gets explicit treatment beyond Windows-1252.
pub fn decode_ansi(bytes: &[u8], font: &Font) -> String {
bytes
.iter()
.map(|&b| match b {
0x80 => '€',
0x82 => '‚',
0x84 => '„',
0x85 => '…',
0x91 => '\u{2018}',
0x92 => '\u{2019}',
0x93 => '“',
0x94 => '”',
0x95 => '•',
0x96 => '–',
0x97 => '—',
0xD0 if font.charset == 162 => 'Ğ',
0xDD if font.charset == 162 => 'İ',
0xDE if font.charset == 162 => 'Ş',
0xF0 if font.charset == 162 => 'ğ',
0xFD if font.charset == 162 => 'ı',
0xFE if font.charset == 162 => 'ş',
b => b as char,
})
.collect()
}
+501
View File
@@ -0,0 +1,501 @@
// WMF (16-bit Windows metafile) record player, plus the WMFC → EMF
// re-assembly used when a memory WMF is only a compatibility wrapper around
// an embedded enhanced metafile (the form Office writes into OlePres000).
use super::raster::{Canvas, Rgba};
use super::{canvas_for_box, colorref, dib, text, Brush, Dc, Font, Obj, Pen};
#[inline]
fn u16_at(d: &[u8], o: usize) -> u16 {
u16::from_le_bytes(d[o..o + 2].try_into().unwrap())
}
#[inline]
fn i16_at(d: &[u8], o: usize) -> i16 {
i16::from_le_bytes(d[o..o + 2].try_into().unwrap())
}
#[inline]
fn u32_at(d: &[u8], o: usize) -> u32 {
u32::from_le_bytes(d[o..o + 4].try_into().unwrap())
}
const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
/// Strip a placeable header if present. Returns `(records_region, bbox)` where
/// the region starts at the standard WMF header and bbox is the placeable
/// bounds (logical units), if any.
fn strip_placeable(data: &[u8]) -> (&[u8], Option<(f32, f32, f32, f32)>) {
if data.len() >= 22 && u32_at(data, 0) == PLACEABLE_KEY {
let l = i16_at(data, 6) as f32;
let t = i16_at(data, 8) as f32;
let r = i16_at(data, 10) as f32;
let b = i16_at(data, 12) as f32;
(&data[22..], Some((l, t, r, b)))
} else {
(data, None)
}
}
/// Validate the standard WMF header, returning the offset of the first record.
fn header_ok(d: &[u8]) -> Option<usize> {
if d.len() < 18 {
return None;
}
let ty = u16_at(d, 0);
let hs = u16_at(d, 2) as usize;
if (ty != 1 && ty != 2) || hs != 9 {
return None;
}
Some(hs * 2)
}
/// Iterate records as `(function, params)` slices.
struct Records<'a> {
d: &'a [u8],
off: usize,
}
impl<'a> Iterator for Records<'a> {
type Item = (u16, &'a [u8]);
fn next(&mut self) -> Option<(u16, &'a [u8])> {
if self.off + 6 > self.d.len() {
return None;
}
let size_w = u32_at(self.d, self.off) as usize;
let func = u16_at(self.d, self.off + 4);
let size_b = size_w.checked_mul(2)?;
if size_b < 6 || self.off + size_b > self.d.len() {
return None;
}
let params = &self.d[self.off + 6..self.off + size_b];
self.off += size_b;
if func == 0 {
return None; // META_EOF
}
Some((func, params))
}
}
struct Player {
dc: Dc,
objects: Vec<Option<Obj>>,
}
impl Player {
/// WMF object slots are assigned to the lowest free index.
fn create(&mut self, obj: Obj) {
if let Some(slot) = self.objects.iter_mut().find(|s| s.is_none()) {
*slot = Some(obj);
} else {
self.objects.push(Some(obj));
}
}
fn select(&mut self, idx: u16) {
match self.objects.get(idx as usize).and_then(|o| o.clone()) {
Some(Obj::Pen(p)) => self.dc.pen = p,
Some(Obj::Brush(b)) => self.dc.brush = b,
Some(Obj::Font(f)) => self.dc.font = f,
_ => {}
}
}
fn map(&self, pts: &[[f32; 2]]) -> Vec<[f32; 2]> {
pts.iter().map(|p| self.dc.to_canvas(p[0], p[1])).collect()
}
fn fill_and_stroke(&mut self, canvas: &mut Canvas, rings: &[Vec<[f32; 2]>]) {
let mapped: Vec<Vec<[f32; 2]>> = rings.iter().map(|r| self.map(r)).collect();
if !self.dc.brush.null {
canvas.fill_polys(&mapped, self.dc.brush.color);
}
if !self.dc.pen.null {
let w = self.dc.pen_px();
for r in &mapped {
if r.len() >= 2 {
let mut closed = r.clone();
closed.push(r[0]);
canvas.stroke_polyline(&closed, w, self.dc.pen.color);
}
}
}
}
}
/// Blit a packed DIB from a blit record. `dest`/`src` rectangles are in
/// logical / source-pixel units respectively.
#[allow(clippy::too_many_arguments)]
fn blit_dib(
canvas: &mut Canvas,
dc: &Dc,
dibdata: &[u8],
xd: f32,
yd: f32,
wd: f32,
hd: f32,
sx: f32,
sy: f32,
sw: f32,
sh: f32,
) {
let Some((px, w, h)) = dib::decode_packed(dibdata) else {
return;
};
let a = dc.to_canvas(xd, yd);
let b = dc.to_canvas(xd + wd, yd + hd);
let (sw, sh) = if sw <= 0.0 || sh <= 0.0 {
(w as f32, h as f32)
} else {
(sw, sh)
};
canvas.blit(
&px, w as usize, h as usize, sx, sy, sw, sh, a[0], a[1], b[0], b[1],
);
}
pub fn render(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
let (data, placeable_box) = strip_placeable(data);
let first = header_ok(data)?;
// Establish the logical picture box: the first window org/ext pair wins,
// falling back to the placeable bounds.
let mut org: Option<(f32, f32)> = None;
let mut ext: Option<(f32, f32)> = None;
for (func, p) in (Records {
d: data,
off: first,
}) {
match func {
0x020B if p.len() >= 4 && org.is_none() => {
org = Some((i16_at(p, 2) as f32, i16_at(p, 0) as f32));
}
0x020C if p.len() >= 4 && ext.is_none() => {
ext = Some((i16_at(p, 2) as f32, i16_at(p, 0) as f32));
}
_ => {}
}
if org.is_some() && ext.is_some() {
break;
}
}
let (wx, wy, we, he) = match (org, ext, placeable_box) {
(Some((x, y)), Some((w, h)), _) => (x, y, w, h),
(_, _, Some((l, t, r, b))) => (l, t, r - l, b - t),
_ => return None,
};
if we == 0.0 || he == 0.0 {
return None;
}
let (mut canvas, _) = canvas_for_box(we, he)?;
let mut p = Player {
dc: Dc::new(),
objects: Vec::new(),
};
// Window → viewport maps straight onto the canvas; the device→canvas
// stage stays identity.
p.dc.win_org = (wx, wy);
p.dc.win_ext = (we, he);
p.dc.vp_org = (0.0, 0.0);
p.dc.vp_ext = (canvas.w as f32 * we.signum(), canvas.h as f32 * he.signum());
if we < 0.0 {
p.dc.vp_org.0 = canvas.w as f32;
}
if he < 0.0 {
p.dc.vp_org.1 = canvas.h as f32;
}
for (func, prm) in (Records {
d: data,
off: first,
}) {
match func {
0x020B if prm.len() >= 4 => {
p.dc.win_org = (i16_at(prm, 2) as f32, i16_at(prm, 0) as f32)
}
0x020C if prm.len() >= 4 => {
let (cx, cy) = (i16_at(prm, 2) as f32, i16_at(prm, 0) as f32);
if cx != 0.0 && cy != 0.0 {
p.dc.win_ext = (cx, cy);
}
}
0x0201 if prm.len() >= 4 => p.dc.bk_color = colorref(u32_at(prm, 0)),
0x0102 if prm.len() >= 2 => p.dc.bk_mode = u16_at(prm, 0) as u32,
0x0209 if prm.len() >= 4 => p.dc.text_color = colorref(u32_at(prm, 0)),
0x012E if prm.len() >= 2 => p.dc.text_align = u16_at(prm, 0) as u32,
0x001E => p.dc.save(&canvas),
0x0127 => p.dc.restore(&mut canvas),
0x0416 if prm.len() >= 8 => {
let a = p.dc.to_canvas(i16_at(prm, 6) as f32, i16_at(prm, 4) as f32);
let b = p.dc.to_canvas(i16_at(prm, 2) as f32, i16_at(prm, 0) as f32);
let c = &mut canvas.clip;
c.0 = c.0.max(a[0].min(b[0]));
c.1 = c.1.max(a[1].min(b[1]));
c.2 = c.2.min(a[0].max(b[0]));
c.3 = c.3.min(a[1].max(b[1]));
}
0x0214 if prm.len() >= 4 => p.dc.pos = (i16_at(prm, 2) as f32, i16_at(prm, 0) as f32),
0x0213 if prm.len() >= 4 => {
let to = (i16_at(prm, 2) as f32, i16_at(prm, 0) as f32);
if !p.dc.pen.null {
let seg = p.map(&[[p.dc.pos.0, p.dc.pos.1], [to.0, to.1]]);
let w = p.dc.pen_px();
canvas.stroke_polyline(&seg, w, p.dc.pen.color);
}
p.dc.pos = to;
}
0x0324 | 0x0325 if prm.len() >= 2 => {
let n = u16_at(prm, 0) as usize;
if n == 0 || 2 + n * 4 > prm.len() {
continue;
}
let pts: Vec<[f32; 2]> = (0..n)
.map(|i| [i16_at(prm, 2 + i * 4) as f32, i16_at(prm, 4 + i * 4) as f32])
.collect();
if func == 0x0324 {
p.fill_and_stroke(&mut canvas, std::slice::from_ref(&pts));
} else if !p.dc.pen.null {
let mapped = p.map(&pts);
let w = p.dc.pen_px();
canvas.stroke_polyline(&mapped, w, p.dc.pen.color);
}
}
0x0538 if prm.len() >= 2 => {
let n_polys = u16_at(prm, 0) as usize;
if n_polys == 0 || 2 + n_polys * 2 > prm.len() {
continue;
}
let mut rings = Vec::with_capacity(n_polys);
let mut pt_off = 2 + n_polys * 2;
for i in 0..n_polys {
let c = u16_at(prm, 2 + i * 2) as usize;
if pt_off + c * 4 > prm.len() {
break;
}
rings.push(
(0..c)
.map(|j| {
[
i16_at(prm, pt_off + j * 4) as f32,
i16_at(prm, pt_off + 2 + j * 4) as f32,
]
})
.collect::<Vec<_>>(),
);
pt_off += c * 4;
}
p.fill_and_stroke(&mut canvas, &rings);
}
0x041B | 0x0418 if prm.len() >= 8 => {
let (b, r, t, l) = (
i16_at(prm, 0) as f32,
i16_at(prm, 2) as f32,
i16_at(prm, 4) as f32,
i16_at(prm, 6) as f32,
);
let ring: Vec<[f32; 2]> = if func == 0x041B {
vec![[l, t], [r, t], [r, b], [l, b]]
} else {
let (cx, cy) = ((l + r) * 0.5, (t + b) * 0.5);
let (rx, ry) = ((r - l) * 0.5, (b - t) * 0.5);
(0..64)
.map(|i| {
let a = i as f32 / 64.0 * std::f32::consts::TAU;
[cx + rx * a.cos(), cy + ry * a.sin()]
})
.collect()
};
p.fill_and_stroke(&mut canvas, std::slice::from_ref(&ring));
}
0x061D if prm.len() >= 12 => {
// META_PATBLT: brush fill of a rectangle.
let rop = u32_at(prm, 0);
let (h, w, y, x) = (
i16_at(prm, 4) as f32,
i16_at(prm, 6) as f32,
i16_at(prm, 8) as f32,
i16_at(prm, 10) as f32,
);
let color = match rop {
0x00F0_0021 | 0x005A_0049 => Some(p.dc.brush.color),
0x0000_0042 => Some(Rgba([0, 0, 0, 255])),
0x00FF_0062 => Some(Rgba([255, 255, 255, 255])),
_ => None,
};
if let Some(c) = color {
let a = p.dc.to_canvas(x, y);
let b = p.dc.to_canvas(x + w, y + h);
canvas.fill_rect(
a[0].min(b[0]),
a[1].min(b[1]),
a[0].max(b[0]),
a[1].max(b[1]),
c,
);
}
}
0x02FA if prm.len() >= 10 => {
let style = u16_at(prm, 0);
p.create(Obj::Pen(Pen {
color: colorref(u32_at(prm, 6)),
width: i16_at(prm, 2) as f32,
null: style & 0xF == 5,
}));
}
0x02FC if prm.len() >= 6 => {
let style = u16_at(prm, 0);
p.create(Obj::Brush(Brush {
color: colorref(u32_at(prm, 2)),
null: style == 1,
}));
}
0x02FB if prm.len() >= 18 => {
let mut name = String::new();
for &b in prm[18..].iter().take(32) {
if b == 0 {
break;
}
name.push(b as char);
}
p.create(Obj::Font(Font {
height: i16_at(prm, 0) as f32,
escapement: i16_at(prm, 4) as f32,
facename: name,
charset: prm[13],
}));
}
// Other object-creating records occupy a slot even when unused.
0x00F7 | 0x01F9 | 0x0142 | 0x06FF => p.create(Obj::Other),
0x012D if prm.len() >= 2 => p.select(u16_at(prm, 0)),
0x01F0 if prm.len() >= 2 => {
let i = u16_at(prm, 0) as usize;
if i < p.objects.len() {
p.objects[i] = None;
}
}
0x0521 if prm.len() >= 2 => {
let n = u16_at(prm, 0) as usize;
let str_end = 2 + n;
let padded = str_end + (str_end & 1);
if n == 0 || padded + 4 > prm.len() {
continue;
}
let s = text::decode_ansi(&prm[2..str_end], &p.dc.font);
let y = i16_at(prm, padded) as f32;
let x = i16_at(prm, padded + 2) as f32;
text::draw_text(&mut canvas, &mut p.dc, x, y, &s, None, None);
}
0x0A32 if prm.len() >= 8 => {
let y = i16_at(prm, 0) as f32;
let x = i16_at(prm, 2) as f32;
let n = u16_at(prm, 4) as usize;
let opts = u16_at(prm, 6);
let mut off = 8;
let mut opaque = None;
if opts & 0x0006 != 0 && prm.len() >= off + 8 {
let rect = [
i16_at(prm, off) as f32,
i16_at(prm, off + 2) as f32,
i16_at(prm, off + 4) as f32,
i16_at(prm, off + 6) as f32,
];
if opts & 0x0002 != 0 {
opaque = Some(rect);
}
off += 8;
}
if n == 0 || off + n > prm.len() {
continue;
}
let s = text::decode_ansi(&prm[off..off + n], &p.dc.font);
let str_pad = n + (n & 1);
let dx_off = off + str_pad;
let dx: Option<Vec<f32>> = if dx_off + n * 2 <= prm.len() {
Some((0..n).map(|i| i16_at(prm, dx_off + i * 2) as f32).collect())
} else {
None
};
text::draw_text(&mut canvas, &mut p.dc, x, y, &s, dx.as_deref(), opaque);
}
0x0F43 if prm.len() >= 22 => {
let (sh, sw, sy, sx) = (
i16_at(prm, 6) as f32,
i16_at(prm, 8) as f32,
i16_at(prm, 10) as f32,
i16_at(prm, 12) as f32,
);
let (dh, dw, dy, dx) = (
i16_at(prm, 14) as f32,
i16_at(prm, 16) as f32,
i16_at(prm, 18) as f32,
i16_at(prm, 20) as f32,
);
blit_dib(
&mut canvas,
&p.dc,
&prm[22..],
dx,
dy,
dw,
dh,
sx,
sy,
sw,
sh,
);
}
0x0B41 if prm.len() >= 20 => {
let (sh, sw, sy, sx) = (
i16_at(prm, 4) as f32,
i16_at(prm, 6) as f32,
i16_at(prm, 8) as f32,
i16_at(prm, 10) as f32,
);
let (dh, dw, dy, dx) = (
i16_at(prm, 12) as f32,
i16_at(prm, 14) as f32,
i16_at(prm, 16) as f32,
i16_at(prm, 18) as f32,
);
blit_dib(
&mut canvas,
&p.dc,
&prm[20..],
dx,
dy,
dw,
dh,
sx,
sy,
sw,
sh,
);
}
0x0940 if prm.len() >= 16 => {
let (sy, sx) = (i16_at(prm, 4) as f32, i16_at(prm, 6) as f32);
let (dh, dw, dy, dx) = (
i16_at(prm, 8) as f32,
i16_at(prm, 10) as f32,
i16_at(prm, 12) as f32,
i16_at(prm, 14) as f32,
);
blit_dib(
&mut canvas,
&p.dc,
&prm[16..],
dx,
dy,
dw,
dh,
sx,
sy,
dw,
dh,
);
}
_ => {}
}
}
let (px, w, h) = canvas.downsample(super::SS);
Some((px, w, h))
}
+6 -41
View File
@@ -182,12 +182,13 @@ impl ImageModel {
}
impl ImageModel {
/// Build an ImageModel from an OLE2FRAME's embedded presentation bitmap.
/// Returns `None` when the frame is degenerate or carries no decodable
/// bitmap (e.g. a metafile-only OLE), so the caller falls back to the frame
/// placeholder.
/// Build an ImageModel from an OLE2FRAME's embedded presentation.
/// The blob's compound file is parsed for the native raster or the cached
/// metafile picture (rasterized by the `gdi` player); returns `None` when
/// the frame is degenerate or nothing in the blob decodes, so the caller
/// falls back to the frame placeholder.
pub fn from_ole2frame(ole: &acadrust::entities::Ole2Frame) -> Option<Self> {
let (pixels, width, height) = decode_ole_bitmap(&ole.binary_data)?;
let (pixels, width, height) = super::ole_pres::decode(&ole.binary_data)?;
// Frame rectangle in WCS. `upper_left`/`lower_right` name the diagonal;
// normalise to left/right/top/bottom so the bitmap sits upright.
@@ -235,42 +236,6 @@ impl ImageModel {
}
}
/// Extract and decode the presentation BMP embedded in an OLE2FRAME data blob.
/// The blob carries the OLE object's cached bitmap (a `BITMAPFILEHEADER` "BM"
/// followed by a DIB); scan for a self-consistent one and decode it. Returns
/// `None` when no valid BMP is present (a metafile/other-format OLE).
fn decode_ole_bitmap(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
let mut i = 0usize;
while i + 54 <= data.len() {
if &data[i..i + 2] != b"BM" {
i += 1;
continue;
}
let file_size = u32::from_le_bytes(data[i + 2..i + 6].try_into().unwrap()) as usize;
let dib_size = u32::from_le_bytes(data[i + 14..i + 18].try_into().unwrap());
// A plausible BITMAPFILEHEADER points at a known DIB-header size.
if !matches!(dib_size, 12 | 40 | 52 | 56 | 64 | 108 | 124) {
i += 1;
continue;
}
let end = if file_size >= 54 && i + file_size <= data.len() {
i + file_size
} else {
data.len()
};
if let Ok(img) = image::load_from_memory_with_format(&data[i..end], image::ImageFormat::Bmp)
{
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
if w > 0 && h > 0 {
return Some((rgba.into_raw(), w, h));
}
}
i += 1;
}
None
}
/// Decoded RGBA image shared between the raster pipeline and the
/// unresolved-reference probe. Cheap to clone — the pixels are `Arc`-shared.
#[derive(Clone)]
+2
View File
@@ -1,7 +1,9 @@
pub mod wire_model;
pub mod gdi;
pub mod hatch_model;
pub mod hatch_patterns;
pub mod image_model;
pub mod ole_pres;
pub mod mesh_model;
pub mod solid_model;
pub mod object;
+28
View File
@@ -0,0 +1,28 @@
// OLE2FRAME binary data → presentation RGBA image.
//
// Extraction (compound-file walk, OlePres header parse, WMFC → EMF
// reassembly) lives in acadrust — `extract_presentation` returns the picture
// bytes tagged by kind. This module only decodes them: rasters through the
// `image` crate, metafiles through the `gdi` player.
use acadrust::entities::{extract_presentation, OlePresentation};
use super::gdi;
/// Decode the picture for an OLE2FRAME data blob. Returns RGBA + dimensions.
pub fn decode(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
match extract_presentation(data)? {
OlePresentation::Raster(bytes) => {
let img = image::load_from_memory(&bytes).ok()?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
if w == 0 || h == 0 {
return None;
}
Some((rgba.into_raw(), w, h))
}
OlePresentation::Dib(bytes) => gdi::dib::decode_packed(&bytes),
OlePresentation::Emf(bytes) => gdi::render_emf(&bytes),
OlePresentation::Wmf(bytes) => gdi::render_wmf(&bytes),
}
}