mod media_parts;
mod zip_io;
use std::collections::HashMap;
use std::path::Path;
use crate::error::{PackageError, PartNotFoundExt, PptxError, PptxResult};
use crate::opc::content_type::ContentTypeMap;
use crate::opc::pack_uri::PackURI;
use crate::opc::part::Part;
use crate::opc::relationship::Relationships;
const DEFAULT_TEMPLATE: &[u8] = include_bytes!("../../templates/default.pptx");
const MAX_PPTX_SIZE: u64 = 2 * 1024 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct OpcPackage {
pub(super) parts: HashMap<String, Part>,
pub(crate) pkg_rels: Relationships,
}
impl OpcPackage {
pub fn open(path: impl AsRef<Path>) -> PptxResult<Self> {
let metadata = std::fs::metadata(path.as_ref()).map_err(|e| {
PptxError::Package(PackageError::PackageNotFound(format!(
"{}: {}",
path.as_ref().display(),
e
)))
})?;
if metadata.len() > MAX_PPTX_SIZE {
return Err(PptxError::ResourceLimit {
message: format!(
"PPTX file size {} bytes exceeds the limit of {} bytes",
metadata.len(),
MAX_PPTX_SIZE
),
});
}
let data = std::fs::read(path.as_ref()).map_err(|e| {
PptxError::Package(PackageError::PackageNotFound(format!(
"{}: {}",
path.as_ref().display(),
e
)))
})?;
Self::from_bytes(&data)
}
pub fn from_bytes(data: &[u8]) -> PptxResult<Self> {
zip_io::read_from_bytes(data)
}
pub fn new() -> PptxResult<Self> {
Self::from_bytes(DEFAULT_TEMPLATE)
}
pub fn save(&self, path: impl AsRef<Path>) -> PptxResult<()> {
let bytes = self.to_bytes()?;
std::fs::write(path, bytes)?;
Ok(())
}
pub fn to_bytes(&self) -> PptxResult<Vec<u8>> {
zip_io::write_to_bytes(self)
}
#[must_use]
pub fn part(&self, partname: &PackURI) -> Option<&Part> {
self.parts.get(partname.as_str())
}
pub fn part_mut(&mut self, partname: &PackURI) -> Option<&mut Part> {
self.parts.get_mut(partname.as_str())
}
pub fn put_part(&mut self, part: Part) {
self.parts.insert(part.partname.to_string(), part);
}
pub fn remove_part(&mut self, partname: &PackURI) -> Option<Part> {
self.parts.remove(partname.as_str())
}
pub fn parts(&self) -> impl Iterator<Item = &Part> {
self.parts.values()
}
pub fn parts_mut(&mut self) -> impl Iterator<Item = &mut Part> {
self.parts.values_mut()
}
pub fn part_by_reltype(&self, reltype: &str) -> PptxResult<&Part> {
let rel = self.pkg_rels.by_reltype(reltype)?;
let partname = rel.target_partname(self.pkg_rels.base_uri())?;
self.part(&partname).or_part_not_found(partname.as_str())
}
pub fn part_by_reltype_mut(&mut self, reltype: &str) -> PptxResult<&mut Part> {
let rel = self.pkg_rels.by_reltype(reltype)?;
let partname = rel.target_partname(self.pkg_rels.base_uri())?;
let key = partname.to_string();
self.parts
.get_mut(&key)
.or_part_not_found(partname.as_str())
}
pub fn next_partname(&self, template: &str) -> PptxResult<PackURI> {
for n in 1..10000 {
let candidate = template.replace("{}", &n.to_string());
let uri = PackURI::new(&candidate)?;
if self.part(&uri).is_none() {
return Ok(uri);
}
}
Err(PptxError::Package(PackageError::InvalidPackUri(
"could not find available partname".to_string(),
)))
}
pub fn next_image_partname(&self, ext: &str) -> PptxResult<PackURI> {
let template = format!("/ppt/media/image{{}}.{ext}");
self.next_partname(&template)
}
pub fn next_media_partname(&self, ext: &str) -> PptxResult<PackURI> {
let template = format!("/ppt/media/media{{}}.{ext}");
self.next_partname(&template)
}
pub(super) fn build_content_type_map(&self) -> ContentTypeMap {
ContentTypeMap::from_parts(
self.parts
.values()
.map(|p| (&p.partname, p.content_type.as_str())),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_from_template() {
let pkg = OpcPackage::new().unwrap();
assert!(!pkg.pkg_rels.is_empty());
let pres = pkg.part_by_reltype(
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
);
assert!(pres.is_ok());
}
#[test]
fn test_round_trip() {
let pkg = OpcPackage::new().unwrap();
let part_count = pkg.parts.len();
let bytes = pkg.to_bytes().unwrap();
let pkg2 = OpcPackage::from_bytes(&bytes).unwrap();
assert_eq!(pkg2.parts.len(), part_count);
assert!(!pkg2.pkg_rels.is_empty());
}
#[test]
fn test_part() {
let pkg = OpcPackage::new().unwrap();
let uri = PackURI::new("/ppt/presentation.xml").unwrap();
assert!(pkg.part(&uri).is_some());
}
#[test]
fn test_next_partname() {
let pkg = OpcPackage::new().unwrap();
let next = pkg.next_partname("/ppt/slides/slide{}.xml").unwrap();
assert_eq!(next.as_str(), "/ppt/slides/slide1.xml");
}
#[test]
fn test_save_and_reopen() {
let pkg = OpcPackage::new().unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
pkg.save(&path).unwrap();
let pkg2 = OpcPackage::open(&path).unwrap();
assert!(!pkg2.pkg_rels.is_empty());
}
}