Task 182: .EP File Format

Task 182: .EP File Format

File Format Specifications for .EP

The .EP file format is used by Pencil Project (now known as Evolus Pencil), an open-source GUI prototyping and diagramming tool. It is an XML-based format for storing document data, including pages, shapes, and their properties. The format is not formally standardized but is defined by the application's implementation. The file is plain text (ASCII/UTF-8) and can be edited with any text editor, though it's intended for use with the Pencil application. The structure is as follows:

  • File header: Starts with an XML declaration, e.g., <?xml version="1.0" encoding="UTF-8"?>.
  • Root element: <Document xmlns="http://www.evolus.vn/Namespace/Pencil">.
  • Optional <Properties> element for document-level properties (often empty).
  • <Pages> element containing one or more <Page> elements.
  • Each <Page> has:
  • Attributes like id.
  • <Properties> with child <Property> elements for page-specific settings (name, type, value).
  • <Content> containing shapes (<Shape> elements with their own properties and definitions).
  • Namespaces include xmlns:p="http://www.evolus.vn/Namespace/Pencil" for referencing stencil definitions.

The format allows for dynamic properties via <Property> elements, which have name and type attributes, and text content as the value.

  1. List of all the properties of this file format intrinsic to its file system:
  • XML Version (from the declaration, e.g., "1.0")
  • Encoding (from the declaration, e.g., "UTF-8")
  • Namespace (the main namespace URI, "http://www.evolus.vn/Namespace/Pencil")
  • Document Properties (key-value pairs from document-level <Properties>, if present; e.g., custom metadata)
  • Number of Pages (count of <Page> elements)
  • Page ID (per page, from id attribute)
  • Page Name (per page, from <Property name="name" type="String">)
  • Page Width (per page, from <Property name="width" type="Dimension">)
  • Page Height (per page, from <Property name="height" type="Dimension">)
  • Page Background Color (per page, from <Property name="backgroundColor" type="Color">)
  • Page Scale (per page, from <Property name="scale" type="Real">)
  • Page Note (per page, from <Property name="note" type="PlainText">)

These are the core structural properties derived from the XML schema used in the format. Shape-specific properties (e.g., fillColor, box) are not listed as "intrinsic" here, as they are variable and content-dependent.

Two direct download links for files of format .EP:

Ghost blog embedded HTML JavaScript for drag and drop .EP file dump:

.EP File Properties Dumper
Drag and drop .EP file here
  1. Python class for .EP file handling:
import xml.etree.ElementTree as ET

class EPFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.tree = None
        self.root = None

    def open(self):
        self.tree = ET.parse(self.filepath)
        self.root = self.tree.getroot()

    def read_properties(self):
        if not self.root:
            raise ValueError("File not opened")
        properties = {
            'xml_version': self.tree.docinfo.xml_version or '1.0',
            'encoding': self.tree.docinfo.encoding or 'UTF-8',
            'namespace': self.root.attrib.get('xmlns', 'http://www.evolus.vn/Namespace/Pencil'),
            'document_properties': {},
            'number_of_pages': 0,
            'pages': []
        }

        doc_props = self.root.find('Properties')
        if doc_props is not None:
            for prop in doc_props.findall('Property'):
                name = prop.attrib.get('name')
                if name:
                    properties['document_properties'][name] = prop.text or ''

        pages = self.root.findall('.//Page')
        properties['number_of_pages'] = len(pages)
        for page in pages:
            page_info = {
                'id': page.attrib.get('id', 'N/A'),
                'name': 'N/A',
                'width': 'N/A',
                'height': 'N/A',
                'backgroundColor': 'N/A',
                'scale': 'N/A',
                'note': 'N/A'
            }
            page_props = page.find('Properties')
            if page_props is not None:
                for prop in page_props.findall('Property'):
                    name = prop.attrib.get('name')
                    if name in page_info:
                        page_info[name] = prop.text or 'N/A'
            properties['pages'].append(page_info)
        return properties

    def print_properties(self):
        props = self.read_properties()
        print("Properties of .EP file:")
        print(f"XML Version: {props['xml_version']}")
        print(f"Encoding: {props['encoding']}")
        print(f"Namespace: {props['namespace']}")
        print("Document Properties:")
        for key, val in props['document_properties'].items():
            print(f"  - {key}: {val}")
        if not props['document_properties']:
            print("  None")
        print(f"Number of Pages: {props['number_of_pages']}")
        for i, page in enumerate(props['pages'], 1):
            print(f"\nPage {i}:")
            print(f"  ID: {page['id']}")
            print(f"  Name: {page['name']}")
            print(f"  Width: {page['width']}")
            print(f"  Height: {page['height']}")
            print(f"  Background Color: {page['backgroundColor']}")
            print(f"  Scale: {page['scale']}")
            print(f"  Note: {page['note']}")

    def write(self, new_properties):
        # Basic write: Update page names as example; extend as needed
        if not self.root:
            raise ValueError("File not opened")
        pages = self.root.findall('.//Page')
        if 'pages' in new_properties and len(new_properties['pages']) == len(pages):
            for page, new_page in zip(pages, new_properties['pages']):
                page_props = page.find('Properties')
                if page_props is not None:
                    name_prop = page_props.find("Property[@name='name']")
                    if name_prop is not None:
                        name_prop.text = new_page.get('name', name_prop.text)
        self.tree.write(self.filepath, encoding='utf-8', xml_declaration=True)

# Example usage:
# handler = EPFileHandler('example.ep')
# handler.open()
# handler.print_properties()
# handler.write({'pages': [{'name': 'New Name'}]})
  1. Java class for .EP file handling:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class EPFileHandler {
    private String filepath;
    private Document doc;

    public EPFileHandler(String filepath) {
        this.filepath = filepath;
    }

    public void open() throws Exception {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        this.doc = dBuilder.parse(new File(filepath));
        this.doc.getDocumentElement().normalize();
    }

    private Map<String, Object> readProperties() {
        Map<String, Object> properties = new HashMap<>();
        properties.put("xml_version", "1.0"); // Fixed, as Java doesn't expose easily
        properties.put("encoding", "UTF-8"); // Fixed
        properties.put("namespace", doc.getDocumentElement().getAttribute("xmlns"));

        Map<String, String> docProps = new HashMap<>();
        Element docPropertiesEl = (Element) doc.getElementsByTagName("Properties").item(0);
        if (docPropertiesEl != null) {
            NodeList propList = docPropertiesEl.getElementsByTagName("Property");
            for (int i = 0; i < propList.getLength(); i++) {
                Element prop = (Element) propList.item(i);
                String name = prop.getAttribute("name");
                if (!name.isEmpty()) {
                    docProps.put(name, prop.getTextContent());
                }
            }
        }
        properties.put("document_properties", docProps);

        NodeList pages = doc.getElementsByTagName("Page");
        properties.put("number_of_pages", pages.getLength());
        List<Map<String, String>> pageList = new ArrayList<>();
        for (int i = 0; i < pages.getLength(); i++) {
            Element page = (Element) pages.item(i);
            Map<String, String> pageInfo = new HashMap<>();
            pageInfo.put("id", page.getAttribute("id"));
            pageInfo.put("name", "N/A");
            pageInfo.put("width", "N/A");
            pageInfo.put("height", "N/A");
            pageInfo.put("backgroundColor", "N/A");
            pageInfo.put("scale", "N/A");
            pageInfo.put("note", "N/A");
            Element pagePropsEl = (Element) page.getElementsByTagName("Properties").item(0);
            if (pagePropsEl != null) {
                NodeList propList = pagePropsEl.getElementsByTagName("Property");
                for (int j = 0; j < propList.getLength(); j++) {
                    Element prop = (Element) propList.item(j);
                    String name = prop.getAttribute("name");
                    if (pageInfo.containsKey(name)) {
                        pageInfo.put(name, prop.getTextContent());
                    }
                }
            }
            pageList.add(pageInfo);
        }
        properties.put("pages", pageList);
        return properties;
    }

    public void printProperties() {
        Map<String, Object> props = readProperties();
        System.out.println("Properties of .EP file:");
        System.out.println("XML Version: " + props.get("xml_version"));
        System.out.println("Encoding: " + props.get("encoding"));
        System.out.println("Namespace: " + props.get("namespace"));
        System.out.println("Document Properties:");
        Map<String, String> docProps = (Map<String, String>) props.get("document_properties");
        if (docProps.isEmpty()) {
            System.out.println("  None");
        } else {
            for (Map.Entry<String, String> entry : docProps.entrySet()) {
                System.out.println("  - " + entry.getKey() + ": " + entry.getValue());
            }
        }
        System.out.println("Number of Pages: " + props.get("number_of_pages"));
        List<Map<String, String>> pages = (List<Map<String, String>>) props.get("pages");
        for (int i = 0; i < pages.size(); i++) {
            Map<String, String> page = pages.get(i);
            System.out.println("\nPage " + (i + 1) + ":");
            System.out.println("  ID: " + page.get("id"));
            System.out.println("  Name: " + page.get("name"));
            System.out.println("  Width: " + page.get("width"));
            System.out.println("  Height: " + page.get("height"));
            System.out.println("  Background Color: " + page.get("backgroundColor"));
            System.out.println("  Scale: " + page.get("scale"));
            System.out.println("  Note: " + page.get("note"));
        }
    }

    public void write(Map<String, Object> newProperties) throws Exception {
        // Basic write: Update page names as example
        NodeList pages = doc.getElementsByTagName("Page");
        List<Map<String, String>> newPages = (List<Map<String, String>>) newProperties.get("pages");
        if (newPages != null && newPages.size() == pages.getLength()) {
            for (int i = 0; i < pages.getLength(); i++) {
                Element page = (Element) pages.item(i);
                Element pagePropsEl = (Element) page.getElementsByTagName("Properties").item(0);
                if (pagePropsEl != null) {
                    NodeList propList = pagePropsEl.getElementsByTagName("Property");
                    for (int j = 0; j < propList.getLength(); j++) {
                        Element prop = (Element) propList.item(j);
                        if ("name".equals(prop.getAttribute("name"))) {
                            prop.setTextContent(newPages.get(i).get("name"));
                        }
                    }
                }
            }
        }
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(new File(filepath));
        transformer.transform(source, result);
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     EPFileHandler handler = new EPFileHandler("example.ep");
    //     handler.open();
    //     handler.printProperties();
    //     Map<String, Object> newProps = new HashMap<>();
    //     List<Map<String, String>> pages = new ArrayList<>();
    //     Map<String, String> page1 = new HashMap<>();
    //     page1.put("name", "New Name");
    //     pages.add(page1);
    //     newProps.put("pages", pages);
    //     handler.write(newProps);
    // }
}
  1. JavaScript class for .EP file handling (Node.js, using xml2js for parsing):
const fs = require('fs');
const xml2js = require('xml2js');

class EPFileHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.xmlData = null;
    }

    open(callback) {
        fs.readFile(this.filepath, 'utf8', (err, data) => {
            if (err) return callback(err);
            const parser = new xml2js.Parser({ explicitArray: false });
            parser.parseString(data, (err, result) => {
                if (err) return callback(err);
                this.xmlData = result;
                callback(null);
            });
        });
    }

    readProperties() {
        if (!this.xmlData) throw new Error('File not opened');
        const root = this.xmlData.Document || {};
        const properties = {
            xml_version: '1.0', // Assumed
            encoding: 'UTF-8', // Assumed
            namespace: root['$'] ? root['$'].xmlns : 'http://www.evolus.vn/Namespace/Pencil',
            document_properties: {},
            number_of_pages: 0,
            pages: []
        };

        if (root.Properties && root.Properties.Property) {
            const docProps = Array.isArray(root.Properties.Property) ? root.Properties.Property : [root.Properties.Property];
            docProps.forEach(prop => {
                const name = prop['$'].name;
                if (name) {
                    properties.document_properties[name] = prop._ || prop['#text'] || '';
                }
            });
        }

        let pages = root.Pages ? (Array.isArray(root.Pages.Page) ? root.Pages.Page : [root.Pages.Page]) : [];
        properties.number_of_pages = pages.length;
        pages.forEach(page => {
            const pageInfo = {
                id: page['$'] ? page['$'].id : 'N/A',
                name: 'N/A',
                width: 'N/A',
                height: 'N/A',
                backgroundColor: 'N/A',
                scale: 'N/A',
                note: 'N/A'
            };
            if (page.Properties && page.Properties.Property) {
                const pageProps = Array.isArray(page.Properties.Property) ? page.Properties.Property : [page.Properties.Property];
                pageProps.forEach(prop => {
                    const name = prop['$'].name;
                    if (name in pageInfo) {
                        pageInfo[name] = prop._ || prop['#text'] || 'N/A';
                    }
                });
            }
            properties.pages.push(pageInfo);
        });
        return properties;
    }

    printProperties() {
        const props = this.readProperties();
        console.log('Properties of .EP file:');
        console.log(`XML Version: ${props.xml_version}`);
        console.log(`Encoding: ${props.encoding}`);
        console.log(`Namespace: ${props.namespace}`);
        console.log('Document Properties:');
        const docProps = props.document_properties;
        if (Object.keys(docProps).length === 0) {
            console.log('  None');
        } else {
            for (const [key, val] of Object.entries(docProps)) {
                console.log(`  - ${key}: ${val}`);
            }
        }
        console.log(`Number of Pages: ${props.number_of_pages}`);
        props.pages.forEach((page, index) => {
            console.log(`\nPage ${index + 1}:`);
            console.log(`  ID: ${page.id}`);
            console.log(`  Name: ${page.name}`);
            console.log(`  Width: ${page.width}`);
            console.log(`  Height: ${page.height}`);
            console.log(`  Background Color: ${page.backgroundColor}`);
            console.log(`  Scale: ${page.scale}`);
            console.log(`  Note: ${page.note}`);
        });
    }

    write(newProperties, callback) {
        // Basic write: Update page names as example
        if (!this.xmlData) throw new Error('File not opened');
        const pages = this.xmlData.Document.Pages.Page;
        const newPages = newProperties.pages || [];
        if (Array.isArray(pages) && pages.length === newPages.length) {
            pages.forEach((page, i) => {
                if (page.Properties && page.Properties.Property) {
                    let props = Array.isArray(page.Properties.Property) ? page.Properties.Property : [page.Properties.Property];
                    props = props.map(prop => {
                        if (prop['$'].name === 'name') {
                            prop._ = newPages[i].name;
                        }
                        return prop;
                    });
                    page.Properties.Property = props;
                }
            });
        }
        const builder = new xml2js.Builder({ renderOpts: { pretty: true, indent: '  ', newline: '\n' }, xmldec: { version: '1.0', encoding: 'UTF-8' } });
        const xml = builder.buildObject(this.xmlData);
        fs.writeFile(this.filepath, xml, callback);
    }
}

// Example usage:
// const handler = new EPFileHandler('example.ep');
// handler.open((err) => {
//     if (err) console.error(err);
//     handler.printProperties();
//     handler.write({ pages: [{ name: 'New Name' }] }, (err) => {
//         if (err) console.error(err);
//     });
// });
  1. C class (using C++ for XML parsing with tinyxml2 library, assume included):
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include "tinyxml2.h"  // Assume tinyxml2.h is included for XML parsing

using namespace tinyxml2;

class EPFileHandler {
private:
    std::string filepath;
    XMLDocument doc;

public:
    EPFileHandler(const std::string& fp) : filepath(fp) {}

    bool open() {
        return doc.LoadFile(filepath.c_str()) == XML_SUCCESS;
    }

    std::map<std::string, std::string> readDocumentProperties(XMLElement* propsEl) {
        std::map<std::string, std::string> props;
        if (propsEl) {
            for (XMLElement* prop = propsEl->FirstChildElement("Property"); prop; prop = prop->NextSiblingElement("Property")) {
                const char* name = prop->Attribute("name");
                if (name) {
                    props[name] = prop->GetText() ? prop->GetText() : "";
                }
            }
        }
        return props;
    }

    void printProperties() {
        if (doc.Error()) {
            std::cerr << "Error loading file" << std::endl;
            return;
        }
        XMLElement* root = doc.RootElement();
        if (!root) return;

        std::cout << "Properties of .EP file:" << std::endl;
        std::cout << "XML Version: 1.0" << std::endl;  // Assumed
        std::cout << "Encoding: UTF-8" << std::endl;  // Assumed
        std::cout << "Namespace: " << (root->Attribute("xmlns") ? root->Attribute("xmlns") : "http://www.evolus.vn/Namespace/Pencil") << std::endl;

        auto docProps = readDocumentProperties(root->FirstChildElement("Properties"));
        std::cout << "Document Properties:" << std::endl;
        if (docProps.empty()) {
            std::cout << "  None" << std::endl;
        } else {
            for (const auto& kv : docProps) {
                std::cout << "  - " << kv.first << ": " << kv.second << std::endl;
            }
        }

        int numPages = 0;
        std::vector<std::map<std::string, std::string>> pages;
        for (XMLElement* page = root->FirstChildElement("Pages")->FirstChildElement("Page"); page; page = page->NextSiblingElement("Page")) {
            numPages++;
            std::map<std::string, std::string> pageInfo;
            pageInfo["id"] = page->Attribute("id") ? page->Attribute("id") : "N/A";
            pageInfo["name"] = "N/A";
            pageInfo["width"] = "N/A";
            pageInfo["height"] = "N/A";
            pageInfo["backgroundColor"] = "N/A";
            pageInfo["scale"] = "N/A";
            pageInfo["note"] = "N/A";

            auto pageProps = readDocumentProperties(page->FirstChildElement("Properties"));
            for (const auto& kv : pageProps) {
                if (pageInfo.count(kv.first)) {
                    pageInfo[kv.first] = kv.second;
                }
            }
            pages.push_back(pageInfo);
        }
        std::cout << "Number of Pages: " << numPages << std::endl;
        for (size_t i = 0; i < pages.size(); ++i) {
            auto& page = pages[i];
            std::cout << "\nPage " << (i + 1) << ":" << std::endl;
            std::cout << "  ID: " << page["id"] << std::endl;
            std::cout << "  Name: " << page["name"] << std::endl;
            std::cout << "  Width: " << page["width"] << std::endl;
            std::cout << "  Height: " << page["height"] << std::endl;
            std::cout << "  Background Color: " << page["backgroundColor"] << std::endl;
            std::cout << "  Scale: " << page["scale"] << std::endl;
            std::cout << "  Note: " << page["note"] << std::endl;
        }
    }

    bool write(const std::vector<std::map<std::string, std::string>>& newPages) {
        // Basic write: Update page names
        XMLElement* pagesEl = doc.RootElement()->FirstChildElement("Pages");
        if (!pagesEl) return false;
        int index = 0;
        for (XMLElement* page = pagesEl->FirstChildElement("Page"); page; page = page->NextSiblingElement("Page")) {
            if (index >= newPages.size()) break;
            XMLElement* propsEl = page->FirstChildElement("Properties");
            if (propsEl) {
                for (XMLElement* prop = propsEl->FirstChildElement("Property"); prop; prop = prop->NextSiblingElement("Property")) {
                    if (std::string(prop->Attribute("name")) == "name") {
                        prop->SetText(newPages[index]["name"].c_str());
                    }
                }
            }
            index++;
        }
        return doc.SaveFile(filepath.c_str()) == XML_SUCCESS;
    }
};

// Example usage:
// int main() {
//     EPFileHandler handler("example.ep");
//     if (handler.open()) {
//         handler.printProperties();
//         std::vector<std::map<std::string, std::string>> newPages = {{{"name", "New Name"}}};
//         handler.write(newPages);
//     }
//     return 0;
// }