#include "DocVisitor.h"
#include "Util.h"
const float fontSizes[6] = { 12.0f, 30.0f, 24.0f, 18.0f, 14.0f, 10.0f };
const float lineSpacing = 1.5f;
const float spaceFromDotToText = 8.0f;
//const float pageLeftMargin = 60.0f;
//const float pageRightMargin = 60.0f;
//const float pageTopMargin = 120.0f;
//const float pageBottomMargin = 60.0f;
// <blockquote> - an indented section - draw a thick vertical line and indent
// <pre><code> - code which is pre-formatted - interpret white-space literally
// <code> - inline quoted text - italics/highlight
/*
HPDF_Page_SetTextRenderingMode(page, HPDF_FILL);
HPDF_Page_SetTextRenderingMode(page, HPDF_STROKE);
*/
DocVisitor::DocVisitor(DocOutputDevice* doc, DocStyle* style, DocTemplate* templ, int pageCount) :
m_doc(doc),
m_page(0),
m_style(style),
m_templ(templ)
{
// For pre/code tags, we need to preserve the white space in the contained text
TiXmlBase::SetCondenseWhiteSpace(false);
x = y = 0.0f;
currentStyle.push(FT_Normal);
currentHeadingLevel.push(0);
insideCode = 0;
insideLink = 0;
insidePreformatted = 0;
insideBlockQuote = 0;
needBlockQuoteBar = false;
startOfListItem = false;
insideTable = false;
alignCenter = false;
// reset
FlushBufferedText();
for (int i = 0; i < 32; i++)
indentLevels[i] = 30.0f * i;
indentLevel = 0;
m_pageNum = 0;
m_pageCount = pageCount;
m_needPageCount = false;
//m_pageLeftMarginX = 60.0f;
//m_pageRightMarginX = 60.0f;
//m_pageTopMarginY = 120.0f;
//m_pageBottomMarginY = 60.0f;
// defaults
#define HPDF_DEF_PAGE_WIDTH 595.276F
#define HPDF_DEF_PAGE_HEIGHT 841.89F
Point topLeft;
Point bottomRight;
templ->PageBounds(topLeft, bottomRight);
m_pageLeftMarginX = topLeft.x;
m_pageTopMarginY = topLeft.y;
m_pageRightMarginX = bottomRight.x;
m_pageBottomMarginY = bottomRight.y;
m_columnCount = 1;
m_columnSpacing = 20.0;
templ->Columns(m_columnCount, m_columnSpacing);
m_columnNum = m_columnCount - 1; // Need to start on last column to force an AddPage to happen
m_truePageLeftMargin = m_pageLeftMarginX;
float usableWidth = bottomRight.x - topLeft.x;
m_columnWidth = (usableWidth - ((m_columnCount - 1) * m_columnSpacing)) / m_columnCount;
m_pageLeftMarginX = m_truePageLeftMargin;
m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;
}
DocVisitor::~DocVisitor()
{
delete m_page; // calls endText
}
void DocVisitor::DrawCodeBlockRect()
{
/*
// Output a grey-background to the code/preformatted section
float fontSize = fontSizes[currentHeadingLevel.top()];
m_page->setFillColor(0xB0F0B0); // light green
m_page->drawRect(m_pageLeftMarginX, y + m_pageTopMarginY - fontSize,
m_pageRightMarginX - m_pageLeftMarginX, lineCount * (fontSize * lineSpacing));
*/
}
void SVGTest(const char* a_fileName, double scale, DocOutputPage* outputPage);
void DocVisitor::AddPage() // TODO: rename to NextColumn or something like that
{
/*
if (insideCode)
{
// output the rectangle around it
DrawCodeBlockRect();
}
*/
// Doing columns in here by having it count which column we are up to
// and adjusting the left and right margins to make it seem like a thin page
// That means that the m_page[Top|Left|Right|Bottom]Margin variables are
// really the bounds for the margin, not the page so should rename these
m_columnNum++;
m_pageLeftMarginX += m_columnWidth + m_columnSpacing;
if (m_columnNum >= m_columnCount)
{
m_columnNum = 0;
m_pageLeftMarginX = m_truePageLeftMargin;
m_pageNum++;
if (m_page)
m_style->adornOldPage(*m_page);
delete m_page; // calls endText
m_page = m_doc->newPage();
// Re-calculate margin positions using the actual page size
Point topLeft;
Point bottomRight;
m_templ->PageBounds(topLeft, bottomRight);
// internally we used the defaults to save it, undo this
float rightMargin = HPDF_DEF_PAGE_WIDTH - bottomRight.x;
float bottomMargin = HPDF_DEF_PAGE_HEIGHT - bottomRight.y;
// and redo the calc using the actual page size
m_pageRightMarginX = m_page->width() - rightMargin;
m_pageBottomMarginY = m_page->height() - bottomMargin;
float usableWidth = m_pageRightMarginX - m_truePageLeftMargin;
m_columnWidth = (usableWidth - ((m_columnCount - 1) * m_columnSpacing)) / m_columnCount;
m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;
m_style->adornNewPage(*m_page);
m_templ->Apply(*m_page);
/*
printf("Start SVG\n");
//SVGTest("../templates/bars.svg", 0.02, m_page);
//SVGTest("../templates/curve.svg", 0.02, m_page);
SVGTest("../test-gradient-stops.svg", 0.02, m_page);
printf("End SVG\n");
*/
m_page->setFillColor(0);
m_page->setFontType(currentStyle.top());
m_page->setFontSize(fontSizes[currentHeadingLevel.top()]);
printf("End new page\n");
}
m_pageRightMarginX = m_pageLeftMarginX + m_columnWidth;
/*
if (insideCode)
{
codeStartX = 0;
codeStartY = 0;
}
*/
}
void DocVisitor::CheckIfEndOfPage()
{
if ((y + m_pageTopMarginY) > m_pageBottomMarginY)
{
// Start a new page
AddPage();
y = 0.0f;
}
}
/// Visit a document.
bool DocVisitor::VisitEnter( const TiXmlDocument& /*doc*/ )
{
AddPage();
return true;
}
/// Visit a document.
bool DocVisitor::VisitExit( const TiXmlDocument& /*doc*/ )
{
if (m_page)
m_style->adornOldPage(*m_page);
delete m_page; // calls endText
m_page = 0;
return true;
}
/// Visit a declaration
bool DocVisitor::Visit(const TiXmlDeclaration& /*declaration*/)
{
return true;
}
/// Visit a stylesheet reference
bool DocVisitor::Visit(const TiXmlStylesheetReference& /*stylesheet*/)
{
return true;
}
/// Visit a comment node
bool DocVisitor::Visit( const TiXmlComment& /*comment*/ )
{
return true;
}
/// Visit an unknown node
bool DocVisitor::Visit(const TiXmlUnknown& /*unknown*/)
{
return true;
}
void DocVisitor::NewLine(float amount)
{
x = 0.0f;
float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * amount;
//needBlockQuoteBar = true;
m_page->setPenWidth(5.0);
m_page->setPenColor(0xE0E0E0);
for (int i = 0; i < insideBlockQuote; i++)
{
x += 2.5;
m_page->drawLine(m_pageLeftMarginX + x, m_pageTopMarginY + y - h * 0.3, m_pageLeftMarginX + x, m_pageTopMarginY + y + h - h * 0.3);
x += 13.0;
}
m_page->setPenWidth(1.0);
if (!insideBlockQuote)
{
m_page->setPenColor(0);
m_page->setFillColor(0);
}
y += h;
CheckIfEndOfPage();
if (indentLevel)
x += indentLevels[indentLevel] + spaceFromDotToText;
}
/// Visit an element.
bool DocVisitor::VisitEnter( const TiXmlElement& element, const TiXmlAttribute* firstAttribute )
{
if (strcmp(element.ValueTStr().c_str(), "img") == 0)
{
std::string altText = "";
std::string srcFile = "";
while (firstAttribute)
{
if (firstAttribute->Name() == std::string("alt"))
{
altText = firstAttribute->Value();
}
else if (firstAttribute->Name() == std::string("src"))
{
srcFile = firstAttribute->Value();
}
firstAttribute = firstAttribute->Next();
}
if (!srcFile.empty() || !altText.empty())
{
//printf("image src = %s, alt = %s\n", srcFile.c_str(), altText.c_str());
// TODO: this is hacky way to get relative path to where images might be - but if this is a new document the
// user is editing and they haven't saved yet, how do we know which directory to look in?
// Also perhaps we need to collect up a list of referenced images that are used so they can be copied around together
// with the markdown file - idea of a bundle - or perhaps multiple docs refer to the same shared library of images
// so perhaps copied to a specified images directory. Need user preferences for this.
std::string fileLocation = "../Resources/Documents/" + srcFile;
float w, h;
bool valid = m_page->imageSize(fileLocation.c_str(), w, h);
w *= 0.25;
h *= 0.25;
if (valid && w > 1.0 && h > 1.0)
{
float aspectRatio = w / h;
int maxWidth = m_pageRightMarginX - m_pageLeftMarginX;
int maxHeight = m_pageBottomMarginY - m_pageTopMarginY;
// clamp image to page bounds, maintaining aspect ratio of the image
if (w > maxWidth)
w = maxWidth, h = maxWidth / aspectRatio;
if (h > maxHeight)
h = maxHeight, w = maxHeight * aspectRatio;
// Need to check remaining height in page and start new page if not enough
if ((y + m_pageTopMarginY + h) >= m_pageBottomMarginY)
{
AddPage();
y = 0.0f;
}
if (alignCenter)
{
// center the image
NewLine(0.5);
x = (maxWidth - w) / 2;
}
m_page->drawImage(fileLocation.c_str(), x + m_pageLeftMarginX, y + m_pageTopMarginY, w, h);
x = 0.0;
y += h;
}
else
{
// TODO: draw a box around where the image would have been?
OutputText(altText.c_str());
NewLine();
}
}
}
else if (strcmp(element.ValueTStr().c_str(), "p") == 0)
{
if (indentLevel == 0)
NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "ol") == 0)
{
orderedList = true;
listItemIndex = 1;
NewLine(0.5);
}
else if (strcmp(element.ValueTStr().c_str(), "ul") == 0)
{
orderedList = false;
NewLine(0.5);
}
else if (strcmp(element.ValueTStr().c_str(), "li") == 0)
{
NewLine();
if (indentLevel)
x -= indentLevels[indentLevel] + spaceFromDotToText;
indentLevel++;
x += indentLevels[indentLevel] + spaceFromDotToText;
if (orderedList)
{
std::string str = std::to_string(listItemIndex) + ".";
x -= spaceFromDotToText;
//DrawText(m_pageLeftMarginX + x, y + m_pageTopMarginY, str.c_str());
OutputText(str.c_str());
x += spaceFromDotToText;
// TODO: this is not correct across multiple depths of lists - need to have an array of indexes for different list depths
listItemIndex++;
}
else
{
// TODO: draw bullet according to style settings (this is hard-coded as a circle/dot bullet)
m_page->setFillColor(0x0);
m_page->drawCircle(m_pageLeftMarginX + x - spaceFromDotToText, y + m_pageTopMarginY - fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.25f, 2.0f);
}
startOfListItem = true;
}
else if (strcmp(element.ValueTStr().c_str(), "code") == 0)
{
if (insideCode == 0)
{
codeStartX = x;
codeStartY = y;
codeText = "";
currentStyle.push(FT_Fixed);
}
insideCode++;
}
else if (strcmp(element.ValueTStr().c_str(), "a") == 0)
{
std::string srcFile = "";
while (firstAttribute)
{
if (firstAttribute->Name() == std::string("href"))
{
srcFile = firstAttribute->Value();
}
firstAttribute = firstAttribute->Next();
}
float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75f;
m_page->startLink(srcFile.c_str(), m_pageLeftMarginX + x, m_pageTopMarginY + y - textHeight);
if (insideLink == 0)
currentStyle.push(FT_Bold);
insideLink++;
m_page->setFillColor(0x0000FF); // blue
}
else if (strcmp(element.ValueTStr().c_str(), "h1") == 0)
currentHeadingLevel.push(1), NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h2") == 0)
currentHeadingLevel.push(2), NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h3") == 0)
currentHeadingLevel.push(3), NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h4") == 0)
currentHeadingLevel.push(4), NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h5") == 0)
currentHeadingLevel.push(5), NewLine();
else if (strcmp(element.ValueTStr().c_str(), "strong") == 0)
currentStyle.push(FT_Bold);
else if (strcmp(element.ValueTStr().c_str(), "em") == 0)
currentStyle.push(FT_Oblique);
else if (strcmp(element.ValueTStr().c_str(), "hr") == 0)
{
}
else if (strcmp(element.ValueTStr().c_str(), "pre") == 0)
{
currentStyle.push(FT_Fixed);
insidePreformatted++;
NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "br") == 0)
NewLine();
else if (strcmp(element.ValueTStr().c_str(), "table") == 0)
{
tableData.clear();
insideTable = true;
}
else if (strcmp(element.ValueTStr().c_str(), "thead") == 0)
{
// ignore
}
else if (strcmp(element.ValueTStr().c_str(), "tbody") == 0)
{
// ignore
}
else if (strcmp(element.ValueTStr().c_str(), "tr") == 0)
{
tableData.emplace_back();
}
else if (strcmp(element.ValueTStr().c_str(), "td") == 0 || strcmp(element.ValueTStr().c_str(), "th") == 0)
{
std::string styleStr = "";
while (firstAttribute)
{
if (firstAttribute->Name() == std::string("style"))
{
styleStr = firstAttribute->Value();
break;
}
firstAttribute = firstAttribute->Next();
}
TDAlign align = TDA_Left;
if (!styleStr.empty())
{
if (styleStr == "text-align: center")
align = TDA_Center;
else if (styleStr == "text-align: right")
align = TDA_Right;
else if (styleStr == "text-align: left")
align = TDA_Left;
else
printf("unexpected table data style: %s\n", styleStr.c_str());
}
tableData.back().emplace_back(TD{ align, "" });
}
else if (strcmp(element.ValueTStr().c_str(), "blockquote") == 0)
{
insideBlockQuote++; // block quotes can be nested
m_page->setPenColor(0xE0E0E0);
m_page->setFillColor(0xA0A0A0);
}
else
{
return false;
}
return true;
}
/// Visit an element.
bool DocVisitor::VisitExit( const TiXmlElement& element )
{
if (strcmp(element.ValueTStr().c_str(), "img") == 0)
{
//NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "p") == 0)
{
if (indentLevel == 0)
NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "ol") == 0)
{
if (indentLevel == 0)
NewLine();
else
NewLine(0.5);
}
else if (strcmp(element.ValueTStr().c_str(), "ul") == 0)
{
if (indentLevel == 0)
NewLine();
else
NewLine(0.5);
}
else if (strcmp(element.ValueTStr().c_str(), "li") == 0)
{
indentLevel--;
//NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "code") == 0)
{
insideCode--;
if (insideCode == 0)
{
float fontSize = fontSizes[currentHeadingLevel.top()];
float fh = fontSize * lineSpacing;
const char* str = codeText.c_str();
if (insidePreformatted)
{
// Count how many lines this code section has
int lineCount = 0;
for (int i = 0; str[i] != 0; i++ ) {
if (str[i] == '\n') {
lineCount++;
}
}
//DrawCodeBlockRect();
// Output a grey-background to the code/preformatted section
m_page->setPenWidth(1);
m_page->setFillColor(0xF0F0F0); // light gray
m_page->setPenColor(0xF0F0F0); // light gray
m_page->setAlpha(0.7);
m_page->drawRect(m_pageLeftMarginX, y + m_pageTopMarginY - fontSize,
m_pageRightMarginX - m_pageLeftMarginX, (lineCount + 1.5) * fh);
// Output the code text
m_page->setFontType(currentStyle.top());
m_page->setFontSize(fontSize);
m_page->setFillColor(0); // black
m_page->setPenColor(0); // black
m_page->setAlpha(1.0);
y += (fontSize * lineSpacing) * 0.75; // half of 1.5 a line-height border
// x += (fontSize * lineSpacing) * 0.75; // half of 1.5 a line-height border
}
if (!insidePreformatted)
{
//insideCode = 1;
StartRedirect();
}
else
{
x += (fontSize * lineSpacing) * 1.0; // half a line-height border
}
std::string currentWord;
int i = 0;
while ( str[i] != 0 ) {
if ( str[i] == ' ' || str[i] == '\t' || str[i] == '\n' ) {
//char s[2] = { str[i], 0 };
//currentWord += s;
OutputCurrentWord(currentWord.c_str());
// x -= m_page->textWidth(" ");
if (str[i] == '\n') {
NewLine();
x += (fontSize * lineSpacing) * 1.0; // half a line-height border
}
currentWord = "";
}
else
{
char s[2] = { str[i], 0 };
currentWord += s;
}
i++;
}
if ( str[i] == 0 ) {
OutputCurrentWord(currentWord.c_str());
//x -= m_page->textWidth(" ");
}
if (!insidePreformatted)
{
// insideCode = 0;
float x1, y1, x2, y2;
RedirectBounds(x1, y1, x2, y2);
// Output a grey-background to the code/preformatted section
m_page->setPenWidth(1);
m_page->setFillColor(0xF0F0F0); // light gray
m_page->setPenColor(0xF0F0F0); // light gray
m_page->setAlpha(0.7);
m_page->drawRect(x1 - 1.0, y1, x2-x1 + 2.0, y2-y1);
m_page->setAlpha(1.0);
/*
//m_pageLeftMarginX, y + m_pageTopMarginY - fontSize, m_pageRightMarginX - m_pageLeftMarginX, lineCount * (fontSize * lineSpacing));
// Output the code text
//m_page->setFontType(currentStyle.top());
//m_page->setFontSize(fontSize);
*/
m_page->setFillColor(0); // black
m_page->setPenColor(0); // black
EndRedirect();
}
else
{
NewLine();
}
currentStyle.pop();
//m_page->setFillColor(0); // black
}
}
else if (strcmp(element.ValueTStr().c_str(), "a") == 0)
{
insideLink--;
if (insideLink == 0)
{
float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.25f;
m_page->endLink(m_pageLeftMarginX + x, m_pageTopMarginY + y + textHeight);
m_page->setFillColor(0x000000); // black
currentStyle.pop();
}
}
else if (strcmp(element.ValueTStr().c_str(), "hr") == 0)
{
float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75;
float y0 = y + m_pageTopMarginY + h;
m_page->setFillColor(0xF0F0F0); // light gray
m_page->setPenColor(0xF0F0F0); // light gray
//m_page->drawRect(x1, y1, x2, y2);
// m_page->drawLine(m_pageLeftMarginX, y0, m_pageRightMarginX, y0);
m_page->drawRect(m_pageLeftMarginX, y0, m_pageRightMarginX - m_pageLeftMarginX, 3.0);
m_page->setFillColor(0); // black
m_page->setPenColor(0); // black
NewLine(1.5);
}
else if (strcmp(element.ValueTStr().c_str(), "h1") == 0) {
currentHeadingLevel.pop();
//NewLine();
}
else if (strcmp(element.ValueTStr().c_str(), "h2") == 0)
{
currentHeadingLevel.pop();
//NewLine();
/*
x = 0.0;
y += fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.5f;
m_page->drawLine(m_pageLeftMarginX, y + m_pageTopMarginY, m_pageRightMarginX, y + m_pageTopMarginY);
y += fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.5f;
CheckIfEndOfPage();
*/
}
else if (strcmp(element.ValueTStr().c_str(), "h3") == 0)
currentHeadingLevel.pop();//, NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h4") == 0)
currentHeadingLevel.pop();//, NewLine();
else if (strcmp(element.ValueTStr().c_str(), "h5") == 0)
currentHeadingLevel.pop();//, NewLine();
else if (strcmp(element.ValueTStr().c_str(), "strong") == 0)
currentStyle.pop();
else if (strcmp(element.ValueTStr().c_str(), "em") == 0)
currentStyle.pop();
else if (strcmp(element.ValueTStr().c_str(), "pre") == 0)
{
insidePreformatted--;
currentStyle.pop();
}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "br") == 0)
{}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "table") == 0)
{
insideTable = false;
OutputTable();
}
else if (strcmp(element.ValueTStr().c_str(), "thead") == 0)
{}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "tbody") == 0)
{}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "tr") == 0)
{}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "td") == 0)
{}//NewLine();
else if (strcmp(element.ValueTStr().c_str(), "th") == 0)
{}// NewLine();
else if (strcmp(element.ValueTStr().c_str(), "blockquote") == 0)
{
insideBlockQuote--;
if (!insideBlockQuote)
{
m_page->setPenColor(0);
m_page->setFillColor(0);
}
}// NewLine();
else {
return false;
}
return true;
}
void DocVisitor::RedirectBounds(float& x1, float& y1, float& x2, float& y2)
{
x1 = std::numeric_limits<float>::max();
y1 = std::numeric_limits<float>::max();
x2 = std::numeric_limits<float>::min();
y2 = std::numeric_limits<float>::min();
float h = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.8;
for (auto& t : redirectedText)
{
float w = m_page->textWidth(t.text.c_str());
float _x1 = t.x;
float _y1 = t.y - h * 0.75;
float _x2 = _x1 + w;
float _y2 = _y1 + h;
if (_x1 < x1) x1 = _x1;
if (_y1 < y1) y1 = _y1;
if (_x2 > x2) x2 = _x2;
if (_y2 > y2) y2 = _y2;
}
}
float DocVisitor::WidestWordInText(std::string& str, bool& stretch)
{
float maxW = 0.0;
std::vector<std::string> words = split(str, ' ');
stretch = false;
if (words.size() >= 2)
{
if (words[0] == "<<" && words[words.size()-1] == ">>")
{
str = join(std::vector<std::string>(words.begin() + 1, words.end() - 1), " ");
stretch = true;
}
}
for (auto& word : words)
{
float w = m_page->textWidth(word.c_str());
if (w > maxW)
maxW = w;
}
return maxW;
}
/*
struct ListState
{
bool ordered;
int index;
};
struct TextNode
{
int listLevel;
ListState listState[16]; // up to 16 nested list levels
bool codeFlag;
bool linkFlag;
bool blockQuoteFlag;
bool italicsFlag;
bool boldFlag;
bool preFlag;
bool tableFlag;
FontType style;
std::string words;
};
float bufferedTextWidth;
std::vector<TextNode> bufferedText;
*/
void DocVisitor::FlushBufferedText()
{
bufferedTextWidth = 0.0;
bufferedText.clear();
}
void DocVisitor::OutputTable()
{
const float colSpacing = 15.0;
const float rowSpacing = 2.0;
float fontSize = fontSizes[0];
m_page->setFontType(FT_Bold);
m_page->setFontSize(fontSize);
m_page->setFillColor(0x0);
int colCount = 0;
for (const auto& tr : tableData)
if (tr.size() > colCount)
colCount = tr.size();
int stretchCount = 0;
std::vector<float> colWidths; // if no wrapping, what would be the widest width of text in the column
std::vector<float> minColWidths; // if wrapping, what would be the smallest width of the column without wrapping mid-word
std::vector<bool> stretchCols; // if wrapping, what would be the smallest width of the column without wrapping mid-word
colWidths.reserve(colCount);
for (int i = 0; i < colCount; i++)
{
colWidths.push_back(0.0);
minColWidths.push_back(0.0);
stretchCols.push_back(false);
}
for (auto& tr : tableData)
{
int col = 0;
for (auto& td : tr)
{
bool stretch = false;
float w = m_page->textWidth(td.str.c_str());
if (w > colWidths[col])
colWidths[col] = w;
w = WidestWordInText(td.str, stretch);
if (!stretchCols[col])
{
stretchCount += (stretch) ? 1 : 0;
stretchCols[col] = stretch;
}
if (w > minColWidths[col])
minColWidths[col] = w;
col++;
}
}
float totalWidth = 0.0;
for (float w : colWidths)
totalWidth += w + colSpacing;
// See if this table is too wide and some columns will need to wrap text over several lines
float spaceRemaining = m_pageRightMarginX - (m_pageLeftMarginX + totalWidth);
if (spaceRemaining > 0.0 && stretchCount)
{
// the table is not full-width, lets see if any columns are stretched - divide the space left across these
float spacePerStretchCol = spaceRemaining / stretchCount;
printf("distributing space to cols: %f\n", spacePerStretchCol);
for (int i = 0; i < colCount; i++)
{
if (stretchCols[i])
colWidths[i] += spacePerStretchCol;
}
}
float needToReduceBy = -spaceRemaining;
while (needToReduceBy > 0.0)
{
// some lines need to wrap - start with the widest column first
int col = 0;
int widestCol = -1;
float widest = 0.0;
for (float w : colWidths)
{
if (w > widest)
{
// skip if already at it's min
if (w != minColWidths[col])
{
widest = w;
widestCol = col;
}
}
col++;
}
// Stop if all columns are already reduced to their mins
if (widestCol == -1)
break;
// halve the column width, or if that is too much, shink it to make the table fit
float newColWidth = widest * 0.5;
if (newColWidth > needToReduceBy)
{
newColWidth = widest - needToReduceBy;
}
// don't resize below what will cause words to be broken mid-word
if (newColWidth < minColWidths[col])
{
newColWidth = minColWidths[col];
}
colWidths[widestCol] = newColWidth;
needToReduceBy -= widest;
needToReduceBy += newColWidth;
// this will loop, halving the widest column until they all fit.
// the theory is that the widest column if wrapped would gain the most
// but after doing that and wrapping is happening, it will be okay to
// have other columns wrap if they will be wrapping on same lines as other column
}
// recalc
totalWidth = 0.0;
for (float w : colWidths)
totalWidth += w + colSpacing;
// TODO: we have dropped all formatting inside the table - all text is normal text - need to somehow save the formatting and apply it
float totalHeight = tableData.size() * (fontSize * lineSpacing + rowSpacing);
NewLine();
// Check if need to start a new page
if ((y + m_pageTopMarginY + totalHeight) > m_pageBottomMarginY)
{
// Start a new page
AddPage();
y = 0.0f;
}
m_page->setFontType(FT_Bold);
m_page->setFontSize(fontSize);
m_page->setPenColor(0x98bc53);//B0F0B0);
m_page->setFillColor(0x0);
float startY = y;
int row = 0;
for (const auto& tr : tableData)
{
if (alignCenter)
{
x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
}
float y2 = m_pageTopMarginY + y - fontSize * lineSpacing + 3.0;
m_page->drawLine(m_pageLeftMarginX + x, y2, m_pageLeftMarginX + x + totalWidth, y2);
//m_page->setPenWidth(0);
//m_page->setFillColor(0xB0F0B0); // light green
m_page->setAlpha(0.7);
m_page->setFillColor((row == 0) ? 0xD0D0D0 : ((row % 2) ? 0xF0F0F0 : 0xE8E8E8)); // light gray
m_page->drawRect(m_pageLeftMarginX + x, y2, totalWidth, fontSize * lineSpacing + rowSpacing);
m_page->setFillColor(0);
m_page->setAlpha(1.0);
int col = 0;
for (const auto& td : tr)
{
// OutputText(str);
float strW = m_page->textWidth(td.str.c_str());
float colW = colWidths[col] + colSpacing;
if ( td.align == TDA_Left )
m_page->drawText(m_pageLeftMarginX + x + (colSpacing * 0.5), y + m_pageTopMarginY, td.str.c_str());
else if ( td.align == TDA_Center )
m_page->drawText(m_pageLeftMarginX + x + ((colW - strW) * 0.5), y + m_pageTopMarginY, td.str.c_str());
else if ( td.align == TDA_Right )
m_page->drawText(m_pageLeftMarginX + x + colW - strW - (colSpacing * 0.5), y + m_pageTopMarginY, td.str.c_str());
x += colW;
col++;
}
x = 0.0;
y += fontSize * lineSpacing + rowSpacing;
if (row == 0)
m_page->setFontType(FT_Normal);
row++;
}
if (alignCenter)
{
x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
}
float y2 = m_pageTopMarginY + y - fontSize * lineSpacing + 3.0;
m_page->drawLine(m_pageLeftMarginX + x, y2, m_pageLeftMarginX + x + totalWidth, y2);
// Draw the vertical lines
x = 0.0;
float y1 = m_pageTopMarginY + startY - fontSize * lineSpacing + 3.0;
for (int i = 0; i < colCount; i++)
{
if (alignCenter)
{
x += ((m_pageRightMarginX - m_pageLeftMarginX) - totalWidth) * 0.5;
}
m_page->drawLine(m_pageLeftMarginX + x, y1, m_pageLeftMarginX + x, y1 + totalHeight);
x += colWidths[i] + colSpacing;
}
m_page->drawLine(m_pageLeftMarginX + x, y1, m_pageLeftMarginX + x, y1 + totalHeight);
x = 0.0;
}
static std::vector<std::string> split_keep_delim(const std::string &s, char delim, bool splitBefore=true)
{
std::vector<std::string> elems;
std::string cur;
for (char c : s)
{
if (splitBefore && c == delim && !cur.empty())
{
elems.push_back(cur);
cur = "";
}
cur += c;
if (!splitBefore && c == delim)
{
elems.push_back(cur);
cur = "";
}
}
if (!cur.empty())
elems.push_back(cur);
return elems;
}
float DocVisitor::OutputTextHelper(const char* str, bool doSplit)
{
std::string sstr(str);
const auto p1 = sstr.find('[');
const auto p2 = sstr.find(']');
if (doSplit && p1 != std::string::npos && p2 != std::string::npos)
{
for (const auto& str2 : split_keep_delim(str, '['))
{
if ((x + 50) >= m_pageRightMarginX)
{
NewLine();
}
for (const auto& str3 : split_keep_delim(str2, ']', false))
{
// x += OutputTextHelper(("XX" + str3 + "YY").c_str(), false);
x += OutputTextHelper(str3.c_str(), false);
}
}
return 0.0;
}
if (sstr == "<center>")
{
alignCenter = true;
return 0;
}
else if (sstr == "</center>")
{
alignCenter = false;
return 0;
}
// Custom page-break markdown (TODO: probably shouldn't be parsing markdown in here)
// - yep, if output html, we would want that filtered out from the html, so where to parse this?
if (sstr == "<<<<<>>>>>" || sstr == "--<<>>--")
{
// Start a new page
AddPage();
y = 0.0f;
x = 0.0f;
return 0;
}
// TODO: be able to insert [AUTHOR], [TITLE], [VERSION], [COMPANY] and [COPYRIGHT] - can do, but it is [%TITLE] etc
// and also for the below, probably need to filter out from when doing html output, so probably this is
// not the right place to make these substitutions
else if (sstr == "[PAGENUM]")
{
std::string num = std::to_string(m_pageNum);
return OutputTextHelper(num.c_str());
}
else if (sstr == "[PAGECOUNT]")
{
if (m_pageCount > 0)
{
std::string num = std::to_string(m_pageCount);
return OutputTextHelper(num.c_str());
}
else
{
// needs to output/parse the PDF twice, once to determine the page count and second time to insert it
m_needPageCount = true;
return OutputTextHelper("-1");
}
}
else if (sstr == "[DATE]")
{
std::string datestr = get_date_string("%A, %e %B %Y");
return OutputTextHelper(datestr.c_str());
}
else if (sstr == "[TIME]")
{
std::string datestr = get_date_string("%X");
return OutputTextHelper(datestr.c_str());
}
else if (sstr == "[TIMESTAMP]")
{
std::string datestr = get_date_string("%F %X");
return OutputTextHelper(datestr.c_str());
}
float w = m_page->textWidth(str);
/*
if (insideCode)// || insideLink)
{
m_page->drawRect(m_pageLeftMarginX + x - 1, y + m_pageTopMarginY + 2, w + 2, fontSizes[currentHeadingLevel.top()] + 2);
if (insideCode)
m_page->setColor(0.1f, 0.1f, 0.1f); // dark green
else
m_page->setColor(0.3f, 0.3f, 1.0f); // blue
}
*/
/*
if (insideCode)
{
// Output a grey-background to the code/preformatted section
float fontSize = fontSizes[currentHeadingLevel.top()];
m_page->setPenWidth(0);
m_page->setFillColor(0xB0F0B0); // light green
m_page->drawRect(m_pageLeftMarginX + x, y + m_pageTopMarginY - fontSize, w, fontSize * lineSpacing);
// Output the code text
m_page->setFontType(currentStyle.top());
m_page->setFontSize(fontSize);
m_page->setFillColor(0x208010); // dark green
}
*/
// snprintf((char*)buf, 16, "&#%u;", code);
DrawText(m_pageLeftMarginX + x, y + m_pageTopMarginY, str);
/*
if (insideCode)
{
m_page->setFillColor(0); // black
}
*/
return w;
}
// outputs current word to pdf
// splits the string up by words if it has to, and outputs new words on a new line if it will over fill the current line
// but also if the word is too big for a complete line, then will split that word mid-word as needed.
void DocVisitor::OutputCurrentWord(const char* str)
{
if (!str || !str[0])
return;
// Need to see if need to break mid-word if the word is too long for the line
float wordWidthAndMargins = m_pageLeftMarginX + m_page->textWidth(str);
if ((wordWidthAndMargins + x) >= m_pageRightMarginX) // Checking if it can fit on the current line or not
{
if (wordWidthAndMargins >= m_pageRightMarginX) // Can it fit on a new line by itself?
{
// we need to break up the text, write it char by char
int i = 0;
while (str[i]) {
char s[2] = { str[i], 0 };
// TODO: adding the dash is okay if the text is English, but not if text is other language, eg Japanese or Chinese
float chWidthAndMargins = m_pageLeftMarginX + m_page->textWidth(s) + m_page->textWidth("-");
if ((chWidthAndMargins + x) >= m_pageRightMarginX)
{
OutputTextHelper("-");
NewLine();
}
x += OutputTextHelper(s);
i++;
}
x += m_page->textWidth(" ");//OutputTextHelper(" ");
/*
float spaceWidth = m_page->textWidth(" ");
if (insideCode)
{
// Output a grey-background to the code/preformatted section
float fontSize = fontSizes[currentHeadingLevel.top()];
m_page->setPenWidth(0);
m_page->setFillColor(0xB0F0B0); // light green
m_page->drawRect(m_pageLeftMarginX + x, y + m_pageTopMarginY - fontSize, spaceWidth, fontSize * lineSpacing);
}
x += spaceWidth;
*/
return;
}
else
{
NewLine();
}
}
x += OutputTextHelper(str);
x += m_page->textWidth(" ");//OutputTextHelper(" ");//m_page->textWidth(" ");
}
void DocVisitor::OutputText(const char* str)
{
if (insideTable)
{
tableData.back().back().str += str;
return;
}
if (insideCode)
{
codeText += str;
return;
}
m_page->setFontType(currentStyle.top());
m_page->setFontSize(fontSizes[currentHeadingLevel.top()]);
// if (startOfListItem)
{
if (strlen(str) >= 3 && str[0] == '[' && str[2] == ']')
{
bool checked = str[1] == 'x' || str[1] == 'X';
float textHeight = fontSizes[currentHeadingLevel.top()] * lineSpacing * 0.75;
m_page->drawCheckBox(checked, m_pageLeftMarginX + x, m_pageTopMarginY + y - textHeight, textHeight, textHeight);
x += textHeight * 1.5;
str += 3;
}
startOfListItem = false;
}
std::string currentWord;
int i = 0;
while ( str[i] != 0 ) {
if ( str[i] == ' ' || str[i] == '\t' || str[i] == '\n' ) {
OutputCurrentWord(currentWord.c_str());
if (insideCode && str[i] == '\n') { // for html, this is wrong
NewLine();
}
currentWord = "";
//break;
}
else
{
char s[2] = { str[i], 0 };
currentWord += s;
}
i++;
}
//if ( str[i] == 0 ) {
if ( !currentWord.empty() ) {
OutputCurrentWord(currentWord.c_str());
}
}
/// Visit a text node
bool DocVisitor::Visit( const TiXmlText& text )
{
const char* str = text.ValueTStr().c_str();
OutputText(str);
return true;
}