mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-11 16:30:16 -06:00
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.
76 lines
2.2 KiB
Rust
76 lines
2.2 KiB
Rust
//! XML text helpers and DrawingML units.
|
|
|
|
/// English Metric Units per point.
|
|
pub const EMU_PER_PT: f64 = 12_700.0;
|
|
|
|
/// Points → EMU, saturating (input-derived geometry may be huge or not finite).
|
|
pub fn emu(pt: f64) -> i64 {
|
|
if !pt.is_finite() {
|
|
return 0;
|
|
}
|
|
(pt * EMU_PER_PT).round().clamp(-27_273_042_316_900.0, 27_273_042_316_900.0) as i64
|
|
}
|
|
|
|
/// A non-negative extent in EMU (DrawingML rejects negative sizes).
|
|
pub fn emu_len(pt: f64) -> i64 {
|
|
emu(pt).max(0)
|
|
}
|
|
|
|
/// Degrees → DrawingML angle units (60 000ths of a degree), normalised to 0..360°.
|
|
pub fn angle(deg: f64) -> i64 {
|
|
if !deg.is_finite() {
|
|
return 0;
|
|
}
|
|
(deg.rem_euclid(360.0) * 60_000.0).round() as i64 % 21_600_000
|
|
}
|
|
|
|
/// A 0..1 fraction → DrawingML percentage units (1000ths of a percent).
|
|
pub fn pct(v: f64) -> i64 {
|
|
if !v.is_finite() {
|
|
return 0;
|
|
}
|
|
(v * 100_000.0).round().clamp(-100_000_000.0, 100_000_000.0) as i64
|
|
}
|
|
|
|
/// Escape text for element content and attribute values. Characters XML 1.0 can't hold are
|
|
/// dropped.
|
|
pub fn esc(s: &str) -> String {
|
|
let mut o = String::with_capacity(s.len());
|
|
for c in s.chars() {
|
|
match c {
|
|
'&' => o.push_str("&"),
|
|
'<' => o.push_str("<"),
|
|
'>' => o.push_str(">"),
|
|
'"' => o.push_str("""),
|
|
'\'' => o.push_str("'"),
|
|
'\t' | '\n' | '\r' => o.push(c),
|
|
c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}
|
|
c => o.push(c),
|
|
}
|
|
}
|
|
o
|
|
}
|
|
|
|
/// sRGB 0..1 → `RRGGBB`.
|
|
pub fn hex(rgb: [f32; 3]) -> String {
|
|
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
|
|
format!("{:02X}{:02X}{:02X}", q(rgb[0]), q(rgb[1]), q(rgb[2]))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn units() {
|
|
assert_eq!(emu(1.0), 12_700);
|
|
assert_eq!(emu(f64::NAN), 0);
|
|
assert_eq!(emu_len(-3.0), 0);
|
|
assert_eq!(angle(-90.0), 16_200_000);
|
|
assert_eq!(angle(360.0), 0);
|
|
assert_eq!(pct(0.5), 50_000);
|
|
assert_eq!(esc("a<b & \"c\"\u{1}"), "a<b & "c"");
|
|
assert_eq!(hex([1.0, 0.0, 0.5]), "FF0080");
|
|
}
|
|
}
|