Task 496: .OTP File Format

Task 496: .OTP File Format

1. List of all the properties of this file format intrinsic to its file system

The .OTP file format is an OpenDocument Presentation Template, which is a ZIP archive containing a specific set of files and directories following the OASIS OpenDocument standard. The "properties intrinsic to its file system" refer to the structured components within the ZIP package, including required and optional files, directories, MIME types, versions, and other format-specific attributes as defined in the OpenDocument specification (v1.3). Based on the package structure, here is a comprehensive list:

  • File Extension: .otp
  • MIME Type: application/vnd.oasis.opendocument.presentation-template (stored in the optional 'mimetype' file and/or the manifest:media-type attribute for the root entry in META-INF/manifest.xml)
  • File Signature: PK\x03\x04 (standard ZIP header, as .OTP is a ZIP archive)
  • ZIP Structure Requirements: The ZIP must use STORED (non-compressed) or DEFLATED compression. The 'mimetype' file, if present, must be the first file, uncompressed, with no extra field in its local header.
  • Required Files:
  • META-INF/manifest.xml: An XML file listing all files in the package (except 'mimetype' and other META-INF files), with manifest:manifest as root, including entries for files and directories. It must have a manifest:version attribute (e.g., "1.3").
  • Optional Files and Directories:
  • mimetype: Plain text file containing the MIME type string.
  • META-INF/signatures.xml: Digital signature files (e.g., META-INF/documentsignatures.xml), with dsig:document-signatures root and dsig:version (e.g., "1.2").
  • manifest.rdf: RDF/XML metadata manifest in the package root or sub-directories, using pkg: namespace for listing metadata files.
  • Thumbnails/thumbnail.png: PNG preview image of the first slide.
  • content.xml: XML file containing the presentation content, with office:document-content root and office:bodyoffice:presentation for slides/templates.
  • styles.xml: XML file for styles, with office:document-styles root.
  • meta.xml: XML file for metadata, with office:document-meta root, including elements like dc:title, dc:creator, meta:creation-date.
  • settings.xml: XML file for document settings, with office:document-settings root.
  • Pictures/: Directory for embedded images (e.g., Pictures/image1.png).
  • Configurations2/: Directory for configuration data (e.g., toolbars, accelerators).
  • Other sub-directories: For sub-documents or additional resources, listed in manifest.xml with manifest:full-path.
  • RDF metadata files: Arbitrary RDF/XML files listed in manifest.rdf.
  • Versioning: manifest:version on manifest:manifest (e.g., "1.3"); optional manifest:version on manifest:file-entry for specific files.
  • Encryption Properties: If encrypted, manifest:encryption-data elements in manifest.xml, including algorithm (e.g., Blowfish CFB), key derivation (e.g., PBKDF2), salt, IV, checksum type (e.g., SHA256/1K).
  • Digital Signature Properties: If signed, ds:Signature elements in signature files, with Id, ds:KeyInfo (X.509), signing time, and references to signed files.
  • Presentation-Specific Properties (from content.xml and styles.xml): XML namespace for presentation (urn:oasis:names:tc:opendocument:xmlns:presentation:1.0); key elements like office:presentation, presentation:placeholder for templates, style:presentation-page-layout; attributes like presentation:class, presentation:effect, presentation:speed.
  • Metadata Properties (from meta.xml): dc:title (document title), dc:creator (author), meta:creation-date (creation timestamp), office:version (document version, e.g., "1.3").
  • Other Intrinsic Properties: Relative IRIs for references; support for multiple sub-documents in directories; conformance to XML namespaces and ID requirements.

These properties define the internal "file system" of the .OTP format within the ZIP container.

3. Ghost blog embedded HTML JavaScript for drag n drop .OTP file dump

.OTP File Properties Dumper
Drag and drop .OTP file here

4. Python class for .OTP file handling

import zipfile
import xml.etree.ElementTree as ET
from io import BytesIO

class OTPFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.zip = zipfile.ZipFile(filepath, 'r')
        self.properties = self._decode_properties()

    def _decode_properties(self):
        properties = {}

        # MIME type
        if 'mimetype' in self.zip.namelist():
            with self.zip.open('mimetype') as f:
                properties['MIME Type'] = f.read().decode('utf-8').strip()

        # Manifest
        if 'META-INF/manifest.xml' in self.zip.namelist():
            with self.zip.open('META-INF/manifest.xml') as f:
                manifest_xml = f.read()
                root = ET.fromstring(manifest_xml)
                ns = {'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'}
                properties['Manifest Version'] = root.get(f'{{{ns["manifest"]}}}version')
                root_entry = root.find(".//manifest:file-entry[@manifest:full-path='/']", ns)
                if root_entry is not None:
                    properties['Document MIME Type'] = root_entry.get(f'{{{ns["manifest"]}}}media-type')
                    properties['Document Version'] = root_entry.get(f'{{{ns["manifest"]}}}version', 'N/A')
                files = []
                for entry in root.findall('manifest:file-entry', ns):
                    files.append({
                        'path': entry.get(f'{{{ns["manifest"]}}}full-path'),
                        'media_type': entry.get(f'{{{ns["manifest"]}}}media-type')
                    })
                properties['Files in Manifest'] = files
                # Encryption
                properties['Encrypted'] = any(entry.find('manifest:encryption-data', ns) is not None for entry in root.findall('manifest:file-entry', ns))

        # Thumbnail
        properties['Thumbnail Present'] = 'Thumbnails/thumbnail.png' in self.zip.namelist()

        # Meta
        if 'meta.xml' in self.zip.namelist():
            with self.zip.open('meta.xml') as f:
                meta_xml = f.read()
                root = ET.fromstring(meta_xml)
                ns = {'dc': 'http://purl.org/dc/elements/1.1/', 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'}
                title = root.find('.//dc:title', ns)
                properties['Title'] = title.text if title is not None else 'N/A'
                creator = root.find('.//dc:creator', ns)
                properties['Creator'] = creator.text if creator is not None else 'N/A'
                creation_date = root.find('.//meta:creation-date', ns)
                properties['Creation Date'] = creation_date.text if creation_date is not None else 'N/A'

        # Content - Number of pages
        if 'content.xml' in self.zip.namelist():
            with self.zip.open('content.xml') as f:
                content_xml = f.read()
                root = ET.fromstring(content_xml)
                ns = {'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'}
                pages = root.findall('.//draw:page', ns)
                properties['Number of Presentation Pages'] = len(pages)

        # Signatures
        signatures = [name for name in self.zip.namelist() if name.startswith('META-INF/') and 'signatures' in name]
        properties['Signed'] = 'Yes' if signatures else 'No'

        return properties

    def print_properties(self):
        for key, value in self.properties.items():
            if isinstance(value, list):
                print(f'{key}:')
                for item in value:
                    print(f'  - {item["path"]} ({item["media_type"]})')
            else:
                print(f'{key}: {value}')

    def write(self, new_filepath):
        # For write, copy the original ZIP to new file (simple save, no modify here)
        with zipfile.ZipFile(new_filepath, 'w') as new_zip:
            for item in self.zip.infolist():
                data = self.zip.read(item.filename)
                new_zip.writestr(item, data)

    def close(self):
        self.zip.close()

# Example usage:
# otp = OTPFile('example.otp')
# otp.print_properties()
# otp.write('modified.otp')
# otp.close()

5. Java class for .OTP file handling

import java.io.*;
import java.util.zip.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.InputSource;

public class OTPFile {
    private String filepath;
    private ZipFile zip;
    private Document manifestDoc;
    private Document metaDoc;
    private Document contentDoc;
    private java.util.Map<String, Object> properties = new java.util.HashMap<>();

    public OTPFile(String filepath) throws Exception {
        this.filepath = filepath;
        this.zip = new ZipFile(filepath);
        decodeProperties();
    }

    private void decodeProperties() throws Exception {
        // MIME type
        ZipEntry mimeEntry = zip.getEntry("mimetype");
        if (mimeEntry != null) {
            properties.put("MIME Type", new BufferedReader(new InputStreamReader(zip.getInputStream(mimeEntry))).readLine());
        }

        // Manifest
        ZipEntry manifestEntry = zip.getEntry("META-INF/manifest.xml");
        if (manifestEntry != null) {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            DocumentBuilder db = dbf.newDocumentBuilder();
            manifestDoc = db.parse(zip.getInputStream(manifestEntry));
            Element manifest = manifestDoc.getDocumentElement();
            properties.put("Manifest Version", manifest.getAttribute("manifest:version"));
            NodeList entries = manifest.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "file-entry");
            for (int i = 0; i < entries.getLength(); i++) {
                Element entry = (Element) entries.item(i);
                if (entry.getAttribute("manifest:full-path").equals("/")) {
                    properties.put("Document MIME Type", entry.getAttribute("manifest:media-type"));
                    properties.put("Document Version", entry.getAttribute("manifest:version"));
                    break;
                }
            }
            java.util.List<java.util.Map<String, String>> files = new java.util.ArrayList<>();
            for (int i = 0; i < entries.getLength(); i++) {
                Element entry = (Element) entries.item(i);
                java.util.Map<String, String> fileInfo = new java.util.HashMap<>();
                fileInfo.put("path", entry.getAttribute("manifest:full-path"));
                fileInfo.put("media_type", entry.getAttribute("manifest:media-type"));
                files.add(fileInfo);
            }
            properties.put("Files in Manifest", files);
            // Encryption
            properties.put("Encrypted", manifest.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "encryption-data").getLength() > 0 ? "Yes" : "No");
        }

        // Thumbnail
        properties.put("Thumbnail Present", zip.getEntry("Thumbnails/thumbnail.png") != null ? "Yes" : "No");

        // Meta
        ZipEntry metaEntry = zip.getEntry("meta.xml");
        if (metaEntry != null) {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            DocumentBuilder db = dbf.newDocumentBuilder();
            metaDoc = db.parse(zip.getInputStream(metaEntry));
            Element title = (Element) metaDoc.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", "title").item(0);
            properties.put("Title", title != null ? title.getTextContent() : "N/A");
            Element creator = (Element) metaDoc.getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", "creator").item(0);
            properties.put("Creator", creator != null ? creator.getTextContent() : "N/A");
            Element creationDate = (Element) metaDoc.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "creation-date").item(0);
            properties.put("Creation Date", creationDate != null ? creationDate.getTextContent() : "N/A");
        }

        // Content - Number of pages
        ZipEntry contentEntry = zip.getEntry("content.xml");
        if (contentEntry != null) {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            DocumentBuilder db = dbf.newDocumentBuilder();
            contentDoc = db.parse(zip.getInputStream(contentEntry));
            NodeList pages = contentDoc.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", "page");
            properties.put("Number of Presentation Pages", pages.getLength());
        }

        // Signatures
        boolean signed = false;
        java.util.Enumeration<? extends ZipEntry> entries = zip.entries();
        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            if (entry.getName().startsWith("META-INF/") && entry.getName().contains("signatures")) {
                signed = true;
                break;
            }
        }
        properties.put("Signed", signed ? "Yes" : "No");
    }

    public void printProperties() {
        for (java.util.Map.Entry<String, Object> prop : properties.entrySet()) {
            if (prop.getValue() instanceof java.util.List) {
                System.out.println(prop.getKey() + ":");
                @SuppressWarnings("unchecked")
                java.util.List<java.util.Map<String, String>> files = (java.util.List<java.util.Map<String, String>>) prop.getValue();
                for (java.util.Map<String, String> file : files) {
                    System.out.println("  - " + file.get("path") + " (" + file.get("media_type") + ")");
                }
            } else {
                System.out.println(prop.getKey() + ": " + prop.getValue());
            }
        }
    }

    public void write(String newFilepath) throws IOException {
        try (ZipOutputStream newZip = new ZipOutputStream(new FileOutputStream(newFilepath))) {
            java.util.Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                newZip.putNextEntry(entry);
                InputStream is = zip.getInputStream(entry);
                byte[] buf = new byte[1024];
                int len;
                while ((len = is.read(buf)) > 0) {
                    newZip.write(buf, 0, len);
                }
                is.close();
                newZip.closeEntry();
            }
        }
    }

    public void close() throws IOException {
        zip.close();
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     OTPFile otp = new OTPFile("example.otp");
    //     otp.printProperties();
    //     otp.write("modified.otp");
    //     otp.close();
    // }
}

6. JavaScript class for .OTP file handling

// Assume JSZip is loaded (e.g., via <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>)
// For node.js, require('jszip')

class OTPFile {
    constructor(buffer) {
        this.buffer = buffer;
        this.properties = {};
    }

    async decode() {
        const zip = await JSZip.loadAsync(this.buffer);
        // MIME type
        const mimeFile = zip.file('mimetype');
        if (mimeFile) {
            this.properties['MIME Type'] = await mimeFile.async('string');
        }

        // Manifest
        const manifestFile = zip.file('META-INF/manifest.xml');
        if (manifestFile) {
            const manifestXml = await manifestFile.async('string');
            const parser = new DOMParser();
            const doc = parser.parseFromString(manifestXml, 'application/xml');
            const manifest = doc.getElementsByTagName('manifest:manifest')[0];
            this.properties['Manifest Version'] = manifest.getAttribute('manifest:version');
            const rootEntry = Array.from(doc.getElementsByTagName('manifest:file-entry')).find(e => e.getAttribute('manifest:full-path') === '/');
            if (rootEntry) {
                this.properties['Document MIME Type'] = rootEntry.getAttribute('manifest:media-type');
                this.properties['Document Version'] = rootEntry.getAttribute('manifest:version') || 'N/A';
            }
            const files = [];
            doc.querySelectorAll('manifest\\:file-entry').forEach(entry => {
                files.push({
                    path: entry.getAttribute('manifest:full-path'),
                    media_type: entry.getAttribute('manifest:media-type')
                });
            });
            this.properties['Files in Manifest'] = files;
            this.properties['Encrypted'] = doc.querySelector('manifest\\:encryption-data') ? 'Yes' : 'No';
        }

        // Thumbnail
        this.properties['Thumbnail Present'] = zip.file('Thumbnails/thumbnail.png') ? 'Yes' : 'No';

        // Meta
        const metaFile = zip.file('meta.xml');
        if (metaFile) {
            const metaXml = await metaFile.async('string');
            const parser = new DOMParser();
            const doc = parser.parseFromString(metaXml, 'application/xml');
            const title = doc.querySelector('dc\\:title');
            this.properties['Title'] = title ? title.textContent : 'N/A';
            const creator = doc.querySelector('dc\\:creator');
            this.properties['Creator'] = creator ? creator.textContent : 'N/A';
            const creationDate = doc.querySelector('meta\\:creation-date');
            this.properties['Creation Date'] = creationDate ? creationDate.textContent : 'N/A';
        }

        // Content - Number of pages
        const contentFile = zip.file('content.xml');
        if (contentFile) {
            const contentXml = await contentFile.async('string');
            const parser = new DOMParser();
            const doc = parser.parseFromString(contentXml, 'application/xml');
            const pages = doc.querySelectorAll('draw\\:page');
            this.properties['Number of Presentation Pages'] = pages.length;
        }

        // Signatures
        const signatureFiles = zip.file(/META-INF\/.*signatures.*\.xml/);
        this.properties['Signed'] = signatureFiles.length > 0 ? 'Yes' : 'No';

        return this.properties;
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            if (Array.isArray(value)) {
                console.log(`${key}:`);
                value.forEach(item => {
                    console.log(`  - ${item.path} (${item.media_type})`);
                });
            } else {
                console.log(`${key}: ${value}`);
            }
        }
    }

    async write() {
        // Return new blob for save (no modify)
        return await new JSZip().loadAsync(this.buffer).generateAsync({type: 'blob'});
    }
}

// Example usage (browser):
// const input = document.getElementById('file-input');
// input.addEventListener('change', async (e) => {
//     const file = e.target.files[0];
//     const buffer = await file.arrayBuffer();
//     const otp = new OTPFile(buffer);
//     await otp.decode();
//     otp.printProperties();
//     const newBlob = await otp.write();
//     // Save newBlob as file
// });

7. C class (using C++ for class support, assuming libzip and tinyxml2 libraries)

#include <iostream>
#include <zip.h>
#include <tinyxml2.h>
#include <string>
#include <vector>
#include <map>

class OTPFile {
private:
    std::string filepath;
    zip_t* zip;
    std::map<std::string, std::string> properties; // Simple properties
    std::vector<std::map<std::string, std::string>> manifestFiles;

    void decodeProperties() {
        int err = 0;
        zip = zip_open(filepath.c_str(), ZIP_RDONLY, &err);
        if (zip == nullptr) {
            std::cerr << "Error opening ZIP" << std::endl;
            return;
        }

        // MIME type
        zip_file_t* mimeFile = zip_fopen(zip, "mimetype", 0);
        if (mimeFile) {
            char buf[100];
            zip_int64_t len = zip_fread(mimeFile, buf, 99);
            buf[len] = '\0';
            properties["MIME Type"] = std::string(buf);
            zip_fclose(mimeFile);
        }

        // Manifest
        zip_file_t* manifestFile = zip_fopen(zip, "META-INF/manifest.xml", 0);
        if (manifestFile) {
            zip_stat_t stat;
            zip_stat(zip, "META-INF/manifest.xml", 0, &stat);
            char* xml = new char[stat.size + 1];
            zip_fread(manifestFile, xml, stat.size);
            xml[stat.size] = '\0';
            zip_fclose(manifestFile);

            tinyxml2::XMLDocument doc;
            doc.Parse(xml);
            tinyxml2::XMLElement* manifest = doc.FirstChildElement("manifest:manifest");
            if (manifest) {
                properties["Manifest Version"] = manifest->Attribute("manifest:version");
                tinyxml2::XMLElement* entry = manifest->FirstChildElement("manifest:file-entry");
                while (entry) {
                    std::map<std::string, std::string> fileInfo;
                    fileInfo["path"] = entry->Attribute("manifest:full-path");
                    fileInfo["media_type"] = entry->Attribute("manifest:media-type");
                    manifestFiles.push_back(fileInfo);
                    if (fileInfo["path"] == "/") {
                        properties["Document MIME Type"] = fileInfo["media_type"];
                        properties["Document Version"] = entry->Attribute("manifest:version") ? entry->Attribute("manifest:version") : "N/A";
                    }
                    if (entry->FirstChildElement("manifest:encryption-data")) {
                        properties["Encrypted"] = "Yes";
                    }
                    entry = entry->NextSiblingElement("manifest:file-entry");
                }
                if (properties.find("Encrypted") == properties.end()) {
                    properties["Encrypted"] = "No";
                }
            }
            delete[] xml;
        }

        // Thumbnail
        properties["Thumbnail Present"] = (zip_name_locate(zip, "Thumbnails/thumbnail.png", 0) >= 0) ? "Yes" : "No";

        // Meta
        zip_file_t* metaFile = zip_fopen(zip, "meta.xml", 0);
        if (metaFile) {
            zip_stat_t stat;
            zip_stat(zip, "meta.xml", 0, &stat);
            char* xml = new char[stat.size + 1];
            zip_fread(metaFile, xml, stat.size);
            xml[stat.size] = '\0';
            zip_fclose(metaFile);

            tinyxml2::XMLDocument doc;
            doc.Parse(xml);
            tinyxml2::XMLElement* title = doc.FirstChildElement("office:document-meta")->FirstChildElement("office:meta")->FirstChildElement("dc:title");
            properties["Title"] = title ? title->GetText() : "N/A";
            tinyxml2::XMLElement* creator = doc.FirstChildElement("office:document-meta")->FirstChildElement("office:meta")->FirstChildElement("dc:creator");
            properties["Creator"] = creator ? creator->GetText() : "N/A";
            tinyxml2::XMLElement* creationDate = doc.FirstChildElement("office:document-meta")->FirstChildElement("office:meta")->FirstChildElement("meta:creation-date");
            properties["Creation Date"] = creationDate ? creationDate->GetText() : "N/A";
            delete[] xml;
        }

        // Content - Number of pages
        zip_file_t* contentFile = zip_fopen(zip, "content.xml", 0);
        if (contentFile) {
            zip_stat_t stat;
            zip_stat(zip, "content.xml", 0, &stat);
            char* xml = new char[stat.size + 1];
            zip_fread(contentFile, xml, stat.size);
            xml[stat.size] = '\0';
            zip_fclose(contentFile);

            tinyxml2::XMLDocument doc;
            doc.Parse(xml);
            int count = 0;
            tinyxml2::XMLElement* page = doc.FirstChildElement("office:document-content")->FirstChildElement("office:body")->FirstChildElement("office:presentation")->FirstChildElement("draw:page");
            while (page) {
                count++;
                page = page->NextSiblingElement("draw:page");
            }
            properties["Number of Presentation Pages"] = std::to_string(count);
            delete[] xml;
        }

        // Signatures
        bool signed = false;
        int num = zip_get_num_entries(zip, 0);
        for (int i = 0; i < num; i++) {
            const char* name = zip_get_name(zip, i, 0);
            if (std::string(name).find("META-INF/") == 0 && std::string(name).find("signatures") != std::string::npos) {
                signed = true;
                break;
            }
        }
        properties["Signed"] = signed ? "Yes" : "No";
    }

public:
    OTPFile(const std::string& fp) : filepath(fp) {
        decodeProperties();
    }

    ~OTPFile() {
        if (zip) zip_close(zip);
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
        std::cout << "Files in Manifest:" << std::endl;
        for (const auto& file : manifestFiles) {
            std::cout << "  - " << file.at("path") << " (" << file.at("media_type") << ")" << std::endl;
        }
    }

    void write(const std::string& newFilepath) {
        // Simple copy for write
        zip_t* newZip = zip_open(newFilepath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, nullptr);
        if (newZip) {
            int num = zip_get_num_entries(zip, 0);
            for (int i = 0; i < num; i++) {
                const char* name = zip_get_name(zip, i, 0);
                zip_file_t* f = zip_fopen_index(zip, i, 0);
                zip_stat_t stat;
                zip_stat_index(zip, i, 0, &stat);
                char* buf = new char[stat.size];
                zip_fread(f, buf, stat.size);
                zip_file_add(newZip, name, zip_buffer_create(buf, stat.size), ZIP_FL_ENC_UTF_8);
                zip_fclose(f);
            }
            zip_close(newZip);
        }
    }
};

// Example usage:
// int main() {
//     OTPFile otp("example.otp");
//     otp.printProperties();
//     otp.write("modified.otp");
//     return 0;
// }