Task 627: .RTF File Format

Task 627: .RTF File Format

RTF File Format Specifications

The Rich Text Format (RTF) is a proprietary document file format developed by Microsoft Corporation, with the latest specification being version 1.9.1 released in March 2008. The official specification document can be downloaded from Microsoft at https://go.microsoft.com/fwlink/?LinkID=120977. It describes RTF as a text-based format using 7-bit ASCII with control words (starting with \), groups enclosed in {}, and delimiters. The format supports text formatting, images, tables, and metadata, with backward compatibility across versions. RTF files always begin with {\rtf followed by the version number (e.g., {\rtf1). The structure includes a header defining global settings (character sets, fonts, colors), followed by document content in groups.

List of Properties Intrinsic to the RTF File Format
Based on the RTF specification, the following are key properties that can be extracted from an RTF file. These include header declarations and document metadata from the \info group. They are "intrinsic" in that they define the file's structure, encoding, and content metadata, independent of the file system but specific to the format:

  • RTF Version (\rtfN)
  • Character Set (\ansi, \mac, \pc, or \pca)
  • ANSI Code Page (\ansicpgN)
  • Default Font (\deffN)
  • Default Language (\deflangN)
  • Font Table (list of font entries with numbers, families, charsets, and names from {\fonttbl ...})
  • Color Table (list of RGB color entries from {\colortbl ...})
  • Stylesheet (list of style definitions from {\stylesheet ...})
  • Title (from \info {\title ...})
  • Subject (from \info {\subject ...})
  • Author (from \info {\author ...})
  • Keywords (from \info {\keywords ...})
  • Comment (from \info {\comment ...})
  • Operator (from \info {\operator ...})
  • Creation Time (from \info {\creatim \yrN \moN \dyN \hrN \minN})
  • Revision Time (from \info {\revtim \yrN \moN \dyN \hrN \minN})
  • Print Time (from \info {\printim \yrN \moN \dyN \hrN \minN})
  • Backup Time (from \info {\buptim \yrN \moN \dyN \hrN \minN})
  • Total Editing Time in Minutes (\edminsN)
  • Number of Pages (\nofpagesN)
  • Number of Words (\nofwordsN)
  • Number of Characters (\nofcharsN)
  • Number of Characters Including Spaces (\nofcharswsN)
  • Internal Version Number (\vernN)
  • Document ID (\idN)

Two Direct Download Links for .RTF Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop RTF Property Dump
The following is a self-contained HTML snippet with embedded JavaScript that can be inserted into a Ghost blog post (e.g., via the HTML card). It creates a drop zone where users can drag and drop an .RTF file. The script reads the file as text, performs basic parsing to extract the listed properties, and dumps them to the screen in a readable format. Note: This is a simple parser using regex and string matching; it handles basic cases but may not cover deeply nested or malformed RTF.

Drag and drop an .RTF file here

Python Class for RTF Handling
The following Python class can open an RTF file, read and decode the properties, print them to console, and write the file (unmodified for simplicity; extend write for modifications). It uses regex for basic parsing.

import re

class RtfHandler:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.text = None
        self.properties = {}

    def open(self, filepath):
        self.filepath = filepath
        with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
            self.text = f.read()
        self.decode_properties()

    def decode_properties(self):
        if not self.text:
            return

        # RTF Version
        version_match = re.search(r'\\rtf(\d+)', self.text)
        self.properties['RTF Version'] = version_match.group(1) if version_match else 'Unknown'

        # Character Set
        charset_match = re.search(r'\\(ansi|mac|pc|pca)', self.text)
        self.properties['Character Set'] = charset_match.group(1) if charset_match else 'Unknown'

        # ANSI Code Page
        ansicpg_match = re.search(r'\\ansicpg(\d+)', self.text)
        self.properties['ANSI Code Page'] = ansicpg_match.group(1) if ansicpg_match else 'Unknown'

        # Default Font
        deff_match = re.search(r'\\deff(\d+)', self.text)
        self.properties['Default Font'] = deff_match.group(1) if deff_match else 'Unknown'

        # Default Language
        deflang_match = re.search(r'\\deflang(\d+)', self.text)
        self.properties['Default Language'] = deflang_match.group(1) if deflang_match else 'Unknown'

        # Font Table
        fonttbl_match = re.search(r'{\\fonttbl([^}]+)}', self.text)
        self.properties['Font Table'] = fonttbl_match.group(1).strip().split(';') if fonttbl_match else 'Unknown'

        # Color Table
        colortbl_match = re.search(r'{\\colortbl([^}]+)}', self.text)
        self.properties['Color Table'] = colortbl_match.group(1).strip().split(';') if colortbl_match else 'Unknown'

        # Stylesheet
        stylesheet_match = re.search(r'{\\stylesheet([^}]+)}', self.text)
        self.properties['Stylesheet'] = stylesheet_match.group(1).strip() if stylesheet_match else 'Unknown'

        # \info group
        info_match = re.search(r'{\\info([^}]+)}', self.text)
        if info_match:
            info_text = info_match.group(1)
            self.properties['Title'] = (re.search(r'{\\title([^}]+)}', info_text) or [None, 'Unknown'])[1]
            self.properties['Subject'] = (re.search(r'{\\subject([^}]+)}', info_text) or [None, 'Unknown'])[1]
            self.properties['Author'] = (re.search(r'{\\author([^}]+)}', info_text) or [None, 'Unknown'])[1]
            self.properties['Keywords'] = (re.search(r'{\\keywords([^}]+)}', info_text) or [None, 'Unknown'])[1]
            self.properties['Comment'] = (re.search(r'{\\comment([^}]+)}', info_text) or [None, 'Unknown'])[1]
            self.properties['Operator'] = (re.search(r'{\\operator([^}]+)}', info_text) or [None, 'Unknown'])[1]

            creatim_match = re.search(r'{\\creatim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)}', info_text)
            self.properties['Creation Time'] = f"{creatim_match.group(1)}-{creatim_match.group(2)}-{creatim_match.group(3)} {creatim_match.group(4)}:{creatim_match.group(5)}" if creatim_match else 'Unknown'

            revtim_match = re.search(r'{\\revtim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)}', info_text)
            self.properties['Revision Time'] = f"{revtim_match.group(1)}-{revtim_match.group(2)}-{revtim_match.group(3)} {revtim_match.group(4)}:{revtim_match.group(5)}" if revtim_match else 'Unknown'

            printim_match = re.search(r'{\\printim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)}', info_text)
            self.properties['Print Time'] = f"{printim_match.group(1)}-{printim_match.group(2)}-{printim_match.group(3)} {printim_match.group(4)}:{printim_match.group(5)}" if printim_match else 'Unknown'

            buptim_match = re.search(r'{\\buptim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)}', info_text)
            self.properties['Backup Time'] = f"{buptim_match.group(1)}-{buptim_match.group(2)}-{buptim_match.group(3)} {buptim_match.group(4)}:{buptim_match.group(5)}" if buptim_match else 'Unknown'

            self.properties['Total Editing Time'] = (re.search(r'\\edmins(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Number of Pages'] = (re.search(r'\\nofpages(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Number of Words'] = (re.search(r'\\nofwords(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Number of Characters'] = (re.search(r'\\nofchars(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Number of Characters with Spaces'] = (re.search(r'\\nofcharsws(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Internal Version'] = (re.search(r'\\vern(\d+)', info_text) or [None, 'Unknown'])[1]
            self.properties['Document ID'] = (re.search(r'\\id(\d+)', info_text) or [None, 'Unknown'])[1]
        else:
            for key in ['Title', 'Subject', 'Author', 'Keywords', 'Comment', 'Operator', 'Creation Time', 'Revision Time', 'Print Time', 'Backup Time', 'Total Editing Time', 'Number of Pages', 'Number of Words', 'Number of Characters', 'Number of Characters with Spaces', 'Internal Version', 'Document ID']:
                self.properties[key] = 'Unknown'

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

    def write(self, output_path=None):
        if not output_path:
            output_path = self.filepath
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(self.text)

# Example usage:
# handler = RtfHandler()
# handler.open('sample.rtf')
# handler.print_properties()
# handler.write('output.rtf')

Java Class for RTF Handling
The following Java class can open an RTF file, read and decode the properties, print them to console, and write the file. It uses regex for parsing.

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class RtfHandler {
    private String filepath;
    private String text;
    private Map<String, Object> properties = new HashMap<>();

    public void open(String filepath) throws IOException {
        this.filepath = filepath;
        BufferedReader reader = new BufferedReader(new FileReader(filepath));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
        }
        reader.close();
        this.text = sb.toString();
        decodeProperties();
    }

    private void decodeProperties() {
        // RTF Version
        Pattern versionPattern = Pattern.compile("\\\\rtf(\\d+)");
        Matcher versionMatcher = versionPattern.matcher(text);
        properties.put("RTF Version", versionMatcher.find() ? versionMatcher.group(1) : "Unknown");

        // Character Set
        Pattern charsetPattern = Pattern.compile("\\\\(ansi|mac|pc|pca)");
        Matcher charsetMatcher = charsetPattern.matcher(text);
        properties.put("Character Set", charsetMatcher.find() ? charsetMatcher.group(1) : "Unknown");

        // ANSI Code Page
        Pattern ansicpgPattern = Pattern.compile("\\\\ansicpg(\\d+)");
        Matcher ansicpgMatcher = ansicpgPattern.matcher(text);
        properties.put("ANSI Code Page", ansicpgMatcher.find() ? ansicpgMatcher.group(1) : "Unknown");

        // Default Font
        Pattern deffPattern = Pattern.compile("\\\\deff(\\d+)");
        Matcher deffMatcher = deffPattern.matcher(text);
        properties.put("Default Font", deffMatcher.find() ? deffMatcher.group(1) : "Unknown");

        // Default Language
        Pattern deflangPattern = Pattern.compile("\\\\deflang(\\d+)");
        Matcher deflangMatcher = deflangPattern.matcher(text);
        properties.put("Default Language", deflangMatcher.find() ? deflangMatcher.group(1) : "Unknown");

        // Font Table
        Pattern fonttblPattern = Pattern.compile("\\{\\fonttbl([^}]+)\\}");
        Matcher fonttblMatcher = fonttblPattern.matcher(text);
        properties.put("Font Table", fonttblMatcher.find() ? Arrays.asList(fonttblMatcher.group(1).trim().split(";")) : "Unknown");

        // Color Table
        Pattern colortblPattern = Pattern.compile("\\{\\colortbl([^}]+)\\}");
        Matcher colortblMatcher = colortblPattern.matcher(text);
        properties.put("Color Table", colortblMatcher.find() ? Arrays.asList(colortblMatcher.group(1).trim().split(";")) : "Unknown");

        // Stylesheet
        Pattern stylesheetPattern = Pattern.compile("\\{\\stylesheet([^}]+)\\}");
        Matcher stylesheetMatcher = stylesheetPattern.matcher(text);
        properties.put("Stylesheet", stylesheetMatcher.find() ? stylesheetMatcher.group(1).trim() : "Unknown");

        // \info group
        Pattern infoPattern = Pattern.compile("\\{\\info([^}]+)\\}");
        Matcher infoMatcher = infoPattern.matcher(text);
        if (infoMatcher.find()) {
            String infoText = infoMatcher.group(1);

            Pattern titlePattern = Pattern.compile("\\{\\title([^}]+)\\}");
            Matcher titleMatcher = titlePattern.matcher(infoText);
            properties.put("Title", titleMatcher.find() ? titleMatcher.group(1) : "Unknown");

            Pattern subjectPattern = Pattern.compile("\\{\\subject([^}]+)\\}");
            Matcher subjectMatcher = subjectPattern.matcher(infoText);
            properties.put("Subject", subjectMatcher.find() ? subjectMatcher.group(1) : "Unknown");

            Pattern authorPattern = Pattern.compile("\\{\\author([^}]+)\\}");
            Matcher authorMatcher = authorPattern.matcher(infoText);
            properties.put("Author", authorMatcher.find() ? authorMatcher.group(1) : "Unknown");

            Pattern keywordsPattern = Pattern.compile("\\{\\keywords([^}]+)\\}");
            Matcher keywordsMatcher = keywordsPattern.matcher(infoText);
            properties.put("Keywords", keywordsMatcher.find() ? keywordsMatcher.group(1) : "Unknown");

            Pattern commentPattern = Pattern.compile("\\{\\comment([^}]+)\\}");
            Matcher commentMatcher = commentPattern.matcher(infoText);
            properties.put("Comment", commentMatcher.find() ? commentMatcher.group(1) : "Unknown");

            Pattern operatorPattern = Pattern.compile("\\{\\operator([^}]+)\\}");
            Matcher operatorMatcher = operatorPattern.matcher(infoText);
            properties.put("Operator", operatorMatcher.find() ? operatorMatcher.group(1) : "Unknown");

            Pattern creatimPattern = Pattern.compile("\\{\\creatim \\\\yr(\\d+) \\\\mo(\\d+) \\\\dy(\\d+) \\\\hr(\\d+) \\\\min(\\d+)\\}");
            Matcher creatimMatcher = creatimPattern.matcher(infoText);
            properties.put("Creation Time", creatimMatcher.find() ? creatimMatcher.group(1) + "-" + creatimMatcher.group(2) + "-" + creatimMatcher.group(3) + " " + creatimMatcher.group(4) + ":" + creatimMatcher.group(5) : "Unknown");

            Pattern revtimPattern = Pattern.compile("\\{\\revtim \\\\yr(\\d+) \\\\mo(\\d+) \\\\dy(\\d+) \\\\hr(\\d+) \\\\min(\\d+)\\}");
            Matcher revtimMatcher = revtimPattern.matcher(infoText);
            properties.put("Revision Time", revtimMatcher.find() ? revtimMatcher.group(1) + "-" + revtimMatcher.group(2) + "-" + revtimMatcher.group(3) + " " + revtimMatcher.group(4) + ":" + revtimMatcher.group(5) : "Unknown");

            Pattern printimPattern = Pattern.compile("\\{\\printim \\\\yr(\\d+) \\\\mo(\\d+) \\\\dy(\\d+) \\\\hr(\\d+) \\\\min(\\d+)\\}");
            Matcher printimMatcher = printimPattern.matcher(infoText);
            properties.put("Print Time", printimMatcher.find() ? printimMatcher.group(1) + "-" + printimMatcher.group(2) + "-" + printimMatcher.group(3) + " " + printimMatcher.group(4) + ":" + printimMatcher.group(5) : "Unknown");

            Pattern buptimPattern = Pattern.compile("\\{\\buptim \\\\yr(\\d+) \\\\mo(\\d+) \\\\dy(\\d+) \\\\hr(\\d+) \\\\min(\\d+)\\}");
            Matcher buptimMatcher = buptimPattern.matcher(infoText);
            properties.put("Backup Time", buptimMatcher.find() ? buptimMatcher.group(1) + "-" + buptimMatcher.group(2) + "-" + buptimMatcher.group(3) + " " + buptimMatcher.group(4) + ":" + buptimMatcher.group(5) : "Unknown");

            Pattern edminsPattern = Pattern.compile("\\\\edmins(\\d+)");
            Matcher edminsMatcher = edminsPattern.matcher(infoText);
            properties.put("Total Editing Time", edminsMatcher.find() ? edminsMatcher.group(1) : "Unknown");

            Pattern nofpagesPattern = Pattern.compile("\\\\nofpages(\\d+)");
            Matcher nofpagesMatcher = nofpagesPattern.matcher(infoText);
            properties.put("Number of Pages", nofpagesMatcher.find() ? nofpagesMatcher.group(1) : "Unknown");

            Pattern nofwordsPattern = Pattern.compile("\\\\nofwords(\\d+)");
            Matcher nofwordsMatcher = nofwordsPattern.matcher(infoText);
            properties.put("Number of Words", nofwordsMatcher.find() ? nofwordsMatcher.group(1) : "Unknown");

            Pattern nofcharsPattern = Pattern.compile("\\\\nofchars(\\d+)");
            Matcher nofcharsMatcher = nofcharsPattern.matcher(infoText);
            properties.put("Number of Characters", nofcharsMatcher.find() ? nofcharsMatcher.group(1) : "Unknown");

            Pattern nofcharswsPattern = Pattern.compile("\\\\nofcharsws(\\d+)");
            Matcher nofcharswsMatcher = nofcharswsPattern.matcher(infoText);
            properties.put("Number of Characters with Spaces", nofcharswsMatcher.find() ? nofcharswsMatcher.group(1) : "Unknown");

            Pattern vernPattern = Pattern.compile("\\\\vern(\\d+)");
            Matcher vernMatcher = vernPattern.matcher(infoText);
            properties.put("Internal Version", vernMatcher.find() ? vernMatcher.group(1) : "Unknown");

            Pattern idPattern = Pattern.compile("\\\\id(\\d+)");
            Matcher idMatcher = idPattern.matcher(infoText);
            properties.put("Document ID", idMatcher.find() ? idMatcher.group(1) : "Unknown");
        } else {
            String[] keys = {"Title", "Subject", "Author", "Keywords", "Comment", "Operator", "Creation Time", "Revision Time", "Print Time", "Backup Time", "Total Editing Time", "Number of Pages", "Number of Words", "Number of Characters", "Number of Characters with Spaces", "Internal Version", "Document ID"};
            for (String key : keys) {
                properties.put(key, "Unknown");
            }
        }
    }

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

    public void write(String outputPath) throws IOException {
        if (outputPath == null) {
            outputPath = filepath;
        }
        BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath));
        writer.write(text);
        writer.close();
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     RtfHandler handler = new RtfHandler();
    //     handler.open("sample.rtf");
    //     handler.printProperties();
    //     handler.write("output.rtf");
    // }
}

JavaScript Class for RTF Handling
The following JavaScript class (for Node.js) can open an RTF file using fs, read and decode the properties, print them to console, and write the file. It uses regex for parsing. Requires Node.js.

const fs = require('fs');

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

    open(filepath) {
        this.filepath = filepath;
        this.text = fs.readFileSync(filepath, 'utf8');
        this.decodeProperties();
    }

    decodeProperties() {
        if (!this.text) return;

        // RTF Version
        const versionMatch = this.text.match(/\\rtf(\d+)/);
        this.properties['RTF Version'] = versionMatch ? versionMatch[1] : 'Unknown';

        // Character Set
        const charsetMatch = this.text.match(/\\(ansi|mac|pc|pca)/);
        this.properties['Character Set'] = charsetMatch ? charsetMatch[1] : 'Unknown';

        // ANSI Code Page
        const ansicpgMatch = this.text.match(/\\ansicpg(\d+)/);
        this.properties['ANSI Code Page'] = ansicpgMatch ? ansicpgMatch[1] : 'Unknown';

        // Default Font
        const deffMatch = this.text.match(/\\deff(\d+)/);
        this.properties['Default Font'] = deffMatch ? deffMatch[1] : 'Unknown';

        // Default Language
        const deflangMatch = this.text.match(/\\deflang(\d+)/);
        this.properties['Default Language'] = deflangMatch ? deflangMatch[1] : 'Unknown';

        // Font Table
        const fonttblMatch = this.text.match(/{\\fonttbl([^}]+)}/);
        this.properties['Font Table'] = fonttblMatch ? fonttblMatch[1].trim().split(';') : 'Unknown';

        // Color Table
        const colortblMatch = this.text.match(/{\\colortbl([^}]+)}/);
        this.properties['Color Table'] = colortblMatch ? colortblMatch[1].trim().split(';') : 'Unknown';

        // Stylesheet
        const stylesheetMatch = this.text.match(/{\\stylesheet([^}]+)}/);
        this.properties['Stylesheet'] = stylesheetMatch ? stylesheetMatch[1].trim() : 'Unknown';

        // \info group
        const infoMatch = this.text.match(/{\\info([^}]+)}/);
        if (infoMatch) {
            const infoText = infoMatch[1];
            this.properties['Title'] = (infoText.match(/{\\title([^}]+)}/) || [])[1] || 'Unknown';
            this.properties['Subject'] = (infoText.match(/{\\subject([^}]+)}/) || [])[1] || 'Unknown';
            this.properties['Author'] = (infoText.match(/{\\author([^}]+)}/) || [])[1] || 'Unknown';
            this.properties['Keywords'] = (infoText.match(/{\\keywords([^}]+)}/) || [])[1] || 'Unknown';
            this.properties['Comment'] = (infoText.match(/{\\comment([^}]+)}/) || [])[1] || 'Unknown';
            this.properties['Operator'] = (infoText.match(/{\\operator([^}]+)}/) || [])[1] || 'Unknown';

            const creatimMatch = infoText.match(/{\\creatim \\yr(\\d+) \\mo(\\d+) \\dy(\\d+) \\hr(\\d+) \\min(\\d+)}/);
            this.properties['Creation Time'] = creatimMatch ? `${creatimMatch[1]}-${creatimMatch[2]}-${creatimMatch[3]} ${creatimMatch[4]}:${creatimMatch[5]}` : 'Unknown';

            const revtimMatch = infoText.match(/{\\revtim \\yr(\\d+) \\mo(\\d+) \\dy(\\d+) \\hr(\\d+) \\min(\\d+)}/);
            this.properties['Revision Time'] = revtimMatch ? `${revtimMatch[1]}-${revtimMatch[2]}-${revtimMatch[3]} ${revtimMatch[4]}:${revtimMatch[5]}` : 'Unknown';

            const printimMatch = infoText.match(/{\\printim \\yr(\\d+) \\mo(\\d+) \\dy(\\d+) \\hr(\\d+) \\min(\\d+)}/);
            this.properties['Print Time'] = printimMatch ? `${printimMatch[1]}-${printimMatch[2]}-${printimMatch[3]} ${printimMatch[4]}:${printimMatch[5]}` : 'Unknown';

            const buptimMatch = infoText.match(/{\\buptim \\yr(\\d+) \\mo(\\d+) \\dy(\\d+) \\hr(\\d+) \\min(\\d+)}/);
            this.properties['Backup Time'] = buptimMatch ? `${buptimMatch[1]}-${buptimMatch[2]}-${buptimMatch[3]} ${buptimMatch[4]}:${buptimMatch[5]}` : 'Unknown';

            this.properties['Total Editing Time'] = (infoText.match(/\\edmins(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Number of Pages'] = (infoText.match(/\\nofpages(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Number of Words'] = (infoText.match(/\\nofwords(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Number of Characters'] = (infoText.match(/\\nofchars(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Number of Characters with Spaces'] = (infoText.match(/\\nofcharsws(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Internal Version'] = (infoText.match(/\\vern(\\d+)/) || [])[1] || 'Unknown';
            this.properties['Document ID'] = (infoText.match(/\\id(\\d+)/) || [])[1] || 'Unknown';
        } else {
            ['Title', 'Subject', 'Author', 'Keywords', 'Comment', 'Operator', 'Creation Time', 'Revision Time', 'Print Time', 'Backup Time', 'Total Editing Time', 'Number of Pages', 'Number of Words', 'Number of Characters', 'Number of Characters with Spaces', 'Internal Version', 'Document ID'].forEach(key => {
                this.properties[key] = 'Unknown';
            });
        }
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    write(outputPath = null) {
        if (!outputPath) outputPath = this.filepath;
        fs.writeFileSync(outputPath, this.text, 'utf8');
    }
}

// Example usage:
// const handler = new RtfHandler();
// handler.open('sample.rtf');
// handler.printProperties();
// handler.write('output.rtf');

C++ Class for RTF Handling
The following C++ class can open an RTF file, read and decode the properties, print them to console, and write the file. It uses <regex> for parsing. Compile with C++11 or later.

#include <iostream>
#include <fstream>
#include <string>
#include <regex>
#include <vector>
#include <map>

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

public:
    void open(const std::string& fp) {
        filepath = fp;
        std::ifstream file(filepath);
        if (file) {
            std::string line;
            while (std::getline(file, line)) {
                text += line + "\n";
            }
            file.close();
            decodeProperties();
        }
    }

    void decodeProperties() {
        // RTF Version
        std::regex versionRegex(R"(\\rtf(\d+))");
        std::smatch match;
        if (std::regex_search(text, match, versionRegex)) {
            properties["RTF Version"] = match[1];
        } else {
            properties["RTF Version"] = "Unknown";
        }

        // Character Set
        std::regex charsetRegex(R"(\\ (ansi|mac|pc|pca))");
        if (std::regex_search(text, match, charsetRegex)) {
            properties["Character Set"] = match[1];
        } else {
            properties["Character Set"] = "Unknown";
        }

        // ANSI Code Page
        std::regex ansicpgRegex(R"(\\ansicpg(\d+))");
        if (std::regex_search(text, match, ansicpgRegex)) {
            properties["ANSI Code Page"] = match[1];
        } else {
            properties["ANSI Code Page"] = "Unknown";
        }

        // Default Font
        std::regex deffRegex(R"(\\deff(\d+))");
        if (std::regex_search(text, match, deffRegex)) {
            properties["Default Font"] = match[1];
        } else {
            properties["Default Font"] = "Unknown";
        }

        // Default Language
        std::regex deflangRegex(R"(\\deflang(\d+))");
        if (std::regex_search(text, match, deflangRegex)) {
            properties["Default Language"] = match[1];
        } else {
            properties["Default Language"] = "Unknown";
        }

        // Font Table
        std::regex fonttblRegex(R"(\{\\fonttbl([^}]+)\})");
        if (std::regex_search(text, match, fonttblRegex)) {
            std::string fontStr = match[1];
            // Simple split by ';'
            std::string fonts;
            size_t pos = 0;
            while ((pos = fontStr.find(";")) != std::string::npos) {
                fonts += fontStr.substr(0, pos) + ", ";
                fontStr.erase(0, pos + 1);
            }
            fonts += fontStr;
            properties["Font Table"] = fonts;
        } else {
            properties["Font Table"] = "Unknown";
        }

        // Color Table
        std::regex colortblRegex(R"(\{\\colortbl([^}]+)\})");
        if (std::regex_search(text, match, colortblRegex)) {
            std::string colorStr = match[1];
            std::string colors;
            size_t pos = 0;
            while ((pos = colorStr.find(";")) != std::string::npos) {
                colors += colorStr.substr(0, pos) + ", ";
                colorStr.erase(0, pos + 1);
            }
            colors += colorStr;
            properties["Color Table"] = colors;
        } else {
            properties["Color Table"] = "Unknown";
        }

        // Stylesheet
        std::regex stylesheetRegex(R"(\{\\stylesheet([^}]+)\})");
        if (std::regex_search(text, match, stylesheetRegex)) {
            properties["Stylesheet"] = match[1];
        } else {
            properties["Stylesheet"] = "Unknown";
        }

        // \info group
        std::regex infoRegex(R"(\{\\info([^}]+)\})");
        if (std::regex_search(text, match, infoRegex)) {
            std::string infoText = match[1];

            std::regex titleRegex(R"(\{\\title([^}]+)\})");
            if (std::regex_search(infoText, match, titleRegex)) {
                properties["Title"] = match[1];
            } else {
                properties["Title"] = "Unknown";
            }

            std::regex subjectRegex(R"(\{\\subject([^}]+)\})");
            if (std::regex_search(infoText, match, subjectRegex)) {
                properties["Subject"] = match[1];
            } else {
                properties["Subject"] = "Unknown";
            }

            std::regex authorRegex(R"(\{\\author([^}]+)\})");
            if (std::regex_search(infoText, match, authorRegex)) {
                properties["Author"] = match[1];
            } else {
                properties["Author"] = "Unknown";
            }

            std::regex keywordsRegex(R"(\{\\keywords([^}]+)\})");
            if (std::regex_search(infoText, match, keywordsRegex)) {
                properties["Keywords"] = match[1];
            } else {
                properties["Keywords"] = "Unknown";
            }

            std::regex commentRegex(R"(\{\\comment([^}]+)\})");
            if (std::regex_search(infoText, match, commentRegex)) {
                properties["Comment"] = match[1];
            } else {
                properties["Comment"] = "Unknown";
            }

            std::regex operatorRegex(R"(\{\\operator([^}]+)\})");
            if (std::regex_search(infoText, match, operatorRegex)) {
                properties["Operator"] = match[1];
            } else {
                properties["Operator"] = "Unknown";
            }

            std::regex creatimRegex(R"(\{\\creatim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)\})");
            if (std::regex_search(infoText, match, creatimRegex)) {
                properties["Creation Time"] = match[1] + "-" + match[2] + "-" + match[3] + " " + match[4] + ":" + match[5];
            } else {
                properties["Creation Time"] = "Unknown";
            }

            std::regex revtimRegex(R"(\{\\revtim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)\})");
            if (std::regex_search(infoText, match, revtimRegex)) {
                properties["Revision Time"] = match[1] + "-" + match[2] + "-" + match[3] + " " + match[4] + ":" + match[5];
            } else {
                properties["Revision Time"] = "Unknown";
            }

            std::regex printimRegex(R"(\{\\printim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)\})");
            if (std::regex_search(infoText, match, printimRegex)) {
                properties["Print Time"] = match[1] + "-" + match[2] + "-" + match[3] + " " + match[4] + ":" + match[5];
            } else {
                properties["Print Time"] = "Unknown";
            }

            std::regex buptimRegex(R"(\{\\buptim \\yr(\d+) \\mo(\d+) \\dy(\d+) \\hr(\d+) \\min(\d+)\})");
            if (std::regex_search(infoText, match, buptimRegex)) {
                properties["Backup Time"] = match[1] + "-" + match[2] + "-" + match[3] + " " + match[4] + ":" + match[5];
            } else {
                properties["Backup Time"] = "Unknown";
            }

            std::regex edminsRegex(R"(\\edmins(\d+))");
            if (std::regex_search(infoText, match, edminsRegex)) {
                properties["Total Editing Time"] = match[1];
            } else {
                properties["Total Editing Time"] = "Unknown";
            }

            std::regex nofpagesRegex(R"(\\nofpages(\d+))");
            if (std::regex_search(infoText, match, nofpagesRegex)) {
                properties["Number of Pages"] = match[1];
            } else {
                properties["Number of Pages"] = "Unknown";
            }

            std::regex nofwordsRegex(R"(\\nofwords(\d+))");
            if (std::regex_search(infoText, match, nofwordsRegex)) {
                properties["Number of Words"] = match[1];
            } else {
                properties["Number of Words"] = "Unknown";
            }

            std::regex nofcharsRegex(R"(\\nofchars(\d+))");
            if (std::regex_search(infoText, match, nofcharsRegex)) {
                properties["Number of Characters"] = match[1];
            } else {
                properties["Number of Characters"] = "Unknown";
            }

            std::regex nofcharswsRegex(R"(\\nofcharsws(\d+))");
            if (std::regex_search(infoText, match, nofcharswsRegex)) {
                properties["Number of Characters with Spaces"] = match[1];
            } else {
                properties["Number of Characters with Spaces"] = "Unknown";
            }

            std::regex vernRegex(R"(\\vern(\d+))");
            if (std::regex_search(infoText, match, vernRegex)) {
                properties["Internal Version"] = match[1];
            } else {
                properties["Internal Version"] = "Unknown";
            }

            std::regex idRegex(R"(\\id(\d+))");
            if (std::regex_search(infoText, match, idRegex)) {
                properties["Document ID"] = match[1];
            } else {
                properties["Document ID"] = "Unknown";
            }
        } else {
            std::vector<std::string> keys = {"Title", "Subject", "Author", "Keywords", "Comment", "Operator", "Creation Time", "Revision Time", "Print Time", "Backup Time", "Total Editing Time", "Number of Pages", "Number of Words", "Number of Characters", "Number of Characters with Spaces", "Internal Version", "Document ID"};
            for (const auto& key : keys) {
                properties[key] = "Unknown";
            }
        }
    }

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

    void write(const std::string& outputPath = "") {
        std::string out = outputPath.empty() ? filepath : outputPath;
        std::ofstream file(out);
        if (file) {
            file << text;
            file.close();
        }
    }
};

// Example usage:
// int main() {
//     RtfHandler handler;
//     handler.open("sample.rtf");
//     handler.printProperties();
//     handler.write("output.rtf");
//     return 0;
// }