Task 494: .OTG File Format

Task 494: .OTG File Format

File Format Specifications for .OTG

The .OTG file format is the OpenDocument Drawing Template, part of the OpenDocument Format (ODF) standard developed by OASIS. It is used for vector graphics templates in applications like LibreOffice Draw and Apache OpenOffice Draw. The format is an XML-based standard packaged in a ZIP archive, conforming to ISO/IEC 26300. The package includes specific XML files for content, styles, metadata, and settings, along with a manifest and optional thumbnails or images. The MIME type is application/vnd.oasis.opendocument.graphics-template. The format supports version identification via the office:version attribute in root XML elements, and it can include encryption using algorithms like AES or Blowfish.

1. List of All Properties Intrinsic to This File Format

Based on the ODF specification for .OTG, the intrinsic properties include the following key elements and fields that define the format's structure and metadata. These are extracted from the package structure, XML schemas, and required components:

  • File Signature: PK\003\004 (standard ZIP header magic number, as .OTG is a ZIP archive).
  • MIME Type: application/vnd.oasis.opendocument.graphics-template (stored in the uncompressed mimetype file).
  • Office Version: The office:version attribute in root XML elements (e.g., "1.2" in <office:document-content>).
  • Generator: Metadata field indicating the software that created the file (from meta.xml).
  • Title: Document title (from meta.xml).
  • Description: Document description (from meta.xml).
  • Subject: Document subject (from meta.xml).
  • Keywords: Document keywords (from meta.xml).
  • Initial Creator: Original author (from meta.xml).
  • Creator: Last modifier (from meta.xml).
  • Printed By: User who last printed the document (from meta.xml).
  • Creation Date and Time: Timestamp when the document was created (from meta.xml).
  • Modification Date and Time: Timestamp of last modification (from meta.xml).
  • Print Date and Time: Timestamp of last print (from meta.xml).
  • Document Template: Reference to any base template used (from meta.xml).
  • Automatic Reload: Settings for auto-reload behavior (from meta.xml).
  • Hyperlink Behavior: Settings for hyperlink handling (from meta.xml).
  • Language: Primary language of the document (from meta.xml).
  • Editing Cycles: Number of editing sessions (from meta.xml).
  • Editing Duration: Total editing time (from meta.xml).
  • Document Statistics: Counts such as number of pages, objects, images, etc. (from meta.xml, e.g., <meta:document-statistic meta:object-count="X" meta:page-count="Y"/>).
  • Encryption Status: Whether the file is encrypted (checked via manifest.xml encryption entries).
  • Package Files List: List of all files in the archive (from META-INF/manifest.xml).
  • Styles Count: Number of defined styles (from styles.xml).
  • Drawing Objects Count: Number of drawing shapes or objects (from content.xml, e.g., <draw:rect>, <draw:circle>, etc.).

These properties are intrinsic to the format's structure on the file system, as they define how the ZIP package is organized, validated, and interpreted.

  1. https://templates.libreoffice.org/assets/uploads/sites/3/2015/08/cdlabel.otg (a CD label drawing template).
  2. https://templates.libreoffice.org/assets/uploads/sites/3/2015/08/businesscard.otg (a business card drawing template).

These are sample OpenDocument Drawing Templates from the LibreOffice template repository.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .OTG File Dump

Here is a self-contained HTML page with embedded JavaScript that allows dragging and dropping a .OTG file. It uses JSZip to unzip the file and DOMParser to parse XML, then dumps all the properties from the list above to the screen. Save this as an HTML file and open in a browser (requires including JSZip via CDN).

.OTG File Property Dumper

Drag and Drop .OTG File

Drop .OTG file here

4. Python Class for .OTG File Handling

Here is a Python class that can open, decode, read, write, and print the properties. It uses zipfile and xml.etree.ElementTree for parsing. For writing, it demonstrates updating the title metadata and saving a new file.

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

class OTGFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.zip = None
        self._open()

    def _open(self):
        self.zip = zipfile.ZipFile(self.filepath, 'r')
        self._read_properties()

    def _read_properties(self):
        # File Signature (check ZIP)
        with open(self.filepath, 'rb') as f:
            signature = f.read(4)
            self.properties['File Signature'] = ''.join(f'\\{b:03o}' for b in signature) if signature == b'PK\x03\x04' else 'Invalid'

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

        # Parse meta.xml
        with self.zip.open('meta.xml') as f:
            tree = ET.parse(f)
            root = tree.getroot()
            ns = {'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
                  'dc': 'http://purl.org/dc/elements/1.1/',
                  'office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'}
            self.properties['Generator'] = root.find('.//meta:generator', ns).text if root.find('.//meta:generator', ns) else 'N/A'
            self.properties['Title'] = root.find('.//dc:title', ns).text if root.find('.//dc:title', ns) else 'N/A'
            self.properties['Description'] = root.find('.//dc:description', ns).text if root.find('.//dc:description', ns) else 'N/A'
            self.properties['Subject'] = root.find('.//dc:subject', ns).text if root.find('.//dc:subject', ns) else 'N/A'
            self.properties['Keywords'] = root.find('.//meta:keyword', ns).text if root.find('.//meta:keyword', ns) else 'N/A'
            self.properties['Initial Creator'] = root.find('.//meta:initial-creator', ns).text if root.find('.//meta:initial-creator', ns) else 'N/A'
            self.properties['Creator'] = root.find('.//dc:creator', ns).text if root.find('.//dc:creator', ns) else 'N/A'
            self.properties['Printed By'] = root.find('.//meta:printed-by', ns).text if root.find('.//meta:printed-by', ns) else 'N/A'
            self.properties['Creation Date and Time'] = root.find('.//meta:creation-date', ns).text if root.find('.//meta:creation-date', ns) else 'N/A'
            self.properties['Modification Date and Time'] = root.find('.//dc:date', ns).text if root.find('.//dc:date', ns) else 'N/A'
            self.properties['Print Date and Time'] = root.find('.//meta:print-date', ns).text if root.find('.//meta:print-date', ns) else 'N/A'
            template = root.find('.//meta:template', ns)
            self.properties['Document Template'] = template.get('{http://www.w3.org/1999/xlink}href') if template else 'N/A'
            reload = root.find('.//meta:auto-reload', ns)
            self.properties['Automatic Reload'] = reload.get('meta:delay') if reload else 'N/A'
            hyperlink = root.find('.//meta:hyperlink-behaviour', ns)
            self.properties['Hyperlink Behavior'] = hyperlink.get('office:target-frame-name') if hyperlink else 'N/A'
            self.properties['Language'] = root.find('.//dc:language', ns).text if root.find('.//dc:language', ns) else 'N/A'
            self.properties['Editing Cycles'] = root.find('.//meta:editing-cycles', ns).text if root.find('.//meta:editing-cycles', ns) else 'N/A'
            self.properties['Editing Duration'] = root.find('.//meta:editing-duration', ns).text if root.find('.//meta:editing-duration', ns) else 'N/A'
            stats = root.find('.//meta:document-statistic', ns)
            if stats is not None:
                self.properties['Document Statistics'] = f"Pages: {stats.get('meta:page-count', 0)}, Objects: {stats.get('meta:object-count', 0)}"
            else:
                self.properties['Document Statistics'] = 'N/A'

        # Office Version from content.xml
        with self.zip.open('content.xml') as f:
            tree = ET.parse(f)
            root = tree.getroot()
            ns = {'office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'}
            self.properties['Office Version'] = root.get('office:version', 'N/A')
            draw_ns = {'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'}
            self.properties['Drawing Objects Count'] = len(root.findall('.//*[@draw]', draw_ns))

        # Styles Count from styles.xml
        with self.zip.open('styles.xml') as f:
            tree = ET.parse(f)
            root = tree.getroot()
            style_ns = {'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'}
            self.properties['Styles Count'] = len(root.findall('.//style:style', style_ns))

        # Encryption and Files List from manifest.xml
        with self.zip.open('META-INF/manifest.xml') as f:
            tree = ET.parse(f)
            root = tree.getroot()
            manifest_ns = {'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'}
            self.properties['Encryption Status'] = 'Yes' if root.find('.//*[@manifest:start-key-generation-name]', manifest_ns) else 'No'
            files = [entry.get('manifest:full-path') for entry in root.findall('manifest:file-entry', manifest_ns)]
            self.properties['Package Files List'] = ', '.join(files)

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, new_filepath, update_title=None):
        with zipfile.ZipFile(new_filepath, 'w', compression=zipfile.ZIP_DEFLATED) as new_zip:
            for item in self.zip.infolist():
                data = self.zip.read(item.filename)
                if item.filename == 'meta.xml' and update_title:
                    tree = ET.parse(BytesIO(data))
                    root = tree.getroot()
                    ns = {'dc': 'http://purl.org/dc/elements/1.1/'}
                    title_elem = root.find('.//dc:title', ns)
                    if title_elem is not None:
                        title_elem.text = update_title
                    data = ET.tostring(root, encoding='utf-8', xml_declaration=True)
                new_zip.writestr(item, data)
        print(f"File written to {new_filepath}")

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

# Example usage:
# otg = OTGFile('example.otg')
# otg.print_properties()
# otg.write('modified.otg', update_title='New Title')
# otg.close()

5. Java Class for .OTG File Handling

Here is a Java class using ZipFile and DocumentBuilderFactory for parsing. For writing, it updates the title and saves a new file.

import java.io.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class OTGFile {
    private String filepath;
    private Map<String, String> properties = new HashMap<>();
    private ZipFile zip;

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

    private void readProperties() throws Exception {
        // File Signature
        try (InputStream is = new FileInputStream(filepath)) {
            byte[] sig = new byte[4];
            is.read(sig);
            if (sig[0] == 0x50 && sig[1] == 0x4B && sig[2] == 0x03 && sig[3] == 0x04) {
                properties.put("File Signature", "PK\\003\\004 (ZIP)");
            } else {
                properties.put("File Signature", "Invalid");
            }
        }

        // MIME Type
        ZipEntry mimeEntry = zip.getEntry("mimetype");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(zip.getInputStream(mimeEntry)))) {
            properties.put("MIME Type", reader.readLine().trim());
        }

        // Parse meta.xml
        ZipEntry metaEntry = zip.getEntry("meta.xml");
        Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zip.getInputStream(metaEntry));
        doc.getDocumentElement().normalize();
        properties.put("Generator", getNodeText(doc, "meta:generator"));
        properties.put("Title", getNodeText(doc, "dc:title"));
        properties.put("Description", getNodeText(doc, "dc:description"));
        properties.put("Subject", getNodeText(doc, "dc:subject"));
        properties.put("Keywords", getNodeText(doc, "meta:keyword"));
        properties.put("Initial Creator", getNodeText(doc, "meta:initial-creator"));
        properties.put("Creator", getNodeText(doc, "dc:creator"));
        properties.put("Printed By", getNodeText(doc, "meta:printed-by"));
        properties.put("Creation Date and Time", getNodeText(doc, "meta:creation-date"));
        properties.put("Modification Date and Time", getNodeText(doc, "dc:date"));
        properties.put("Print Date and Time", getNodeText(doc, "meta:print-date"));
        properties.put("Document Template", doc.getElementsByTagName("meta:template").item(0) != null ? doc.getElementsByTagName("meta:template").item(0).getAttributes().getNamedItem("xlink:href").getTextContent() : "N/A");
        properties.put("Automatic Reload", doc.getElementsByTagName("meta:auto-reload").item(0) != null ? doc.getElementsByTagName("meta:auto-reload").item(0).getAttributes().getNamedItem("meta:delay").getTextContent() : "N/A");
        properties.put("Hyperlink Behavior", doc.getElementsByTagName("meta:hyperlink-behaviour").item(0) != null ? doc.getElementsByTagName("meta:hyperlink-behaviour").item(0).getAttributes().getNamedItem("office:target-frame-name").getTextContent() : "N/A");
        properties.put("Language", getNodeText(doc, "dc:language"));
        properties.put("Editing Cycles", getNodeText(doc, "meta:editing-cycles"));
        properties.put("Editing Duration", getNodeText(doc, "meta:editing-duration"));
        org.w3c.dom.Element stats = (org.w3c.dom.Element) doc.getElementsByTagName("meta:document-statistic").item(0);
        properties.put("Document Statistics", stats != null ? "Pages: " + stats.getAttribute("meta:page-count") + ", Objects: " + stats.getAttribute("meta:object-count") : "N/A");

        // Office Version and Drawing Objects Count from content.xml
        ZipEntry contentEntry = zip.getEntry("content.xml");
        doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zip.getInputStream(contentEntry));
        doc.getDocumentElement().normalize();
        properties.put("Office Version", doc.getDocumentElement().getAttribute("office:version"));
        properties.put("Drawing Objects Count", String.valueOf(doc.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", "*").getLength()));

        // Styles Count from styles.xml
        ZipEntry stylesEntry = zip.getEntry("styles.xml");
        doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zip.getInputStream(stylesEntry));
        doc.getDocumentElement().normalize();
        properties.put("Styles Count", String.valueOf(doc.getElementsByTagName("style:style").getLength()));

        // Encryption and Files List from manifest.xml
        ZipEntry manifestEntry = zip.getEntry("META-INF/manifest.xml");
        doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zip.getInputStream(manifestEntry));
        doc.getDocumentElement().normalize();
        properties.put("Encryption Status", doc.getElementsByTagNameNS("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "start-key-generation-name").getLength() > 0 ? "Yes" : "No");
        NodeList entries = doc.getElementsByTagName("manifest:file-entry");
        StringBuilder files = new StringBuilder();
        for (int i = 0; i < entries.getLength(); i++) {
            files.append(entries.item(i).getAttributes().getNamedItem("manifest:full-path").getTextContent()).append(", ");
        }
        properties.put("Package Files List", files.toString().trim());

    }

    private String getNodeText(Document doc, String tag) {
        NodeList list = doc.getElementsByTagName(tag);
        return list.getLength() > 0 ? list.item(0).getTextContent() : "N/A";
    }

    public void printProperties() {
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String newFilepath, String newTitle) throws Exception {
        try (ZipOutputStream newZip = new ZipOutputStream(new FileOutputStream(newFilepath))) {
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                InputStream is = zip.getInputStream(entry);
                newZip.putNextEntry(new ZipEntry(entry.getName()));
                if (entry.getName().equals("meta.xml") && newTitle != null) {
                    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
                    NodeList titleList = doc.getElementsByTagName("dc:title");
                    if (titleList.getLength() > 0) {
                        titleList.item(0).setTextContent(newTitle);
                    }
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    javax.xml.transform.TransformerFactory.newInstance().newTransformer().transform(
                        new javax.xml.transform.dom.DOMSource(doc),
                        new javax.xml.transform.stream.StreamResult(baos));
                    byte[] data = baos.toByteArray();
                    newZip.write(data, 0, data.length);
                } else {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) > 0) {
                        newZip.write(buffer, 0, len);
                    }
                }
                newZip.closeEntry();
                is.close();
            }
        }
        System.out.println("File written to " + newFilepath);
    }

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

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

6. JavaScript Class for .OTG File Handling

Here is a JavaScript class (node.js compatible, requires 'jszip' and 'xmldom' modules installed via npm). It reads from a file, parses, prints properties, and writes an updated file.

const fs = require('fs');
const JSZip = require('jszip');
const DOMParser = require('xmldom').DOMParser;

class OTGFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {};
        this.zip = null;
        this.load();
    }

    async load() {
        const data = fs.readFileSync(this.filepath);
        this.zip = await JSZip.loadAsync(data);
        await this.readProperties();
    }

    async readProperties() {
        // File Signature
        const sig = fs.readFileSync(this.filepath, { encoding: 'hex' }).substring(0, 8);
        this.properties['File Signature'] = sig === '504b0304' ? 'PK\\003\\004 (ZIP)' : 'Invalid';

        // MIME Type
        this.properties['MIME Type'] = (await this.zip.file('mimetype').async('string')).trim();

        // Parse meta.xml
        const metaXml = await this.zip.file('meta.xml').async('string');
        const doc = new DOMParser().parseFromString(metaXml);
        this.properties['Generator'] = doc.getElementsByTagName('meta:generator')[0]?.textContent || 'N/A';
        this.properties['Title'] = doc.getElementsByTagName('dc:title')[0]?.textContent || 'N/A';
        this.properties['Description'] = doc.getElementsByTagName('dc:description')[0]?.textContent || 'N/A';
        this.properties['Subject'] = doc.getElementsByTagName('dc:subject')[0]?.textContent || 'N/A';
        this.properties['Keywords'] = doc.getElementsByTagName('meta:keyword')[0]?.textContent || 'N/A';
        this.properties['Initial Creator'] = doc.getElementsByTagName('meta:initial-creator')[0]?.textContent || 'N/A';
        this.properties['Creator'] = doc.getElementsByTagName('dc:creator')[0]?.textContent || 'N/A';
        this.properties['Printed By'] = doc.getElementsByTagName('meta:printed-by')[0]?.textContent || 'N/A';
        this.properties['Creation Date and Time'] = doc.getElementsByTagName('meta:creation-date')[0]?.textContent || 'N/A';
        this.properties['Modification Date and Time'] = doc.getElementsByTagName('dc:date')[0]?.textContent || 'N/A';
        this.properties['Print Date and Time'] = doc.getElementsByTagName('meta:print-date')[0]?.textContent || 'N/A';
        const template = doc.getElementsByTagName('meta:template')[0];
        this.properties['Document Template'] = template ? template.getAttribute('xlink:href') || 'N/A' : 'N/A';
        const reload = doc.getElementsByTagName('meta:auto-reload')[0];
        this.properties['Automatic Reload'] = reload ? reload.getAttribute('meta:delay') || 'N/A' : 'N/A';
        const hyperlink = doc.getElementsByTagName('meta:hyperlink-behaviour')[0];
        this.properties['Hyperlink Behavior'] = hyperlink ? hyperlink.getAttribute('office:target-frame-name') || 'N/A' : 'N/A';
        this.properties['Language'] = doc.getElementsByTagName('dc:language')[0]?.textContent || 'N/A';
        this.properties['Editing Cycles'] = doc.getElementsByTagName('meta:editing-cycles')[0]?.textContent || 'N/A';
        this.properties['Editing Duration'] = doc.getElementsByTagName('meta:editing-duration')[0]?.textContent || 'N/A';
        const stats = doc.getElementsByTagName('meta:document-statistic')[0];
        this.properties['Document Statistics'] = stats ? `Pages: ${stats.getAttribute('meta:page-count') || 0}, Objects: ${stats.getAttribute('meta:object-count') || 0}` : 'N/A';

        // Office Version and Drawing Objects Count from content.xml
        const contentXml = await this.zip.file('content.xml').async('string');
        const contentDoc = new DOMParser().parseFromString(contentXml);
        this.properties['Office Version'] = contentDoc.documentElement.getAttribute('office:version') || 'N/A';
        this.properties['Drawing Objects Count'] = contentDoc.getElementsByTagNameNS('urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', '*').length;

        // Styles Count from styles.xml
        const stylesXml = await this.zip.file('styles.xml').async('string');
        const stylesDoc = new DOMParser().parseFromString(stylesXml);
        this.properties['Styles Count'] = stylesDoc.getElementsByTagName('style:style').length;

        // Encryption and Files List from manifest.xml
        const manifestXml = await this.zip.file('META-INF/manifest.xml').async('string');
        const manifestDoc = new DOMParser().parseFromString(manifestXml);
        this.properties['Encryption Status'] = manifestDoc.getElementsByTagNameNS('urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', 'start-key-generation-name').length > 0 ? 'Yes' : 'No';
        const entries = manifestDoc.getElementsByTagName('manifest:file-entry');
        const files = Array.from(entries).map(entry => entry.getAttribute('manifest:full-path')).join(', ');
        this.properties['Package Files List'] = files;
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    async write(newFilepath, newTitle = null) {
        const newZip = new JSZip();
        for (const filename in this.zip.files) {
            let content = await this.zip.file(filename).async('nodebuffer');
            if (filename === 'meta.xml' && newTitle) {
                const doc = new DOMParser().parseFromString(content.toString('utf8'));
                const titleElem = doc.getElementsByTagName('dc:title')[0];
                if (titleElem) titleElem.textContent = newTitle;
                content = Buffer.from(new (require('xmldom').XMLSerializer)().serializeToString(doc));
            }
            newZip.file(filename, content);
        }
        const buffer = await newZip.generateAsync({type: 'nodebuffer', compression: 'DEFLATE'});
        fs.writeFileSync(newFilepath, buffer);
        console.log(`File written to ${newFilepath}`);
    }
}

// Example usage:
// const otg = new OTGFile('example.otg');
// otg.printProperties();
// otg.write('modified.otg', 'New Title');

7. C++ Class for .OTG File Handling

Here is a C++ class using libzip and libxml2 (assume installed). It opens, reads, prints properties, and writes with updated title. Compile with -lzip -lxml2.

#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <zip.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>

class OTGFile {
private:
    std::string filepath;
    std::map<std::string, std::string> properties;
    zip_t *zip;

public:
    OTGFile(const std::string& fp) : filepath(fp), zip(nullptr) {
        int err = 0;
        zip = zip_open(filepath.c_str(), ZIP_RDONLY, &err);
        if (!zip) {
            std::cerr << "Error opening zip: " << err << std::endl;
            return;
        }
        readProperties();
    }

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

    void readProperties() {
        // File Signature
        std::ifstream file(filepath, std::ios::binary);
        char sig[4];
        file.read(sig, 4);
        if (sig[0] == 'P' && sig[1] == 'K' && sig[2] == 3 && sig[3] == 4) {
            properties["File Signature"] = "PK\\003\\004 (ZIP)";
        } else {
            properties["File Signature"] = "Invalid";
        }
        file.close();

        // MIME Type
        auto mime = readZipFile("mimetype");
        properties["MIME Type"] = mime.empty() ? "N/A" : mime;

        // Parse meta.xml
        auto metaXml = readZipFile("meta.xml");
        if (!metaXml.empty()) {
            xmlDocPtr doc = xmlReadMemory(metaXml.data(), metaXml.size(), nullptr, nullptr, 0);
            if (doc) {
                xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
                if (xpathCtx) {
                    xmlXPathObjectPtr xpathObj;
                    // Register namespaces
                    xmlXPathRegisterNs(xpathCtx, BAD_CAST "meta", BAD_CAST "urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
                    xmlXPathRegisterNs(xpathCtx, BAD_CAST "dc", BAD_CAST "http://purl.org/dc/elements/1.1/");
                    xmlXPathRegisterNs(xpathCtx, BAD_CAST "office", BAD_CAST "urn:oasis:names:tc:opendocument:xmlns:office:1.0");
                    xmlXPathRegisterNs(xpathCtx, BAD_CAST "xlink", BAD_CAST "http://www.w3.org/1999/xlink");

                    auto getText = [&](const char* path) -> std::string {
                        xpathObj = xmlXPathEvalExpression(BAD_CAST path, xpathCtx);
                        if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                            auto node = xpathObj->nodesetval->nodeTab[0];
                            std::string text = (char*)xmlNodeGetContent(node);
                            xmlXPathFreeObject(xpathObj);
                            return text;
                        }
                        xmlXPathFreeObject(xpathObj);
                        return "N/A";
                    };

                    properties["Generator"] = getText("//meta:generator");
                    properties["Title"] = getText("//dc:title");
                    properties["Description"] = getText("//dc:description");
                    properties["Subject"] = getText("//dc:subject");
                    properties["Keywords"] = getText("//meta:keyword");
                    properties["Initial Creator"] = getText("//meta:initial-creator");
                    properties["Creator"] = getText("//dc:creator");
                    properties["Printed By"] = getText("//meta:printed-by");
                    properties["Creation Date and Time"] = getText("//meta:creation-date");
                    properties["Modification Date and Time"] = getText("//dc:date");
                    properties["Print Date and Time"] = getText("//meta:print-date");
                    properties["Language"] = getText("//dc:language");
                    properties["Editing Cycles"] = getText("//meta:editing-cycles");
                    properties["Editing Duration"] = getText("//meta:editing-duration");

                    xpathObj = xmlXPathEvalExpression(BAD_CAST "//meta:template", xpathCtx);
                    if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                        auto node = xpathObj->nodesetval->nodeTab[0];
                        properties["Document Template"] = (char*)xmlGetProp(node, BAD_CAST "xlink:href");
                    } else {
                        properties["Document Template"] = "N/A";
                    }
                    xmlXPathFreeObject(xpathObj);

                    xpathObj = xmlXPathEvalExpression(BAD_CAST "//meta:auto-reload", xpathCtx);
                    if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                        auto node = xpathObj->nodesetval->nodeTab[0];
                        properties["Automatic Reload"] = (char*)xmlGetProp(node, BAD_CAST "meta:delay");
                    } else {
                        properties["Automatic Reload"] = "N/A";
                    }
                    xmlXPathFreeObject(xpathObj);

                    xpathObj = xmlXPathEvalExpression(BAD_CAST "//meta:hyperlink-behaviour", xpathCtx);
                    if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                        auto node = xpathObj->nodesetval->nodeTab[0];
                        properties["Hyperlink Behavior"] = (char*)xmlGetProp(node, BAD_CAST "office:target-frame-name");
                    } else {
                        properties["Hyperlink Behavior"] = "N/A";
                    }
                    xmlXPathFreeObject(xpathObj);

                    xpathObj = xmlXPathEvalExpression(BAD_CAST "//meta:document-statistic", xpathCtx);
                    if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                        auto node = xpathObj->nodesetval->nodeTab[0];
                        std::string pages = (char*)xmlGetProp(node, BAD_CAST "meta:page-count");
                        std::string objects = (char*)xmlGetProp(node, BAD_CAST "meta:object-count");
                        properties["Document Statistics"] = "Pages: " + (pages.empty() ? "0" : pages) + ", Objects: " + (objects.empty() ? "0" : objects);
                    } else {
                        properties["Document Statistics"] = "N/A";
                    }
                    xmlXPathFreeObject(xpathObj);

                    xmlXPathFreeContext(xpathCtx);
                }
                xmlFreeDoc(doc);
            }
        }

        // Office Version and Drawing Objects Count from content.xml
        auto contentXml = readZipFile("content.xml");
        if (!contentXml.empty()) {
            xmlDocPtr doc = xmlReadMemory(contentXml.data(), contentXml.size(), nullptr, nullptr, 0);
            if (doc) {
                xmlChar* version = xmlGetProp(doc->children, BAD_CAST "office:version");
                properties["Office Version"] = version ? (char*)version : "N/A";
                xmlFree(version);

                xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
                xmlXPathRegisterNs(xpathCtx, BAD_CAST "draw", BAD_CAST "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0");
                auto xpathObj = xmlXPathEvalExpression(BAD_CAST "//draw:*", xpathCtx);
                properties["Drawing Objects Count"] = std::to_string(xpathObj->nodesetval ? xpathObj->nodesetval->nodeNr : 0);
                xmlXPathFreeObject(xpathObj);
                xmlXPathFreeContext(xpathCtx);
                xmlFreeDoc(doc);
            }
        }

        // Styles Count from styles.xml
        auto stylesXml = readZipFile("styles.xml");
        if (!stylesXml.empty()) {
            xmlDocPtr doc = xmlReadMemory(stylesXml.data(), stylesXml.size(), nullptr, nullptr, 0);
            if (doc) {
                xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
                xmlXPathRegisterNs(xpathCtx, BAD_CAST "style", BAD_CAST "urn:oasis:names:tc:opendocument:xmlns:style:1.0");
                auto xpathObj = xmlXPathEvalExpression(BAD_CAST "//style:style", xpathCtx);
                properties["Styles Count"] = std::to_string(xpathObj->nodesetval ? xpathObj->nodesetval->nodeNr : 0);
                xmlXPathFreeObject(xpathObj);
                xmlXPathFreeContext(xpathCtx);
                xmlFreeDoc(doc);
            }
        }

        // Encryption and Files List from manifest.xml
        auto manifestXml = readZipFile("META-INF/manifest.xml");
        if (!manifestXml.empty()) {
            xmlDocPtr doc = xmlReadMemory(manifestXml.data(), manifestXml.size(), nullptr, nullptr, 0);
            if (doc) {
                xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
                xmlXPathRegisterNs(xpathCtx, BAD_CAST "manifest", BAD_CAST "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0");
                auto xpathObj = xmlXPathEvalExpression(BAD_CAST "//manifest:start-key-generation-name", xpathCtx);
                properties["Encryption Status"] = (xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) ? "Yes" : "No";
                xmlXPathFreeObject(xpathObj);

                xpathObj = xmlXPathEvalExpression(BAD_CAST "//manifest:file-entry", xpathCtx);
                std::string files;
                if (xpathObj->nodesetval) {
                    for (int i = 0; i < xpathObj->nodesetval->nodeNr; ++i) {
                        auto node = xpathObj->nodesetval->nodeTab[i];
                        xmlChar* path = xmlGetProp(node, BAD_CAST "manifest:full-path");
                        if (path) {
                            files += (char*)path + std::string(", ");
                            xmlFree(path);
                        }
                    }
                }
                if (!files.empty()) files = files.substr(0, files.length() - 2);
                properties["Package Files List"] = files;
                xmlXPathFreeObject(xpathObj);
                xmlXPathFreeContext(xpathCtx);
                xmlFreeDoc(doc);
            }
        }
    }

    std::string readZipFile(const std::string& name) {
        zip_file_t *zf = zip_fopen(zip, name.c_str(), 0);
        if (!zf) return "";
        std::string content;
        char buf[1024];
        zip_int64_t len;
        while ((len = zip_fread(zf, buf, sizeof(buf))) > 0) {
            content.append(buf, len);
        }
        zip_fclose(zf);
        return content;
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
    }

    void write(const std::string& newFilepath, const std::string& newTitle = "") {
        zip_t *newZip = zip_open(newFilepath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, nullptr);
        if (!newZip) {
            std::cerr << "Error creating new zip" << std::endl;
            return;
        }

        zip_stat_t stat;
        zip_int64_t num = zip_get_num_entries(zip, 0);
        for (zip_int64_t i = 0; i < num; ++i) {
            zip_stat_index(zip, i, 0, &stat);
            std::string name(stat.name);
            auto content = readZipFile(name);

            if (name == "meta.xml" && !newTitle.empty()) {
                xmlDocPtr doc = xmlReadMemory(content.data(), content.size(), nullptr, nullptr, 0);
                if (doc) {
                    xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
                    xmlXPathRegisterNs(xpathCtx, BAD_CAST "dc", BAD_CAST "http://purl.org/dc/elements/1.1/");
                    auto xpathObj = xmlXPathEvalExpression(BAD_CAST "//dc:title", xpathCtx);
                    if (xpathObj->nodesetval && xpathObj->nodesetval->nodeNr > 0) {
                        xmlNodeSetContent(xpathObj->nodesetval->nodeTab[0], BAD_CAST newTitle.c_str());
                    }
                    xmlXPathFreeObject(xpathObj);
                    xmlXPathFreeContext(xpathCtx);

                    xmlChar *mem;
                    int size;
                    xmlDocDumpMemoryEnc(doc, &mem, &size, "UTF-8");
                    content = std::string((char*)mem, size);
                    xmlFree(mem);
                    xmlFreeDoc(doc);
                }
            }

            zip_source_t *source = zip_source_buffer(newZip, content.data(), content.size(), 0);
            if (source) {
                zip_file_add(newZip, name.c_str(), source, ZIP_FL_OVERWRITE | ZIP_FL_ENC_UTF_8);
            } else {
                zip_source_free(source);
            }
        }
        zip_close(newZip);
        std::cout << "File written to " << newFilepath << std::endl;
    }
};

// Example usage:
// int main() {
//     OTGFile otg("example.otg");
//     otg.printProperties();
//     otg.write("modified.otg", "New Title");
//     return 0;
// }