Task 568: .PPTX File Format

Task 568: .PPTX File Format

File Format Specifications for .PPTX

The .PPTX file format is part of the Office Open XML (OOXML) standard, defined in ECMA-376 and ISO/IEC 29500. It is a ZIP-based container using Open Packaging Conventions (OPC), with content structured in XML files for presentations, including slides, metadata, and media. The primary markup languages are PresentationML for structure and DrawingML for graphics.

List of Properties Intrinsic to the .PPTX File Format
These properties include both static format identifiers and extractable metadata/structure from the file's ZIP container and XML parts. They are "intrinsic to its file system" in the sense that they are embedded in the file's package structure (e.g., ZIP headers, mandatory XML files like core.xml and app.xml).

  • File Extension: .pptx
  • MIME Type: application/vnd.openxmlformats-officedocument.presentationml.presentation
  • Magic Number (File Signature): 50 4B 03 04 (hex for PK\x03\x04, indicating ZIP container)
  • Core Metadata Properties (from docProps/core.xml):
  • Title
  • Subject
  • Creator
  • Keywords
  • Description
  • Last Modified By
  • Revision
  • Created Date
  • Modified Date
  • Extended Metadata Properties (from docProps/app.xml):
  • Application
  • App Version
  • Company
  • Number of Slides
  • Number of Hidden Slides
  • Number of Notes
  • Number of Paragraphs
  • Number of Words
  • Number of Characters
  • Template
  • Structural Properties:
  • Number of Files in ZIP Archive
  • Presence of Mandatory Parts: [Content_Types].xml, _rels/.rels, ppt/presentation.xml, docProps/core.xml
  • Number of Slides (cross-referenced from ppt/presentation.xml slide list)

Two Direct Download Links for .PPTX Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .PPTX Property Dump
This is a self-contained HTML page with embedded JavaScript (using JSZip library for ZIP handling; include it via CDN). It allows drag-and-drop of a .PPTX file and dumps the properties to the screen.

PPTX Property Dumper
Drag and drop a .PPTX file here

Python Class for .PPTX Properties
This class uses built-in zipfile and xml.etree.ElementTree to read, print, and write properties. Writing updates core/app.xml and saves a new file.

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

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

    def _read_properties(self):
        properties = {
            'File Extension': '.pptx',
            'MIME Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'Magic Number': '50 4B 03 04',
            'Number of Files in ZIP Archive': len(self.zip.namelist()),
        }

        mandatory_parts = ['[Content_Types].xml', '_rels/.rels', 'ppt/presentation.xml', 'docProps/core.xml']
        properties['Mandatory Parts Present'] = all(part in self.zip.namelist() for part in mandatory_parts)

        # Core Metadata
        if 'docProps/core.xml' in self.zip.namelist():
            core_xml = self.zip.read('docProps/core.xml').decode('utf-8')
            root = ET.fromstring(core_xml)
            ns = {'cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
                  'dc': 'http://purl.org/dc/elements/1.1/', 'dcterms': 'http://purl.org/dc/terms/'}
            properties['Title'] = root.find('dc:title', ns).text if root.find('dc:title', ns) is not None else 'N/A'
            properties['Subject'] = root.find('cp:subject', ns).text if root.find('cp:subject', ns) is not None else 'N/A'
            properties['Creator'] = root.find('dc:creator', ns).text if root.find('dc:creator', ns) is not None else 'N/A'
            properties['Keywords'] = root.find('cp:keywords', ns).text if root.find('cp:keywords', ns) is not None else 'N/A'
            properties['Description'] = root.find('dc:description', ns).text if root.find('dc:description', ns) is not None else 'N/A'
            properties['Last Modified By'] = root.find('cp:lastModifiedBy', ns).text if root.find('cp:lastModifiedBy', ns) is not None else 'N/A'
            properties['Revision'] = root.find('cp:revision', ns).text if root.find('cp:revision', ns) is not None else 'N/A'
            properties['Created Date'] = root.find('dcterms:created', ns).text if root.find('dcterms:created', ns) is not None else 'N/A'
            properties['Modified Date'] = root.find('dcterms:modified', ns).text if root.find('dcterms:modified', ns) is not None else 'N/A'

        # Extended Metadata
        if 'docProps/app.xml' in self.zip.namelist():
            app_xml = self.zip.read('docProps/app.xml').decode('utf-8')
            root = ET.fromstring(app_xml)
            ns = {'': 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'}
            properties['Application'] = root.find('Application', ns).text if root.find('Application', ns) is not None else 'N/A'
            properties['App Version'] = root.find('AppVersion', ns).text if root.find('AppVersion', ns) is not None else 'N/A'
            properties['Company'] = root.find('Company', ns).text if root.find('Company', ns) is not None else 'N/A'
            properties['Number of Slides'] = root.find('Slides', ns).text if root.find('Slides', ns) is not None else 'N/A'
            properties['Number of Hidden Slides'] = root.find('HiddenSlides', ns).text if root.find('HiddenSlides', ns) is not None else 'N/A'
            properties['Number of Notes'] = root.find('Notes', ns).text if root.find('Notes', ns) is not None else 'N/A'
            properties['Number of Paragraphs'] = root.find('Paragraphs', ns).text if root.find('Paragraphs', ns) is not None else 'N/A'
            properties['Number of Words'] = root.find('Words', ns).text if root.find('Words', ns) is not None else 'N/A'
            properties['Number of Characters'] = root.find('Characters', ns).text if root.find('Characters', ns) is not None else 'N/A'
            properties['Template'] = root.find('Template', ns).text if root.find('Template', ns) is not None else 'N/A'

        # Number of Slides from presentation.xml
        if 'ppt/presentation.xml' in self.zip.namelist():
            pres_xml = self.zip.read('ppt/presentation.xml').decode('utf-8')
            root = ET.fromstring(pres_xml)
            ns = {'p': 'http://schemas.openxmlformats.org/presentationml/2006/main'}
            properties['Number of Slides (from presentation.xml)'] = len(root.findall('.//p:sldId', ns))

        return properties

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

    def write_properties(self, new_properties, output_path):
        with zipfile.ZipFile(output_path, 'w') as new_zip:
            for item in self.zip.infolist():
                data = self.zip.read(item.filename)
                if item.filename == 'docProps/core.xml' and 'core' in new_properties:
                    root = ET.fromstring(data)
                    ns = {'cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
                          'dc': 'http://purl.org/dc/elements/1.1/', 'dcterms': 'http://purl.org/dc/terms/'}
                    for key, value in new_properties['core'].items():
                        elem = root.find(f'.//{key}', ns)
                        if elem is not None:
                            elem.text = value
                        else:
                            # Add if missing (simplified)
                            ET.SubElement(root, key, xmlns=ns['dc' if key in ['title', 'creator', 'description'] else 'cp']).text = value
                    data = ET.tostring(root, encoding='utf-8')
                elif item.filename == 'docProps/app.xml' and 'app' in new_properties:
                    root = ET.fromstring(data)
                    ns = {'': 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'}
                    for key, value in new_properties['app'].items():
                        elem = root.find(key, ns)
                        if elem is not None:
                            elem.text = value
                        else:
                            ET.SubElement(root, key).text = value
                    data = ET.tostring(root, encoding='utf-8')
                new_zip.writestr(item, data)

Java Class for .PPTX Properties
This class uses java.util.zip and javax.xml.parsers to read, print, and write properties.

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

public class PPTXHandler {
    private String filepath;
    private ZipFile zip;
    private Map<String, String> properties;

    public PPTXHandler(String filepath) throws IOException, Exception {
        this.filepath = filepath;
        this.zip = new ZipFile(filepath);
        this.properties = readProperties();
    }

    private Map<String, String> readProperties() throws Exception {
        Map<String, String> props = new HashMap<>();
        props.put("File Extension", ".pptx");
        props.put("MIME Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
        props.put("Magic Number", "50 4B 03 04");
        props.put("Number of Files in ZIP Archive", String.valueOf(zip.size()));

        String[] mandatoryParts = {"[Content_Types].xml", "_rels/.rels", "ppt/presentation.xml", "docProps/core.xml"};
        boolean allPresent = true;
        for (String part : mandatoryParts) {
            if (zip.getEntry(part) == null) {
                allPresent = false;
                break;
            }
        }
        props.put("Mandatory Parts Present", allPresent ? "Yes" : "No");

        // Core Metadata
        ZipEntry coreEntry = zip.getEntry("docProps/core.xml");
        if (coreEntry != null) {
            InputStream is = zip.getInputStream(coreEntry);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().parse(new InputSource(is));
            props.put("Title", getNodeValue(doc, "//*[local-name()='title']"));
            props.put("Subject", getNodeValue(doc, "//*[local-name()='subject']"));
            props.put("Creator", getNodeValue(doc, "//*[local-name()='creator']"));
            props.put("Keywords", getNodeValue(doc, "//*[local-name()='keywords']"));
            props.put("Description", getNodeValue(doc, "//*[local-name()='description']"));
            props.put("Last Modified By", getNodeValue(doc, "//*[local-name()='lastModifiedBy']"));
            props.put("Revision", getNodeValue(doc, "//*[local-name()='revision']"));
            props.put("Created Date", getNodeValue(doc, "//*[local-name()='created']"));
            props.put("Modified Date", getNodeValue(doc, "//*[local-name()='modified']"));
        }

        // Extended Metadata
        ZipEntry appEntry = zip.getEntry("docProps/app.xml");
        if (appEntry != null) {
            InputStream is = zip.getInputStream(appEntry);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().parse(new InputSource(is));
            props.put("Application", getNodeValue(doc, "//*[local-name()='Application']"));
            props.put("App Version", getNodeValue(doc, "//*[local-name()='AppVersion']"));
            props.put("Company", getNodeValue(doc, "//*[local-name()='Company']"));
            props.put("Number of Slides", getNodeValue(doc, "//*[local-name()='Slides']"));
            props.put("Number of Hidden Slides", getNodeValue(doc, "//*[local-name()='HiddenSlides']"));
            props.put("Number of Notes", getNodeValue(doc, "//*[local-name()='Notes']"));
            props.put("Number of Paragraphs", getNodeValue(doc, "//*[local-name()='Paragraphs']"));
            props.put("Number of Words", getNodeValue(doc, "//*[local-name()='Words']"));
            props.put("Number of Characters", getNodeValue(doc, "//*[local-name()='Characters']"));
            props.put("Template", getNodeValue(doc, "//*[local-name()='Template']"));
        }

        // Number of Slides from presentation.xml
        ZipEntry presEntry = zip.getEntry("ppt/presentation.xml");
        if (presEntry != null) {
            InputStream is = zip.getInputStream(presEntry);
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().parse(new InputSource(is));
            NodeList slides = doc.getElementsByTagNameNS("http://schemas.openxmlformats.org/presentationml/2006/main", "sldId");
            props.put("Number of Slides (from presentation.xml)", String.valueOf(slides.getLength()));
        }

        return props;
    }

    private String getNodeValue(Document doc, String xpath) throws Exception {
        javax.xml.xpath.XPath xp = javax.xml.xpath.XPathFactory.newInstance().newXPath();
        Node node = (Node) xp.evaluate(xpath, doc, javax.xml.xpath.XPathConstants.NODE);
        return node != null ? node.getTextContent() : "N/A";
    }

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

    public void writeProperties(Map<String, Map<String, String>> newProperties, String outputPath) throws Exception {
        try (ZipOutputStream newZip = new ZipOutputStream(new FileOutputStream(outputPath))) {
            byte[] buffer = new byte[1024];
            for (java.util.Enumeration<? extends ZipEntry> e = zip.entries(); e.hasMoreElements(); ) {
                ZipEntry entry = e.nextElement();
                InputStream is = zip.getInputStream(entry);
                newZip.putNextEntry(new ZipEntry(entry.getName()));

                if (entry.getName().equals("docProps/core.xml") && newProperties.containsKey("core")) {
                    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                    dbf.setNamespaceAware(true);
                    Document doc = dbf.newDocumentBuilder().parse(is);
                    for (Map.Entry<String, String> prop : newProperties.get("core").entrySet()) {
                        Node node = doc.getElementsByTagNameNS("*", prop.getKey()).item(0);
                        if (node != null) {
                            node.setTextContent(prop.getValue());
                        } else {
                            // Add if missing (simplified)
                            Element newElem = doc.createElement(prop.getKey());
                            newElem.setTextContent(prop.getValue());
                            doc.getDocumentElement().appendChild(newElem);
                        }
                    }
                    TransformerFactory tf = TransformerFactory.newInstance();
                    Transformer t = tf.newTransformer();
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    t.transform(new DOMSource(doc), new StreamResult(baos));
                    byte[] data = baos.toByteArray();
                    newZip.write(data, 0, data.length);
                } else if (entry.getName().equals("docProps/app.xml") && newProperties.containsKey("app")) {
                    // Similar logic for app.xml
                    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                    dbf.setNamespaceAware(true);
                    Document doc = dbf.newDocumentBuilder().parse(is);
                    for (Map.Entry<String, String> prop : newProperties.get("app").entrySet()) {
                        Node node = doc.getElementsByTagName(prop.getKey()).item(0);
                        if (node != null) {
                            node.setTextContent(prop.getValue());
                        } else {
                            Element newElem = doc.createElement(prop.getKey());
                            newElem.setTextContent(prop.getValue());
                            doc.getDocumentElement().appendChild(newElem);
                        }
                    }
                    TransformerFactory tf = TransformerFactory.newInstance();
                    Transformer t = tf.newTransformer();
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    t.transform(new DOMSource(doc), new StreamResult(baos));
                    byte[] data = baos.toByteArray();
                    newZip.write(data, 0, data.length);
                } else {
                    int len;
                    while ((len = is.read(buffer)) > 0) {
                        newZip.write(buffer, 0, len);
                    }
                }
                newZip.closeEntry();
                is.close();
            }
        }
    }
}

JavaScript Class for .PPTX Properties
This class is for Node.js (uses jszip and fs; install jszip via npm). It reads, prints, and writes properties.

const fs = require('fs');
const JSZip = require('jszip');
const { DOMParser, XMLSerializer } = require('xmldom');

class PPTXHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = null;
    }

    async readProperties() {
        const data = fs.readFileSync(this.filepath);
        const zip = await JSZip.loadAsync(data);
        this.properties = {
            'File Extension': '.pptx',
            'MIME Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'Magic Number': '50 4B 03 04',
            'Number of Files in ZIP Archive': Object.keys(zip.files).length,
        };

        const mandatoryParts = ['[Content_Types].xml', '_rels/.rels', 'ppt/presentation.xml', 'docProps/core.xml'];
        this.properties['Mandatory Parts Present'] = mandatoryParts.every(part => zip.file(part) !== null) ? 'Yes' : 'No';

        // Core Metadata
        const coreXml = await zip.file('docProps/core.xml')?.async('text');
        if (coreXml) {
            const doc = new DOMParser().parseFromString(coreXml);
            this.properties['Title'] = doc.getElementsByTagName('dc:title')[0]?.textContent || 'N/A';
            this.properties['Subject'] = doc.getElementsByTagName('cp:subject')[0]?.textContent || 'N/A';
            this.properties['Creator'] = doc.getElementsByTagName('dc:creator')[0]?.textContent || 'N/A';
            this.properties['Keywords'] = doc.getElementsByTagName('cp:keywords')[0]?.textContent || 'N/A';
            this.properties['Description'] = doc.getElementsByTagName('dc:description')[0]?.textContent || 'N/A';
            this.properties['Last Modified By'] = doc.getElementsByTagName('cp:lastModifiedBy')[0]?.textContent || 'N/A';
            this.properties['Revision'] = doc.getElementsByTagName('cp:revision')[0]?.textContent || 'N/A';
            this.properties['Created Date'] = doc.getElementsByTagName('dcterms:created')[0]?.textContent || 'N/A';
            this.properties['Modified Date'] = doc.getElementsByTagName('dcterms:modified')[0]?.textContent || 'N/A';
        }

        // Extended Metadata
        const appXml = await zip.file('docProps/app.xml')?.async('text');
        if (appXml) {
            const doc = new DOMParser().parseFromString(appXml);
            this.properties['Application'] = doc.getElementsByTagName('Application')[0]?.textContent || 'N/A';
            this.properties['App Version'] = doc.getElementsByTagName('AppVersion')[0]?.textContent || 'N/A';
            this.properties['Company'] = doc.getElementsByTagName('Company')[0]?.textContent || 'N/A';
            this.properties['Number of Slides'] = doc.getElementsByTagName('Slides')[0]?.textContent || 'N/A';
            this.properties['Number of Hidden Slides'] = doc.getElementsByTagName('HiddenSlides')[0]?.textContent || 'N/A';
            this.properties['Number of Notes'] = doc.getElementsByTagName('Notes')[0]?.textContent || 'N/A';
            this.properties['Number of Paragraphs'] = doc.getElementsByTagName('Paragraphs')[0]?.textContent || 'N/A';
            this.properties['Number of Words'] = doc.getElementsByTagName('Words')[0]?.textContent || 'N/A';
            this.properties['Number of Characters'] = doc.getElementsByTagName('Characters')[0]?.textContent || 'N/A';
            this.properties['Template'] = doc.getElementsByTagName('Template')[0]?.textContent || 'N/A';
        }

        // Number of Slides from presentation.xml
        const presXml = await zip.file('ppt/presentation.xml')?.async('text');
        if (presXml) {
            const doc = new DOMParser().parseFromString(presXml);
            this.properties['Number of Slides (from presentation.xml)'] = doc.getElementsByTagName('p:sldId').length;
        }

        return this.properties;
    }

    printProperties() {
        if (!this.properties) {
            console.log('Properties not read yet.');
            return;
        }
        Object.entries(this.properties).forEach(([key, value]) => {
            console.log(`${key}: ${value}`);
        });
    }

    async writeProperties(newProperties, outputPath) {
        const data = fs.readFileSync(this.filepath);
        const zip = await JSZip.loadAsync(data);

        if (newProperties.core) {
            let coreXml = await zip.file('docProps/core.xml')?.async('text');
            if (coreXml) {
                const doc = new DOMParser().parseFromString(coreXml);
                for (const [key, value] of Object.entries(newProperties.core)) {
                    const node = doc.getElementsByTagNameNS('*', key)[0];
                    if (node) {
                        node.textContent = value;
                    } else {
                        const newElem = doc.createElement(key);
                        newElem.textContent = value;
                        doc.documentElement.appendChild(newElem);
                    }
                }
                coreXml = new XMLSerializer().serializeToString(doc);
                zip.file('docProps/core.xml', coreXml);
            }
        }

        if (newProperties.app) {
            let appXml = await zip.file('docProps/app.xml')?.async('text');
            if (appXml) {
                const doc = new DOMParser().parseFromString(appXml);
                for (const [key, value] of Object.entries(newProperties.app)) {
                    const node = doc.getElementsByTagName(key)[0];
                    if (node) {
                        node.textContent = value;
                    } else {
                        const newElem = doc.createElement(key);
                        newElem.textContent = value;
                        doc.documentElement.appendChild(newElem);
                    }
                }
                appXml = new XMLSerializer().serializeToString(doc);
                zip.file('docProps/app.xml', appXml);
            }
        }

        const newBuffer = await zip.generateAsync({ type: 'nodebuffer' });
        fs.writeFileSync(outputPath, newBuffer);
    }
}

C++ Class for .PPTX Properties
This class uses libzip and tinyxml2 (assume installed) for ZIP and XML handling. It reads, prints, and writes properties.

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

class PPTXHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> properties;

    std::string readZipFile(zip_t* zip, const std::string& name) {
        zip_file_t* file = zip_fopen(zip, name.c_str(), 0);
        if (!file) return "";
        zip_stat_t stat;
        zip_stat_init(&stat);
        zip_stat(zip, name.c_str(), 0, &stat);
        char* buffer = new char[stat.size + 1];
        zip_fread(file, buffer, stat.size);
        buffer[stat.size] = '\0';
        std::string content(buffer);
        delete[] buffer;
        zip_fclose(file);
        return content;
    }

public:
    PPTXHandler(const std::string& filepath) : filepath(filepath) {}

    void readProperties() {
        int err = 0;
        zip_t* zip = zip_open(filepath.c_str(), ZIP_RDONLY, &err);
        if (!zip) return;

        properties["File Extension"] = ".pptx";
        properties["MIME Type"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
        properties["Magic Number"] = "50 4B 03 04";
        properties["Number of Files in ZIP Archive"] = std::to_string(zip_get_num_entries(zip, 0));

        std::string mandatoryParts[4] = {"[Content_Types].xml", "_rels/.rels", "ppt/presentation.xml", "docProps/core.xml"};
        bool allPresent = true;
        for (const auto& part : mandatoryParts) {
            if (zip_name_locate(zip, part.c_str(), 0) < 0) {
                allPresent = false;
                break;
            }
        }
        properties["Mandatory Parts Present"] = allPresent ? "Yes" : "No";

        // Core Metadata
        std::string coreXml = readZipFile(zip, "docProps/core.xml");
        if (!coreXml.empty()) {
            tinyxml2::XMLDocument doc;
            doc.Parse(coreXml.c_str());
            properties["Title"] = doc.FirstChildElement()->FirstChildElement("dc:title") ? doc.FirstChildElement()->FirstChildElement("dc:title")->GetText() : "N/A";
            properties["Subject"] = doc.FirstChildElement()->FirstChildElement("cp:subject") ? doc.FirstChildElement()->FirstChildElement("cp:subject")->GetText() : "N/A";
            properties["Creator"] = doc.FirstChildElement()->FirstChildElement("dc:creator") ? doc.FirstChildElement()->FirstChildElement("dc:creator")->GetText() : "N/A";
            properties["Keywords"] = doc.FirstChildElement()->FirstChildElement("cp:keywords") ? doc.FirstChildElement()->FirstChildElement("cp:keywords")->GetText() : "N/A";
            properties["Description"] = doc.FirstChildElement()->FirstChildElement("dc:description") ? doc.FirstChildElement()->FirstChildElement("dc:description")->GetText() : "N/A";
            properties["Last Modified By"] = doc.FirstChildElement()->FirstChildElement("cp:lastModifiedBy") ? doc.FirstChildElement()->FirstChildElement("cp:lastModifiedBy")->GetText() : "N/A";
            properties["Revision"] = doc.FirstChildElement()->FirstChildElement("cp:revision") ? doc.FirstChildElement()->FirstChildElement("cp:revision")->GetText() : "N/A";
            properties["Created Date"] = doc.FirstChildElement()->FirstChildElement("dcterms:created") ? doc.FirstChildElement()->FirstChildElement("dcterms:created")->GetText() : "N/A";
            properties["Modified Date"] = doc.FirstChildElement()->FirstChildElement("dcterms:modified") ? doc.FirstChildElement()->FirstChildElement("dcterms:modified")->GetText() : "N/A";
        }

        // Extended Metadata
        std::string appXml = readZipFile(zip, "docProps/app.xml");
        if (!appXml.empty()) {
            tinyxml2::XMLDocument doc;
            doc.Parse(appXml.c_str());
            properties["Application"] = doc.FirstChildElement()->FirstChildElement("Application") ? doc.FirstChildElement()->FirstChildElement("Application")->GetText() : "N/A";
            properties["App Version"] = doc.FirstChildElement()->FirstChildElement("AppVersion") ? doc.FirstChildElement()->FirstChildElement("AppVersion")->GetText() : "N/A";
            properties["Company"] = doc.FirstChildElement()->FirstChildElement("Company") ? doc.FirstChildElement()->FirstChildElement("Company")->GetText() : "N/A";
            properties["Number of Slides"] = doc.FirstChildElement()->FirstChildElement("Slides") ? doc.FirstChildElement()->FirstChildElement("Slides")->GetText() : "N/A";
            properties["Number of Hidden Slides"] = doc.FirstChildElement()->FirstChildElement("HiddenSlides") ? doc.FirstChildElement()->FirstChildElement("HiddenSlides")->GetText() : "N/A";
            properties["Number of Notes"] = doc.FirstChildElement()->FirstChildElement("Notes") ? doc.FirstChildElement()->FirstChildElement("Notes")->GetText() : "N/A";
            properties["Number of Paragraphs"] = doc.FirstChildElement()->FirstChildElement("Paragraphs") ? doc.FirstChildElement()->FirstChildElement("Paragraphs")->GetText() : "N/A";
            properties["Number of Words"] = doc.FirstChildElement()->FirstChildElement("Words") ? doc.FirstChildElement()->FirstChildElement("Words")->GetText() : "N/A";
            properties["Number of Characters"] = doc.FirstChildElement()->FirstChildElement("Characters") ? doc.FirstChildElement()->FirstChildElement("Characters")->GetText() : "N/A";
            properties["Template"] = doc.FirstChildElement()->FirstChildElement("Template") ? doc.FirstChildElement()->FirstChildElement("Template")->GetText() : "N/A";
        }

        // Number of Slides from presentation.xml
        std::string presXml = readZipFile(zip, "ppt/presentation.xml");
        if (!presXml.empty()) {
            tinyxml2::XMLDocument doc;
            doc.Parse(presXml.c_str());
            int count = 0;
            for (tinyxml2::XMLElement* elem = doc.FirstChildElement()->FirstChildElement("p:sldIdLst")->FirstChildElement("p:sldId"); elem; elem = elem->NextSiblingElement("p:sldId")) {
                count++;
            }
            properties["Number of Slides (from presentation.xml)"] = std::to_string(count);
        }

        zip_close(zip);
    }

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

    void writeProperties(const std::map<std::string, std::map<std::string, std::string>>& newProperties, const std::string& outputPath) {
        int err = 0;
        zip_t* srcZip = zip_open(filepath.c_str(), ZIP_RDONLY, &err);
        zip_t* destZip = zip_open(outputPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &err);

        for (int i = 0; i < zip_get_num_entries(srcZip, 0); ++i) {
            const char* name = zip_get_name(srcZip, i, 0);
            std::string content = readZipFile(srcZip, name);

            if (std::string(name) == "docProps/core.xml" && newProperties.find("core") != newProperties.end()) {
                tinyxml2::XMLDocument doc;
                doc.Parse(content.c_str());
                for (const auto& prop : newProperties.at("core")) {
                    tinyxml2::XMLElement* elem = doc.FirstChildElement()->FirstChildElement(prop.first.c_str());
                    if (elem) {
                        elem->SetText(prop.second.c_str());
                    } else {
                        tinyxml2::XMLElement* newElem = doc.NewElement(prop.first.c_str());
                        newElem->SetText(prop.second.c_str());
                        doc.FirstChildElement()->InsertEndChild(newElem);
                    }
                }
                tinyxml2::XMLPrinter printer;
                doc.Print(&printer);
                content = printer.CStr();
            } else if (std::string(name) == "docProps/app.xml" && newProperties.find("app") != newProperties.end()) {
                tinyxml2::XMLDocument doc;
                doc.Parse(content.c_str());
                for (const auto& prop : newProperties.at("app")) {
                    tinyxml2::XMLElement* elem = doc.FirstChildElement()->FirstChildElement(prop.first.c_str());
                    if (elem) {
                        elem->SetText(prop.second.c_str());
                    } else {
                        tinyxml2::XMLElement* newElem = doc.NewElement(prop.first.c_str());
                        newElem->SetText(prop.second.c_str());
                        doc.FirstChildElement()->InsertEndChild(newElem);
                    }
                }
                tinyxml2::XMLPrinter printer;
                doc.Print(&printer);
                content = printer.CStr();
            }

            zip_source_t* source = zip_source_buffer(destZip, content.c_str(), content.size(), 0);
            zip_file_add(destZip, name, source, ZIP_FL_OVERWRITE);
        }

        zip_close(srcZip);
        zip_close(destZip);
    }
};