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
mimetypefile). - Office Version: The
office:versionattribute 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.
2. Two Direct Download Links for .OTG Files
- https://templates.libreoffice.org/assets/uploads/sites/3/2015/08/cdlabel.otg (a CD label drawing template).
- 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).
Drag and Drop .OTG File
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;
// }