mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-01 14:37:05 -06:00
`sniff` searched only the leading 4096 bytes for the content-types part, but OOXML writers may place that part anywhere in the archive. Recent PowerPoint builds (16.x) store it as the last zip entry, so every deck saved by them was rejected with "not a presentation DeckCraft can read" although it imports fine. In one real folder 169 of 300 decks were affected. Look the part up in the zip's central directory instead. The regression test re-packs the sample deck with the part last behind 64 KB of padding and checks that it is both recognised and imported. Co-authored-by: Matthias Weber <webermatt@me.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
86 lines
2.9 KiB
Rust
86 lines
2.9 KiB
Rust
//! PPTX import and export: Office Open XML PresentationML / DrawingML, written from the public
|
|
//! ECMA-376 standard.
|
|
//!
|
|
//! [`import`] reads a package leniently (unknown parts are skipped, malformed parts are logged and
|
|
//! ignored, every size and depth is bounded) into the DeckCraft model; [`export`] writes a valid
|
|
//! package that PowerPoint opens without repair.
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
mod custgeom;
|
|
mod maps;
|
|
mod opc;
|
|
mod png;
|
|
pub(crate) mod read;
|
|
mod tables;
|
|
mod write;
|
|
mod xml;
|
|
|
|
use deckcraft_model::Presentation;
|
|
|
|
/// Extension URI of the PowerPoint 2010 section list in `presentation.xml`.
|
|
pub(crate) const SECTION_EXT_URI: &str = "{521415D9-36F7-43E2-AB2F-B90AF26B5E84}";
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum PptxError {
|
|
#[error("not a PPTX file: {0}")]
|
|
NotPptx(String),
|
|
#[error("unsupported: {0}")]
|
|
Unsupported(String),
|
|
#[error("write failed: {0}")]
|
|
Write(String),
|
|
#[error("malformed XML: {0}")]
|
|
Xml(String),
|
|
}
|
|
|
|
/// Does this look like an OOXML package (zip with `[Content_Types].xml`)?
|
|
///
|
|
/// The part is looked up in the zip's central directory, not in the leading bytes: writers may
|
|
/// store it anywhere in the archive (recent PowerPoint builds put it last, megabytes in).
|
|
pub fn sniff(bytes: &[u8]) -> bool {
|
|
if !bytes.starts_with(b"PK") {
|
|
return false;
|
|
}
|
|
zip::ZipArchive::new(std::io::Cursor::new(bytes)).map(|z| z.index_for_name("[Content_Types].xml").is_some()).unwrap_or(false)
|
|
}
|
|
|
|
/// Read a `.pptx` / `.potx` / `.ppsx` package.
|
|
pub fn import(bytes: &[u8]) -> Result<Presentation, PptxError> {
|
|
read::import(bytes)
|
|
}
|
|
|
|
/// Write a `.pptx` package.
|
|
pub fn export(p: &Presentation) -> Result<Vec<u8>, PptxError> {
|
|
write::export(p)
|
|
}
|
|
|
|
/// Content type for a file name's extension.
|
|
pub(crate) fn mime_for(name: &str) -> &'static str {
|
|
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase()).unwrap_or_default();
|
|
match ext.as_str() {
|
|
"png" => "image/png",
|
|
"jpg" | "jpeg" | "jpe" => "image/jpeg",
|
|
"gif" => "image/gif",
|
|
"bmp" => "image/bmp",
|
|
"tif" | "tiff" => "image/tiff",
|
|
"svg" => "image/svg+xml",
|
|
"emf" => "image/x-emf",
|
|
"wmf" => "image/x-wmf",
|
|
"webp" => "image/webp",
|
|
"mp4" | "m4v" => "video/mp4",
|
|
"mov" => "video/quicktime",
|
|
"wmv" => "video/x-ms-wmv",
|
|
"avi" => "video/x-msvideo",
|
|
"webm" => "video/webm",
|
|
"mp3" => "audio/mpeg",
|
|
"wav" => "audio/wav",
|
|
"m4a" => "audio/mp4",
|
|
"wma" => "audio/x-ms-wma",
|
|
"ogg" => "audio/ogg",
|
|
"flac" => "audio/flac",
|
|
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"fntdata" => "application/x-fontdata",
|
|
_ => "application/octet-stream",
|
|
}
|
|
}
|