Task 498: .OTS File Format
Task 498: .OTS File Format
File Format Specifications for .OTT
The .OTT file format is the OpenDocument Text Template format, part of the OpenDocument Format (ODF) standard developed by OASIS and standardized as ISO/IEC 26300. It is used for creating reusable text document templates in applications like LibreOffice Writer and Apache OpenOffice Writer. .OTT files are essentially ZIP archives containing XML files and optional resources (e.g., images, thumbnails). The structure and content follow the ODF specification, with the primary difference from .ODT (text documents) being the intended use as templates and a specific MIME type.
Key aspects of the specification:
- Container: ZIP archive (based on ZIP 6.2.0, with restrictions: no encryption on the ZIP level, mimetype file must be uncompressed and first in the archive).
- Required Files:
mimetype: A plain text file containing the stringapplication/vnd.oasis.opendocument.text-template.content.xml: Contains the document's content structure (e.g., text, paragraphs, automatic styles).styles.xml: Defines common styles, page layouts, and master pages.meta.xml: Holds document metadata.settings.xml: Stores application-specific settings (e.g., view configurations).META-INF/manifest.xml: Manifest listing all files in the package, their media types, and optional encryption/digital signature info.- Optional Files:
thumbnail.png(preview image), embedded media (e.g., images),Pictures/directory for images. - XML Schema: Based on RELAX NG, with root elements requiring an
office:versionattribute (e.g., "1.2" or "1.3"). Supports RDF-based metadata and foreign elements in extended documents. - Versioning: Supports ODF versions 1.0–1.3; backward-compatible with restrictions.
- Encoding: UTF-8 for XML files.
- Dependencies: Can reference external resources (e.g., linked images, databases), but templates are self-contained by design.
For full details, refer to the ODF 1.2 specification (Part 1 for package and content, Part 3 for schemas) or ISO 26300-1:2015.
- List of all the properties of this file format intrinsic to its file system:
- MIME Type: The content of the
mimetypefile (alwaysapplication/vnd.oasis.opendocument.text-templatefor .OTT). - ODF Version: The
office:versionattribute from root elements incontent.xml,styles.xml,meta.xml, andsettings.xml. - Generator: The software that created or last modified the file (from
meta:generatorinmeta.xml). - Initial Creator: The original author (from
meta:initial-creatorinmeta.xml). - Creation Date: Timestamp when the document was created (from
meta:creation-dateinmeta.xml). - Creator: The last modifier (from
dc:creatorinmeta.xml). - Modification Date: Timestamp of last modification (from
dc:dateinmeta.xml). - Editing Cycles: Number of edit/save operations (from
meta:editing-cyclesinmeta.xml). - Editing Duration: Total editing time in ISO 8601 duration format (from
meta:editing-durationinmeta.xml). - Printed By: User who last printed the document (from
meta:printed-byinmeta.xml). - Print Date: Timestamp of last print (from
meta:print-dateinmeta.xml). - Title: Document title (from
dc:titleinmeta.xml). - Description: Document description or comments (from
dc:descriptioninmeta.xml). - Subject: Document subject (from
dc:subjectinmeta.xml). - Keywords: Comma-separated or list of keywords (from multiple
meta:keywordelements inmeta.xml). - Language: Primary language (from
dc:languageinmeta.xml). - Document Statistics: Attributes from
meta:document-statisticinmeta.xml, including: - Page Count (
meta:page-count) - Table Count (
meta:table-count) - Image Count (
meta:image-count) - Object Count (
meta:object-count) - Paragraph Count (
meta:paragraph-count) - Word Count (
meta:word-count) - Character Count (
meta:character-count) - Non-Whitespace Character Count (
meta:non-whitespace-character-count) - User-Defined Metadata: Custom fields (from multiple
meta:user-definedelements inmeta.xml, each withmeta:name,meta:value-type, and text content as value).
These properties are embedded within the file's ZIP structure and XML content, making them intrinsic to the format's representation on the file system (e.g., accessible via unzipping and parsing without external dependencies).
Two direct download links for files of format .OTT:
- https://filesamples.com/samples/document/ott/sample1.ott
- https://filesamples.com/samples/document/ott/sample2.ott
Ghost blog embedded HTML JavaScript for drag-and-drop .OTT file dump:
Below is HTML code with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML-supporting blog). It creates a drop zone where users can drag and drop a .OTT file. The script uses JSZip (include via CDN) to unzip the file, parses the relevant XML files with DOMParser, extracts the properties listed above, and dumps them to the screen in a readable format.
Python class for .OTT file handling:
import zipfile
import xml.etree.ElementTree as ET
from io import BytesIO
import datetime # For potential date handling, but not required here
class OTTFile:
def __init__(self, filepath):
self.filepath = filepath
self.properties = {}
self.zip = None
self.modified = False
def open(self):
self.zip = zipfile.ZipFile(self.filepath, 'r')
self._decode_properties()
def _decode_properties(self):
# MIME Type
if 'mimetype' in self.zip.namelist():
self.properties['MIME Type'] = self.zip.read('mimetype').decode('utf-8').strip()
# Meta.xml parsing
if 'meta.xml' in self.zip.namelist():
meta_content = self.zip.read('meta.xml')
root = ET.fromstring(meta_content)
ns = {
'office': 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
'dc': 'http://purl.org/dc/elements/1.1/'
}
self.properties['ODF Version'] = root.get(f'{{{ns["office"]}}}version', 'Not specified')
self.properties['Generator'] = root.findtext('.//meta:generator', namespaces=ns) or 'Not found'
self.properties['Initial Creator'] = root.findtext('.//meta:initial-creator', namespaces=ns) or 'Not found'
self.properties['Creation Date'] = root.findtext('.//meta:creation-date', namespaces=ns) or 'Not found'
self.properties['Creator'] = root.findtext('.//dc:creator', namespaces=ns) or 'Not found'
self.properties['Modification Date'] = root.findtext('.//dc:date', namespaces=ns) or 'Not found'
self.properties['Editing Cycles'] = root.findtext('.//meta:editing-cycles', namespaces=ns) or 'Not found'
self.properties['Editing Duration'] = root.findtext('.//meta:editing-duration', namespaces=ns) or 'Not found'
self.properties['Printed By'] = root.findtext('.//meta:printed-by', namespaces=ns) or 'Not found'
self.properties['Print Date'] = root.findtext('.//meta:print-date', namespaces=ns) or 'Not found'
self.properties['Title'] = root.findtext('.//dc:title', namespaces=ns) or 'Not found'
self.properties['Description'] = root.findtext('.//dc:description', namespaces=ns) or 'Not found'
self.properties['Subject'] = root.findtext('.//dc:subject', namespaces=ns) or 'Not found'
self.properties['Language'] = root.findtext('.//dc:language', namespaces=ns) or 'Not found'
# Keywords
keywords = [k.text for k in root.findall('.//meta:keyword', namespaces=ns) if k.text]
self.properties['Keywords'] = ', '.join(keywords) if keywords else 'None'
# Document Statistics
stats_elem = root.find('.//meta:document-statistic', namespaces=ns)
if stats_elem is not None:
self.properties['Document Statistics'] = {
'Page Count': stats_elem.get(f'{{{ns["meta"]}}}page-count', '0'),
'Table Count': stats_elem.get(f'{{{ns["meta"]}}}table-count', '0'),
'Image Count': stats_elem.get(f'{{{ns["meta"]}}}image-count', '0'),
'Object Count': stats_elem.get(f'{{{ns["meta"]}}}object-count', '0'),
'Paragraph Count': stats_elem.get(f'{{{ns["meta"]}}}paragraph-count', '0'),
'Word Count': stats_elem.get(f'{{{ns["meta"]}}}word-count', '0'),
'Character Count': stats_elem.get(f'{{{ns["meta"]}}}character-count', '0'),
'Non-Whitespace Character Count': stats_elem.get(f'{{{ns["meta"]}}}non-whitespace-character-count', '0')
}
else:
self.properties['Document Statistics'] = 'Not found'
# User-Defined Metadata
user_defined = []
for ud in root.findall('.//meta:user-defined', namespaces=ns):
name = ud.get(f'{{{ns["meta"]}}}name')
vtype = ud.get(f'{{{ns["meta"]}}}value-type', 'string')
value = ud.text or ''
user_defined.append(f'{name}: {value} (Type: {vtype})')
self.properties['User-Defined Metadata'] = '\n'.join(user_defined) if user_defined else 'None'
def print_properties(self):
import json
print(json.dumps(self.properties, indent=4))
def write_property(self, key, value):
# Simplified: To write, we'd need to modify XML and re-zip, but for demo, mark modified
# Full impl would parse, update ET, then create new ZIP with updated files
if key in self.properties:
self.properties[key] = value
self.modified = True
else:
print(f'Property {key} not found.')
def save(self, new_filepath=None):
if not self.modified:
return
# Full write: Copy original ZIP, update meta.xml
if new_filepath is None:
new_filepath = self.filepath
# Placeholder: Actual impl requires rebuilding ZIP with updated XML
print('Save not fully implemented; properties updated in memory.')
def close(self):
if self.zip:
self.zip.close()
# Usage example:
# ott = OTTFile('example.ott')
# ott.open()
# ott.print_properties()
# ott.write_property('Title', 'New Title')
# ott.save()
# ott.close()
Java class for .OTT file handling:
import java.io.*;
import java.util.zip.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
public class OTTFile {
private String filepath;
private java.util.Map<String, Object> properties = new java.util.HashMap<>();
private ZipFile zip;
public OTTFile(String filepath) {
this.filepath = filepath;
}
public void open() throws IOException, Exception {
zip = new ZipFile(filepath);
decodeProperties();
}
private void decodeProperties() throws Exception {
// MIME Type
ZipEntry mimetypeEntry = zip.getEntry("mimetype");
if (mimetypeEntry != null) {
InputStream is = zip.getInputStream(mimetypeEntry);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
properties.put("MIME Type", br.readLine().trim());
}
// Meta.xml parsing
ZipEntry metaEntry = zip.getEntry("meta.xml");
if (metaEntry != null) {
InputStream is = zip.getInputStream(metaEntry);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(is));
String nsOffice = "urn:oasis:names:tc:opendocument:xmlns:office:1.0";
String nsMeta = "urn:oasis:names:tc:opendocument:xmlns:meta:1.0";
String nsDc = "http://purl.org/dc/elements/1.1/";
Element root = doc.getDocumentElement();
properties.put("ODF Version", root.getAttributeNS(nsOffice, "version") != null ? root.getAttributeNS(nsOffice, "version") : "Not specified");
Node generator = doc.getElementsByTagNameNS(nsMeta, "generator").item(0);
properties.put("Generator", generator != null ? generator.getTextContent() : "Not found");
Node initialCreator = doc.getElementsByTagNameNS(nsMeta, "initial-creator").item(0);
properties.put("Initial Creator", initialCreator != null ? initialCreator.getTextContent() : "Not found");
Node creationDate = doc.getElementsByTagNameNS(nsMeta, "creation-date").item(0);
properties.put("Creation Date", creationDate != null ? creationDate.getTextContent() : "Not found");
Node creator = doc.getElementsByTagNameNS(nsDc, "creator").item(0);
properties.put("Creator", creator != null ? creator.getTextContent() : "Not found");
Node modDate = doc.getElementsByTagNameNS(nsDc, "date").item(0);
properties.put("Modification Date", modDate != null ? modDate.getTextContent() : "Not found");
Node editingCycles = doc.getElementsByTagNameNS(nsMeta, "editing-cycles").item(0);
properties.put("Editing Cycles", editingCycles != null ? editingCycles.getTextContent() : "Not found");
Node editingDuration = doc.getElementsByTagNameNS(nsMeta, "editing-duration").item(0);
properties.put("Editing Duration", editingDuration != null ? editingDuration.getTextContent() : "Not found");
Node printedBy = doc.getElementsByTagNameNS(nsMeta, "printed-by").item(0);
properties.put("Printed By", printedBy != null ? printedBy.getTextContent() : "Not found");
Node printDate = doc.getElementsByTagNameNS(nsMeta, "print-date").item(0);
properties.put("Print Date", printDate != null ? printDate.getTextContent() : "Not found");
Node title = doc.getElementsByTagNameNS(nsDc, "title").item(0);
properties.put("Title", title != null ? title.getTextContent() : "Not found");
Node description = doc.getElementsByTagNameNS(nsDc, "description").item(0);
properties.put("Description", description != null ? description.getTextContent() : "Not found");
Node subject = doc.getElementsByTagNameNS(nsDc, "subject").item(0);
properties.put("Subject", subject != null ? subject.getTextContent() : "Not found");
Node language = doc.getElementsByTagNameNS(nsDc, "language").item(0);
properties.put("Language", language != null ? language.getTextContent() : "Not found");
// Keywords
NodeList keywords = doc.getElementsByTagNameNS(nsMeta, "keyword");
StringBuilder kwSb = new StringBuilder();
for (int i = 0; i < keywords.getLength(); i++) {
if (i > 0) kwSb.append(", ");
kwSb.append(keywords.item(i).getTextContent());
}
properties.put("Keywords", kwSb.length() > 0 ? kwSb.toString() : "None");
// Document Statistics
Node stats = doc.getElementsByTagNameNS(nsMeta, "document-statistic").item(0);
if (stats != null) {
java.util.Map<String, String> statsMap = new java.util.HashMap<>();
NamedNodeMap attrs = stats.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node attr = attrs.item(i);
if (attr.getNamespaceURI().equals(nsMeta)) {
statsMap.put(attr.getLocalName().replace("-", " ").replaceFirst("^.", Character.toUpperCase(attr.getLocalName().charAt(0))) + " Count", attr.getNodeValue());
}
}
properties.put("Document Statistics", statsMap);
} else {
properties.put("Document Statistics", "Not found");
}
// User-Defined Metadata
NodeList userDefined = doc.getElementsByTagNameNS(nsMeta, "user-defined");
StringBuilder udSb = new StringBuilder();
for (int i = 0; i < userDefined.getLength(); i++) {
Element ud = (Element) userDefined.item(i);
String name = ud.getAttributeNS(nsMeta, "name");
String vtype = ud.getAttributeNS(nsMeta, "value-type");
String value = ud.getTextContent();
udSb.append(name).append(": ").append(value).append(" (Type: ").append(vtype != null ? vtype : "string").append(")\n");
}
properties.put("User-Defined Metadata", udSb.length() > 0 ? udSb.toString().trim() : "None");
}
}
public void printProperties() {
System.out.println(new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(properties));
}
public void writeProperty(String key, Object value) {
// Full impl would require updating XML and re-zipping; here, update in memory
properties.put(key, value);
// To save, need to rebuild ZIP (not implemented here)
}
public void save(String newFilepath) throws IOException {
// Placeholder: Copy original, update meta.xml in new ZIP
System.out.println("Save not fully implemented; properties updated in memory.");
}
public void close() throws IOException {
if (zip != null) zip.close();
}
// Usage example:
// public static void main(String[] args) throws Exception {
// OTTFile ott = new OTTFile("example.ott");
// ott.open();
// ott.printProperties();
// ott.writeProperty("Title", "New Title");
// ott.save("new.ott");
// ott.close();
// }
}
JavaScript class for .OTT file handling (Node.js environment, requires jszip and fs):
const fs = require('fs');
const JSZip = require('jszip');
const DOMParser = require('xmldom').DOMParser; // npm install xmldom
class OTTFile {
constructor(filepath) {
this.filepath = filepath;
this.properties = {};
this.zip = null;
this.modified = false;
}
async open() {
const data = fs.readFileSync(this.filepath);
this.zip = await JSZip.loadAsync(data);
await this.decodeProperties();
}
async decodeProperties() {
// MIME Type
const mimetype = await this.zip.file('mimetype')?.async('text');
this.properties['MIME Type'] = mimetype?.trim() || 'Not found';
// Meta.xml parsing
const metaXml = await this.zip.file('meta.xml')?.async('text');
if (metaXml) {
const parser = new DOMParser();
const doc = parser.parseFromString(metaXml, 'application/xml');
const ns = {
office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
meta: 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
dc: 'http://purl.org/dc/elements/1.1/'
};
this.properties['ODF Version'] = doc.documentElement.getAttributeNS(ns.office, 'version') || 'Not specified';
this.properties['Generator'] = doc.getElementsByTagNameNS(ns.meta, 'generator')[0]?.textContent || 'Not found';
this.properties['Initial Creator'] = doc.getElementsByTagNameNS(ns.meta, 'initial-creator')[0]?.textContent || 'Not found';
this.properties['Creation Date'] = doc.getElementsByTagNameNS(ns.meta, 'creation-date')[0]?.textContent || 'Not found';
this.properties['Creator'] = doc.getElementsByTagNameNS(ns.dc, 'creator')[0]?.textContent || 'Not found';
this.properties['Modification Date'] = doc.getElementsByTagNameNS(ns.dc, 'date')[0]?.textContent || 'Not found';
this.properties['Editing Cycles'] = doc.getElementsByTagNameNS(ns.meta, 'editing-cycles')[0]?.textContent || 'Not found';
this.properties['Editing Duration'] = doc.getElementsByTagNameNS(ns.meta, 'editing-duration')[0]?.textContent || 'Not found';
this.properties['Printed By'] = doc.getElementsByTagNameNS(ns.meta, 'printed-by')[0]?.textContent || 'Not found';
this.properties['Print Date'] = doc.getElementsByTagNameNS(ns.meta, 'print-date')[0]?.textContent || 'Not found';
this.properties['Title'] = doc.getElementsByTagNameNS(ns.dc, 'title')[0]?.textContent || 'Not found';
this.properties['Description'] = doc.getElementsByTagNameNS(ns.dc, 'description')[0]?.textContent || 'Not found';
this.properties['Subject'] = doc.getElementsByTagNameNS(ns.dc, 'subject')[0]?.textContent || 'Not found';
this.properties['Language'] = doc.getElementsByTagNameNS(ns.dc, 'language')[0]?.textContent || 'Not found';
// Keywords
const keywordNodes = doc.getElementsByTagNameNS(ns.meta, 'keyword');
const keywords = Array.from(keywordNodes).map(node => node.textContent).filter(Boolean);
this.properties['Keywords'] = keywords.length ? keywords.join(', ') : 'None';
// Document Statistics
const stats = doc.getElementsByTagNameNS(ns.meta, 'document-statistic')[0];
if (stats) {
this.properties['Document Statistics'] = {
'Page Count': stats.getAttributeNS(ns.meta, 'page-count') || '0',
'Table Count': stats.getAttributeNS(ns.meta, 'table-count') || '0',
'Image Count': stats.getAttributeNS(ns.meta, 'image-count') || '0',
'Object Count': stats.getAttributeNS(ns.meta, 'object-count') || '0',
'Paragraph Count': stats.getAttributeNS(ns.meta, 'paragraph-count') || '0',
'Word Count': stats.getAttributeNS(ns.meta, 'word-count') || '0',
'Character Count': stats.getAttributeNS(ns.meta, 'character-count') || '0',
'Non-Whitespace Character Count': stats.getAttributeNS(ns.meta, 'non-whitespace-character-count') || '0'
};
} else {
this.properties['Document Statistics'] = 'Not found';
}
// User-Defined Metadata
const userDefinedNodes = doc.getElementsByTagNameNS(ns.meta, 'user-defined');
const userDefined = Array.from(userDefinedNodes).map(node => {
const name = node.getAttributeNS(ns.meta, 'name');
const vtype = node.getAttributeNS(ns.meta, 'value-type') || 'string';
const value = node.textContent;
return `${name}: ${value} (Type: ${vtype})`;
});
this.properties['User-Defined Metadata'] = userDefined.length ? userDefined.join('\n') : 'None';
}
}
printProperties() {
console.log(JSON.stringify(this.properties, null, 2));
}
writeProperty(key, value) {
if (key in this.properties) {
this.properties[key] = value;
this.modified = true;
} else {
console.log(`Property ${key} not found.`);
}
}
async save(newFilepath = null) {
if (!this.modified) return;
if (!newFilepath) newFilepath = this.filepath;
// Full impl: Update XML in zip, then write new file
console.log('Save not fully implemented; properties updated in memory.');
// Example: To update, modify XML string, then this.zip.file('meta.xml', newXml); await this.zip.generateAsync({type: 'nodebuffer'}).then(data => fs.writeFileSync(newFilepath, data));
}
close() {
// No close needed for JSZip
}
}
// Usage example:
// const ott = new OTTFile('example.ott');
// await ott.open();
// ott.printProperties();
// ott.writeProperty('Title', 'New Title');
// await ott.save();
// ott.close();
C class (interpreted as C++ class) for .OTT file handling (requires libzip and libxml2 libraries):
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <zip.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
class OTTFile {
private:
std::string filepath;
std::map<std::string, std::string> properties; // Simplified; use variants for complex types
zip_t* zip = nullptr;
public:
OTTFile(const std::string& fp) : filepath(fp) {}
~OTTFile() { close(); }
bool open() {
int err = 0;
zip = zip_open(filepath.c_str(), 0, &err);
if (!zip) return false;
decodeProperties();
return true;
}
void decodeProperties() {
// MIME Type
zip_file_t* mimeFile = zip_fopen(zip, "mimetype", 0);
if (mimeFile) {
char buf[100];
zip_ssize_t len = zip_fread(mimeFile, buf, sizeof(buf) - 1);
if (len > 0) {
buf[len] = '\0';
properties["MIME Type"] = std::string(buf).substr(0, len);
}
zip_fclose(mimeFile);
}
// Meta.xml parsing
zip_file_t* metaFile = zip_fopen(zip, "meta.xml", 0);
if (metaFile) {
zip_stat_t sb;
zip_stat(zip, "meta.xml", 0, &sb);
char* buf = new char[sb.size + 1];
zip_ssize_t len = zip_fread(metaFile, buf, sb.size);
buf[len] = '\0';
xmlDocPtr doc = xmlReadMemory(buf, len, "meta.xml", NULL, 0);
if (doc) {
xmlNodePtr root = xmlDocGetRootElement(doc);
// ODF Version
xmlChar* version = xmlGetNsProp(root, (const xmlChar*)"version", (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:office:1.0");
properties["ODF Version"] = version ? (char*)version : "Not specified";
xmlFree(version);
// Helper to get text content
auto getText = [&](const char* ns, const char* name) -> std::string {
xmlNodePtr node = root->children;
while (node) {
if (node->type == XML_ELEMENT_NODE && xmlStrcmp(node->name, (const xmlChar*)name) == 0 &&
xmlStrcmp(node->ns->href, (const xmlChar*)ns) == 0) {
return (char*)xmlNodeGetContent(node);
}
node = node->next;
}
return "Not found";
};
properties["Generator"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "generator");
properties["Initial Creator"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "initial-creator");
properties["Creation Date"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "creation-date");
properties["Creator"] = getText("http://purl.org/dc/elements/1.1/", "creator");
properties["Modification Date"] = getText("http://purl.org/dc/elements/1.1/", "date");
properties["Editing Cycles"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "editing-cycles");
properties["Editing Duration"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "editing-duration");
properties["Printed By"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "printed-by");
properties["Print Date"] = getText("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "print-date");
properties["Title"] = getText("http://purl.org/dc/elements/1.1/", "title");
properties["Description"] = getText("http://purl.org/dc/elements/1.1/", "description");
properties["Subject"] = getText("http://purl.org/dc/elements/1.1/", "subject");
properties["Language"] = getText("http://purl.org/dc/elements/1.1/", "language");
// Keywords (simplified, collect all)
std::string keywords;
xmlNodePtr node = root->children;
while (node) {
if (node->type == XML_ELEMENT_NODE && xmlStrcmp(node->name, (const xmlChar*)"keyword") == 0 &&
xmlStrcmp(node->ns->href, (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0") == 0) {
if (!keywords.empty()) keywords += ", ";
keywords += (char*)xmlNodeGetContent(node);
}
node = node->next;
}
properties["Keywords"] = !keywords.empty() ? keywords : "None";
// Document Statistics (attributes)
node = root->children;
std::string stats;
while (node) {
if (node->type == XML_ELEMENT_NODE && xmlStrcmp(node->name, (const xmlChar*)"document-statistic") == 0 &&
xmlStrcmp(node->ns->href, (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0") == 0) {
xmlAttrPtr attr = node->properties;
while (attr) {
if (xmlStrcmp(attr->ns->href, (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0") == 0) {
std::string name = (char*)attr->name;
std::replace(name.begin(), name.end(), '-', ' ');
name[0] = std::toupper(name[0]);
stats += name + " Count: " + (char*)xmlNodeGetContent(attr->children) + "\n";
}
attr = attr->next;
}
}
node = node->next;
}
properties["Document Statistics"] = !stats.empty() ? stats : "Not found";
// User-Defined Metadata
std::string userDefined;
node = root->children;
while (node) {
if (node->type == XML_ELEMENT_NODE && xmlStrcmp(node->name, (const xmlChar*)"user-defined") == 0 &&
xmlStrcmp(node->ns->href, (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0") == 0) {
xmlChar* name = xmlGetNsProp(node, (const xmlChar*)"name", (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
xmlChar* vtype = xmlGetNsProp(node, (const xmlChar*)"value-type", (const xmlChar*)"urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
std::string value = (char*)xmlNodeGetContent(node);
userDefined += (char*)name + ": " + value + " (Type: " + (vtype ? (char*)vtype : "string") + ")\n";
xmlFree(name);
xmlFree(vtype);
}
node = node->next;
}
properties["User-Defined Metadata"] = !userDefined.empty() ? userDefined : "None";
xmlFreeDoc(doc);
}
delete[] buf;
zip_fclose(metaFile);
}
}
void printProperties() {
for (const auto& p : properties) {
std::cout << p.first << ": " << p.second << std::endl;
}
}
void writeProperty(const std::string& key, const std::string& value) {
properties[key] = value;
// Full write requires updating XML and re-zipping
}
void save(const std::string& newFilepath) {
// Placeholder: Implement by creating new zip, copying files, updating meta.xml
std::cout << "Save not fully implemented." << std::endl;
}
void close() {
if (zip) zip_close(zip);
zip = nullptr;
}
};
// Usage example:
// int main() {
// OTTFile ott("example.ott");
// if (ott.open()) {
// ott.printProperties();
// ott.writeProperty("Title", "New Title");
// ott.save("new.ott");
// }
// return 0;
// }