lo_odf 0.1.0

ODF package serializers for text, spreadsheet, presentation, drawing, formula, and database documents
Documentation
use std::path::Path;

use lo_core::{escape_text, Metadata, Result, XmlBuilder};
use lo_zip::{write_zip_file, ZipEntry};

pub const MIME_ODT: &str = "application/vnd.oasis.opendocument.text";
pub const MIME_ODS: &str = "application/vnd.oasis.opendocument.spreadsheet";
pub const MIME_ODP: &str = "application/vnd.oasis.opendocument.presentation";
pub const MIME_ODG: &str = "application/vnd.oasis.opendocument.graphics";
pub const MIME_ODF: &str = "application/vnd.oasis.opendocument.formula";
pub const MIME_ODB: &str = "application/vnd.oasis.opendocument.database";

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExtraFile {
    pub path: String,
    pub media_type: String,
    pub data: Vec<u8>,
}

impl ExtraFile {
    pub fn new(path: impl Into<String>, media_type: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
        Self {
            path: path.into(),
            media_type: media_type.into(),
            data: data.into(),
        }
    }
}

pub fn content_root_attrs() -> Vec<(&'static str, String)> {
    vec![
        ("xmlns:office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0".to_string()),
        ("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0".to_string()),
        ("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0".to_string()),
        ("xmlns:table", "urn:oasis:names:tc:opendocument:xmlns:table:1.0".to_string()),
        ("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0".to_string()),
        ("xmlns:presentation", "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0".to_string()),
        ("xmlns:xlink", "http://www.w3.org/1999/xlink".to_string()),
        ("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0".to_string()),
        ("xmlns:svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0".to_string()),
        ("xmlns:math", "http://www.w3.org/1998/Math/MathML".to_string()),
        ("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0".to_string()),
        ("xmlns:dc", "http://purl.org/dc/elements/1.1/".to_string()),
        ("xmlns:of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2".to_string()),
        ("office:version", "1.3".to_string()),
    ]
}

fn meta_root_attrs() -> Vec<(&'static str, String)> {
    vec![
        ("xmlns:office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0".to_string()),
        ("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0".to_string()),
        ("xmlns:dc", "http://purl.org/dc/elements/1.1/".to_string()),
        ("office:version", "1.3".to_string()),
    ]
}

fn styles_root_attrs() -> Vec<(&'static str, String)> {
    vec![
        ("xmlns:office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0".to_string()),
        ("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0".to_string()),
        ("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0".to_string()),
        ("xmlns:table", "urn:oasis:names:tc:opendocument:xmlns:table:1.0".to_string()),
        ("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0".to_string()),
        ("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0".to_string()),
        ("office:version", "1.3".to_string()),
    ]
}

fn settings_root_attrs() -> Vec<(&'static str, String)> {
    vec![
        ("xmlns:office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0".to_string()),
        ("office:version", "1.3".to_string()),
    ]
}

pub fn meta_xml(meta: &Metadata) -> String {
    let mut xml = XmlBuilder::new();
    xml.declaration();
    xml.open("office:document-meta", &meta_root_attrs());
    xml.open("office:meta", &[]);
    if !meta.title.is_empty() {
        xml.element("dc:title", &meta.title, &[]);
    }
    if !meta.subject.is_empty() {
        xml.element("dc:subject", &meta.subject, &[]);
    }
    if !meta.description.is_empty() {
        xml.element("dc:description", &meta.description, &[]);
    }
    if !meta.creator.is_empty() {
        xml.element("meta:initial-creator", &meta.creator, &[]);
        xml.element("dc:creator", &meta.creator, &[]);
    }
    if !meta.created.is_empty() {
        xml.element("meta:creation-date", &meta.created, &[]);
    }
    if !meta.modified.is_empty() {
        xml.element("dc:date", &meta.modified, &[]);
    }
    if !meta.keywords.is_empty() {
        xml.element("meta:keyword", &meta.keywords.join(", "), &[]);
    }
    xml.close();
    xml.close();
    xml.finish()
}

pub fn styles_xml() -> String {
    let mut xml = XmlBuilder::new();
    xml.declaration();
    xml.open("office:document-styles", &styles_root_attrs());
    xml.open("office:styles", &[]);
    xml.raw(
        r#"<style:style style:name="Standard" style:family="paragraph"><style:text-properties fo:font-size="11pt"/></style:style>"#,
    );
    xml.raw(
        r#"<style:style style:name="Strong" style:family="text"><style:text-properties fo:font-weight="bold" style:font-weight-asian="bold"/></style:style>"#,
    );
    xml.raw(
        r#"<style:style style:name="Emphasis" style:family="text"><style:text-properties fo:font-style="italic" style:font-style-asian="italic"/></style:style>"#,
    );
    xml.raw(
        r#"<style:style style:name="Code" style:family="text"><style:text-properties style:font-name="monospace"/></style:style>"#,
    );
    xml.close();
    xml.close();
    xml.finish()
}

pub fn settings_xml() -> String {
    let mut xml = XmlBuilder::new();
    xml.declaration();
    xml.open("office:document-settings", &settings_root_attrs());
    xml.empty("office:settings", &[]);
    xml.close();
    xml.finish()
}

pub fn manifest_xml(mimetype: &str, extras: &[ExtraFile]) -> String {
    let mut xml = XmlBuilder::new();
    xml.declaration();
    xml.open(
        "manifest:manifest",
        &[
            ("xmlns:manifest", "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0".to_string()),
            ("manifest:version", "1.3".to_string()),
        ],
    );
    xml.empty(
        "manifest:file-entry",
        &[
            ("manifest:full-path", "/".to_string()),
            ("manifest:media-type", mimetype.to_string()),
        ],
    );
    for component in ["content.xml", "styles.xml", "meta.xml", "settings.xml"] {
        xml.empty(
            "manifest:file-entry",
            &[
                ("manifest:full-path", component.to_string()),
                ("manifest:media-type", "text/xml".to_string()),
            ],
        );
    }
    for extra in extras {
        xml.empty(
            "manifest:file-entry",
            &[
                ("manifest:full-path", extra.path.clone()),
                ("manifest:media-type", extra.media_type.clone()),
            ],
        );
    }
    xml.close();
    xml.finish()
}

pub fn package_document(
    path: impl AsRef<Path>,
    mimetype: &str,
    content_xml: String,
    meta: &Metadata,
    extras: Vec<ExtraFile>,
) -> Result<()> {
    let mut entries = Vec::new();
    entries.push(ZipEntry::new("mimetype", mimetype.as_bytes().to_vec()));
    entries.push(ZipEntry::new("content.xml", content_xml.into_bytes()));
    entries.push(ZipEntry::new("styles.xml", styles_xml().into_bytes()));
    entries.push(ZipEntry::new("meta.xml", meta_xml(meta).into_bytes()));
    entries.push(ZipEntry::new("settings.xml", settings_xml().into_bytes()));
    entries.push(ZipEntry::new(
        "META-INF/manifest.xml",
        manifest_xml(mimetype, &extras).into_bytes(),
    ));
    for extra in extras {
        entries.push(ZipEntry::new(extra.path, extra.data));
    }
    write_zip_file(path, &entries)
}

pub fn image_extras(images: Vec<(String, String, Vec<u8>)>) -> Vec<ExtraFile> {
    images
        .into_iter()
        .map(|(name, media_type, data)| ExtraFile::new(format!("Pictures/{name}"), media_type, data))
        .collect()
}

pub fn escaped_text_paragraph(text: &str) -> String {
    format!("<text:p>{}</text:p>", escape_text(text))
}