// SPDX-License-Identifier: GPL-2.0-only WITH linking exception
// SPDX-FileCopyrightText: 2020–2026 grommunio GmbH
// This file is part of Gromox.
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iconv.h>
#include <string>
#include <unordered_map>
#include <vector>
#include <fmt/format.h>
#include <libHX/ctype_helper.h>
#include <libHX/defs.h>
#include <libHX/scope.hpp>
#include <libHX/string.h>
#include <gromox/element_data.hpp>
#include <gromox/ext_buffer.hpp>
#include <gromox/fileio.h>
#include <gromox/mail_func.hpp>
#include <gromox/simple_tree.hpp>
#include <gromox/textmaps.hpp>
#include <gromox/util.hpp>
#define QRF(expr) do { if (pack_result{expr} != pack_result::ok) return false; } while (false)
#define QRF2(expr) do { if (pack_result{expr} != pack_result::ok) return ecInvalidParam; } while (false)
#define ICONV_UNSET iconv_t(-1)

#define MAX_ATTRS						10000
#define MAX_GROUP_DEPTH					1000
#define MAX_COLORS						1024
#define MAX_FONTS						1024
#define MAX_CONTROL_LEN					32
#define MAX_FINTNAME_LEN				64

#define AUTO_COLOR -1
#define DEFAULT_FONT_STR				"Times,TimesRoman,TimesNewRoman"

#define FONTNIL_STR						"Times,TimesRoman,TimesNewRoman"
#define FONTROMAN_STR					"Times,Palatino"
#define FONTSWISS_STR					"Helvetica,Arial"
#define FONTMODERN_STR					"Courier,Verdana"
#define FONTSCRIPT_STR					"Cursive,ZapfChancery"
#define FONTDECOR_STR					"ZapfChancery"
#define FONTTECH_STR					"Symbol"

#define TAG_COMMENT_BEGIN				"<!--"
#define TAG_COMMENT_END					"-->"
#define TAG_DOCUMENT_BEGIN				"<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n<html>\r\n"
#define TAG_DOCUMENT_END				"</html>\r\n"
#define TAG_HEADER_BEGIN				"<head>\r\n"
#define TAG_HEADER_END					"</head>\r\n"
#define TAG_DOCUMENT_TITLE_BEGIN		"<title>"
#define TAG_DOCUMENT_TITLE_END			"</title>\r\n"
#define TAG_DOCUMENT_AUTHOR_BEGIN		"<meta name=\"author\" content=\""
#define TAG_DOCUMENT_AUTHOR_END			"\">\r\n"
#define TAG_DOCUMENT_CHANGEDATE_BEGIN	"<!-- changed:"
#define TAG_DOCUMENT_CHANGEDATE_END		"-->\r\n"
#define TAG_BODY_BEGIN					"<body>\r\n"
#define TAG_BODY_END					"</body>\r\n"
#define TAG_PARAGRAPH_BEGIN				"<p>"
#define TAG_PARAGRAPH_END				"</p>\r\n"
#define TAG_CENTER_BEGIN				"<center>"
#define TAG_CENTER_END					"</center>\r\n"
#define TAG_JUSTIFY_BEGIN				"<div align=\"justify\">\r\n"
#define TAG_JUSTIFY_END					"</div>\r\n"
#define TAG_ALIGN_LEFT_BEGIN			"<div align=\"left\">\r\n"
#define TAG_ALIGN_LEFT_END				"</div>\r\n"
#define TAG_ALIGN_RIGHT_BEGIN			"<div align=\"right\">\r\n"
#define TAG_ALIGN_RIGHT_END				"</div>\r\n"
#define TAG_FORCED_SPACE				"&nbsp;"
#define TAG_LINE_BREAK					"<br>"
#define TAG_PAGE_BREAK					"<p><hr><p>\r\n"
#define TAG_HYPERLINK_BEGIN				"<a href=%s>"
#define TAG_HYPERLINK_END				"</a>"
#define TAG_IMAGELINK_BEGIN				"<img src=\""
#define TAG_IMAGELINK_END				"\">"
#define TAG_TABLE_BEGIN					"<table border=\"2\">\r\n"
#define TAG_TABLE_END					"</table>\r\n"
#define TAG_TABLE_ROW_BEGIN				"<tr>\r\n"
#define TAG_TABLE_ROW_END				"</tr>\r\n"
#define TAG_TABLE_CELL_BEGIN			"<td>\r\n"
#define TAG_TABLE_CELL_END				"</td>\r\n"
#define TAG_FONT_BEGIN					"<font face=\"{}\">"
#define TAG_FONT_END					"</font>\r\n"
#define TAG_FONTSIZE_BEGIN				"<span style=\"font-size:%dpt\">"
#define TAG_FONTSIZE_END				"</span>"
#define TAG_FONTSIZE8_BEGIN				"<font size=\"1\">"
#define TAG_FONTSIZE8_END				"</font>"
#define TAG_FONTSIZE10_BEGIN			"<font size=\"2\">"
#define TAG_FONTSIZE10_END				"</font>"
#define TAG_FONTSIZE12_BEGIN			"<font size=\"3\">"
#define TAG_FONTSIZE12_END				"</font>"
#define TAG_FONTSIZE14_BEGIN			"<font size=\"4\">"
#define TAG_FONTSIZE14_END				"</font>"
#define TAG_FONTSIZE18_BEGIN			"<font size=\"5\">"
#define TAG_FONTSIZE18_END				"</font>"
#define TAG_FONTSIZE24_BEGIN			"<font size=\"6\">"
#define TAG_FONTSIZE24_END				"</font>"
#define TAG_SMALLER_BEGIN				"<small>"
#define TAG_SMALLER_END					"</small>"
#define TAG_BIGGER_BEGIN				"<big>"
#define TAG_BIGGER_END					"</big>"
#define TAG_FOREGROUND_BEGIN			"<font color=\"#{:06x}\">"
#define TAG_FOREGROUND_END				"</font>"
#define TAG_BACKGROUND_BEGIN			"<span style=\"background:#%06x\">"
#define TAG_BACKGROUND_END				"</span>"
#define TAG_BOLD_BEGIN					"<b>"
#define TAG_BOLD_END					"</b>"
#define TAG_ITALIC_BEGIN				"<i>"
#define TAG_ITALIC_END					"</i>"
#define TAG_UNDERLINE_BEGIN				"<u>"
#define TAG_UNDERLINE_END				"</u>"
#define TAG_DBL_UNDERLINE_BEGIN			"<u>"
#define TAG_DBL_UNDERLINE_END			"</u>"
#define TAG_SUPERSCRIPT_BEGIN			"<sup>"
#define TAG_SUPERSCRIPT_END				"</sup>"
#define TAG_SUBSCRIPT_BEGIN				"<sub>"
#define TAG_SUBSCRIPT_END				"</sub>"
#define TAG_STRIKETHRU_BEGIN			"<s>"
#define TAG_STRIKETHRU_END				"</s>"
#define TAG_DBL_STRIKETHRU_BEGIN		"<s>"
#define TAG_DBL_STRIKETHRU_END			"</s>"
#define TAG_EMBOSS_BEGIN				"<span style=\"background:gray\"><font color=\"black\">"
#define TAG_EMBOSS_END					"</font></span>"
#define TAG_ENGRAVE_BEGIN				"<span style=\"background:gray\"><font color=\"navyblue\">"
#define TAG_ENGRAVE_END					"</font></span>"
#define TAG_SHADOW_BEGIN				"<span style=\"background:gray\">"
#define TAG_SHADOW_END					"</span>"
#define TAG_OUTLINE_BEGIN				"<span style=\"background:gray\">"
#define TAG_OUTLINE_END					"</span>"
#define TAG_EXPAND_BEGIN				"<span style=\"letter-spacing: %d\">"
#define TAG_EXPAND_END					"</span>"
#define TAG_POINTLIST_BEGIN				"<ol>\r\n"
#define TAG_POINTLIST_END				"</ol>\r\n"
#define TAG_POINTLIST_ITEM_BEGIN		"<li>\r\n"
#define TAG_POINTLIST_ITEM_END			"</li>\r\n"
#define TAG_NUMERICLIST_BEGIN			"<ul>\r\n"
#define TAG_NUMERICLIST_END				"</ul>\r\n"
#define TAG_NUMERICLIST_ITEM_BEGIN		"<li>\r\n"
#define TAG_NUMERICLIST_ITEM_END		"</li>\r\n"
#define TAG_UNISYMBOL_PRINT				"&#%d;"
#define TAG_HTML_CHARSET				"<meta http-equiv=\"content-type\" content=\"text/html; charset=%s\">\r\n"
#define TAG_CHARS_RIGHT_QUOTE			"&rsquo;"
#define TAG_CHARS_LEFT_QUOTE			"&lsquo;"
#define TAG_CHARS_RIGHT_DBL_QUOTE		"&rdquo;"
#define TAG_CHARS_LEFT_DBL_QUOTE		"&ldquo;"
#define TAG_CHARS_ENDASH				"&ndash;"
#define TAG_CHARS_EMDASH				"&mdash;"
#define TAG_CHARS_BULLET				"&bull;"
#define TAG_CHARS_NONBREAKING_SPACE		"&nbsp;"
#define TAG_CHARS_SOFT_HYPHEN			"&shy;"

using namespace gromox;

enum {
	ATTR_NONE = 0,
	ATTR_BOLD,
	ATTR_ITALIC,
	ATTR_UNDERLINE,
	ATTR_DOUBLE_UL,
	ATTR_WORD_UL, 
	ATTR_THICK_UL,
	ATTR_WAVE_UL, 
	ATTR_DOT_UL,
	ATTR_DASH_UL,
	ATTR_DOT_DASH_UL,
	ATTR_2DOT_DASH_UL,
	ATTR_FONTSIZE,
	ATTR_STD_FONTSIZE,
	ATTR_FONTFACE,
	ATTR_FOREGROUND,
	ATTR_BACKGROUND,
	ATTR_CAPS,
	ATTR_SMALLCAPS,
	ATTR_PICT,
	ATTR_SHADOW,
	ATTR_OUTLINE, 
	ATTR_EMBOSS, 
	ATTR_ENGRAVE, 
	ATTR_SUPER,
	ATTR_SUB, 
	ATTR_STRIKE, 
	ATTR_DBL_STRIKE, 
	ATTR_EXPAND,
	ATTR_UBYTES,
	ATTR_HTMLTAG,
	ATTR_HIDDEN,
};

enum {
	ALIGN_LEFT = 0,
	ALIGN_RIGHT,
	ALIGN_CENTER,
	ALIGN_JUSTIFY
};

enum {
	PICT_UNKNOWN = 0,
	PICT_WM,
	PICT_MAC,
	PICT_PM,
	PICT_DI,
	PICT_WB,
	PICT_JPEG,
	PICT_PNG,
	PICT_EMF
};

namespace {

struct attrstack_node {
	uint8_t attr_stack[MAX_ATTRS]{};
	int attr_params[MAX_ATTRS]{};
	int tos = -1;
};

struct FONTENTRY {
	std::string name, encoding;
};

struct rtf_reader;
using CMD_PROC_FN   = int(SIMPLE_TREE_NODE *, int, bool, int);
using CMD_PROC_FUNC = int (rtf_reader::*)(SIMPLE_TREE_NODE *, int, bool, int);

struct rtf_reader final {
	rtf_reader() = default;
	~rtf_reader();
	NOMOVE(rtf_reader);

	bool init_reader(std::string_view, ATTACHMENT_LIST *);
	bool riconv_open(const char *);
	bool riconv_flush(bool reset_iconv = false);
	bool put_iconv_cache(int);
	pack_result getchar(int *);
	void ungetchar(int);
	char *read_element();
	bool load_element_tree();
	bool process_info_group(SIMPLE_TREE_NODE *);
	int convert_group_node(SIMPLE_TREE_NODE *);
	bool express_begin_fontsize(int);
	bool express_end_fontsize(int);
	bool express_attr_begin(int, int);
	bool express_attr_end(int, int);
	bool astk_express_all();
	bool astk_pushx(int, int);
	bool astk_popx(int);
	bool astk_popx_all();
	bool astk_find_popx(int);
	int astk_peek() const;
	const int *stack_list_find_attr(int) const;
	bool start_body();
	bool start_text();
	bool start_par(int);
	bool end_par(int);
	bool begin_table();
	bool end_table();
	bool check_for_table();
	const FONTENTRY *lookup_font(int) const;
	bool build_font_table(SIMPLE_TREE_NODE *);
	bool escape_output(char *);
	bool push_text_encoded(const char *, size_t);
	bool word_output_date(SIMPLE_TREE_NODE *);
	int push_da_pic(EXT_PUSH &, const char *, const char *, const char *, const char *);

	CMD_PROC_FN cmd_af, cmd_ansi, cmd_ansicpg, cmd_b, cmd_bullet, cmd_caps, cmd_cb,
	cmd_cf, cmd_colortbl, cmd_continue, cmd_cpg, cmd_dbch, cmd_deff, cmd_deflang, cmd_dn, cmd_emboss,
	cmd_emdash, cmd_emfblip, cmd_emspace, cmd_endash, cmd_engrave, cmd_enspace,
	cmd_expand, cmd_f,
	cmd_fdecor, cmd_field, cmd_fmodern, cmd_fnil, cmd_fonttbl, cmd_froman,
	cmd_fs, cmd_fscript, cmd_fswiss, cmd_ftech, cmd_hich, cmd_highlight, cmd_htmltag,
	cmd_i, cmd_ignore, cmd_info, cmd_intbl, cmd_jpegblip,
	cmd_lang, cmd_langfe, cmd_langfenp, cmd_langnp, cmd_ldblquote,
	cmd_line, cmd_loch, cmd_lquote, cmd_ltrmark, cmd_mac, cmd_macpict, cmd_maybe_ignore,
	cmd_nonbreaking_hyphen, cmd_nonbreaking_space, cmd_nosupersub,
	cmd_objattph, cmd_outl, cmd_page, cmd_par,
	cmd_pc, cmd_pca, cmd_pich, cmd_pict, cmd_picw, cmd_plain,
	cmd_pmmetafile, cmd_pngblip, cmd_qmspace, cmd_rdblquote, cmd_rquote, cmd_rtlmark, cmd_scaps,
	cmd_sect, cmd_shad, cmd_soft_hyphen, cmd_strike, cmd_striked,
	cmd_strikedl, cmd_sub, cmd_super, cmd_tab, cmd_u, cmd_uc, cmd_ul,
	cmd_uld, cmd_uldash, cmd_uldashd, cmd_uldashdd, cmd_uldb, cmd_ulnone,
	cmd_ulth, cmd_ulthd, cmd_ulthdash, cmd_ulw, cmd_ulwave, cmd_up, cmd_upr, cmd_v,
	cmd_wbmbitspixel, cmd_wmetafile, cmd_zwbo, cmd_zwj, cmd_zwnbo, cmd_zwnj, flush_pending;

	bool is_within_table = false, b_printed_row_begin = false;
	bool b_printed_cell_begin = false, b_printed_row_end = false;
	bool b_printed_cell_end = false, b_simulate_smallcaps = false;
	bool b_simulate_allcaps = false, b_ubytes_switch = true;
	bool is_within_picture = false, have_printed_body = false;
	bool is_within_header = true, have_ansicpg = false;
	bool have_fromhtml = false, is_within_htmltag = false;
	bool is_within_htmlrtf = false, paragraph_open = false;
	bool skip_objattph_space = false;
	int coming_pars_tabular = 0, ubytes_num = 1, ubytes_left = 0;
	/*
	 * Character byte mode for mixed single/double-byte text:
	 * 0 = loch (low-byte ASCII), 1 = hich (high-byte), 2 = dbch (double-byte)
	 * This affects which font is used for text interpretation.
	 */
	int char_byte_mode = 0;
	int associated_font_number = -1; /* font number for double-byte chars (\af) */
	int picture_file_number = 1;
	std::string picture_path;
	int picture_width = 0, picture_height = 0, picture_bits_per_pixel = 1;
	int picture_type = 0, picture_wmf_type = 0;
	const char *picture_wmf_str = nullptr;
	int color_table[MAX_COLORS]{}, total_colors = 0;
	int total_chars_in_line = 0;
	std::string default_encoding = "windows-1252", current_encoding, html_charset;
	int default_font_number = -1; /* -1 means no explicit default, 0+ is valid font number */
	std::unordered_map<int, FONTENTRY> pfont_hash;
	std::vector<attrstack_node> attr_stack_list;
	EXT_PULL ext_pull{};
	EXT_PUSH ext_push{};
	int ungot_chars[3] = {-1, -1, -1}, last_returned_ch = 0;
	iconv_t conv_id = ICONV_UNSET;
	EXT_PUSH iconv_push{};
	SIMPLE_TREE element_tree{};
	ATTACHMENT_LIST *pattachments = nullptr;
};
using RTF_READER = rtf_reader;

}

enum {
	CMD_RESULT_ERROR = -1,
	CMD_RESULT_CONTINUE,
	CMD_RESULT_IGNORE_REST,
	CMD_RESULT_HYPERLINKED
};

static CMD_PROC_FUNC rtf_find_cmd_function(const char *);

static constexpr cpid_t CP_UNSET = static_cast<cpid_t>(-1);

static int rtf_decode_hex_char(const char *in)
{
	int retval;
	
	if (strlen(in) < 2)
		return 0;
	if (in[0] >= '0' && in[0] <= '9')
		retval = in[0] - '0';
	else if ((in[0] >= 'a' && in[0] <= 'f'))
		retval = in[0] - 'a' + 10;
	else if (in[0] >= 'A' && in[0] <= 'F')
		retval = in[0] - 'A' + 10;
	else
		return 0;
	retval <<= 4;
	if (in[1] >= '0' && in[1] <= '9')
		retval += in[1] - '0';
	else if ((in[1] >= 'a' && in[1] <= 'f'))
		retval += in[1] - 'a' + 10;
	else if (in[1] >= 'A' && in[1] <= 'F')
		retval += in[1] - 'A' + 10;
	else
		return 0;
	return retval;
}

bool rtf_reader::riconv_open(const char *fromcode)
{
	auto preader = this;
	if (*fromcode == '\0' || strcasecmp(current_encoding.c_str(), fromcode) == 0)
		return true;
	/*
	 * Flush any pending data before switching encodings.
	 * This ensures multi-byte sequences are properly converted
	 * before we change the character set interpretation.
	 */
	if (iconv_push.m_offset > 0 && conv_id != (iconv_t)-1) {
		/*
		 * Force flush of any remaining bytes in the old encoding.
		 * If there's an incomplete sequence, it will be discarded
		 * since we're switching encodings anyway.
		 */
		char out_buff[256];
		auto in_buff   = iconv_push.m_cdata;
		size_t in_size = iconv_push.m_offset;
		char *out_ptr  = out_buff;
		size_t out_size = sizeof(out_buff) - 1;
		auto ret = iconv(conv_id, &in_buff, &in_size, &out_ptr, &out_size);
		if (ret == static_cast<size_t>(-1))
			/* ignore/discard broken sequences or outofspace */;
		ret = iconv(conv_id, nullptr, nullptr, &out_ptr, &out_size);
		if (ret == static_cast<size_t>(-1))
			/* ignore/discard */;
		size_t converted = sizeof(out_buff) - 1 - out_size;
		if (converted > 0) {
			out_buff[converted] = '\0';
			if (!escape_output(out_buff))
				return false;
		}
		iconv_push.m_offset = 0;
	}
	if ((iconv_t)-1 != preader->conv_id) {
		iconv_close(preader->conv_id);
		preader->conv_id = (iconv_t)-1;
	}
	auto cs = replace_iconv_charset(fromcode);
	preader->conv_id = iconv_open("UTF-8", cs);
	if ((iconv_t)-1 == preader->conv_id) {
		mlog(LV_ERR, "E-2114: iconv_open %s: %s", cs, strerror(errno));
		return false;
	}
	current_encoding = fromcode;
	return true;
}

bool rtf_reader::escape_output(char *string)
{
	auto preader = this;
	size_t tmp_len = strlen(string);
	if (ubytes_left > 0 && tmp_len > 0) {
		auto skip = std::min(static_cast<size_t>(ubytes_left), tmp_len);
		ubytes_left -= skip;
		if (skip >= tmp_len)
			return true;
		string += skip;
		tmp_len -= skip;
	}
	
	if (preader->is_within_htmltag) {
		QRF(ext_push.p_bytes(string, tmp_len));
		return true;
	}
	if (preader->b_simulate_allcaps)
		HX_strupper(string);
	if (preader->b_simulate_smallcaps)
		HX_strlower(string);
	for (size_t i = 0; i < tmp_len; ++i) {
		switch (string[i]) {
		case '<':
			QRF(ext_push.p_bytes("&lt;"));
			break;
		case '>':
			QRF(ext_push.p_bytes("&gt;"));
			break;
		case '&':
			QRF(ext_push.p_bytes("&amp;"));
			break;
		default:
			QRF(ext_push.p_uint8(string[i]));
			break;
		}
	}
	return true;
}

bool rtf_reader::push_text_encoded(const char *string, size_t len)
{
	if (b_ubytes_switch && ubytes_left > 0 && len > 0) {
		auto skip = std::min(static_cast<size_t>(ubytes_left), len);
		string += skip;
		len -= skip;
		ubytes_left -= skip;
		if (len == 0)
			return true;
	}
	if (len == 0)
		return true;
	/* suppress output for hidden text (\v) */
	if (stack_list_find_attr(ATTR_HIDDEN) != nullptr)
		return true;
	if (iconv_push.p_bytes(string, len) != pack_result::ok)
		return false;
	return riconv_flush();
}

bool rtf_reader::riconv_flush(bool reset_iconv)
{
	char *out_buff = nullptr;
	size_t out_size = 0;
	auto preader = this;
	
	if (preader->iconv_push.m_offset == 0 && !reset_iconv)
		return true;
	if (preader->conv_id == ICONV_UNSET) {
		if ('\0' == preader->default_encoding[0]) {
			if (!riconv_open("windows-1252"))
				return false;
		} else {
			if (!riconv_open(default_encoding.c_str()))
				return false;
		}
	}
	/*
	 * *4: conversion from ascii to utf8mb4
	 * +4: m_offset may be 0 (in case of reset_iconv=true), but we still
	 * need enough room to potentially emit an extra utf8mb4 char (special
	 * case of combining chars in e.g. cp1258)
	 * +1: for \0
	 */
	size_t tmp_len = 4 * preader->iconv_push.m_offset + 5;
	auto ptmp_buff = me_alloc<char>(tmp_len);
	if (ptmp_buff == nullptr)
		return false;
	auto in_buff = preader->iconv_push.m_cdata;
	size_t in_size = preader->iconv_push.m_offset;
	auto in_start = in_buff;
	out_buff = ptmp_buff;
	out_size = tmp_len;
	auto ret = iconv(conv_id, &in_buff, &in_size, &out_buff, &out_size);
	if (ret == static_cast<size_t>(-1)) {
		if (errno == EINVAL) {
			if (reset_iconv &&
			    iconv(conv_id, nullptr, nullptr, &out_buff, &out_size) ==
			    static_cast<size_t>(-1))
				/* ignore */;
			/*
			 * EINVAL = incomplete multi-byte sequence at end of input.
			 * This is normal for encodings like Shift-JIS where we may
			 * have received only the first byte of a 2-byte character.
			 * Output what was converted and keep the remainder.
			 *
			 * In our case, the input is one RTF group.
			 * MSWord has a similar behavior in that it can take bytes
			 * from multiple groups to produce a character. However,
			 * if the sequence is still incomplete, a garbage byte is
			 * produced.
			 *
			 * Libreoffice emits U+FFFD (REPLACEMENT CHARACTER)
			 * on incomplete sequences.
			 */
			tmp_len -= out_size;
			if (tmp_len > 0) {
				ptmp_buff[tmp_len] = '\0';
				if (!escape_output(ptmp_buff)) {
					free(ptmp_buff);
					return false;
				}
			}
			/* Move unconverted bytes to start of buffer */
			if (in_size > 0 && in_buff != in_start)
				memmove(iconv_push.m_udata, in_buff, in_size);
			iconv_push.m_offset = in_size;
			free(ptmp_buff);
			return true;
		} else if (errno == EILSEQ) {
			if (reset_iconv &&
			    iconv(conv_id, nullptr, nullptr, &out_buff, &out_size) ==
			    static_cast<size_t>(-1))
				/* ignore */;
			/*
			 * EILSEQ = invalid multi-byte sequence.
			 * Skip the problematic byte and try to continue.
			 *
			 * MSWord emits garbage bytes.
			 * Libreoffice emits U+FFFD again.
			 */
			tmp_len -= out_size;
			if (tmp_len > 0) {
				ptmp_buff[tmp_len] = '\0';
				if (!escape_output(ptmp_buff)) {
					free(ptmp_buff);
					return false;
				}
			}
			/* Skip one byte and keep the rest */
			if (in_size > 1) {
				memmove(iconv_push.m_udata, in_buff + 1, in_size - 1);
				iconv_push.m_offset = in_size - 1;
			} else {
				iconv_push.m_offset = 0;
			}
			free(ptmp_buff);
			return true;
		}
		/* Other error - just discard */
		free(ptmp_buff);
		preader->iconv_push.m_offset = 0;
		return true;
	}
	if (reset_iconv &&
	    iconv(conv_id, nullptr, nullptr, &out_buff, &out_size) ==
	    static_cast<size_t>(-1))
		/* ignore */;
	tmp_len -= out_size;
	ptmp_buff[tmp_len] = '\0';
	if (!escape_output(ptmp_buff)) {
		free(ptmp_buff);
		return false;
	}
	free(ptmp_buff);
	preader->iconv_push.m_offset = 0;
	return true;
}

static const char *rtf_cpid_to_encoding(cpid_t num)
{
	auto encoding = cpid_to_cset(num);
	return encoding != nullptr ? encoding : "windows-1252";
}

static int rtf_parse_control(const char *string,
	char *name, int maxlen, int *pnum)
{
    int len;
	
	if (('*' == string[0] || '~' == string[0] ||
		'_' == string[0] || '-' == string[0]) &&
		'\0' == string[1]) {
		name[0] = string[0];
		name[1] = '\0';
		return 0;
	}
	len = 0;
	while (HX_isalpha(*string) && len < maxlen) {
		*name++ = *string++;
        len ++;
    }
	if (len == maxlen)
		return -1;
	*name = '\0';
	if (*string == '\0')
		return 0;
	if (*string != '-' && !HX_isdigit(*string))
		return -1;
	*pnum = strtol(string, nullptr, 0);
	return 1;
}

static uint32_t rtf_fcharset_to_cpid(int num)
{
    switch (num) {
		case 0: return 1252;
		case 1: return CP_ACP;
		case 2: return CP_SYMBOL;
		case 77: return /*CP_MACCP*/ 10000;
		case 78: return 10001;
		case 79: return 10003;
		case 80: return 10008;
		case 81: return 10002;
		case 83: return 10005;
		case 84: return 10004;
		case 85: return 10006;
		case 86: return 10081;
		case 87: return 10021;
		case 88: return 10029;
		case 89: return 10007;
		case 128: return 932;
		case 129: return 949;
		case 130: return 1361;
		case 134: return 936;
		case 136: return 950;
		case 161: return 1253;
		case 162: return 1254;
		case 163: return 1258;
		case 177: return 1255;
		case 178: return 1256;
		case 186: return 1257;
		case 204: return 1251;
		case 222: return 874;
		case 238: return 1250;
		case 254: return 437;
		//case 255: return CP_OEMCP;
    }
	return 1252;
}

const FONTENTRY *rtf_reader::lookup_font(int num) const
{
	static const FONTENTRY fake_entries[] =
		{{FONTNIL_STR, ""}, {FONTROMAN_STR, ""},
		{FONTSWISS_STR, ""}, {FONTMODERN_STR, ""},
		{FONTSCRIPT_STR, ""}, {FONTDECOR_STR, ""},
		{FONTTECH_STR, ""}};
	
	if (num < 0)
		return &fake_entries[-num-1];
	auto preader = this;
	auto i = preader->pfont_hash.find(num);
	return i != preader->pfont_hash.cend() ? &i->second : nullptr;
}

bool rtf_reader::init_reader(std::string_view buf_in, attachment_list *atl)
{
	textmaps_init();
	auto preader = this;
	preader->attr_stack_list.clear();
	preader->ext_pull.init(buf_in.data(), buf_in.size(), [](size_t) -> void * { return nullptr; }, 0);
	if (!preader->ext_push.init(nullptr, 0, 0) ||
	    !preader->iconv_push.init(nullptr, 0, 0))
		return false;
	b_ubytes_switch = true;
	ubytes_num = 1;
	ubytes_left = 0;
	preader->pattachments = std::move(atl);
	return true;
}

static void rtf_delete_tree_node(SIMPLE_TREE_NODE *pnode)
{
	if (pnode->pdata != nullptr)
		free(pnode->pdata);
	free(pnode);
}

rtf_reader::~rtf_reader()
{
	auto preader = this;
	auto proot = preader->element_tree.get_root();
	if (proot != nullptr)
		preader->element_tree.destroy_node(proot, rtf_delete_tree_node);
	preader->element_tree.clear();
	if (preader->conv_id != ICONV_UNSET)
		iconv_close(preader->conv_id);
}

bool rtf_reader::express_begin_fontsize(int size)
{
	auto preader = this;
	char tmp_buff[128];
	
	switch (size) {
	case 8:
		QRF(ext_push.p_bytes(TAG_FONTSIZE8_BEGIN));
		return true;
	case 10:
		QRF(ext_push.p_bytes(TAG_FONTSIZE10_BEGIN));
		return true;
	case 12:
		QRF(ext_push.p_bytes(TAG_FONTSIZE12_BEGIN));
		return true;
	case 14:
		QRF(ext_push.p_bytes(TAG_FONTSIZE14_BEGIN));
		return true;
	case 18:
		QRF(ext_push.p_bytes(TAG_FONTSIZE18_BEGIN));
		return true;
	case 24:
		QRF(ext_push.p_bytes(TAG_FONTSIZE24_BEGIN));
		return true;
	}
	auto tmp_len = gx_snprintf(tmp_buff, std::size(tmp_buff), TAG_FONTSIZE_BEGIN, size);
	QRF(preader->ext_push.p_bytes(tmp_buff, tmp_len));
	return true;
}

bool rtf_reader::express_end_fontsize(int size)
{
	switch (size) {
	case 8:
		QRF(ext_push.p_bytes(TAG_FONTSIZE8_END));
		return true;
	case 10:
		QRF(ext_push.p_bytes(TAG_FONTSIZE10_END));
		return true;
	case 12:
		QRF(ext_push.p_bytes(TAG_FONTSIZE12_END));
		return true;
	case 14:
		QRF(ext_push.p_bytes(TAG_FONTSIZE14_END));
		return true;
	case 18:
		QRF(ext_push.p_bytes(TAG_FONTSIZE18_END));
		return true;
	case 24:
		QRF(ext_push.p_bytes(TAG_FONTSIZE24_END));
		return true;
	}
	QRF(ext_push.p_bytes(TAG_FONTSIZE_END));
	return true;
}

bool rtf_reader::express_attr_begin(int attr, int param)
{
	auto preader = this;
	
	switch (attr) {
	case ATTR_BOLD:
		QRF(ext_push.p_bytes(TAG_BOLD_BEGIN));
		return true;
	case ATTR_ITALIC:
		QRF(ext_push.p_bytes(TAG_ITALIC_BEGIN));
		return true;
	case ATTR_THICK_UL:
	case ATTR_WAVE_UL:
	case ATTR_DASH_UL:
	case ATTR_DOT_UL:
	case ATTR_DOT_DASH_UL:
	case ATTR_2DOT_DASH_UL:
	case ATTR_WORD_UL:
	case ATTR_UNDERLINE:
		QRF(ext_push.p_bytes(TAG_UNDERLINE_BEGIN));
		return true;
	case ATTR_DOUBLE_UL:
		QRF(ext_push.p_bytes(TAG_DBL_UNDERLINE_BEGIN));
		return true;
	case ATTR_FONTSIZE:
		return express_begin_fontsize(param);
	case ATTR_FONTFACE: {
		if (is_within_htmlrtf) {
			auto entry = lookup_font(param);
			auto encoding = entry != nullptr ? entry->encoding.c_str() :
			                default_encoding.c_str();
			return riconv_open(encoding);
		}
		auto pentry = lookup_font(param);
		const char *encoding;
		std::string tb;
		if (NULL == pentry) {
			encoding = default_encoding.c_str();
			mlog(LV_DEBUG, "rtf: invalid font number %d", param);
			tb = fmt::format(TAG_FONT_BEGIN, DEFAULT_FONT_STR);
		} else {
			encoding = pentry->encoding.c_str();
			tb = fmt::format(TAG_FONT_BEGIN, pentry->name);
		}
		if (!preader->have_fromhtml)
			QRF(ext_push.p_bytes(tb));
		if (!riconv_open(encoding))
			return false;
		return true;
	}
	case ATTR_FOREGROUND:
		QRF(ext_push.p_bytes(fmt::format(TAG_FOREGROUND_BEGIN, param)));
		return true;
	case ATTR_BACKGROUND:
		QRF(ext_push.p_bytes(fmt::format(TAG_BACKGROUND_BEGIN, param)));
		return true;
	case ATTR_SUPER:
		QRF(ext_push.p_bytes(TAG_SUPERSCRIPT_BEGIN));
		return true;
	case ATTR_SUB:
		QRF(ext_push.p_bytes(TAG_SUBSCRIPT_BEGIN));
		return true;
	case ATTR_STRIKE:
		QRF(ext_push.p_bytes(TAG_STRIKETHRU_BEGIN));
		return true;
	case ATTR_DBL_STRIKE:
		QRF(ext_push.p_bytes(TAG_DBL_STRIKETHRU_BEGIN));
		return true;
	case ATTR_EXPAND:
		QRF(ext_push.p_bytes(TAG_EXPAND_BEGIN));
		return true;
	case ATTR_OUTLINE:
		QRF(ext_push.p_bytes(TAG_OUTLINE_BEGIN));
		return true;
	case ATTR_SHADOW:
		QRF(ext_push.p_bytes(TAG_SHADOW_BEGIN));
		return true;
	case ATTR_EMBOSS:
		QRF(ext_push.p_bytes(TAG_EMBOSS_BEGIN));
		return true;
	case ATTR_ENGRAVE:
		QRF(ext_push.p_bytes(TAG_ENGRAVE_BEGIN));
		return true;
	case ATTR_CAPS:
		preader->b_simulate_allcaps = true;
		return true;
	case ATTR_SMALLCAPS:
		preader->b_simulate_smallcaps = true;
		QRF(ext_push.p_bytes(TAG_SMALLER_BEGIN));
		return true;
	case ATTR_UBYTES:
		preader->b_ubytes_switch = true;
		preader->ubytes_num = param;
		preader->ubytes_left = 0;
		return true;
	case ATTR_PICT:
		preader->is_within_picture = true;
		return true;
	case ATTR_HTMLTAG:
		preader->is_within_htmltag = true;
		if (!riconv_open(default_encoding.c_str()))
			return false;
		break;
	}
	return true;
}

const int *rtf_reader::stack_list_find_attr(int attr) const
{
	auto preader = this;
	if (preader->attr_stack_list.empty())
		return nullptr;
	auto pattrstack = preader->attr_stack_list.crbegin();
	for (int i = pattrstack->tos - 1; i >= 0; --i)
		if (attr == pattrstack->attr_stack[i])
			return &pattrstack->attr_params[i];
	for (++pattrstack; pattrstack != preader->attr_stack_list.crend(); ++pattrstack)
		for (int i = pattrstack->tos; i >= 0; --i)
			if (attr == pattrstack->attr_stack[i])
				return &pattrstack->attr_params[i];
	return NULL;
}

bool rtf_reader::express_attr_end(int attr, int param)
{
	auto preader = this;
	
	switch (attr) {
	case ATTR_BOLD:
		QRF(ext_push.p_bytes(TAG_BOLD_END));
		return true;
	case ATTR_ITALIC:
		QRF(ext_push.p_bytes(TAG_ITALIC_END));
		return true;
	case ATTR_THICK_UL:
	case ATTR_WAVE_UL:
	case ATTR_DASH_UL:
	case ATTR_DOT_UL:
	case ATTR_DOT_DASH_UL:
	case ATTR_2DOT_DASH_UL:
	case ATTR_WORD_UL:
	case ATTR_UNDERLINE:
		QRF(ext_push.p_bytes(TAG_UNDERLINE_END));
		return true;
	case ATTR_DOUBLE_UL:
		QRF(ext_push.p_bytes(TAG_DBL_UNDERLINE_END));
		return true;
	case ATTR_FONTSIZE:
		return express_end_fontsize(param);
	case ATTR_FONTFACE: 
		if (is_within_htmlrtf) {
			auto param = stack_list_find_attr(ATTR_FONTFACE);
			const char *encoding;
			if (param == nullptr) {
				encoding = default_encoding.c_str();
			} else {
				auto entry = lookup_font(*param);
				encoding = entry != nullptr ? entry->encoding.c_str() :
				           default_encoding.c_str();
			}
			return riconv_open(encoding);
		}
		if (!preader->have_fromhtml)
			QRF(ext_push.p_bytes(TAG_FONT_END));
		[[fallthrough]];
	case ATTR_HTMLTAG: {
		if (attr == ATTR_HTMLTAG)
			preader->is_within_htmltag = false;
		auto pparam = stack_list_find_attr(ATTR_FONTFACE);
		const char *encoding;
		if (NULL == pparam) {
			encoding = default_encoding.c_str();
		} else {
			auto pentry = lookup_font(*pparam);
			encoding = pentry != nullptr ? pentry->encoding.c_str() :
			           default_encoding.c_str();
		}
		if (!riconv_open(encoding))
			return false;
		return true;
	}
	case ATTR_FOREGROUND:
		QRF(ext_push.p_bytes(TAG_FOREGROUND_END));
		return true;
	case ATTR_BACKGROUND:
		QRF(ext_push.p_bytes(TAG_BACKGROUND_END));
		return true;
	case ATTR_SUPER:
		QRF(ext_push.p_bytes(TAG_SUPERSCRIPT_END));
		return true;
	case ATTR_SUB:
		QRF(ext_push.p_bytes(TAG_SUBSCRIPT_END));
		return true;
	case ATTR_STRIKE:
		QRF(ext_push.p_bytes(TAG_STRIKETHRU_END));
		return true;
	case ATTR_DBL_STRIKE:
		QRF(ext_push.p_bytes(TAG_DBL_STRIKETHRU_END));
		return true;
	case ATTR_OUTLINE:
		QRF(ext_push.p_bytes(TAG_OUTLINE_END));
		return true;
	case ATTR_SHADOW:
		QRF(ext_push.p_bytes(TAG_SHADOW_END));
		return true;
	case ATTR_EMBOSS:
		QRF(ext_push.p_bytes(TAG_EMBOSS_END));
		return true;
	case ATTR_ENGRAVE: 
		QRF(ext_push.p_bytes(TAG_ENGRAVE_END));
		return true;
	case ATTR_EXPAND: 
		QRF(ext_push.p_bytes(TAG_EXPAND_END));
		return true;
	case ATTR_CAPS:
		preader->b_simulate_allcaps = false;
		return true;
	case ATTR_SMALLCAPS: 
		QRF(ext_push.p_bytes(TAG_SMALLER_END));
		preader->b_simulate_smallcaps = false;
		return true;
	case ATTR_UBYTES:
		preader->b_ubytes_switch = false;
		return true;
	case ATTR_PICT:
		preader->is_within_picture = false;
		return true;
	}
	return true;
}

bool rtf_reader::astk_express_all()
{
	auto preader = this;
	if (preader->attr_stack_list.empty()) {
		mlog(LV_DEBUG, "rtf: no stack to express all attribute from");
		return true;
	}
	auto pattrstack = &*preader->attr_stack_list.crbegin();
	for (int i = 0; i <= pattrstack->tos; ++i)
		if (!express_attr_begin(pattrstack->attr_stack[i],
		    pattrstack->attr_params[i]))
			return false;
	return true;
}

bool rtf_reader::astk_pushx(int attr, int param)
{
	auto preader = this;
	if (preader->attr_stack_list.empty()) {
		mlog(LV_DEBUG, "rtf: cannot find stack node for pushing attribute");
		return false;
	}
	auto pattrstack = &*preader->attr_stack_list.rbegin();
	if (pattrstack->tos >= MAX_ATTRS - 1) {
		mlog(LV_DEBUG, "rtf: too many attributes");
		return false;
	}
	if (!is_within_htmlrtf && (!start_body() || !start_text()))
		return false;
	pattrstack->tos ++;
	pattrstack->attr_stack[pattrstack->tos] = attr;
	pattrstack->attr_params[pattrstack->tos] = param;
	return express_attr_begin(attr, param);
}

bool rtf_reader::astk_popx(int attr)
{
	auto preader = this;
	if (preader->attr_stack_list.empty())
		return true;
	auto pattrstack = preader->attr_stack_list.rbegin();
	if (pattrstack->tos < 0 || pattrstack->attr_stack[pattrstack->tos] != attr)
		return true;
	if (!express_attr_end(attr, pattrstack->attr_params[pattrstack->tos]))
		return false;
	pattrstack->tos--;
	return true;
}

int rtf_reader::astk_peek() const
{
	auto preader = this;
	if (preader->attr_stack_list.empty()) {
		mlog(LV_DEBUG, "rtf: cannot find stack node for peeking attribute");
		return ATTR_NONE;
	}
	auto pattrstack = &*preader->attr_stack_list.rbegin();
	return pattrstack->tos >= 0 ? pattrstack->attr_stack[pattrstack->tos] : ATTR_NONE;
}

bool rtf_reader::astk_popx_all()
{
	auto preader = this;
	if (preader->attr_stack_list.empty())
		return true;
	auto pattrstack = &*preader->attr_stack_list.rbegin();
	for (; pattrstack->tos>=0; pattrstack->tos--)
		if (!express_attr_end(pattrstack->attr_stack[pattrstack->tos],
		    pattrstack->attr_params[pattrstack->tos]))
			return false;
	return true;
}

bool rtf_reader::astk_find_popx(int attr)
{
	auto preader = this;
	int i;
	
	if (preader->attr_stack_list.empty()) {
		mlog(LV_DEBUG, "rtf: cannot find stack node for finding attribute");
		return true;
	}
	auto pattrstack = &*preader->attr_stack_list.rbegin();
	bool b_found = false;
	for (i=0; i<=pattrstack->tos; i++) {
		if (pattrstack->attr_stack[i] == attr) {
			b_found = true;
			break;
		}
	}
	if (!b_found) {
		mlog(LV_DEBUG, "rtf: cannot find attribute in stack node");
		return true;
	}
	for (i=pattrstack->tos; i>=0; i--) {
		if (!express_attr_end(pattrstack->attr_stack[i],
		    pattrstack->attr_params[i]))
			return false;
		if (pattrstack->attr_stack[i] == attr) {
			memmove(pattrstack->attr_stack + i,
				pattrstack->attr_stack + i + 1,
				pattrstack->tos - i);
			memmove(pattrstack->attr_params + i,
				pattrstack->attr_params + i + 1,
				sizeof(char*)*(pattrstack->tos - i));
			pattrstack->tos --;
			break;
		}
	}
	for (; i <= pattrstack->tos; ++i)
		if (!express_attr_begin(pattrstack->attr_stack[i],
		    pattrstack->attr_params[i]))
			return false;
	return true;
}

void rtf_reader::ungetchar(int ch)
{
	auto preader = this;
	if (preader->ungot_chars[0] >= 0 && preader->ungot_chars[1] >= 0 &&
	    preader->ungot_chars[2] >= 0)
		mlog(LV_DEBUG, "rtf: more than 3 ungot chars");
	preader->ungot_chars[2] = preader->ungot_chars[1];
	preader->ungot_chars[1] = preader->ungot_chars[0];
	preader->ungot_chars[0] = ch;
}

pack_result rtf_reader::getchar(int *pch)
{
	auto preader = this;
	int ch;

	if (preader->ungot_chars[0] >= 0) {
		ch = preader->ungot_chars[0]; 
		preader->ungot_chars[0] = preader->ungot_chars[1]; 
		preader->ungot_chars[1] = preader->ungot_chars[2];
		preader->ungot_chars[2] = -1;
		preader->last_returned_ch = ch;
		*pch = ch;
		return pack_result::ok;
	}
	do {
		uint8_t tmp_char = 0;
		auto status = preader->ext_pull.g_uint8(&tmp_char);
		if (status != pack_result::success)
			return status;
		ch = tmp_char;
		if (ch != '\n')
			continue;
		/* Convert \(newline) into \par here */
		if ('\\' == preader->last_returned_ch) {
			ungetchar(' ');
			ungetchar('r');
			ungetchar('a');
			ch = 'p';
			break;
		}
	} while (ch == '\r');
	if (ch == '\t')
		ch = ' ';
	preader->last_returned_ch = ch;
	*pch = ch;
	return pack_result::ok;
}

char *rtf_reader::read_element()
{
	auto preader = this;
	int ch, ch2;
	unsigned int ix;
	bool need_unget = false, have_whitespace = false;
	bool is_control_word = false, b_numeric_param = false;
	unsigned int current_max_length;
	
	
	ix = 0;
	current_max_length = 10;
	auto input_str = static_cast<char *>(calloc(1, current_max_length));
	if (NULL == input_str) {
		mlog(LV_DEBUG, "rtf: cannot allocate word storage");
		return NULL;
	}
	
	do {
		if (getchar(&ch) != pack_result::ok) {
			free(input_str);
			mlog(LV_DEBUG, "rtf: failed to get char from reader");
			return NULL;
		}
	} while ('\n' == ch);
	
	if (' ' == ch) {
		/* trim multiple space chars into one */
		while (' ' == ch) {
			if (getchar(&ch) != pack_result::ok) {
				free(input_str);
				mlog(LV_DEBUG, "rtf: failed to get char from reader");
				return NULL;
			}
			have_whitespace = true;
		}
		if (have_whitespace) {
			ungetchar(ch);
			input_str[0] = ' '; 
			input_str[1] = 0;
			return input_str;
		}
	}

	switch (ch) {
	case '\\':
		if (getchar(&ch2) != pack_result::ok) {
			free(input_str);
			mlog(LV_DEBUG, "rtf: failed to get char from reader");
			return NULL;
		}
		/* look for two-character command words */
		switch (ch2) {
		case '\n':
			strcpy (input_str, "\\par");
			return input_str;
		case '~':
		case '{':
		case '}':
		case '\\':
		case '_':
		case '-':
			input_str[0] = '\\';
			input_str[1] = ch2;
			input_str[2] = '\0';
			return input_str;
		case '\'':
			/* preserve \'## expressions (hex char exprs) for later */
			input_str[0]='\\'; 
			input_str[1]='\'';
			if (getchar(&ch) != pack_result::ok) {
				free(input_str);
				mlog(LV_DEBUG, "rtf: failed to get char from reader");
				return nullptr;
			}
			input_str[2] = ch;
			if (getchar(&ch) != pack_result::ok) {
				free(input_str);
				mlog(LV_DEBUG, "rtf: failed to get char from reader");
				return NULL;
			}
			input_str[3] = ch;
			input_str[4] = '\0';
			return input_str;
		}
		is_control_word = true;
		ix = 1;
		input_str[0] = ch;
		ch = ch2;
		break;
	case '\t':
		/* in rtf, a tab char is the same as \tab */
		strcpy (input_str, "\\tab");
		return input_str;
	case '{':
	case '}':
	case ';':
		input_str[0]=ch; 
		input_str[1]=0;
		return input_str;
	}

	while (true) {
		if ('\t' == ch || '{' == ch || '}' == ch || '\\' == ch) {
			need_unget = true;
			break;
		}
		if ('\n' == ch) { 
			if (is_control_word)
				break;
			if (getchar(&ch) != pack_result::ok) {
				free(input_str);
				mlog(LV_DEBUG, "rtf: failed to get char from reader");
				return NULL;
			}
			continue; 
		}
		if (';' == ch) {
			if (is_control_word) {
				need_unget = true;
				break;
			}
		}
		if (' ' == ch) {
			if (!is_control_word)
				need_unget = true;
			break;
		}
		if (is_control_word) {
			if (!b_numeric_param && (HX_isdigit(ch) || ch == '-')) {
				b_numeric_param = true;
			} else {
				if (b_numeric_param && !HX_isdigit(ch)) {
					if (ch != ' ')
						need_unget = true;
					break;
				}
			}
		}
		
		input_str[ix++] = ch;
		if (ix == current_max_length) {
			current_max_length *= 2;
			auto input_new = re_alloc<char>(input_str, current_max_length);
			if (NULL == input_new) {
				free(input_str);
				mlog(LV_DEBUG, "rtf: out of memory");
				return NULL;
			}
			input_str = input_new;
		}
		if (getchar(&ch) != pack_result::ok) {
			free(input_str);
			mlog(LV_DEBUG, "rtf: failed to get char from reader");
			return NULL;
		}
	}
	if (need_unget)
		ungetchar(ch);
	input_str[ix] = '\0';
	if (strncmp(input_str, "\\bin", 4) == 0 && HX_isdigit(input_str[4]))
		preader->ext_pull.advance(strtol(input_str + 4, nullptr, 0));
	return input_str;
}

bool rtf_reader::load_element_tree()
{
	auto preader = this;
	char *input_word;
	tree_node *plast_group = nullptr, *plast_node = nullptr;
	
	while ((input_word = read_element()) != nullptr) {
		if (input_word[0] == '{') {
			free(input_word);
			auto pgroup = me_alloc<tree_node>();
			if (NULL == pgroup) {
				mlog(LV_DEBUG, "rtf: out of memory");
				return false;
			}
			pgroup->pdata = nullptr;
			if (plast_group == nullptr)
				preader->element_tree.set_root(pgroup);
			else if (plast_node != nullptr)
				preader->element_tree.insert_sibling(
					plast_node, pgroup,
					SIMPLE_TREE_INSERT_AFTER);
			else
				preader->element_tree.add_child(plast_group,
					pgroup, SIMPLE_TREE_ADD_LAST);
			plast_group = pgroup;
			plast_node = NULL;
			continue;
		} else if (input_word[0] == '}') {
			free(input_word);
			if (NULL == plast_group) {
				mlog(LV_DEBUG, "rtf: rtf format error, missing first '{'");
				return false;
			}
			plast_node  = plast_group;
			plast_group = plast_group->get_parent();
			if (plast_group == nullptr)
				return true;
			continue;
		}

		if (NULL == plast_group) {
			free(input_word);
			mlog(LV_DEBUG, "rtf: rtf format error, missing first '{'");
			return false;
		}
		auto pword = me_alloc<tree_node>();
		if (NULL == pword) {
			free(input_word);
			mlog(LV_DEBUG, "rtf: out of memory");
			return false;
		}
		pword->pdata = input_word;
		if (plast_node == nullptr)
			preader->element_tree.add_child(plast_group, pword,
				SIMPLE_TREE_ADD_LAST);
		else
			preader->element_tree.insert_sibling(plast_node,
				pword, SIMPLE_TREE_INSERT_AFTER);
		plast_node = pword;
	}
	/* incomplete RTF... pretend it's ok */
	return true;
}

bool rtf_reader::start_body()
{
	auto preader = this;
	if (preader->have_printed_body)
		return true;
	preader->is_within_header = false;
	preader->have_printed_body = true;
	if (preader->have_fromhtml)
		return true;
	QRF(ext_push.p_bytes(TAG_HEADER_END));
	QRF(ext_push.p_bytes(TAG_BODY_BEGIN));
	return true;
}

bool rtf_reader::start_text()
{
	auto preader = this;
	if (!preader->is_within_table)
		return true;
	if (!preader->b_printed_row_begin) {
		QRF(ext_push.p_bytes(TAG_TABLE_ROW_BEGIN));
		preader->b_printed_row_begin = true;
		preader->b_printed_row_end = false;
		preader->b_printed_cell_begin = false;
	}
	if (!preader->b_printed_cell_begin) {
		QRF(ext_push.p_bytes(TAG_TABLE_CELL_BEGIN));
		if (!astk_express_all())
			return false;
		preader->b_printed_cell_begin = true;
		preader->b_printed_cell_end = false;
	}
	return true;
}

bool rtf_reader::start_par(int align)
{
	auto preader = this;
	if (preader->is_within_header && align != ALIGN_LEFT &&
	    !start_body())
		return false;
	switch (align) {
	case ALIGN_CENTER:
		QRF(ext_push.p_bytes(TAG_CENTER_BEGIN));
		break;
	case ALIGN_LEFT:
		break;
	case ALIGN_RIGHT:
		QRF(ext_push.p_bytes(TAG_ALIGN_RIGHT_BEGIN));
		break;
	case ALIGN_JUSTIFY:
		QRF(ext_push.p_bytes(TAG_JUSTIFY_BEGIN));
		break;
	}
	if (!have_fromhtml && !paragraph_open) {
		QRF(ext_push.p_bytes(TAG_PARAGRAPH_BEGIN));
		paragraph_open = true;
		total_chars_in_line = 0;
	}
	return true;
}

bool rtf_reader::end_par(int align)
{
	if (paragraph_open && !have_fromhtml) {
		QRF(ext_push.p_bytes(TAG_PARAGRAPH_END));
		paragraph_open = false;
	}
	switch (align) {
	case ALIGN_CENTER:
		QRF(ext_push.p_bytes(TAG_CENTER_END));
		break;
	case ALIGN_LEFT:
		break;
	case ALIGN_RIGHT:
		QRF(ext_push.p_bytes(TAG_ALIGN_RIGHT_END));
		break;
	case ALIGN_JUSTIFY:
		QRF(ext_push.p_bytes(TAG_JUSTIFY_END));
		break;
	}
	return true;
}

bool rtf_reader::begin_table() try
{
	auto preader = this;
	preader->is_within_table = true;
	preader->b_printed_row_begin = false;
	preader->b_printed_cell_begin = false;
	preader->b_printed_row_end = false;
	preader->b_printed_cell_end = false;
	preader->attr_stack_list.emplace_back();
	if (!start_body())
		return false;
	QRF(ext_push.p_bytes(TAG_TABLE_BEGIN));
	return true;
} catch (const std::bad_alloc &) {
	return false;
}

bool rtf_reader::end_table()
{
	auto preader = this;
	if (!preader->is_within_table)
		return true;
	if (!preader->b_printed_cell_end) {
		if (!astk_popx_all())
			return false;
		QRF(ext_push.p_bytes(TAG_TABLE_CELL_END));
	}
	if (!preader->b_printed_row_end)
		QRF(ext_push.p_bytes(TAG_TABLE_ROW_END));
	QRF(ext_push.p_bytes(TAG_TABLE_END));
	preader->is_within_table = false;
	preader->b_printed_row_begin = false;
	preader->b_printed_cell_begin = false;
	preader->b_printed_row_end = false;
	preader->b_printed_cell_end = false;
	return true;
}

bool rtf_reader::check_for_table()
{
	auto preader = this;
	if (preader->coming_pars_tabular == 0 && preader->is_within_table)
		return end_table();
	else if (preader->coming_pars_tabular != 0 && !preader->is_within_table)
		return begin_table();
	return true;
}

bool rtf_reader::put_iconv_cache(int ch)
{
	auto preader = this;
	if (preader->b_ubytes_switch && preader->ubytes_left > 0) {
		preader->ubytes_left --;
		return true;
	}
	QRF(preader->iconv_push.p_uint8(ch));
	return true;
}

bool rtf_reader::build_font_table(SIMPLE_TREE_NODE *pword)
{
	auto preader = this;
	int ret;
	int num;
	int param;
	char *ptoken;
	char name[1024];
	FONTENTRY tmp_entry;
	char tmp_buff[1024];
	char tmp_name[MAX_CONTROL_LEN];
	
	do {
		auto pword2 = pword->get_child();
		if (pword2 == nullptr || pword2->pdata == nullptr)
			return true;
		do {
			if (rtf_parse_control(&pword2->cdata[1],
			    tmp_name, MAX_CONTROL_LEN, &num) > 0 &&
			    strcmp(tmp_name, "f") == 0)
				break;
		} while ((pword2 = pword2->get_sibling()) != nullptr);
		if (pword2 == nullptr)
			continue;
		if (num < 0) {
			mlog(LV_DEBUG, "rtf: illegal font id in font table");
			return false;
		}
		tmp_buff[0] = '\0';
		cpid_t cpid = CP_UNSET, fcharsetcp = CP_UNSET;
		size_t tmp_offset = 0;
		while ((pword2 = pword2->get_sibling()) != nullptr) {
			if (pword2->pdata == nullptr)
				continue;
			auto string = pword2->cdata;
			if ('\\' != string[0]) {
				auto tmp_len = strlen(string);
				if (tmp_len + tmp_offset > sizeof(tmp_buff) - 1) {
					mlog(LV_DEBUG, "rtf: invalid font name");
					return false;
				}
				memcpy(tmp_buff + tmp_offset, string, tmp_len);
				tmp_offset += tmp_len;
				continue;
			} else if (string[1] == '\'' && string[2] != '\0' && string[3] != '\0') {
				if (tmp_offset + 1 > sizeof(tmp_buff) - 1) {
					mlog(LV_DEBUG, "rtf: invalid font name");
					return false;
				}
				tmp_buff[tmp_offset++] = rtf_decode_hex_char(string + 2);
				continue;
			}
			ret = rtf_parse_control(string + 1,
			      tmp_name, MAX_CONTROL_LEN, &param);
			if (ret < 0) {
				mlog(LV_DEBUG, "rtf: illegal control word in font table");
				continue;
			} else if (ret == 0) {
				continue;
			}
			/* ret > 0 */
			if (0 == strcmp(tmp_name, "u")) {
				auto buf = wchar_to_utf8(param);
				gx_strlcpy(tmp_name, buf.c_str(), std::size(tmp_name));
				cpid_t tmp_cpid = cpid != CP_UNSET ? cpid :
				                   fcharsetcp != CP_UNSET ? fcharsetcp :
				                   static_cast<cpid_t>(1252);
				if (!string_utf8_to_mb(rtf_cpid_to_encoding(tmp_cpid),
				    tmp_name, name, std::size(name))) {
					mlog(LV_DEBUG, "rtf: invalid font name");
					return false;
				}
				auto tmp_len = strlen(name);
				if (tmp_len + tmp_offset >
				    sizeof(tmp_buff) - 1) {
					mlog(LV_DEBUG, "rtf: invalid font name");
					return false;
				}
				memcpy(tmp_buff + tmp_offset, name, tmp_len);
				tmp_offset += tmp_len;
			} else if (0 == strcmp(tmp_name, "fcharset")) {
				fcharsetcp = static_cast<cpid_t>(rtf_fcharset_to_cpid(param));
			} else if (0 == strcmp(tmp_name, "cpg")) {
				cpid = static_cast<cpid_t>(param);
			}
		}
		if (0 == tmp_offset) {
			mlog(LV_DEBUG, "rtf: invalid font name");
			return false;
		}
		tmp_buff[tmp_offset] = '\0';
		if (cpid == CP_UNSET)
			cpid = fcharsetcp;
		if (cpid != CP_UNSET)
			tmp_entry.encoding = rtf_cpid_to_encoding(cpid);
		else if (strcasestr(name, "symbol") != nullptr)
			tmp_entry.encoding.clear();
		else
			tmp_entry.encoding = "windows-1252";
		if (cpid == CP_UNSET)
			cpid = static_cast<cpid_t>(1252);
		if (!string_mb_to_utf8(rtf_cpid_to_encoding(cpid), tmp_buff,
		    name, std::size(name))) {
			mlog(LV_DEBUG, "rtf: invalid font name");
			strcpy(name, DEFAULT_FONT_STR);
		}
		ptoken = strchr(name, ';');
		if (ptoken != nullptr)
			*ptoken = '\0';
		tmp_entry.name = name;
		try {
			if (preader->pfont_hash.size() < MAX_FONTS)
				preader->pfont_hash.emplace(num, std::move(tmp_entry));
		} catch (const std::bad_alloc &) {
			mlog(LV_ERR, "E-1986: ENOMEM");
		}
	} while ((pword = pword->get_sibling()) != nullptr);
	if (default_encoding.empty())
		default_encoding = "windows-1252";
	if (!have_ansicpg && default_font_number >= 0) {
		auto entry = lookup_font(default_font_number);
		if (entry != nullptr && !entry->encoding.empty())
			default_encoding = entry->encoding;
	}
	return true;
}

bool rtf_reader::word_output_date(SIMPLE_TREE_NODE *pword)
{
	int day;
	int hour;
	int year;
	int month;
	int minute;
	char tmp_buff[32];
	
	day = 0;
	hour = -1;
	year = 0;
	month = 0;
	minute = -1;
	do {
		if (pword->pdata == nullptr)
			return false;
		auto string = pword->cdata;
		if ('\\' == *string) {
			string ++;
			if (0 == strncmp(string, "yr", 2) && HX_isdigit(string[2]))
				year = strtol(string + 2, nullptr, 0);
			else if (strncmp(string, "mo", 2) == 0 && HX_isdigit(string[2]))
				month = strtol(string + 2, nullptr, 0);
			else if (strncmp(string, "dy", 2) == 0 && HX_isdigit(string[2]))
				day = strtol(string + 2, nullptr, 0);
			else if (strncmp(string, "min", 3) == 0 && HX_isdigit(string[3]))
				minute = strtol(string + 3, nullptr, 0);
			else if (strncmp(string, "hr", 2) == 0 && HX_isdigit(string[2]))
				hour = strtol(string + 2, nullptr, 0);
		}
	} while ((pword = pword->get_sibling()) != nullptr);
	year   = std::max(0, std::min(9999, year));
	month  = std::max(0, std::min(99, month)); /* fit within %02d */
	day    = std::max(0, std::min(99, day));
	hour   = std::max(0, std::min(99, hour));
	minute = std::max(0, std::min(99, minute));
	auto tmp_len = gx_snprintf(tmp_buff, std::size(tmp_buff), "%04d-%02d-%02d ", year, month, day);
	if (hour >= 0 && minute >= 0)
		tmp_len += gx_snprintf(&tmp_buff[tmp_len], std::size(tmp_buff)-tmp_len, "%02d:%02d ", hour, minute);
	QRF(ext_push.p_bytes(tmp_buff, tmp_len));
	return true;
}

bool rtf_reader::process_info_group(SIMPLE_TREE_NODE *pword)
{
	int ch;

	for (; pword != nullptr; pword = pword->get_sibling()) {
		auto pchild = pword->get_child();
		if (pchild == nullptr)
			continue;
		if (pchild->pdata == nullptr)
			return true;
		if (strcmp(pchild->cdata, "\\title") == 0) {
			QRF(ext_push.p_bytes(TAG_DOCUMENT_TITLE_BEGIN));
			for (auto pword2 = pchild->get_sibling();
			     pword2 != nullptr; pword2 = pword2->get_sibling()) {
				if (pword2->pdata == nullptr)
					continue;
				if (pword2->cdata[0] != '\\') {
					if (!riconv_flush())
						return false;
					auto slen = strlen(pword2->cdata);
					if (!push_text_encoded(pword2->cdata, slen))
						return false;
				} else if (pword2->cdata[1] == '\'') {
					ch = rtf_decode_hex_char(&pword2->cdata[2]);
					if (!put_iconv_cache(ch))
						return false;
				}
			}
			if (!riconv_flush())
				return false;
			QRF(ext_push.p_bytes(TAG_DOCUMENT_TITLE_END));
		} else if (strcmp(pchild->cdata, "\\author") == 0) {
			QRF(ext_push.p_bytes(TAG_DOCUMENT_AUTHOR_BEGIN));
			for (auto pword2 = pchild->get_sibling();
			     pword2 != nullptr; pword2 = pword2->get_sibling()) {
				if (pword2->pdata == nullptr)
					continue;
				if (pword2->cdata[0] != '\\') {
					if (!riconv_flush())
						return false;
					auto slen = strlen(pword2->cdata);
					if (!push_text_encoded(pword2->cdata, slen))
						return false;
				} else if (pword2->cdata[1] == '\'') {
					ch = rtf_decode_hex_char(&pword2->cdata[2]);
					if (!put_iconv_cache(ch))
						return false;
				}
			}
			if (!riconv_flush())
				return false;
			QRF(ext_push.p_bytes(TAG_DOCUMENT_AUTHOR_END));
		} else if (strcmp(pchild->cdata, "\\creatim") == 0) {
			QRF(ext_push.p_bytes(TAG_COMMENT_BEGIN));
			QRF(ext_push.p_bytes("creation date: "));
			if (pchild->get_sibling() != nullptr &&
			    !word_output_date(pchild->get_sibling()))
				return false;
			QRF(ext_push.p_bytes(TAG_COMMENT_END));
		} else if (strcmp(pchild->cdata, "\\printim") == 0) {
			QRF(ext_push.p_bytes(TAG_COMMENT_BEGIN));
			QRF(ext_push.p_bytes("last print date: "));
			if (pchild->get_sibling() != nullptr &&
			    !word_output_date(pchild->get_sibling()))
				return false;
			QRF(ext_push.p_bytes(TAG_COMMENT_END));
		} else if (strcmp(pchild->cdata, "\\buptim") == 0) {
			QRF(ext_push.p_bytes(TAG_COMMENT_BEGIN));
			QRF(ext_push.p_bytes("last backup date: "));
			if (pchild->get_sibling() != nullptr &&
			    !word_output_date(pchild->get_sibling()))
					return false;
			QRF(ext_push.p_bytes(TAG_COMMENT_END));
		} else if (strcmp(pchild->cdata, "\\revtim") == 0) {
			QRF(ext_push.p_bytes(TAG_COMMENT_BEGIN));
			QRF(ext_push.p_bytes("modified date: "));
			if (pchild->get_sibling() != nullptr &&
			    !word_output_date(pchild->get_sibling()))
				return false;
			QRF(ext_push.p_bytes(TAG_COMMENT_END));
		}
	}
	return true;
}


static void rtf_process_color_table(
	RTF_READER *preader, SIMPLE_TREE_NODE *pword)
{
	int r;
	int g;
	int b;
	bool has_color = false;
	
	r = 0;
	g = 0;
	b = 0;
	do {
		if (pword->pdata == nullptr || preader->total_colors >= MAX_COLORS)
			break;
		if (strncmp("\\red", pword->cdata, 4) == 0) {
			r = strtol(&pword->cdata[4], nullptr, 0);
			while (r > 255)
				r >>= 8;
			has_color = true;
		} else if (strncmp("\\green", pword->cdata, 6) == 0) {
			g = strtol(&pword->cdata[6], nullptr, 0);
			while (g > 255)
				g >>= 8;
			has_color = true;
		} else if (strncmp("\\blue", pword->cdata, 5) == 0) {
			b = strtol(&pword->cdata[5], nullptr, 0);
			while (b > 255)
				b >>= 8;
			has_color = true;
		} else if (strcmp(pword->cdata, ";") == 0) {
			/*
			 * If color definition is omitted (semicolon without
			 * RGB values), it means "auto" color, inherited from
			 * context. Use -1 as marker for auto color.
			 */
			preader->color_table[preader->total_colors++] =
				has_color ? ((r << 16) | (g << 8) | b) : AUTO_COLOR;
			if (preader->total_colors >= MAX_COLORS)
				return;
			r = 0;
			g = 0;
			b = 0;
			has_color = false;
		}
	} while ((pword = pword->get_sibling()) != nullptr);
}

int rtf_reader::cmd_continue(SIMPLE_TREE_NODE *, int, bool, int)
{
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_cf(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param || num < 0 || num >= total_colors) {
		mlog(LV_DEBUG, "rtf: font color change to %xh is invalid", num);
	} else if (color_table[num] == AUTO_COLOR) {
		/* Reset to default color */
		if (!astk_find_popx(ATTR_FOREGROUND))
			return CMD_RESULT_ERROR;
		return CMD_RESULT_CONTINUE;
	} else if (!astk_pushx(ATTR_FOREGROUND, color_table[num])) {
		return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_cb(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param || num < 0 || num >= total_colors) {
		mlog(LV_DEBUG, "rtf: font color change attempted is invalid");
	} else if (color_table[num] == AUTO_COLOR) {
		/* Reset to default background */
		if (!astk_find_popx(ATTR_BACKGROUND))
			return CMD_RESULT_ERROR;
		return CMD_RESULT_CONTINUE;
	} else if (!astk_pushx(ATTR_BACKGROUND, color_table[num])) {
		return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_fs(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	num /= 2;
	return astk_pushx(ATTR_FONTSIZE, num) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_field(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	char tmp_buff[1024];
	bool b_endnotecitations = false;
	
	do {
		auto pchild = pword->get_child();
		if (pchild == nullptr || pchild->pdata == nullptr)
			return CMD_RESULT_IGNORE_REST;
		if (strcmp(pchild->cdata, "\\fldrslt") == 0)
			return CMD_RESULT_CONTINUE;
		if (strcmp(pchild->cdata, "\\*") != 0)
			continue;
		for (auto pword2 = pchild->get_sibling(); pword2 != nullptr;
		     pword2 = pword2->get_sibling()) {
			if (pword2->pdata == nullptr ||
			    strcmp(pword2->cdata, "\\fldinst") != 0)
				continue;
			auto pword3 = pword2->get_sibling();
			if (pword3 != nullptr && pword3->pdata != nullptr &&
			    strcmp(pword3->cdata, "SYMBOL") == 0) {
				auto pword4 = pword3->get_sibling();
				while (pword4 != nullptr && pword4->pdata != nullptr &&
				    strcmp(pword4->cdata, " ") == 0)
					pword4 = pword4->get_sibling();
				if (NULL != pword4 && NULL != pword4->pdata) {
					int ch = strtol(pword4->cdata, nullptr, 0);
					if (!astk_pushx(ATTR_FONTFACE, -7))
						return CMD_RESULT_ERROR;
					auto tmp_len = gx_snprintf(tmp_buff, std::size(tmp_buff),
					          TAG_UNISYMBOL_PRINT, ch);
					if (ext_push.p_bytes(tmp_buff, tmp_len) != pack_result::ok)
						return CMD_RESULT_ERROR;
				}
			}
			for (; pword3 != nullptr; pword3 = pword3->get_sibling())
				if (pword3->get_child() != nullptr)
					break;
			if (pword3 != nullptr)
				pword3 = pword3->get_child();
			for (; pword3 != nullptr; pword3 = pword3->get_sibling()) {
				if (pword3->pdata == nullptr)
					return CMD_RESULT_CONTINUE;
				if (strcmp(pword3->cdata, "EN.CITE") == 0) {
					b_endnotecitations = true;
					continue;
				} else if (strcmp(pword3->cdata, "HYPERLINK") != 0) {
					continue;
				}
				if (b_endnotecitations)
					continue;
				auto pword4 = pword3->get_sibling();
				while (pword4 != nullptr && pword4->pdata != nullptr &&
				    strcmp(pword4->cdata, " ") == 0)
					pword4 = pword4->get_sibling();
				if (NULL != pword4 && NULL != pword4->pdata) {
					auto tmp_len = gx_snprintf(tmp_buff, std::size(tmp_buff),
						  TAG_HYPERLINK_BEGIN, pword4->cdata);
					if (ext_push.p_bytes(tmp_buff, tmp_len) != pack_result::ok)
						return CMD_RESULT_ERROR;
					return CMD_RESULT_HYPERLINKED;
				}
			}
		}
	} while ((pword = pword->get_sibling()) != nullptr);
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_f(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	auto pentry = lookup_font(num);
	if (pentry == nullptr || strcasestr(pentry->name.c_str(), "symbol") != nullptr)
		return CMD_RESULT_CONTINUE;
	return astk_pushx(ATTR_FONTFACE, num) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_af(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param)
		associated_font_number = num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_loch(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/* Flush any pending multi-byte data before switching modes */
	if (!riconv_flush())
		return CMD_RESULT_ERROR;
	char_byte_mode = 0;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_hich(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/* Flush any pending multi-byte data before switching modes */
	if (!riconv_flush())
		return CMD_RESULT_ERROR;
	char_byte_mode = 1;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_dbch(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	auto preader = this;
	/* Flush any pending single-byte data before switching modes */
	if (!riconv_flush())
		return CMD_RESULT_ERROR;
	char_byte_mode = 2;
	/*
	 * When switching to double-byte mode, use the associated font (\af)
	 * if one was specified, as it typically has the correct CJK encoding.
	 */
	if (preader->associated_font_number < 0)
		return CMD_RESULT_CONTINUE;
	auto font = lookup_font(preader->associated_font_number);
	if (font != nullptr && font->encoding[0] != '\0' &&
	    !riconv_open(font->encoding.c_str()))
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

/*
 * Map LCID (Language Code ID) to an appropriate code page.
 * This helps when the font doesn't specify an encoding but the
 * language tag gives us a hint about what encoding to use.
 */
static cpid_t rtf_lcid_to_cpid(int lcid)
{
	/*
	 * LCID format: primary language (low byte), sublanguage (high byte)
	 * We mainly care about the primary language for CJK support.
	 */
	int primary = lcid & 0x3FF;
	switch (primary) {
	case 0x01: return static_cast<cpid_t>(1256); /* Arabic */
	case 0x04: return static_cast<cpid_t>(936);  /* Chinese (Simplified) */
	case 0x08: return static_cast<cpid_t>(1253); /* Greek */
	case 0x0D: return static_cast<cpid_t>(1255); /* Hebrew */
	case 0x11: return static_cast<cpid_t>(932);  /* Japanese */
	case 0x12: return static_cast<cpid_t>(949);  /* Korean */
	case 0x19: return static_cast<cpid_t>(1251); /* Russian/Cyrillic */
	case 0x1E: return static_cast<cpid_t>(874);  /* Thai */
	case 0x1F: return static_cast<cpid_t>(1254); /* Turkish */
	case 0x2A: return static_cast<cpid_t>(1258); /* Vietnamese */
	default:
		/* Check for Traditional Chinese (Taiwan, Hong Kong, Macao) */
		if ((lcid & 0xFFFF) == 0x0404 || /* zh-TW */
		    (lcid & 0xFFFF) == 0x0C04 || /* zh-HK */
		    (lcid & 0xFFFF) == 0x1404)   /* zh-MO */
			return static_cast<cpid_t>(950); /* Big5 */
		return CP_ACP;
	}
}

int rtf_reader::cmd_lang(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	/*
	 * Only use lang to set encoding if we don't already have a specific
	 * encoding from ansicpg or font table. Lang provides a fallback hint.
	 */
	if (have_ansicpg)
		return CMD_RESULT_CONTINUE;
	auto cpid = rtf_lcid_to_cpid(num);
	if (cpid == CP_ACP)
		return CMD_RESULT_CONTINUE;
	auto enc = rtf_cpid_to_encoding(cpid);
	if (enc != nullptr && default_encoding[0] == '\0')
		default_encoding = enc;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_langfe(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	/*
	 * \langfe specifically indicates Far East language, which is a strong
	 * hint for CJK encoding. This is particularly useful when \dbch is used.
	 */
	auto cpid = rtf_lcid_to_cpid(num);
	if (cpid == CP_ACP)
		return CMD_RESULT_CONTINUE;
	auto enc = rtf_cpid_to_encoding(cpid);
	if (enc == nullptr)
		return CMD_RESULT_CONTINUE;
	/*
	 * When we see langfe, set it up so that \dbch mode
	 * will use this encoding for double-byte characters.
	 */
	if (char_byte_mode == 2 && !riconv_open(enc))
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_deflang(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	/* Set default encoding from default language if not already set */
	if (!have_ansicpg && default_encoding[0] == '\0') {
		auto cpid = rtf_lcid_to_cpid(num);
		if (cpid != CP_ACP) {
			auto enc = rtf_cpid_to_encoding(cpid);
			if (enc != nullptr)
				default_encoding = enc;
		}
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::flush_pending(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/*
	 * LTR/RTL character property. Flush pending data. For proper LTR/RTL
	 * support we would need to insert Unicode bidi controls, but for now
	 * we just ensure encoding state is consistent.
	 */
	if (!riconv_flush())
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_deff(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (have_param)
		preader->default_font_number = num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_highlight(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (!have_param || num < 0 || num >= total_colors) {
		mlog(LV_DEBUG, "rtf: font background "
			"color change attempted is invalid");
	} else if (color_table[num] == AUTO_COLOR) {
		/* Reset to default background */
		if (!astk_find_popx(ATTR_BACKGROUND))
			return CMD_RESULT_ERROR;
		return CMD_RESULT_CONTINUE;
	} else if (!astk_pushx(ATTR_BACKGROUND, color_table[num])) {
		return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_tab(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	auto preader = this;
	int need;
	
	if (preader->have_fromhtml) {
		if (preader->ext_push.p_uint8(0x09) != pack_result::ok)
			return CMD_RESULT_ERROR;
		++preader->total_chars_in_line;
		return CMD_RESULT_CONTINUE;
	}
	need = 8 - preader->total_chars_in_line % 8;
	preader->total_chars_in_line += need;
	while (need > 0) {
		if (ext_push.p_bytes(TAG_FORCED_SPACE) != pack_result::ok)
			return CMD_RESULT_ERROR;
		need--;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_plain(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	/*
	 * Per the spec, \plain resets character formatting to defaults:
	 *
	 * - Disable bold, italic, underline, etc.
	 * - Reset font size to 12pt (24 half-points)
	 * - Reset to default font
	 *
	 * First, close all current formatting, then apply defaults.
	 */
	if (!astk_popx_all())
		return CMD_RESULT_ERROR;
	/* Reset to default font (if defined via \deff) */
	if (default_font_number >= 0 && !astk_pushx(ATTR_FONTFACE, default_font_number))
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_fnil(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -1) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_froman(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -2) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_fswiss(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -3) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_fmodern(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -4) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_fscript(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -5) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_fdecor(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -6) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ftech(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_FONTFACE, -7) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_expand(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	if (0 == num) {
		if (!astk_popx(ATTR_EXPAND))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_EXPAND, num / 4))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_emboss(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_find_popx(ATTR_EMBOSS))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_EMBOSS, num))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_engrave(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_ENGRAVE))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_ENGRAVE, num))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_caps(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_CAPS))
			return CMD_RESULT_ERROR;
	} else { 
		if (!astk_pushx(ATTR_CAPS, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_scaps(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_SMALLCAPS))
			return CMD_RESULT_ERROR;
	} else { 
		if (!astk_pushx(ATTR_SMALLCAPS, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_bullet(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_BULLET) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_ldblquote(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_LEFT_DBL_QUOTE) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_rdblquote(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_RIGHT_DBL_QUOTE) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_lquote(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_LEFT_QUOTE) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_nonbreaking_space(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_NONBREAKING_SPACE) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_nonbreaking_hyphen(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (ext_push.p_bytes(wchar_to_utf8(0x2011)) != pack_result::ok)
		return CMD_RESULT_ERROR;
	++total_chars_in_line;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_soft_hyphen(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_SOFT_HYPHEN) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_emdash(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_EMDASH) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_endash(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_ENDASH) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_rquote(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_CHARS_RIGHT_QUOTE) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_par(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	auto preader = this;
	if (preader->have_fromhtml)
		return preader->ext_push.p_bytes("\r\n") == pack_result::ok ?
		       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
	if (!start_body() || !start_text() || !end_par(align) || !start_par(align))
		return CMD_RESULT_ERROR;
	total_chars_in_line = 0;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_line(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (have_fromhtml) {
		if (ext_push.p_bytes("\r\n") != pack_result::ok)
			return CMD_RESULT_ERROR;
		return CMD_RESULT_CONTINUE;
	}
	if (ext_push.p_bytes(TAG_LINE_BREAK) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_page(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (ext_push.p_bytes(TAG_PAGE_BREAK) != pack_result::ok)
		return CMD_RESULT_ERROR;
	preader->total_chars_in_line ++;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_intbl(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->coming_pars_tabular ++;
	return check_for_table() ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulnone(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	while (true) {
		auto attr = astk_peek();
		if (ATTR_UNDERLINE == attr || ATTR_DOT_UL == attr ||
			ATTR_DASH_UL == attr || ATTR_DOT_DASH_UL == attr||
		    ATTR_2DOT_DASH_UL == attr || ATTR_WORD_UL == attr ||
			ATTR_WAVE_UL == attr || ATTR_THICK_UL == attr ||
		    ATTR_DOUBLE_UL == attr) {
			if (!astk_popx(attr))
				break;
		} else {
			break;
		}
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_ul(SIMPLE_TREE_NODE *pword, int align,
    bool b_param, int num)
{
	if (b_param && num == 0)
		return cmd_ulnone(pword, align, b_param, num);
	return astk_pushx(ATTR_UNDERLINE, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_uld(SIMPLE_TREE_NODE *pword, int align,
    bool b_param, int num)
{
	return astk_pushx(ATTR_DOUBLE_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_uldb(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_DOUBLE_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_uldash(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_DASH_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_uldashd(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_DOT_DASH_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_uldashdd(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_2DOT_DASH_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulw(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return astk_pushx(ATTR_WORD_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulth(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_THICK_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulthd(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_THICK_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulthdash(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_THICK_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_ulwave(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_pushx(ATTR_WAVE_UL, 0) ? CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_strike(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_STRIKE))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_STRIKE, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_strikedl(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_DBL_STRIKE))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_DBL_STRIKE, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_striked(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_DBL_STRIKE))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_DBL_STRIKE, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_up(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param || num == 0) { // XXX
		if (!astk_popx(ATTR_SUPER))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_SUPER, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_upr(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/*
	 * \upr groups contain two representations:
	 * {\upr {ansi_text} {\*\ud {unicode_text}}}
	 *
	 * The first child is the ANSI representation for legacy readers. The
	 * \ud destination contains the Unicode representation. As a
	 * Unicode-aware reader, we skip the ANSI part and process only the \ud
	 * content.
	 */
	for (auto sibling = pword->get_sibling(); sibling != nullptr;
	     sibling = sibling->get_sibling()) {
		/* Look for a group node (pdata == nullptr means it's a group) */
		if (sibling->pdata != nullptr)
			continue;
		auto child = sibling->get_child();
		if (child == nullptr || child->pdata == nullptr)
			continue;
		/* Check for \* prefix */
		if (strcmp(child->cdata, "\\*") != 0)
			continue;
		/* Look for \ud in the group */
		for (auto word2 = child->get_sibling(); word2 != nullptr;
		     word2 = word2->get_sibling()) {
			if (word2->pdata == nullptr ||
			    strcmp(word2->cdata, "\\ud") != 0)
				continue;
			/* Found \ud; process the rest of this group */
			auto ud_content = word2->get_sibling();
			if (ud_content != nullptr) {
				auto ret = convert_group_node(ud_content);
				if (ret != 0)
					return CMD_RESULT_ERROR;
			}
			/* Skip the ANSI representation */
			return CMD_RESULT_IGNORE_REST;
		}
	}
	/* No \ud found, fall back to processing everything (ANSI content) */
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_v(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_HIDDEN))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_HIDDEN, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_u(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/*
	 * RTF uses signed 16-bit values for Unicode. Values > 32767 are
	 * represented as negative numbers (e.g., \u-10000 means U+D8F0).
	 * Convert to unsigned 16-bit first to get the correct codepoint.
	 */
	uint32_t codepoint = static_cast<uint16_t>(num);
	if (!escape_output(wchar_to_utf8(codepoint).data()))
		return CMD_RESULT_ERROR;
	auto preader = this;
	if (preader->b_ubytes_switch)
		preader->ubytes_left = preader->ubytes_num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_uc(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		num = ubytes_num != 0 ? ubytes_num : 1;
	if (num < 0)
		num = 0;
	b_ubytes_switch = true;
	ubytes_num = num;
	ubytes_left = 0;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_dn(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_SUB))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_SUB, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_nosupersub(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return astk_popx(ATTR_SUPER) && astk_popx(ATTR_SUB) ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

/**
 * Object Attachment Placeholder
 *
 * MS-OXRTFEX v15 §2.2.3.4: This control word marks where in the text stream
 * the next attachment should appear. It is followed by a space character that
 * acts as a placeholder for the attachment.
 *
 * For HTML output, the Unicode Object Replacement Character is emitted, which
 * is the standard placeholder for inline objects.
 */
int rtf_reader::cmd_objattph(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (ext_push.p_bytes(wchar_to_utf8(0xFFFC)) != pack_result::ok)
		return CMD_RESULT_ERROR;
	++total_chars_in_line;
	skip_objattph_space = true;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_super(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_SUPER))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_SUPER, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_sub(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_SUB))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_SUB, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_shad(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_SHADOW))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_SHADOW, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_b(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_find_popx(ATTR_BOLD))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_BOLD, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_i(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_find_popx(ATTR_ITALIC))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_ITALIC, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_sect(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (ext_push.p_bytes(TAG_PARAGRAPH_BEGIN) != pack_result::ok)
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_outl(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	if (have_param && num == 0) {
		if (!astk_popx(ATTR_OUTLINE))
			return CMD_RESULT_ERROR;
	} else {
		if (!astk_pushx(ATTR_OUTLINE, 0))
			return CMD_RESULT_ERROR;
	}
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_ansi(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	default_encoding = "windows-1252";
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_ansicpg(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	default_encoding = rtf_cpid_to_encoding(static_cast<cpid_t>(num));
	preader->have_ansicpg = true;
	return CMD_RESULT_CONTINUE;
}

/* \cpg - code page switch within document body */
int rtf_reader::cmd_cpg(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (!have_param)
		return CMD_RESULT_CONTINUE;
	auto enc = rtf_cpid_to_encoding(static_cast<cpid_t>(num));
	if (enc == nullptr)
		return CMD_RESULT_CONTINUE;
	if (!riconv_open(enc))
		return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_pc(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	default_encoding = "CP437";
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_pca(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	default_encoding = "CP850";
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_mac(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	default_encoding = "MAC";
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_colortbl(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	pword = pword->get_sibling();
	if (pword != nullptr)
		rtf_process_color_table(preader, pword);
	return CMD_RESULT_IGNORE_REST;
}

int rtf_reader::cmd_fonttbl(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	pword = pword->get_sibling();
	if (pword != nullptr && !build_font_table(pword))
		return CMD_RESULT_ERROR;
	return CMD_RESULT_IGNORE_REST;
}

int rtf_reader::cmd_ignore(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	return CMD_RESULT_IGNORE_REST;
}

int rtf_reader::cmd_maybe_ignore(SIMPLE_TREE_NODE *pword,
    int align, bool b_param, int num)
{
	int param;
	char name[MAX_CONTROL_LEN];
	
	pword = pword->get_sibling();
	if (pword == nullptr || pword->pdata == nullptr ||
	    pword->cdata[0] == '\\')
		return CMD_RESULT_IGNORE_REST;
	if (rtf_parse_control(&pword->cdata[1],
	    name, MAX_CONTROL_LEN, &param) < 0)
		return CMD_RESULT_ERROR;
	if (rtf_find_cmd_function(name) != nullptr)
		return CMD_RESULT_CONTINUE;
	return CMD_RESULT_IGNORE_REST;
}

int rtf_reader::cmd_info(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto pword1 = pword->get_sibling();
	if (pword1 != nullptr)
		process_info_group(pword1);
	return CMD_RESULT_IGNORE_REST;
}

int rtf_reader::cmd_pict(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (!astk_pushx(ATTR_PICT, 0))
		return CMD_RESULT_ERROR;
	preader->picture_width = 0;
	preader->picture_height = 0;
	preader->picture_type = PICT_WB;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_macpict(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_MAC;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_jpegblip(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_JPEG;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_pngblip(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_PNG;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_emfblip(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_EMF;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_emspace(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (ext_push.p_bytes("&emsp;") != pack_result::ok)
		return CMD_RESULT_ERROR;
	++total_chars_in_line;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_enspace(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (ext_push.p_bytes("&ensp;") != pack_result::ok)
		return CMD_RESULT_ERROR;
	++total_chars_in_line;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_qmspace(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	if (ext_push.p_bytes("&emsp14;") != pack_result::ok)
		return CMD_RESULT_ERROR;
	++total_chars_in_line;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_zwbo(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	/* Not updating total_chars_in_line, since this is zero-width */
	return ext_push.p_bytes(wchar_to_utf8(0x200B)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_zwj(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return ext_push.p_bytes(wchar_to_utf8(0x200D)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_zwnbo(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return ext_push.p_bytes(wchar_to_utf8(0xFEFF)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_zwnj(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return ext_push.p_bytes(wchar_to_utf8(0x200C)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_pmmetafile(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_PM;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_wmetafile(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	preader->picture_type = PICT_WM;
	if (!preader->is_within_picture || !have_param)
		return CMD_RESULT_CONTINUE;
	preader->picture_wmf_type = num;
	static const char *pws[] = {
		"default:MM_TEXT", "MM_TEXT", "MM_LOMETRIC", "MM_HIMETRIC",
		"MM_LOENGLISH", "MM_HIENGLISH", "MM_TWIPS", "MM_ISOTROPIC",
		"MM_ANISOTROPIC"
	};
	preader->picture_wmf_str = num >= 0 && static_cast<size_t>(num) < std::size(pws) ?
	                           pws[num] : pws[0];
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_wbmbitspixel(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (preader->is_within_picture && have_param)
		preader->picture_bits_per_pixel = num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_picw(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (preader->is_within_picture && have_param)
		preader->picture_width = num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_pich(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (preader->is_within_picture && have_param)
		preader->picture_height = num;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_htmltag(SIMPLE_TREE_NODE *pword,
    int align, bool have_param, int num)
{
	auto preader = this;
	if (!preader->have_fromhtml)
		return CMD_RESULT_IGNORE_REST;
	if (!preader->is_within_htmltag)
		if (!astk_pushx(ATTR_HTMLTAG, 0))
			return CMD_RESULT_ERROR;
	return CMD_RESULT_CONTINUE;
}

int rtf_reader::cmd_ltrmark(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return ext_push.p_bytes(wchar_to_utf8(0x200E)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

int rtf_reader::cmd_rtlmark(SIMPLE_TREE_NODE *pword, int align,
    bool have_param, int num)
{
	return ext_push.p_bytes(wchar_to_utf8(0x200F)) == pack_result::ok ?
	       CMD_RESULT_CONTINUE : CMD_RESULT_ERROR;
}

static void rtf_unescape_string(char *string)
{
	auto tmp_len = strlen(string);
	for (size_t i = 0; i < tmp_len; ++i)
		if ('\\' == string[i] && ('\\' == string[i + 1] ||
			'{' == string[i + 1] || '}' == string[i + 1])) {
			memmove(&string[i], &string[i+1], tmp_len - i);
			tmp_len --;
		}
}

static void pictype_to(unsigned int t, const char *&m, const char *&x)
{
	switch (t) {
	case PICT_WB: m = "image/bmp"; x = "bmp"; break;
	case PICT_WM: m = "application/x-msmetafile"; x = "wmf"; break;
	case PICT_MAC: m = "image/x-pict"; x = "pict"; break;
	case PICT_JPEG: m = "image/jpeg"; x = "jpg"; break;
	case PICT_PNG: m = "image/png"; x = "png"; break;
	case PICT_DI: m = "image/bmp"; x = "dib"; break;
	case PICT_PM: m = "application/octet-stream"; x = "pmm"; break;
	case PICT_EMF: m = "image/x-emf"; x = "emf"; break;
	}
}

static CMD_PROC_FUNC rtf_find_fromhtml_func(const char *s)
{
	for (const auto x : {"par", "line", "tab", "lquote", "rquote",
	     "ldblquote", "rdblquote", "bullet", "endash", "emdash",
	     "colortbl", "fonttbl", "htmltag", "uc", "u", "f", "deff",
	     "-", "~", "_"})
		if (strcmp(s, x) == 0)
			return rtf_find_cmd_function(s);
	return nullptr;
}

int rtf_reader::push_da_pic(EXT_PUSH &picture_push, const char *img_ctype,
    const char *pext, const char *cid_name, const char *picture_name)
{
	auto reader = this;
	if (reader->pattachments == nullptr)
		return 0;
	BINARY bin;

	bin.cb = picture_push.m_offset / 2;
	bin.pv = malloc(bin.cb);
	if (bin.pv == nullptr ||
	    picture_push.p_uint8(0) != pack_result::ok ||
	    !decode_hex_binary(picture_push.m_cdata, bin.pv, bin.cb)) {
		free(bin.pv);
		return -EINVAL;
	}
	auto atx = attachment_content_init();
	if (atx == nullptr || !reader->pattachments->append_internal(atx)) {
		free(bin.pv);
		return -EINVAL;
	}
	ec_error_t ret;
	uint32_t flags = ATT_MHTML_REF;
	if ((ret = atx->proplist.set(PR_ATTACH_MIME_TAG, img_ctype)) != ecSuccess ||
	    (ret = atx->proplist.set(PR_ATTACH_CONTENT_ID, cid_name)) != ecSuccess ||
	    (ret = atx->proplist.set(PR_ATTACH_EXTENSION, pext)) != ecSuccess ||
	    (ret = atx->proplist.set(PR_ATTACH_LONG_FILENAME, picture_name)) != ecSuccess ||
	    (ret = atx->proplist.set(PR_ATTACH_FLAGS, &flags)) != ecSuccess ||
	    (ret = atx->proplist.set(PR_ATTACH_DATA_BIN, &bin)) != ecSuccess) {
		free(bin.pv);
		return ece2nerrno(ret);
	}
	free(bin.pv);
	if (ext_push.p_bytes(TAG_IMAGELINK_BEGIN) != pack_result::ok ||
	    ext_push.p_bytes(cid_name) != pack_result::ok ||
	    ext_push.p_bytes(TAG_IMAGELINK_END) != pack_result::ok)
		return -EINVAL;
	return 0;
}

int rtf_reader::convert_group_node(SIMPLE_TREE_NODE *pnode)
{
	int ch;
	int num;
	int ret_val;
	char cid_name[64];
	CMD_PROC_FUNC func;
	int paragraph_align;
	char picture_name[64];
	EXT_PUSH picture_push;
	const char *img_ctype = nullptr, *pext = nullptr;
	bool b_paragraph_begun = false, b_hyperlinked = false;
	char name[MAX_CONTROL_LEN];
	bool have_param = false, is_cell_group = false, b_picture_push = false;
	
	paragraph_align = ALIGN_LEFT;
	if (pnode->get_depth() >= MAX_GROUP_DEPTH) {
		mlog(LV_DEBUG, "rtf: max group depth reached");
		return -ELOOP;
	}
	if (!check_for_table())
		return -EINVAL;
	auto preader = this;
	auto uc_prev_active = b_ubytes_switch;
	auto uc_prev_num = ubytes_num;
	auto htmlrtf_prev = is_within_htmlrtf;
	auto uc_guard = HX::make_scope_exit([&,this]() {
		b_ubytes_switch = uc_prev_active;
		ubytes_num = uc_prev_num;
		ubytes_left = 0;
		/* Restore htmlrtf state per toggle control word semantics (MS-OXRTFEX 2.1.3.1.3) */
		is_within_htmlrtf = htmlrtf_prev;
	});
	try {
		preader->attr_stack_list.emplace_back();
	} catch (const std::bad_alloc &) {
		return -ENOMEM;
	}
	while (NULL != pnode) {    
		if (NULL != pnode->pdata) {
			if (skip_objattph_space && pnode->cdata[0] == '\\' &&
			    pnode->cdata[1] != '\'')
				skip_objattph_space = false;
			if (preader->have_fromhtml) {
				if (strcasecmp(pnode->cdata, "\\htmlrtf") == 0 ||
				    strcasecmp(pnode->cdata, "\\htmlrtf1") == 0) {
					preader->is_within_htmlrtf = true;
				} else if (strcasecmp(pnode->cdata, "\\htmlrtf0") == 0) {
					preader->is_within_htmlrtf = false;
				}
				if (preader->is_within_htmlrtf) {
					if (strncasecmp(pnode->cdata, "\\f", 2) == 0 &&
					    (pnode->cdata[2] >= '0' && pnode->cdata[2] <= '9')) {
						int font_num = strtol(pnode->cdata + 2, nullptr, 10);
						cmd_f(pnode, ALIGN_LEFT, true, font_num);
					}
					pnode = pnode->get_sibling();
					continue;
				}
			}
			if (strncmp(pnode->cdata, "\\'", 2) != 0 &&
			    !riconv_flush())
				return -EINVAL;
			auto string = pnode->cdata;
			if (*string == ' ' && preader->is_within_header) {
				/* do nothing */
			} else if ('\\' != string[0]) {
				if (skip_objattph_space) {
					skip_objattph_space = false;
					if (string[0] == ' ') {
						if (string[1] == '\0') {
							pnode = pnode->get_sibling();
							continue;
						}
						++string;
					}
				}
				if (!start_body() || !start_text())
					return -EINVAL;
				if (!b_paragraph_begun) {
					if (!start_par(paragraph_align))
						return -EINVAL;
					b_paragraph_begun = true;
				}
				if (preader->is_within_picture) {
					if (!start_body())
						return -EINVAL;
					if (!b_picture_push) {
						pictype_to(preader->picture_type, img_ctype, pext);
						sprintf(picture_name, "picture%04d.%s",
							preader->picture_file_number, pext);
						sprintf(cid_name, "\"cid:picture%04d@rtf\"", 
							preader->picture_file_number++);
						if (!picture_push.init(nullptr, 0, 0))
							return -ENOMEM;
						b_picture_push = true;
					}
					if (string[0] != ' ' &&
					    preader->picture_width != 0 &&
					    preader->picture_height != 0 &&
					    preader->picture_bits_per_pixel != 0 &&
					    picture_push.p_bytes(string) != pack_result::ok)
						return -ENOBUFS;
				} else {
					rtf_unescape_string(string);
					auto slen = strlen(string);
					total_chars_in_line += slen;
					if (!push_text_encoded(string, slen))
						return -ENOMEM;
				}
			} else if (string[1] == '\\' || string[1] == '{' || string[1] == '}') {
				rtf_unescape_string(string);
				auto slen = strlen(string);
				total_chars_in_line += slen;
				if (!push_text_encoded(string, slen))
					return -EINVAL;
			} else {
				string ++;
				if (0 == strcmp("ql", string)) {
					paragraph_align = ALIGN_LEFT;
				} else if (0 == strcmp("qr", string)) {
					paragraph_align = ALIGN_RIGHT;
				} else if (0 == strcmp("qj", string)) {
					paragraph_align = ALIGN_JUSTIFY;
				} else if (0 == strcmp("qc", string)) {
					paragraph_align = ALIGN_CENTER;
				} else if (0 == strcmp("pard", string)) {
					/* clear out all font attributes */
					astk_popx_all();
					if (preader->coming_pars_tabular != 0)
						preader->coming_pars_tabular --;
					/* clear out all paragraph attributes */
					if (!end_par(paragraph_align))
						return -EINVAL;
					paragraph_align = ALIGN_LEFT;
					b_paragraph_begun = false;
				} else if (strcmp(string, "cell") == 0 ||
				           strcmp(string, "nestcell") == 0) {
					/* \cell ends table cell, \nestcell ends nested table cell */
					is_cell_group = true;
					if (!preader->b_printed_cell_begin) {
						if (ext_push.p_bytes(TAG_TABLE_CELL_BEGIN) != pack_result::ok)
							return -ENOBUFS;
						astk_express_all();
					}
					astk_popx_all();
					if (ext_push.p_bytes(TAG_TABLE_CELL_END) != pack_result::ok)
						return -ENOBUFS;
					preader->b_printed_cell_begin = false;
					preader->b_printed_cell_end = true;
				} else if (strcmp(string, "row") == 0 ||
				           strcmp(string, "nestrow") == 0) {
					/* \row ends table row, \nestrow ends nested table row */
					if (preader->is_within_table) {
						if (ext_push.p_bytes(TAG_TABLE_ROW_END) != pack_result::ok)
							return -ENOBUFS;
						preader->b_printed_row_begin = false;
						preader->b_printed_row_end = true;
					}
				} else if (string[0] == '\'' && string[1] != '\0' && string[2] != '\0') {
					ch = rtf_decode_hex_char(string + 1);
					if (skip_objattph_space) {
						skip_objattph_space = false;
						if (ch == 0x20) {
							pnode = pnode->get_sibling();
							continue;
						}
					}
					if (!put_iconv_cache(ch))
						return -EINVAL;
				} else {
					ret_val = rtf_parse_control(string,
						name, MAX_CONTROL_LEN, &num);
					if (ret_val < 0) {
						return -EINVAL;
					} else if (ret_val > 0) {
						have_param = true;
					} else {
						have_param = false;
						/* \b is like \b1 */
						num = 1;
					}

					func = preader->have_fromhtml ? rtf_find_fromhtml_func(name) : rtf_find_cmd_function(name);
					if (NULL != func) {
						switch ((preader->*func)(pnode,
							paragraph_align, have_param, num)) {
						case CMD_RESULT_ERROR:
							return -EINVAL;
						case CMD_RESULT_CONTINUE:
							break;
						case CMD_RESULT_HYPERLINKED:
							b_hyperlinked = true;
							break;
						case CMD_RESULT_IGNORE_REST:
							while ((pnode = pnode->get_sibling()) != nullptr)
								/* nothing */;
							break;
						}
						if (func == &rtf_reader::cmd_par)
							b_paragraph_begun = true;
					}
				}
			}
		} else {
			auto pchild = pnode->get_child();
			if (!b_paragraph_begun) {
				if (!start_par(paragraph_align))
					return -EINVAL;
				b_paragraph_begun = true;
			}
			if (pchild != nullptr) {
				auto ret = convert_group_node(pchild);
				if (ret != 0)
					return -EINVAL;
			}
		}
		if (pnode != nullptr)
			pnode = pnode->get_sibling();
	}
	if (preader->is_within_picture && b_picture_push) {
		if (picture_push.m_offset > 0) {
			auto ret = push_da_pic(picture_push, img_ctype,
			           pext, cid_name, picture_name);
			if (ret != 0)
				return -ret;
		}
		preader->is_within_picture = false;
	}
	if (!riconv_flush())
		return -EINVAL;
	if (b_hyperlinked && ext_push.p_bytes(TAG_HYPERLINK_END) != pack_result::ok)
		return -EINVAL;
	if (!is_cell_group && !astk_popx_all())
		return -EINVAL;
	if (b_paragraph_begun && !end_par(paragraph_align))
		return -EINVAL;
	if (preader->attr_stack_list.size() > 0)
		preader->attr_stack_list.pop_back();
	return 0;
}

/**
 * @charset:      desired output charset
 * @pattachments: put things like images in here
 *
 * It is allowed for @input to refer to the same object as @buf_out.
 */
ec_error_t rtf_to_html(std::string_view input, const char *charset,
    std::string &buf_out, ATTACHMENT_LIST *pattachments) try
{
	int i;
	RTF_READER reader;
	char tmp_buff[128];
	SIMPLE_TREE_NODE *pnode;
	
	if (!reader.init_reader(input, pattachments) ||
	    !reader.load_element_tree())
		return ecInvalidParam;
	auto proot = reader.element_tree.get_root();
	if (proot == nullptr)
		return ecInvalidParam;
	for (pnode = proot->get_child(), i = 1; i <= 10 && pnode != nullptr; ++i) {
		if (pnode->pdata == nullptr)
			break;
		if (strcmp(pnode->cdata, "\\fromhtml1") == 0)
			reader.have_fromhtml = true;
		pnode = pnode->get_sibling();
	}
	if (!reader.have_fromhtml) {
		QRF2(reader.ext_push.p_bytes(TAG_DOCUMENT_BEGIN));
		QRF2(reader.ext_push.p_bytes(TAG_HEADER_BEGIN));
		auto tmp_len = gx_snprintf(tmp_buff, std::size(tmp_buff),
		          TAG_HTML_CHARSET, charset);
		QRF2(reader.ext_push.p_bytes(tmp_buff, tmp_len));
	}
	auto ret = reader.convert_group_node(proot);
	if (ret != 0 || !reader.end_table())
		return ecError;
	reader.riconv_flush(true);
	if (!reader.have_fromhtml) {
		QRF2(reader.ext_push.p_bytes(TAG_BODY_END));
		QRF2(reader.ext_push.p_bytes(TAG_DOCUMENT_END));
	}
	if (0 == strcasecmp(charset, "UTF-8") ||
		0 == strcasecmp(charset, "ASCII") ||
		0 == strcasecmp(charset, "US-ASCII")) {
		buf_out.assign(reader.ext_push.m_cdata, reader.ext_push.m_offset);
		return ecSuccess;
	}
	buf_out = iconvtext(std::string_view{reader.ext_push.m_cdata, reader.ext_push.m_offset},
	          "UTF-8", tmp_buff, ICONVTEXT_TRANSLIT);
	return ecSuccess;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "%s: ENOMEM", __func__);
	return ecMAPIOOM;
}

static constexpr std::pair<const char *, CMD_PROC_FUNC> g_cmd_map[] = {
	{"*", &rtf_reader::cmd_maybe_ignore},
	{"-", &rtf_reader::cmd_soft_hyphen},
	{"_", &rtf_reader::cmd_nonbreaking_hyphen},
	{"af", &rtf_reader::cmd_af}, /* associated font for double-byte characters */
	{"afs", &rtf_reader::cmd_continue}, /* associated font size (for CJK fonts) - we don't track this separately from \fs */
	{"alang", &rtf_reader::cmd_lang}, /* associated character language property */
	{"ansi", &rtf_reader::cmd_ansi},
	{"ansicpg", &rtf_reader::cmd_ansicpg},
	{"b", &rtf_reader::cmd_b},
	{"bin", &rtf_reader::cmd_continue},
	{"blipuid", &rtf_reader::cmd_ignore},
	{"bullet", &rtf_reader::cmd_bullet},
	{"caps", &rtf_reader::cmd_caps},
	{"cb", &rtf_reader::cmd_cb},
	{"cf", &rtf_reader::cmd_cf},
	{"colortbl", &rtf_reader::cmd_colortbl},
	{"cpg", &rtf_reader::cmd_cpg},
	{"dbch", &rtf_reader::cmd_dbch}, /* subsequent text is double-byte characters (CJK) */
	{"deff", &rtf_reader::cmd_deff},
	{"deflang", &rtf_reader::cmd_deflang}, /* default document language */
	{"deflangfe", &rtf_reader::cmd_deflang},
	{"dn", &rtf_reader::cmd_dn},
	{"embo", &rtf_reader::cmd_emboss},
	{"emdash", &rtf_reader::cmd_emdash},
	{"emfblip", &rtf_reader::cmd_emfblip},
	{"emspace", &rtf_reader::cmd_emspace},
	{"endash", &rtf_reader::cmd_endash},
	{"enspace", &rtf_reader::cmd_enspace},
	{"expand", &rtf_reader::cmd_expand},
	{"expnd", &rtf_reader::cmd_expand},
	{"f", &rtf_reader::cmd_f},
	{"fbidi", &rtf_reader::cmd_fnil}, /* Bidirectional font family - treat like fnil for now */
	{"fdecor", &rtf_reader::cmd_fdecor},
	{"field", &rtf_reader::cmd_field},
	{"fjgothic", &rtf_reader::cmd_fnil},
	{"fjminchou", &rtf_reader::cmd_fnil},
	{"fmodern", &rtf_reader::cmd_fmodern},
	{"fnil", &rtf_reader::cmd_fnil},
	{"fonttbl", &rtf_reader::cmd_fonttbl},
	{"footer", &rtf_reader::cmd_ignore},
	{"footerf", &rtf_reader::cmd_ignore},
	{"footerl", &rtf_reader::cmd_ignore},
	{"footerr", &rtf_reader::cmd_ignore},
	{"froman", &rtf_reader::cmd_froman},
	{"fromhtml", &rtf_reader::cmd_continue},
	{"fs", &rtf_reader::cmd_fs},
	{"fscript", &rtf_reader::cmd_fscript},
	{"fswiss", &rtf_reader::cmd_fswiss},
	{"ftech", &rtf_reader::cmd_ftech},
	{"fttruetype", &rtf_reader::cmd_continue}, /* TrueType flag is just informational; doesn't affect conversion */
	{"header", &rtf_reader::cmd_ignore},
	{"headerf", &rtf_reader::cmd_ignore},
	{"headerl", &rtf_reader::cmd_ignore},
	{"headerr", &rtf_reader::cmd_ignore},
	{"hich", &rtf_reader::cmd_hich}, /* subsequent text is high-byte characters (high-bit set) */
	{"highlight", &rtf_reader::cmd_highlight},
	{"hl", &rtf_reader::cmd_ignore},
	{"hsv", &rtf_reader::cmd_ignore}, /* shape theme color info */
	{"htmltag", &rtf_reader::cmd_htmltag},
	{"i", &rtf_reader::cmd_i},
	{"impr", &rtf_reader::cmd_engrave},
	{"info", &rtf_reader::cmd_info},
	{"intbl", &rtf_reader::cmd_intbl},
	{"jis", &rtf_reader::cmd_ansi},
	{"jpegblip", &rtf_reader::cmd_jpegblip},
	{"lang", &rtf_reader::cmd_lang}, /* language tag for text (affects encoding interpretation) */
	{"langfe", &rtf_reader::cmd_langfe}, /* language tag for Far East text (CJK encoding hint) */
	{"langfenp", &rtf_reader::cmd_langfe}, /* non-proportional Far East language */
	{"langnp", &rtf_reader::cmd_lang}, /* non-proportional language (same as \lang for our purposes) */
	{"ldblquote", &rtf_reader::cmd_ldblquote},
	{"line", &rtf_reader::cmd_line},
	{"loch", &rtf_reader::cmd_loch}, /* subsequent text is low-byte (ASCII/Latin) characters */
	{"lquote", &rtf_reader::cmd_lquote},
	{"ltrch", &rtf_reader::flush_pending},
	{"ltrmark", &rtf_reader::cmd_ltrmark},
	{"mac", &rtf_reader::cmd_mac},
	{"macpict", &rtf_reader::cmd_macpict},
	{"nesttableprops", &rtf_reader::cmd_ignore}, /* nested table properties destination */
	{"nonesttables", &rtf_reader::cmd_ignore}, /* fallback content for readers without nested table support */
	{"nonshppict", &rtf_reader::cmd_ignore},
	{"noproof", &rtf_reader::cmd_continue},
	{"nosupersub", &rtf_reader::cmd_nosupersub},
	{"objattph", &rtf_reader::cmd_objattph},
	{"objclass", &rtf_reader::cmd_ignore}, /* object class name destination */
	{"objdata", &rtf_reader::cmd_ignore}, /* OLE object data destination - binary data we cannot render */
	{"objname", &rtf_reader::cmd_ignore}, /* object name destination */
	{"objtime", &rtf_reader::cmd_ignore}, /* object update time destination */
	{"outl", &rtf_reader::cmd_outl},
	{"page", &rtf_reader::cmd_page},
	{"par", &rtf_reader::cmd_par},
	{"pc", &rtf_reader::cmd_pc},
	{"pca", &rtf_reader::cmd_pca},
	{"pich", &rtf_reader::cmd_pich},
	{"picprop", &rtf_reader::cmd_ignore},
	{"pict", &rtf_reader::cmd_pict},
	{"picw", &rtf_reader::cmd_picw},
	{"plain", &rtf_reader::cmd_plain},
	{"pmmetafile", &rtf_reader::cmd_pmmetafile},
	{"pngblip", &rtf_reader::cmd_pngblip},
	{"qmspace", &rtf_reader::cmd_qmspace}, /* One Quarter Space */
	{"rdblquote", &rtf_reader::cmd_rdblquote},
	{"rquote", &rtf_reader::cmd_rquote},
	{"rtf", &rtf_reader::cmd_continue},
	{"rtlch", &rtf_reader::flush_pending},
	{"rtlmark", &rtf_reader::cmd_rtlmark},
	{"s", &rtf_reader::cmd_continue},
	{"scaps", &rtf_reader::cmd_scaps},
	{"sect", &rtf_reader::cmd_sect},
	{"shad", &rtf_reader::cmd_shad},
	{"shpinst", &rtf_reader::cmd_ignore}, /* shape instructions - properties we cannot render */
	{"sn", &rtf_reader::cmd_ignore}, /* shape property name */
	{"sp", &rtf_reader::cmd_ignore}, /* shape property */
	{"sv", &rtf_reader::cmd_ignore}, /* shape property value */
	{"svb", &rtf_reader::cmd_ignore}, /* shape binary property value */
	{"strike", &rtf_reader::cmd_strike},
	{"striked", &rtf_reader::cmd_striked},
	{"strikedl", &rtf_reader::cmd_strikedl},
	{"stylesheet", &rtf_reader::cmd_ignore},
	{"sub", &rtf_reader::cmd_sub},
	{"super", &rtf_reader::cmd_super},
	{"tab", &rtf_reader::cmd_tab},
	{"tc", &rtf_reader::cmd_continue},
	{"tcn", &rtf_reader::cmd_ignore},
	{"u", &rtf_reader::cmd_u},
	{"uc", &rtf_reader::cmd_uc},
	/*
	 * \ud - Unicode destination within \upr
	 *
	 * This marks the start of the Unicode version of text in a \upr group.
	 * Content following this should be processed normally (including \u escapes).
	 */
	{"ud", &rtf_reader::cmd_continue},
	{"ul", &rtf_reader::cmd_ul},
	{"uld", &rtf_reader::cmd_uld},
	{"uldash", &rtf_reader::cmd_uldash},
	{"uldashd", &rtf_reader::cmd_uldashd},
	{"uldashdd", &rtf_reader::cmd_uldashdd},
	{"uldb", &rtf_reader::cmd_uldb},
	{"ulnone", &rtf_reader::cmd_ulnone},
	{"ulth", &rtf_reader::cmd_ulth},
	{"ulthd", &rtf_reader::cmd_ulthd},
	{"ulthdash", &rtf_reader::cmd_ulthdash},
	{"ulw", &rtf_reader::cmd_ulw},
	{"ulwave", &rtf_reader::cmd_ulwave},
	{"up", &rtf_reader::cmd_up},
	{"upr", &rtf_reader::cmd_upr}, /* Unicode-preserving group - skip ANSI, process \ud */
	{"v", &rtf_reader::cmd_v}, /* hidden text */
	{"wbmbitspixel", &rtf_reader::cmd_wbmbitspixel},
	{"wmetafile", &rtf_reader::cmd_wmetafile},
	{"xe", &rtf_reader::cmd_continue},
	{"zwbo", &rtf_reader::cmd_zwbo},
	{"zwj", &rtf_reader::cmd_zwj},
	{"zwnbo", &rtf_reader::cmd_zwnbo},
	{"zwnj", &rtf_reader::cmd_zwnj},
	{"~", &rtf_reader::cmd_nonbreaking_space},
};

static CMD_PROC_FUNC rtf_find_cmd_function(const char *cmd)
{
	auto i = std::lower_bound(std::cbegin(g_cmd_map), std::cend(g_cmd_map), cmd,
	         [&](const std::pair<const char *, CMD_PROC_FUNC> &p, const char *c) {
	         	return strcasecmp(p.first, c) < 0;
	         });
	return i != std::cend(g_cmd_map) && strcasecmp(i->first, cmd) == 0 ?
	       i->second : nullptr;
}
