feat(pptx): export documents as editable PowerPoint presentations

New designcraft-pptx crate (L3) and file.exportPptx command (File ›
Export PowerPoint…, CLI --export x.pptx). Each page becomes a slide of
native objects: shapes with fills, gradients, strokes and effects,
pictures cropped like their frames, native tables, and text boxes.

Text keeps DesignCraft's composition by default: composed lines end in
line breaks, justified spacing becomes character spacing, and leading
becomes percentage line spacing (PowerPoint rounds exact spacing to
whole points; its 100% line is 1.2 em for every font). Threaded
stories split at their frames and wrapped lines get their own boxes.
lineBreaks: reflow writes one box per frame in native columns instead.

TrueType fonts are embedded as Embedded OpenType when their licences
allow. Objects PowerPoint can't represent (placed PDF/EPS, type on a
path, some effects) are rasterised through the existing
rasterize_where path.

Checked in PowerPoint for Mac and LibreOffice: on the sample magazine,
word positions match DesignCraft's PDF to within 0.8 pt horizontally
and 1.6 pt vertically.
This commit is contained in:
Philipp Krüger
2026-10-08 23:32:53 +02:00
parent 14e677b242
commit a11bd021cd
18 changed files with 2782 additions and 11 deletions
+11 -1
View File
@@ -1,7 +1,7 @@
//! Headless DesignCraft.
//!
//! ```text
//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub] [--pdf-options JSON] [--all-pages DIR]
//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub|.pptx] [--pdf-options JSON] [--all-pages DIR]
//! designcraft-cli commands [FILTER] # list commands (JSON), optionally only ids/labels/menus containing FILTER
//! designcraft-cli describe ID # one command: label, menu, shortcut, parameters
//! designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]
@@ -209,6 +209,14 @@ fn run(args: &[String]) -> Result<(), String> {
}
fn export(s: &mut Session, out: &str, page: usize, scale: f64) -> Result<(), String> {
if out.to_ascii_lowercase().ends_with(".pptx") {
let r = s.execute("file.exportPptx", &json!({"path": out})).map_err(|e| e.to_string())?;
eprintln!("wrote {out} ({} slides, {} bytes)", r["slides"], r["bytes"]);
for w in r["warnings"].as_array().into_iter().flatten() {
eprintln!("warning: {}", w.as_str().unwrap_or_default());
}
return Ok(());
}
if out.to_ascii_lowercase().ends_with(".idml") {
let r = s.execute("file.exportIdml", &json!({"path": out})).map_err(|e| e.to_string())?;
eprintln!("wrote {out} ({} bytes)", r["bytes"]);
@@ -332,6 +340,8 @@ fn script(args: &[String]) -> Result<(), String> {
exec(b, "file.exportIdml", p)?;
} else if lower.ends_with(".epub") {
exec(b, "file.exportEpub", p)?;
} else if lower.ends_with(".pptx") {
exec(b, "file.exportPptx", p)?;
} else {
// PNG / JPEG of the first page.
b.call("app.export", p)?;