From e8411c1b6d3b7bc812d0aae119dcbc06158c4017 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 01:23:31 +0300 Subject: [PATCH] feat(ole): render EMF/WMF OLE presentations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 2 +- src/scene/model/gdi/dib.rs | 89 +++++ src/scene/model/gdi/emf.rs | 626 +++++++++++++++++++++++++++++++++ src/scene/model/gdi/mod.rs | 344 ++++++++++++++++++ src/scene/model/gdi/raster.rs | 250 +++++++++++++ src/scene/model/gdi/text.rs | 241 +++++++++++++ src/scene/model/gdi/wmf.rs | 501 ++++++++++++++++++++++++++ src/scene/model/image_model.rs | 47 +-- src/scene/model/mod.rs | 2 + src/scene/model/ole_pres.rs | 28 ++ 10 files changed, 2088 insertions(+), 42 deletions(-) create mode 100644 src/scene/model/gdi/dib.rs create mode 100644 src/scene/model/gdi/emf.rs create mode 100644 src/scene/model/gdi/mod.rs create mode 100644 src/scene/model/gdi/raster.rs create mode 100644 src/scene/model/gdi/text.rs create mode 100644 src/scene/model/gdi/wmf.rs create mode 100644 src/scene/model/ole_pres.rs diff --git a/Cargo.lock b/Cargo.lock index 9f745f18d..8e4120472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/src/scene/model/gdi/dib.rs b/src/scene/model/gdi/dib.rs new file mode 100644 index 000000000..849367662 --- /dev/null +++ b/src/scene/model/gdi/dib.rs @@ -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, 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, 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)) +} diff --git a/src/scene/model/gdi/emf.rs b/src/scene/model/gdi/emf.rs new file mode 100644 index 000000000..f73362f39 --- /dev/null +++ b/src/scene/model/gdi/emf.rs @@ -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>, + /// BEGINPATH..ENDPATH contour accumulator (logical coords). + path: Option>>, +} + +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> = 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, 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 = (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> = 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)) +} diff --git a/src/scene/model/gdi/mod.rs b/src/scene/model/gdi/mod.rs new file mode 100644 index 000000000..6b3988185 --- /dev/null +++ b/src/scene/model/gdi/mod.rs @@ -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, +} + +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 { + 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, 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, u32, u32)> { + wmf::render(data) +} diff --git a/src/scene/model/gdi/raster.rs b/src/scene/model/gdi/raster.rs new file mode 100644 index 000000000..0b5aecaad --- /dev/null +++ b/src/scene/model/gdi/raster.rs @@ -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, + /// 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 = 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, 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) + } +} diff --git a/src/scene/model/gdi/text.rs b/src/scene/model/gdi/text.rs new file mode 100644 index 000000000..3c27e56f9 --- /dev/null +++ b/src/scene/model/gdi/text.rs @@ -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 { + 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() +} diff --git a/src/scene/model/gdi/wmf.rs b/src/scene/model/gdi/wmf.rs new file mode 100644 index 000000000..e838b4be2 --- /dev/null +++ b/src/scene/model/gdi/wmf.rs @@ -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 { + 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>, +} + +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> = 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, 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::>(), + ); + 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> = 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)) +} diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index ca66be7e8..566df4225 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -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 { - 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, 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)] diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index e1944976d..bb17c9732 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -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; diff --git a/src/scene/model/ole_pres.rs b/src/scene/model/ole_pres.rs new file mode 100644 index 000000000..b965102fe --- /dev/null +++ b/src/scene/model/ole_pres.rs @@ -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, 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), + } +}