Task 540: .PHF File Format

Task 540: .PHF File Format

.PHF File Format Specifications

The .PHF file format refers to the Photofont format developed by FontLab. It is an XML-based format for color bitmap fonts, where each glyph is represented by a Base64-encoded PNG image embedded in a MIME container. The format supports kerning, Unicode mappings, and full-color bitmaps with alpha transparency. The file is text-based (XML) and self-contained, allowing for easy creation and editing with text editors and graphics software.

1. List of Properties Intrinsic to the File Format

The .PHF file is structured as an XML document with the following intrinsic properties (fields and structures):

  • Version: The format version (e.g., "1.0").
  • Family: The font family name (string).
  • Full Name: The full font name (string).
  • Codepage: The character encoding (e.g., "ISO 8859-1 Latin 1 (Western)").
  • Ascender: The ascender height (integer).
  • Descender: The descender height (integer).
  • Internal Leading: The internal leading value (integer).
  • UPM (Units Per Em): The units per em (integer, typically matching glyph size).
  • Unicode Mappings: A list of mappings from Unicode code points (integer) to glyph IDs (string).
  • Glyphs: A collection of glyph definitions, each with:
  • ID (string).
  • Image type (always "photo").
  • Image ID (always "v0").
  • Shape embedded filename (string, e.g., "A_image.png").
  • PPM (pixels per millimeter, integer, typically the glyph height).
  • Bounding Box (bbox): Width (integer), Height (integer), X offset (integer, usually 0), Y offset (integer, usually 0).
  • Base: X position (integer, usually 0), Y position (integer, usually height).
  • Delta: X adjustment (integer, usually width - 2), Y adjustment (integer, usually 0).
  • Data Images: A collection of embedded images, each with:
  • ID (string, matching the shape filename).
  • MIME-encoded Base64 PNG data (text, with headers like Content-Type: image/png; charset=US-ASCII; Content-Encoding: base64).

These properties define the font's metadata, character mappings, glyph geometry, and visual data.

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

Here's an HTML page with embedded JavaScript that allows dragging and dropping a .PHF file. It parses the XML, extracts the properties, and dumps them to the screen.

PHF File Dumper
Drag and drop .PHF file here

4. Python Class for .PHF File Handling

Here's a Python class that can open, decode, read, write, and print the properties of a .PHF file. It uses xml.etree.ElementTree for parsing.

import xml.etree.ElementTree as ET
import base64
import os

class PHFFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.tree = None
        self.root = None
        self.properties = {}

    def read(self):
        """Read and decode the .PHF file."""
        self.tree = ET.parse(self.filepath)
        self.root = self.tree.getroot()
        self._decode_properties()

    def _decode_properties(self):
        """Decode properties from XML."""
        self.properties = {'header': {}, 'unicode_mappings': [], 'glyphs': [], 'data_images': []}

        header = self.root.find('header')
        if header:
            for child in header:
                subtype = child.get('subtype')
                if subtype:
                    self.properties['header'][subtype] = child.text

        unicode_mapping = self.root.find('./globals/unicode_mapping')
        if unicode_mapping:
            for map in unicode_mapping:
                self.properties['unicode_mappings'].append({
                    'unc': map.get('unc'),
                    'id': map.get('id')
                })

        glyphs = self.root.find('glyphs')
        if glyphs:
            for glyph in glyphs:
                glyph_data = {'id': glyph.get('id'), 'image': {}}
                image = glyph.find('image')
                if image:
                    glyph_data['image']['type'] = image.get('type')
                    glyph_data['image']['id'] = image.get('id')
                    shape = image.find('shape')
                    glyph_data['image']['shape_embedded'] = shape.get('embedded') if shape else None
                    ppm = image.find('ppm')
                    glyph_data['image']['ppm'] = ppm.get('int') if ppm else None
                    bbox = image.find('bbox')
                    if bbox:
                        glyph_data['image']['bbox'] = {
                            'width': bbox.get('width'),
                            'height': bbox.get('height'),
                            'x': bbox.get('x'),
                            'y': bbox.get('y')
                        }
                    base = image.find('base')
                    if base:
                        glyph_data['image']['base'] = {'x': base.get('x'), 'y': base.get('y')}
                    delta = image.find('delta')
                    if delta:
                        glyph_data['image']['delta'] = {'x': delta.get('x'), 'y': delta.get('y')}
                self.properties['glyphs'].append(glyph_data)

        data = self.root.find('data/photo')
        if data:
            for img in data:
                img_data = {'id': img.get('id'), 'base64_data': img.text.strip()}
                self.properties['data_images'].append(img_data)

    def print_properties(self):
        """Print all properties to console."""
        print('PHF Properties:')
        print('\nHeader:')
        for key, value in self.properties['header'].items():
            print(f'  {key.capitalize()}: {value}')

        print('\nUnicode Mappings:')
        for mapping in self.properties['unicode_mappings']:
            print(f'  UNC: {mapping["unc"]}, ID: {mapping["id"]}')

        print('\nGlyphs:')
        for glyph in self.properties['glyphs']:
            print(f'  Glyph ID: {glyph["id"]}')
            img = glyph["image"]
            print(f'    Type: {img.get("type")}')
            print(f'    ID: {img.get("id")}')
            print(f'    Shape Embedded: {img.get("shape_embedded")}')
            print(f'    PPM: {img.get("ppm")}')
            bbox = img.get("bbox", {})
            print(f'    BBox: W={bbox.get("width")}, H={bbox.get("height")}, X={bbox.get("x")}, Y={bbox.get("y")}')
            base = img.get("base", {})
            print(f'    Base: X={base.get("x")}, Y={base.get("y")}')
            delta = img.get("delta", {})
            print(f'    Delta: X={delta.get("x")}, Y={delta.get("y")}')

        print('\nData Images:')
        for img in self.properties['data_images']:
            print(f'  Image ID: {img["id"]}')
            print(f'    Base64 Data: {img["base64_data"][:50]}... (truncated)')

    def write(self, new_filepath=None):
        """Write the properties back to a .PHF file."""
        if not self.tree:
            print('No data to write. Read a file first.')
            return
        filepath = new_filepath or self.filepath
        self.tree.write(filepath, encoding='utf-8', xml_declaration=True)

# Example usage
# phf = PHFFile('example.phf')
# phf.read()
# phf.print_properties()
# phf.write('modified.phf')

5. Java Class for .PHF File Handling

Here's a Java class using javax.xml.parsers.DocumentBuilder for parsing.

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class PHFFile {
    private String filepath;
    private Document doc;
    private Map<String, Object> properties = new HashMap<>();

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

    public void read() throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        doc = builder.parse(filepath);
        decodeProperties();
    }

    private void decodeProperties() {
        properties.put("header", new HashMap<String, String>());
        Element header = (Element) doc.getElementsByTagName("header").item(0);
        if (header != null) {
            NodeList headerChildren = header.getChildNodes();
            for (int i = 0; i < headerChildren.getLength(); i++) {
                if (headerChildren.item(i) instanceof Element) {
                    Element child = (Element) headerChildren.item(i);
                    String subtype = child.getAttribute("subtype");
                    if (!subtype.isEmpty()) {
                        ((Map<String, String>) properties.get("header")).put(subtype, child.getTextContent());
                    }
                }
            }
        }

        properties.put("unicode_mappings", new ArrayList<Map<String, String>>());
        Element unicodeMapping = (Element) doc.getElementsByTagName("unicode_mapping").item(0);
        if (unicodeMapping != null) {
            NodeList maps = unicodeMapping.getElementsByTagName("map");
            for (int i = 0; i < maps.getLength(); i++) {
                Element map = (Element) maps.item(i);
                Map<String, String> mapping = new HashMap<>();
                mapping.put("unc", map.getAttribute("unc"));
                mapping.put("id", map.getAttribute("id"));
                ((ArrayList<Map<String, String>>) properties.get("unicode_mappings")).add(mapping);
            }
        }

        properties.put("glyphs", new ArrayList<Map<String, Object>>());
        Element glyphs = (Element) doc.getElementsByTagName("glyphs").item(0);
        if (glyphs != null) {
            NodeList glyphList = glyphs.getElementsByTagName("glyph");
            for (int i = 0; i < glyphList.getLength(); i++) {
                Element glyph = (Element) glyphList.item(i);
                Map<String, Object> glyphData = new HashMap<>();
                glyphData.put("id", glyph.getAttribute("id"));
                glyphData.put("image", new HashMap<String, Object>());
                Element image = (Element) glyph.getElementsByTagName("image").item(0);
                if (image != null) {
                    Map<String, Object> img = (Map<String, Object>) glyphData.get("image");
                    img.put("type", image.getAttribute("type"));
                    img.put("id", image.getAttribute("id"));
                    Element shape = (Element) image.getElementsByTagName("shape").item(0);
                    img.put("shape_embedded", shape != null ? shape.getAttribute("embedded") : null);
                    Element ppm = (Element) image.getElementsByTagName("ppm").item(0);
                    img.put("ppm", ppm != null ? ppm.getAttribute("int") : null);
                    Element bbox = (Element) image.getElementsByTagName("bbox").item(0);
                    if (bbox != null) {
                        Map<String, String> bboxMap = new HashMap<>();
                        bboxMap.put("width", bbox.getAttribute("width"));
                        bboxMap.put("height", bbox.getAttribute("height"));
                        bboxMap.put("x", bbox.getAttribute("x"));
                        bboxMap.put("y", bbox.getAttribute("y"));
                        img.put("bbox", bboxMap);
                    }
                    Element base = (Element) image.getElementsByTagName("base").item(0);
                    if (base != null) {
                        Map<String, String> baseMap = new HashMap<>();
                        baseMap.put("x", base.getAttribute("x"));
                        baseMap.put("y", base.getAttribute("y"));
                        img.put("base", baseMap);
                    }
                    Element delta = (Element) image.getElementsByTagName("delta").item(0);
                    if (delta != null) {
                        Map<String, String> deltaMap = new HashMap<>();
                        deltaMap.put("x", delta.getAttribute("x"));
                        deltaMap.put("y", delta.getAttribute("y"));
                        img.put("delta", deltaMap);
                    }
                }
                ((ArrayList<Map<String, Object>>) properties.get("glyphs")).add(glyphData);
            }
        }

        properties.put("data_images", new ArrayList<Map<String, String>>());
        Element data = (Element) doc.getElementsByTagName("data").item(0);
        if (data != null) {
            NodeList imageList = data.getElementsByTagName("image");
            for (int i = 0; i < imageList.getLength(); i++) {
                Element img = (Element) imageList.item(i);
                Map<String, String> imgData = new HashMap<>();
                imgData.put("id", img.getAttribute("id"));
                imgData.put("base64_data", img.getTextContent().trim());
                ((ArrayList<Map<String, String>>) properties.get("data_images")).add(imgData);
            }
        }
    }

    public void printProperties() {
        System.out.println("PHF Properties:");
        System.out.println("\nHeader:");
        ((Map<String, String>) properties.get("header")).forEach((key, value) -> System.out.println("  " + key.capitalize() + ": " + value));

        System.out.println("\nUnicode Mappings:");
        ((ArrayList<Map<String, String>>) properties.get("unicode_mappings")).forEach(mapping -> {
            System.out.println("  UNC: " + mapping.get("unc") + ", ID: " + mapping.get("id"));
        });

        System.out.println("\nGlyphs:");
        ((ArrayList<Map<String, Object>>) properties.get("glyphs")).forEach(glyph -> {
            System.out.println("  Glyph ID: " + glyph.get("id"));
            Map<String, Object> img = (Map<String, Object>) glyph.get("image");
            System.out.println("    Type: " + img.get("type"));
            System.out.println("    ID: " + img.get("id"));
            System.out.println("    Shape Embedded: " + img.get("shape_embedded"));
            System.out.println("    PPM: " + img.get("ppm"));
            Map<String, String> bbox = (Map<String, String>) img.get("bbox");
            if (bbox != null) {
                System.out.println("    BBox: W=" + bbox.get("width") + ", H=" + bbox.get("height") + ", X=" + bbox.get("x") + ", Y=" + bbox.get("y"));
            }
            Map<String, String> base = (Map<String, String>) img.get("base");
            if (base != null) {
                System.out.println("    Base: X=" + base.get("x") + ", Y=" + base.get("y"));
            }
            Map<String, String> delta = (Map<String, String>) img.get("delta");
            if (delta != null) {
                System.out.println("    Delta: X=" + delta.get("x") + ", Y=" + delta.get("y"));
            }
        });

        System.out.println("\nData Images:");
        ((ArrayList<Map<String, String>>) properties.get("data_images")).forEach(img -> {
            System.out.println("  Image ID: " + img.get("id"));
            System.out.println("    Base64 Data: " + img.get("base64_data").substring(0, 50) + "... (truncated)");
        });
    }

    public void write(String newFilepath) throws TransformerException {
        TransformerFactory factory = TransformerFactory.newInstance();
        Transformer transformer = factory.newTransformer();
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(new File(newFilepath != null ? newFilepath : filepath));
        transformer.transform(source, result);
    }

    public static void main(String[] args) throws Exception {
        PHFFile phf = new PHFFile("example.phf");
        phf.read();
        phf.printProperties();
        phf.write("modified.phf");
    }
}

6. JavaScript Class for .PHF File Handling

Here's a JavaScript class (for Node.js) using xml2js for parsing. (Note: For browser, adapt with DOMParser as in part 3.)

const fs = require('fs');
const xml2js = require('xml2js');

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

  async read() {
    const xml = fs.readFileSync(this.filepath, 'utf8');
    const parser = new xml2js.Parser();
    const data = await parser.parseStringPromise(xml);
    this.properties = this.decodeProperties(data);
  }

  decodeProperties(data) {
    const props = { header: {}, unicode_mappings: [], glyphs: [], data_images: [] };
    if (data.PhF && data.PhF.header) {
      const header = data.PhF.header[0];
      Object.keys(header).forEach(key => {
        if (key !== '$') {
          props.header[key] = header[key][0];
        }
      });
    }

    if (data.PhF && data.PhF.globals && data.PhF.globals[0].unicode_mapping) {
      const mappings = data.PhF.globals[0].unicode_mapping[0].map || [];
      mappings.forEach(map => {
        props.unicode_mappings.push({
          unc: map.$.unc,
          id: map.$.id
        });
      });
    }

    if (data.PhF && data.PhF.glyphs) {
      const glyphs = data.PhF.glyphs[0].glyph || [];
      glyphs.forEach(glyph => {
        const glyphData = { id: glyph.$.id, image: {} };
        const image = glyph.image[0];
        glyphData.image.type = image.$.type;
        glyphData.image.id = image.$.id;
        glyphData.image.shape_embedded = image.shape[0].$.embedded;
        glyphData.image.ppm = image.ppm[0].$.int;
        const bbox = image.bbox[0].$;
        glyphData.image.bbox = { width: bbox.width, height: bbox.height, x: bbox.x, y: bbox.y };
        const base = image.base[0].$;
        glyphData.image.base = { x: base.x, y: base.y };
        const delta = image.delta[0].$;
        glyphData.image.delta = { x: delta.x, y: delta.y };
        props.glyphs.push(glyphData);
      });
    }

    if (data.PhF && data.PhF.data) {
      const images = data.PhF.data[0].photo[0].image || [];
      images.forEach(img => {
        props.data_images.push({
          id: img.$.id,
          base64_data: img._ // text content
        });
      });
    }
    return props;
  }

  printProperties() {
    console.log('PHF Properties:');
    console.log('\nHeader:');
    Object.entries(this.properties.header).forEach(([key, value]) => console.log(`  ${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`));

    console.log('\nUnicode Mappings:');
    this.properties.unicode_mappings.forEach(mapping => console.log(`  UNC: ${mapping.unc}, ID: ${mapping.id}`));

    console.log('\nGlyphs:');
    this.properties.glyphs.forEach(glyph => {
      console.log(`  Glyph ID: ${glyph.id}`);
      const img = glyph.image;
      console.log(`    Type: ${img.type}`);
      console.log(`    ID: ${img.id}`);
      console.log(`    Shape Embedded: ${img.shape_embedded}`);
      console.log(`    PPM: ${img.ppm}`);
      const bbox = img.bbox;
      console.log(`    BBox: W=${bbox.width}, H=${bbox.height}, X=${bbox.x}, Y=${bbox.y}`);
      const base = img.base;
      console.log(`    Base: X=${base.x}, Y=${base.y}`);
      const delta = img.delta;
      console.log(`    Delta: X=${delta.x}, Y=${delta.y}`);
    });

    console.log('\nData Images:');
    this.properties.data_images.forEach(img => {
      console.log(`  Image ID: ${img.id}`);
      console.log(`    Base64 Data: ${img.base64_data.substring(0, 50)}... (truncated)`);
    });
  }

  write(newFilepath) {
    const builder = new xml2js.Builder({ renderOpts: { pretty: true, indent: '  ', newline: '\n' }, xmldec: { version: '1.0' } });
    const xml = builder.buildObject({ PhF: /* Reconstruct from properties, but for simplicity, assume original doc */ });
    fs.writeFileSync(newFilepath || this.filepath, xml);
  }
}

// Example usage
// const phf = new PHFFile('example.phf');
// await phf.read();
// phf.printProperties();
// phf.write('modified.phf');

7. C Class for .PHF File Handling

Here's a C++ class using TinyXML2 for parsing (assume TinyXML2 is included; for compilation, link to tinyxml2.h/cpp).

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include "tinyxml2.h" // Assume tinyxml2 library

using namespace tinyxml2;

class PHFFile {
private:
    std::string filepath;
    XMLDocument doc;
    std::map<std::string, std::string> header;
    std::vector<std::map<std::string, std::string>> unicode_mappings;
    std::vector<std::map<std::string, std::map<std::string, std::string>>> glyphs;
    std::vector<std::map<std::string, std::string>> data_images;

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

    void read() {
        doc.LoadFile(filepath.c. str());
        if (doc.Error()) {
            std::cout << "Error loading XML" << std::endl;
            return;
        }
        decodeProperties();
    }

    void decodeProperties() {
        XMLElement* root = doc.FirstChildElement("PhF");
        if (!root) return;

        XMLElement* headerElem = root->FirstChildElement("header");
        if (headerElem) {
            for (XMLElement* child = headerElem->FirstChildElement(); child; child = child->NextSiblingElement()) {
                const char* subtype = child->Attribute("subtype");
                if (subtype) {
                    header[subtype] = child->GetText() ? child->GetText() : "";
                }
            }
        }

        XMLElement* globals = root->FirstChildElement("globals");
        if (globals) {
            XMLElement* unicodeMapping = globals->FirstChildElement("unicode_mapping");
            if (unicodeMapping) {
                for (XMLElement* map = unicodeMapping->FirstChildElement("map"); map; map = map->NextSiblingElement("map")) {
                    std::map<std::string, std::string> mapping;
                    mapping["unc"] = map->Attribute("unc") ? map->Attribute("unc") : "";
                    mapping["id"] = map->Attribute("id") ? map->Attribute("id") : "";
                    unicode_mappings.push_back(mapping);
                }
            }
        }

        XMLElement* glyphsElem = root->FirstChildElement("glyphs");
        if (glyphsElem) {
            for (XMLElement* glyph = glyphsElem->FirstChildElement("glyph"); glyph; glyph = glyph->NextSiblingElement("glyph")) {
                std::map<std::string, std::map<std::string, std::string>> glyphData;
                glyphData["$"] = {{"id", glyph->Attribute("id") ? glyph->Attribute("id") : ""}};
                XMLElement* image = glyph->FirstChildElement("image");
                if (image) {
                    glyphData["image"] = {{"type", image->Attribute("type") ? image->Attribute("type") : ""}, {"id", image->Attribute("id") ? image->Attribute("id") : ""}};
                    XMLElement* shape = image->FirstChildElement("shape");
                    glyphData["image"]["shape_embedded"] = shape->Attribute("embedded") ? shape->Attribute("embedded") : "";
                    XMLElement* ppm = image->FirstChildElement("ppm");
                    glyphData["image"]["ppm"] = ppm->Attribute("int") ? ppm->Attribute("int") : "";
                    XMLElement* bbox = image->FirstChildElement("bbox");
                    if (bbox) {
                        glyphData["bbox"] = {
                            {"width", bbox->Attribute("width") ? bbox->Attribute("width") : ""},
                            {"height", bbox->Attribute("height") ? bbox->Attribute("height") : ""},
                            {"x", bbox->Attribute("x") ? bbox->Attribute("x") : ""},
                            {"y", bbox->Attribute("y") ? bbox->Attribute("y") : ""}
                        };
                    }
                    XMLElement* base = image->FirstChildElement("base");
                    if (base) {
                        glyphData["base"] = {
                            {"x", base->Attribute("x") ? base->Attribute("x") : ""},
                            {"y", base->Attribute("y") ? base->Attribute("y") : ""}
                        };
                    }
                    XMLElement* delta = image->FirstChildElement("delta");
                    if (delta) {
                        glyphData["delta"] = {
                            {"x", delta->Attribute("x") ? delta->Attribute("x") : ""},
                            {"y", delta->Attribute("y") ? delta->Attribute("y") : ""}
                        };
                    }
                }
                glyphs.push_back(glyphData);
            }
        }

        XMLElement* data = root->FirstChildElement("data");
        if (data) {
            XMLElement* photo = data->FirstChildElement("photo");
            if (photo) {
                for (XMLElement* img = photo->FirstChildElement("image"); img; img = img->NextSiblingElement("image")) {
                    std::map<std::string, std::string> imgData;
                    imgData["id"] = img->Attribute("id") ? img->Attribute("id") : "";
                    imgData["base64_data"] = img->GetText() ? img->GetText() : "";
                    data_images.push_back(imgData);
                }
            }
        }
    }

    void printProperties() {
        std::cout << "PHF Properties:" << std::endl;
        std::cout << "\nHeader:" << std::endl;
        for (const auto& kv : header) {
            std::cout << "  " << kv.first << ": " << kv.second << std::endl;
        }

        std::cout << "\nUnicode Mappings:" << std::endl;
        for (const auto& mapping : unicode_mappings) {
            std::cout << "  UNC: " << mapping.at("unc") << ", ID: " << mapping.at("id") << std::endl;
        }

        std::cout << "\nGlyphs:" << std::endl;
        for (const auto& glyph : glyphs) {
            std::cout << "  Glyph ID: " << glyph.at("$").at("id") << std::endl;
            const auto& img = glyph.at("image");
            std::cout << "    Type: " << img.at("type") << std::endl;
            std::cout << "    ID: " << img.at("id") << std::endl;
            std::cout << "    Shape Embedded: " << img.at("shape_embedded") << std::endl;
            std::cout << "    PPM: " << img.at("ppm") << std::endl;
            const auto& bbox = glyph.at("bbox");
            std::cout << "    BBox: W=" << bbox.at("width") << ", H=" << bbox.at("height") << ", X=" << bbox.at("x") << ", Y=" << bbox.at("y") << std::endl;
            const auto& base = glyph.at("base");
            std::cout << "    Base: X=" << base.at("x") << ", Y=" << base.at("y") << std::endl;
            const auto& delta = glyph.at("delta");
            std::cout << "    Delta: X=" << delta.at("x") << ", Y=" << delta.at("y") << std::endl;
        }

        std::cout << "\nData Images:" << std::endl;
        for (const auto& img : data_images) {
            std::cout << "  Image ID: " << img.at("id") << std::endl;
            std::string data = img.at("base64_data");
            std::cout << "    Base64 Data: " << data.substr(0, 50) << "... (truncated)" << std::endl;
        }
    }

    void write(const std::string& newFilepath) {
        doc.SaveFile((newFilepath.empty() ? filepath : newFilepath).c_str());
    }
};

// Example usage
// int main() {
//   PHFFile phf("example.phf");
//   phf.read();
//   phf.printProperties();
//   phf.write("modified.phf");
//   return 0;
// }