forked from 1b-agent/opencadstudio-upstream
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:
1 parent
1cfb856224
commit
e8411c1b6d
10 files changed
+2088
-42
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user