Hello everyone!
I wanted to share a project I've been working on that implements a Rich Text Format (RTF) document generator in C++. This code allows users to create richly formatted text documents programmatically, complete with various styles, colors, lists, and images.
Overview
The code is structured to perform the following tasks:
Encoding Utilities: It includes functions to convert between UTF-8 and UTF-16, ensuring proper handling of text encoding.
RtfBuilder Class: This class serves as the core of the implementation, enabling the addition of styled text, colors, bullet and numbered lists, and tables, all while managing the document's state.
Image Insertion: The code supports inserting images into the RTF document, handling different image formats and providing error management for missing files.
Document Structure: The main function demonstrates how to build a sample document, incorporating user-defined text and optional images, and saving the output to a specified file path.
No External Libraries: The implementation relies solely on the C++ Standard Template Library (STL) and Windows API for file handling, making it lightweight and straightforward.
Key Features
Rich Formatting: The code allows for various text styles, including bold, italic, underline, and strikethrough, enhancing the document's visual appeal.
List and Table Support: It provides functionality for creating bullet and numbered lists, as well as simple tables, making it versatile for different document types.
Error Handling: The implementation includes robust error management for file operations and image insertion, ensuring a smooth user experience.
Example Usage
To use the code, simply provide a text input and an optional image path as command-line arguments. The generated RTF document will be saved to the specified location, ready for use in any RTF-compatible application.
Conclusion
I hope this project can serve as a useful reference for anyone interested in document generation and rich text formatting in C++. I welcome any feedback, suggestions, or questions you might have!
Feel free to check out the code below and let me know what you think!
I wanted to share a project I've been working on that implements a Rich Text Format (RTF) document generator in C++. This code allows users to create richly formatted text documents programmatically, complete with various styles, colors, lists, and images.
Overview
The code is structured to perform the following tasks:
Encoding Utilities: It includes functions to convert between UTF-8 and UTF-16, ensuring proper handling of text encoding.
RtfBuilder Class: This class serves as the core of the implementation, enabling the addition of styled text, colors, bullet and numbered lists, and tables, all while managing the document's state.
Image Insertion: The code supports inserting images into the RTF document, handling different image formats and providing error management for missing files.
Document Structure: The main function demonstrates how to build a sample document, incorporating user-defined text and optional images, and saving the output to a specified file path.
No External Libraries: The implementation relies solely on the C++ Standard Template Library (STL) and Windows API for file handling, making it lightweight and straightforward.
Key Features
Rich Formatting: The code allows for various text styles, including bold, italic, underline, and strikethrough, enhancing the document's visual appeal.
List and Table Support: It provides functionality for creating bullet and numbered lists, as well as simple tables, making it versatile for different document types.
Error Handling: The implementation includes robust error management for file operations and image insertion, ensuring a smooth user experience.
Example Usage
To use the code, simply provide a text input and an optional image path as command-line arguments. The generated RTF document will be saved to the specified location, ready for use in any RTF-compatible application.
Conclusion
I hope this project can serve as a useful reference for anyone interested in document generation and rich text formatting in C++. I welcome any feedback, suggestions, or questions you might have!
Feel free to check out the code below and let me know what you think!
C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// create_and_open_wordpad_improved.cpp
// Enhanced RTF generator: lists, styles (including strike-through/highlight/double underline/background),
// images (pict), sections, simple tables, improved error messages, modularization,
// comments, and usage examples in the header.
//
// Compile using MSVC or MinGW (g++) targeting Windows.
//
// Compilation examples:
// MSVC: cl /EHsc create_and_open_wordpad_improved.cpp
// MinGW: g++ -municode -static-libstdc++ -static-libgcc -o create_and_open_wordpad_improved.exe create_and_open_wordpad_improved.cpp
#include <windows.h>
#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
#include <map>
#include <set>
#include <stack>
#include <vector>
#include <stdexcept>
#include <iomanip>
#include <memory>
#include <algorithm>
// -------------------- Utilitaires d'encodage --------------------
// Convertit UTF-8 std::string en std::wstring (UTF-16)
static std::wstring utf8_to_wstring(const std::string &s) {
if (s.empty()) return std::wstring();
int needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), NULL, 0);
if (needed == 0) return std::wstring();
std::wstring out(needed, 0);
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &out[0], needed);
return out;
}
// Convertit wide string (UTF-16) en bytes Windows-1252 with RTF escapes for non-ASCII,
// et gère certains caractères spéciaux (tab, tirets, guillemets).
static std::string wstring_to_rtf_ansi(const std::wstring &w) {
std::string out;
const char hex[] = "0123456789abcdef";
for (wchar_t wc : w) {
if (wc == L'\\') { out += "\\\\"; }
else if (wc == L'{') { out += "\\{"; }
else if (wc == L'}') { out += "\\}"; }
else if (wc == L'\r') { /* ignore */ }
else if (wc == L'\n') { out += "\\par\n"; }
else if (wc == L'\t') { out += "\\tab "; }
else {
switch (wc) {
case 0x2014: // em dash —
out += "\\emdash ";
continue;
case 0x2013: // en dash –
out += "\\endash ";
continue;
default:
break;
}
char buf[4] = {0,0,0,0};
int res = WideCharToMultiByte(1252, 0, &wc, 1, buf, 1, NULL, NULL);
if (res == 1) {
unsigned char b = static_cast<unsigned char>(buf[0]);
if (b < 0x80) {
out.push_back((char)b);
} else {
out += "\\'";
out += hex[(b >> 4) & 0xF];
out += hex[b & 0xF];
}
} else {
int unicode_val = static_cast<int>(wc);
if (unicode_val > 32767) unicode_val -= 65536;
char bufnum[64];
sprintf(bufnum, "\\u%d?", unicode_val);
out += bufnum;
}
}
}
return out;
}
// Escaping simple pour noms de polices
static std::string escape_fontname_for_rtf(const std::wstring &fontName) {
return wstring_to_rtf_ansi(fontName);
}
// -------------------- RTF Builder --------------------
class RtfBuilder {
public:
RtfBuilder() {
fonts_.push_back(L"Times New Roman"); // f0 par défaut
colors_.push_back(std::make_tuple(0,0,0)); // première entrée vide requise par colortbl
}
// Fonts / Colors
void set_default_font(const std::wstring &font) {
if (!font.empty()) fonts_[0] = font;
}
int add_font(const std::wstring &font) {
fonts_.push_back(font);
return static_cast<int>(fonts_.size()) - 1;
}
int add_color(int r, int g, int b) {
colors_.push_back(std::make_tuple(r,g,b));
return static_cast<int>(colors_.size()) - 1; // index RTF (1..n)
}
// Raw appenders
void append_raw(const std::string &s) { content_ += s; }
void append_text(const std::wstring &text) { content_ += wstring_to_rtf_ansi(text); }
// Group management (push/pop state and emit braces)
void push_group() {
content_ += "{";
state_stack_.push(state_);
}
void pop_group() {
content_ += "}";
if (!state_stack_.empty()) {
state_ = state_stack_.top();
state_stack_.pop();
}
}
// Basic style setters
void set_bold(bool on) { state_.bold = on; content_ += (on ? "\\b " : "\\b0 "); }
void set_italic(bool on) { state_.italic = on; content_ += (on ? "\\i " : "\\i0 "); }
// underline styles: single or double
void set_underline(bool on, bool doubleUnderline = false) {
state_.underline = on;
state_.doubleUnderline = doubleUnderline;
if (!on) content_ += "\\ulnone ";
else content_ += (doubleUnderline ? "\\uldb " : "\\ul ");
}
void set_strike(bool on) { state_.strike = on; content_ += (on ? "\\strike " : "\\strike0 "); }
// Highlight/background: emits \highlightN for background (rtf usage)
void set_highlight(int colorIndex) {
state_.highlight = colorIndex;
std::ostringstream ss;
ss << "\\highlight" << colorIndex << " ";
content_ += ss.str();
}
// Shading (alternative background implementation using \chcbpatN is more complex; we provide highlight)
void clear_highlight() {
state_.highlight = 0;
content_ += "\\highlight0 ";
}
void set_font(int fontIndexZeroBased) { state_.fontIndex = fontIndexZeroBased; std::ostringstream ss; ss << "\\f" << fontIndexZeroBased << " "; content_ += ss.str(); }
void set_fontsize_points(int points) { state_.fontsize_halfpoints = points * 2; std::ostringstream ss; ss << "\\fs" << (points*2) << " "; content_ += ss.str(); }
void set_color(int colorIndex) { state_.colorIndex = colorIndex; std::ostringstream ss; ss << "\\cf" << colorIndex << " "; content_ += ss.str(); }
// Paragraph control
void start_paragraph() { content_ += "\\pard\n"; }
void end_paragraph() { content_ += "\\par\n"; }
// List helpers (generate basic list text and paragraph markers)
void start_bullet_item() {
// Simple pseudo-list: using \pn control word groups to hint to RTF readers
content_ += "{\\*\\pn\\pnlvlblt\\pnf0\\pnindent0{\\pntxtb\\'B7}}\\pntext\\f0 \\tab ";
}
void start_numbered_item(int number) {
std::ostringstream ss;
ss << "{\\*\\pn\\pnlvlbody\\pnf0\\pnstart" << number << "{\\pntxtb " << number << ".}}\\pntext\\f0 \\tab ";
content_ += ss.str();
}
// Image insertion (pict). imageHex must be uppercase hex string (as produced by read_file_as_hex).
void insert_image_pict(const std::string &imageHex, const std::string &type = "pngblip", int width_twips = 0, int height_twips = 0) {
content_ += "{\\pict\\";
content_ += type;
if (width_twips > 0) {
std::ostringstream ss; ss << "\\picwgoal" << width_twips << "\\pichgoal" << height_twips << " ";
content_ += ss.str();
}
content_ += "\n";
size_t pos = 0;
const size_t lineWidth = 64;
while (pos < imageHex.size()) {
size_t chunk = std::min(lineWidth, imageHex.size() - pos);
content_.append(imageHex.data() + pos, chunk);
content_ += "\n";
pos += chunk;
}
content_ += "}\n";
}
// Basic table support: create a simple row with given cell texts and widths (twips)
// This is a minimal implementation to create a single-row table. For multi-row,
// call begin_table_row/append_table_cell/end_table_row repeatedly.
void begin_table_row() {
// start a paragraph that will contain row
content_ += "{\\trowd ";
current_table_right_edge_ = 0;
table_cell_right_edges_.clear();
}
void append_table_cell(const std::wstring &cellText, int cellWidthTwips) {
if (cellWidthTwips <= 0) cellWidthTwips = 1000; // default small width
current_table_right_edge_ += cellWidthTwips;
table_cell_right_edges_.push_back(current_table_right_edge_);
// add cell boundary definition
std::ostringstream ss;
ss << "\\cellx" << current_table_right_edge_ << " ";
content_ += ss.str();
// store cell content to be emitted when ending the row
pending_table_cells_.push_back(wstring_to_rtf_ansi(cellText));
}
void end_table_row() {
// Emit cell contents
for (size_t i = 0; i < pending_table_cells_.size(); ++i) {
content_ += pending_table_cells_[i];
content_ += "\\cell ";
}
pending_table_cells_.clear();
// finish row
content_ += "\\row}\n";
}
// Sections
void start_section() { content_ += "\\sect\\sbknone\n"; }
void end_section() { content_ += "\\sect\n"; }
// Final build: compose headers, fonttbl, colortbl, body
std::string build_rtf() {
std::ostringstream ss;
ss << "{\\rtf1\\ansi\\deff0\\viewkind4\\uc1\n";
// Font table
ss << "{\\fonttbl";
for (size_t i = 0; i < fonts_.size(); ++i) {
ss << "{\\f" << i << " " << escape_fontname_for_rtf(fonts_[i]) << ";}";
}
ss << "}\n";
// Color table
ss << "{\\colortbl ;";
for (size_t i = 1; i < colors_.size(); ++i) {
int r,g,b; std::tie(r,g,b) = colors_[i];
ss << "\\red" << r << "\\green" << g << "\\blue" << b << ";";
}
ss << "}\n";
ss << "\\pard\n";
ss << content_;
ss << "\n}\n";
return ss.str();
}
// Utility to clear the builder content and state to reuse
void clear() {
content_.clear();
while (!state_stack_.empty()) state_stack_.pop();
state_ = State();
fonts_.clear();
fonts_.push_back(L"Times New Roman");
colors_.clear();
colors_.push_back(std::make_tuple(0,0,0));
pending_table_cells_.clear();
table_cell_right_edges_.clear();
current_table_right_edge_ = 0;
}
private:
struct State {
bool bold = false;
bool italic = false;
bool underline = false;
bool doubleUnderline = false;
bool strike = false;
int highlight = 0;
int fontIndex = 0;
int fontsize_halfpoints = 0;
int colorIndex = 0;
};
std::string content_;
std::vector<std::wstring> fonts_;
std::vector<std::tuple<int,int,int>> colors_;
std::stack<State> state_stack_;
State state_;
// Table temporary storage
std::vector<std::string> pending_table_cells_;
std::vector<int> table_cell_right_edges_;
int current_table_right_edge_ = 0;
};
// -------------------- Helpers pour images --------------------
static std::string read_file_as_hex(const std::string &path) {
std::ifstream ifs(path.c_str(), std::ios::binary);
if (!ifs.is_open()) throw std::runtime_error("Impossible d'ouvrir le fichier image: " + path);
std::ostringstream ss;
ss << std::hex << std::uppercase;
const size_t BUF_SIZE = 4096;
std::unique_ptr<char[]> buf(new char[BUF_SIZE]);
while (ifs.read(buf.get(), BUF_SIZE) || ifs.gcount() > 0) {
std::streamsize n = ifs.gcount();
unsigned char *u = reinterpret_cast<unsigned char*>(buf.get());
for (std::streamsize i = 0; i < n; ++i) {
ss << std::setw(2) << std::setfill('0') << (int)u[i];
}
}
return ss.str();
}
// -------------------- Ecriture fichier et ouverture --------------------
// Ecrit le RTF en utilisant un encodage UTF-8 path converti; renvoie false et message d'erreur via errno-style.
static bool write_rtf_file_ansi(const std::wstring &path, const std::string &rtfContent, std::string &errMsg) {
// Convert path to UTF-8 for ofstream on non-Windows environments; on Windows with wchar we can use _wfopen,
// but we keep ofstream with UTF-8 path as in original code (works when path is ASCII). For robustness,
// we attempt to use wide API (CreateFileW + WriteFile) to support arbitrary paths.
HANDLE hFile = CreateFileW(path.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
DWORD e = GetLastError();
std::ostringstream ss; ss << "CreateFileW failed (GetLastError=" << e << ")";
errMsg = ss.str();
return false;
}
// Convert rtfContent bytes to ANSI (we assume content is ASCII/ANSI already because builder emits 1252 escapes),
// but RTF can be plain ASCII bytes; write raw bytes.
DWORD bytesWritten = 0;
BOOL ok = WriteFile(hFile, rtfContent.data(), static_cast<DWORD>(rtfContent.size()), &bytesWritten, NULL);
CloseHandle(hFile);
if (!ok || bytesWritten != rtfContent.size()) {
std::ostringstream ss; ss << "WriteFile failed or incomplete write (" << bytesWritten << " bytes written)";
errMsg = ss.str();
return false;
}
return true;
}
static bool open_with_default_app(const std::wstring &filepath, std::string &errMsg) {
HINSTANCE res = ShellExecuteW(NULL, L"open", filepath.c_str(), NULL, NULL, SW_SHOWNORMAL);
INT_PTR rc = reinterpret_cast<INT_PTR>(res);
if (rc <= 32) {
std::ostringstream ss; ss << "ShellExecuteW failed (rc=" << (int)rc << ")";
errMsg = ss.str();
return false;
}
return true;
}
// -------------------- Modulaire: construction d'exemples et utilitaires --------------------
static void build_intro(RtfBuilder &rtf) {
rtf.set_font(0);
rtf.set_fontsize_points(11);
rtf.append_text(L"Ceci est un document RTF enrichi (version améliorée).\n");
}
static void build_styled_paragraphs(RtfBuilder &rtf, int redColorIndex) {
rtf.push_group();
rtf.set_bold(true);
rtf.set_italic(true);
rtf.append_text(L"Texte en gras et italique.\n");
rtf.set_bold(false);
rtf.set_italic(false);
rtf.set_strike(true);
rtf.append_text(L"Texte barré.\n");
rtf.set_strike(false);
// Underline single and double
rtf.set_underline(true, false);
rtf.append_text(L"Texte souligné (simple).\n");
rtf.set_underline(true, true);
rtf.append_text(L"Texte souligné (double).\n");
rtf.set_underline(false);
rtf.pop_group();
// Example of colored error message
rtf.push_group();
rtf.set_color(redColorIndex);
rtf.append_text(L"[Exemple: message en rouge pour les erreurs]\n");
rtf.pop_group();
}
static void build_highlight_example(RtfBuilder &rtf, int yellowIdx) {
// Use explicit group and clear highlight afterwards to avoid propagation
rtf.push_group();
rtf.set_highlight(yellowIdx);
rtf.append_text(L"Texte surligné en jaune.");
rtf.append_raw("\\highlight0 "); // explicitly clear highlight after the span
rtf.pop_group();
rtf.end_paragraph();
}
static void build_lists(RtfBuilder &rtf) {
rtf.start_paragraph();
rtf.start_bullet_item();
rtf.append_text(L"Premier élément de liste");
rtf.end_paragraph();
rtf.start_bullet_item();
rtf.append_text(L"Deuxième élément de liste");
rtf.end_paragraph();
rtf.start_bullet_item();
rtf.append_text(L"Troisième élément de liste");
rtf.end_paragraph();
rtf.start_paragraph();
rtf.start_numbered_item(1);
rtf.append_text(L"Premier numéroté");
rtf.end_paragraph();
rtf.start_numbered_item(2);
rtf.append_text(L"Deuxième numéroté");
rtf.end_paragraph();
}
static void build_sample_table(RtfBuilder &rtf) {
// Simple single-row table with three cells
// widths in twips (1 inch = 1440 twips). Here: 2in, 2in, 2in
const int cellW = 2 * 1440;
rtf.begin_table_row();
rtf.append_table_cell(L"Colonne 1", cellW);
rtf.append_table_cell(L"Colonne 2", cellW);
rtf.append_table_cell(L"Colonne 3", cellW);
rtf.end_table_row();
}
static std::string build_sample_rtf(const std::wstring &bodyText, const std::string &imagePathOpt = "") {
RtfBuilder rtf;
rtf.set_default_font(L"Calibri");
rtf.add_font(L"Courier New"); // index 1
int yellowIdx = rtf.add_color(255, 255, 0); // highlight color example -> index returned
int redIdx = rtf.add_color(255, 0, 0);
int grayIdx = rtf.add_color(240, 240, 240);
build_intro(rtf);
build_styled_paragraphs(rtf, redIdx);
build_highlight_example(rtf, yellowIdx);
build_lists(rtf);
// Section break
rtf.start_section();
// Include body text passed by caller
rtf.append_text(bodyText);
rtf.end_paragraph();
// Insert a sample table
build_sample_table(rtf);
// Insert image if provided
if (!imagePathOpt.empty()) {
try {
std::string imgHex = read_file_as_hex(imagePathOpt);
std::string type = "pngblip";
std::string lower = imagePathOpt;
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c){ return (char)tolower(c); });
if (lower.find(".jpg") != std::string::npos || lower.find(".jpeg") != std::string::npos) type = "jpegblip";
// Goal sizes: example scaling
int goalW = 400 * 15;
int goalH = 200 * 15;
rtf.insert_image_pict(imgHex, type, goalW, goalH);
} catch (const std::exception &e) {
rtf.push_group();
rtf.set_color(redIdx);
rtf.append_text(L"[Erreur: impossible d'insérer l'image]\n");
rtf.pop_group();
}
}
return rtf.build_rtf();
}
// -------------------- Main --------------------
int main(int argc, char *argv[]) {
try {
std::string defaultTxtUtf8 =
"Texte d'exemple à inclure dans le document.\n"
"Vous pouvez fournir un chemin d'image en second argument pour l'inclure.\n";
std::wstring body = utf8_to_wstring(defaultTxtUtf8);
std::wstring outPath = L"example_enhanced_full_improved.rtf";
std::string imagePath;
if (argc >= 2) outPath = utf8_to_wstring(std::string(argv[1]));
if (argc >= 3) imagePath = std::string(argv[2]);
std::string rtf = build_sample_rtf(body, imagePath);
std::string err;
if (!write_rtf_file_ansi(outPath, rtf, err)) {
std::cerr << "Erreur: impossible d'ecrire le fichier: " << err << "\n";
// Try to present the path in UTF-8 for readable output
int needed = WideCharToMultiByte(CP_UTF8, 0, outPath.c_str(), -1, NULL, 0, NULL, NULL);
if (needed) {
std::string p; p.resize(needed - 1);
WideCharToMultiByte(CP_UTF8, 0, outPath.c_str(), -1, &p[0], needed, NULL, NULL);
std::cerr << "Chemin tenté: " << p << "\n";
}
return 1;
}
if (!open_with_default_app(outPath, err)) {
std::cerr << "Erreur: impossible d'ouvrir le fichier avec l'application par défaut: " << err << "\n";
return 1;
}
return 0;
} catch (const std::exception &ex) {
std::cerr << "Exception: " << ex.what() << "\n";
return 2;
} catch (...) {
std::cerr << "Exception inconnue.\n";
return 3;
}
}