mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-30 05:27:06 -06:00
* ActiveX controls show their pictures A slide's p:controls are read: each control with a p:pic (the image PowerPoint shows for it, often a WMF) becomes a picture above the slide's shapes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Metafiles draw their text, pattern and hatch brushes deckcraft-metafile (still kurbo + thiserror only, so it builds for wasm): - Text: WMF EXTTEXTOUT/TEXTOUT and EMF EXTTEXTOUTW/EXTTEXTOUTA/SMALLTEXTOUT with the selected font (CREATEFONTINDIRECT, EXTCREATEFONTINDIRECTW: face, signed LOGFONT height, width, weight, italic, underline, strike-out, escapement, charset), text and background colour, background mode, TA_* alignment and TA_UPDATECP, the dx array (ETO_PDY pairs too), ETO_OPAQUE fills and ETO_CLIPPED clips. 8-bit strings decode through their charset (Windows-1252/1250/1251; symbol fonts keep their codes at U+F000; double-byte sets keep their advances). The neutral Item::Text carries a text-space-to-picture transform, so the mapping and escapement are folded in and glyphs are never mirrored. - Brushes: WMF DIBCREATEPATTERNBRUSH, EMF CREATEDIBPATTERNBRUSHPT and CREATEMONOBRUSH, and BS_HATCHED, as Item::Pattern tiles in 96-dpi pixels; WMF PATBLT. The XOR / AND-pattern / XOR see-through shadow of old PowerPoint WMFs draws its pattern in the XOR colour. - MFCOMMENT escapes with an embedded EMF are ignored: PowerPoint draws the WMF's records. - Pattern pixels and text count against the byte budget; strings are capped. deckcraft-render draws text through deckcraft-fonts with the face slide text would use (document_face), symbol fonts mapped as bullets map them (deckcraft_text::symbol_char, now public, with Webdings), per-character fallback, the file's advances, GDI cell metrics for top/bottom alignment, underline/strike-out and synthetic bold/italic. Tiles below 1.5 output pixels per cell are drawn as their average colour. On the 24 corpus slides that show a metafile, mean abs diff against PowerPoint goes from 28.31 to 5.06 (with the OLE/ActiveX preview commits). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
85 lines
5.7 KiB
Rust
85 lines
5.7 KiB
Rust
//! Legacy OLE objects whose preview picture lives in the slide's VML drawing, and ActiveX controls
|
|
//! shown by their pictures.
|
|
|
|
use std::io::{Cursor, Read, Write};
|
|
|
|
use deckcraft_model::ShapeKind;
|
|
use deckcraft_model::defaults;
|
|
|
|
const FRAME: &str = r#"<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="90" name="Object 3"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/presentationml/2006/ole"><p:oleObj spid="_x0000_s2050" name="Slide" r:id="rIdOle" progId="Thing.1"><p:embed/></p:oleObj></a:graphicData></a:graphic></p:graphicFrame>"#;
|
|
|
|
const VML: &str = r#"<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><v:shape id="_x0000_s2049"><v:imagedata o:relid="rIdA"/></v:shape><v:shape id="_x0000_s2050" style='position:absolute;left:0;top:0;width:1in;height:1in'><v:imagedata o:relid="rIdB" o:title=""/></v:shape></xml>"#;
|
|
|
|
const VML_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdA" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/other.png"/><Relationship Id="rIdB" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/preview.png"/></Relationships>"#;
|
|
|
|
const SLIDE_RELS: &str = r#"<Relationship Id="rIdVml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing1.vml"/><Relationship Id="rIdPic" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/preview.png"/>"#;
|
|
|
|
/// An ActiveX control as PowerPoint writes it: a VML-only choice, and a fallback with its picture.
|
|
const CONTROLS: &str = r#"<p:controls><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:v="urn:schemas-microsoft-com:vml" Requires="v"><p:control spid="1137" name="CheckBox1" r:id="rIdX" imgW="1933560" imgH="514440"/></mc:Choice><mc:Fallback><p:control name="CheckBox1" r:id="rIdX" imgW="1933560" imgH="514440"><p:pic><p:nvPicPr><p:cNvPr id="4" name="CheckBox1"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rIdPic"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="12700" y="25400"/><a:ext cx="127000" cy="63500"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic></p:control></mc:Fallback></mc:AlternateContent><p:control name="NoPicture" r:id="rIdY"/></p:controls>"#;
|
|
|
|
fn png(v: u8) -> Vec<u8> {
|
|
let mut out = Cursor::new(vec![]);
|
|
image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(2, 2, image::Rgb([v, v, v])))
|
|
.write_to(&mut out, image::ImageFormat::Png)
|
|
.expect("png");
|
|
out.into_inner()
|
|
}
|
|
|
|
/// A new deck whose first slide holds [`FRAME`], with the VML drawing, its rels and two pictures.
|
|
fn deck() -> Vec<u8> {
|
|
let bytes = deckcraft_pptx::export(&defaults::new_presentation(None)).expect("export");
|
|
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
|
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
|
let o = zip::write::SimpleFileOptions::default();
|
|
for i in 0..z.len() {
|
|
let mut f = z.by_index(i).expect("entry");
|
|
let mut d = vec![];
|
|
f.read_to_end(&mut d).expect("read");
|
|
let name = f.name().to_string();
|
|
if name == "ppt/slides/slide1.xml" {
|
|
let s = String::from_utf8(d).expect("utf8");
|
|
let at = s.rfind("</p:spTree>").expect("spTree");
|
|
let s = format!("{}{FRAME}{}", &s[..at], &s[at..]);
|
|
let at = s.rfind("</p:cSld>").expect("cSld");
|
|
d = format!("{}{CONTROLS}{}", &s[..at], &s[at..]).into_bytes();
|
|
} else if name == "ppt/slides/_rels/slide1.xml.rels" {
|
|
let s = String::from_utf8(d).expect("utf8");
|
|
d = s.replace("</Relationships>", &format!("{SLIDE_RELS}</Relationships>")).into_bytes();
|
|
}
|
|
out.start_file(name, o).expect("start");
|
|
out.write_all(&d).expect("write");
|
|
}
|
|
for (name, data) in [
|
|
("ppt/drawings/vmlDrawing1.vml", VML.as_bytes().to_vec()),
|
|
("ppt/drawings/_rels/vmlDrawing1.vml.rels", VML_RELS.as_bytes().to_vec()),
|
|
("ppt/media/other.png", png(10)),
|
|
("ppt/media/preview.png", png(200)),
|
|
] {
|
|
out.start_file(name, o).expect("start");
|
|
out.write_all(&data).expect("write");
|
|
}
|
|
out.finish().expect("finish").into_inner()
|
|
}
|
|
|
|
#[test]
|
|
fn an_ole_object_without_a_picture_shows_its_vml_preview() {
|
|
let p = deckcraft_pptx::import(&deck()).expect("import");
|
|
let s = p.slides[0].shapes.iter().find(|s| s.name == "Object 3").expect("frame");
|
|
let ShapeKind::Opaque { preview: Some(m), label, .. } = &s.kind else { panic!("{:?}", s.kind) };
|
|
assert_eq!(label, "Thing.1");
|
|
let media = p.media.iter().find(|x| x.id == *m).expect("media");
|
|
// The shape whose id is the object's spid, not the first one in the drawing.
|
|
assert_eq!(media.name, "preview.png");
|
|
}
|
|
|
|
#[test]
|
|
fn activex_controls_show_their_pictures() {
|
|
let p = deckcraft_pptx::import(&deck()).expect("import");
|
|
let s = p.slides[0].shapes.iter().find(|s| s.name == "CheckBox1").expect("control picture");
|
|
let ShapeKind::Picture { fill } = &s.kind else { panic!("{:?}", s.kind) };
|
|
assert_eq!(p.media.iter().find(|m| m.id == fill.media).map(|m| m.name.as_str()), Some("preview.png"));
|
|
let x = s.xfrm.expect("xfrm");
|
|
assert_eq!((x.x, x.y, x.w, x.h), (1.0, 2.0, 10.0, 5.0));
|
|
assert!(!p.slides[0].shapes.iter().any(|s| s.name == "NoPicture"), "a control without a picture draws nothing");
|
|
}
|