#include <cstdarg>
#include <cstdlib>
#include <string>
#include <iostream>
#include "DocTemplate.h"
#include "Util.h"
#define HPDF_DEF_PAGE_WIDTH 595.276F
#define HPDF_DEF_PAGE_HEIGHT 841.89F
GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
DocItemType,
DIT_Last,
DIT_Unknown,
"Defaults",
"Page",
"Background",
"Label",
"Image",
"Line",
"Box",
"Circle",
"Ellipse",
"Polygon",
"Spline",
"Shape",
"Unknown"
)
GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
TemplateRole,
TR_Last,
TR_Both,
"CoverPage",
"Document",
"Both"
)
GENERATE_ENUM_SERIALIZATION_FUNCTIONS
(
PenStyle,
PS_Last,
PS_Solid,
"Solid",
"Dashed"
)
std::string DocTemplate::m_currentFile = "";
unsigned int DocTemplate::m_currentLine = 0;
#define DebugMsg(fmt, ...) DocTemplate::LogMessage(LL_Debug, fmt, ##__VA_ARGS__)
#define WarningMsg(fmt, ...) DocTemplate::LogMessage(LL_Warning, fmt, ##__VA_ARGS__)
#define ErrorMsg(fmt, ...) DocTemplate::LogMessage(LL_Error, fmt, ##__VA_ARGS__)
#ifndef _WIN32
# define HAVE_CONSTEXPR
#else
# include <windows.h>
#endif
#ifdef HAVE_CONSTEXPR
// TODO: XXX ### This side of HAVE_CONSTEXPR has not been tested
constexpr unsigned int hashString(const char* str, int h = 0)
{
return !str[h] ? 5381 : (hashString(str, h + 1) * 33) ^ str[h];
}
unsigned int hashString(const std::string& str, int h = 0)
{
return !str.c_str()[h] ? 5381 : (hashString(str, h + 1) * 33) ^ str.c_str()[h];
}
// Assumes that for lower case comparing, the values passed in are already lower case
#define Choose(x) do { std::string tttmp = x; switch (hashString(x)) {
#define Option(x) } case hashString(x): if ( tttmp == x ) {
#define OptionLower(x) } case hashString(x): if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define Defaults } default: {
#define Done } } while(false);
#else
#define Choose(x) do { std::string tttmp = x; if (false)
#define Option(x) } else if ( tttmp == x ) {
#define OptionLower(x) } else if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define Defaults } else {
#define Done } while(false);
#endif
/*
// auto means this will work with ints too (but depends how str2lower is defined)
// but means it won't work with const char*s.
#define strSwitch(x) do { auto tttmp = x; if (false)
#define strCase(x) } else if ( tttmp == x ) {
#define strLowerCase(x) } else if ( str2lower(tttmp) == str2lower(std::string(x)) ) {
#define strDefault } else {
#define strSwitchEnd } while(false);
*/
/*
Rules: Any extra white space is allowed, but if the the value, must be quoted,
eg:
text = Blah is okay
text = Foo Bar is not okay, must be quoted
text = "Foo Bar" is okay
pos = 10,10 is okay
pos = 10, 10 is not okay, must be quoted
pos = "10, 10" is okay
Removal of extra white space around the '=' is fine
eg:
pos = 10,10 is okay
pos= 10,10 is okay
pos =10,10 is okay
pos=10,10 is okay
Attributes must be deliminated by white space
eg:
foo=val1 bar=val2 is okay
foo=val1bar=val2 is not okay
foo="val1"bar=val2 is not okay (but might work as you expect, but don't count on it)
*/
DocTemplate::DocTemplate()
{
m_defaultDefaults.m_type = DIT_Defaults;
m_defaultDefaults.m_role = TR_Both;
m_defaultDefaults.m_alpha = 1.0;
m_defaultDefaults.m_bgColor = 0xffffff;
m_defaultDefaults.m_pos.x = 10.0;
m_defaultDefaults.m_pos.y = 10.0;
m_defaultDefaults.m_size.x = 100.0;
m_defaultDefaults.m_size.y = 100.0;
m_defaultDefaults.m_font = "Helvetica";
m_defaultDefaults.m_fontSize = 12.0f;
m_defaultDefaults.m_fontColor = 0x000000;
m_defaultDefaults.m_fillColor = 0x000000;
m_defaultDefaults.m_penColor = 0x000000;
m_defaultDefaults.m_penWidth = 1.0f;
m_defaultDefaults.m_penStyle = PS_Solid;
m_defaultDefaults.m_alignment = Align((int)AL_Left | (int)AL_Top);
m_defaultDefaults.m_boundsAlignment = Align((int)AL_Left | (int)AL_Top);
m_defaultDefaults.m_text = "undefined";
m_defaultDefaults.m_imageFile = "undefined";
m_defaults = m_defaultDefaults;
m_pageTopLeft.x = 60.0;
m_pageTopLeft.y = 120.0;
m_pageBottomRight.x = HPDF_DEF_PAGE_WIDTH - 60.0;
m_pageBottomRight.y = HPDF_DEF_PAGE_HEIGHT - 60.0;
m_columns = 1;
m_columnSpacing = 20.0;
}
DocTemplate::~DocTemplate()
{
}
#ifndef _WIN32
int _vscprintf(const char * format, va_list pargs)
{
int retval;
va_list argcopy;
va_copy(argcopy, pargs);
retval = vsnprintf(NULL, 0, format, argcopy);
va_end(argcopy);
return retval;
}
#endif
void DocTemplate::LogMessage(yqLogLevel a_level, const char* a_msg, ...)
{
va_list args;
va_start(args, a_msg);
#ifdef _WIN32
auto sizeForFileLineAndNull = m_currentFile.size() + 64 + 1;
auto sizeRequired = _vscprintf(a_msg, args) + sizeForFileLineAndNull;
auto a = (char*)_alloca(sizeRequired);
int offset = _snprintf_s(a, sizeRequired, sizeForFileLineAndNull, "%s(%i): ", m_currentFile.c_str(), m_currentLine);
vsnprintf_s(a + offset, sizeRequired - offset, INT_MAX - 1, a_msg, args);
OutputDebugStringA(a);
_freea(a);
#else
vfprintf(stderr, a_msg, args);
#endif
va_end(args);
/*
if (a_level == LL_Warning)
DebugBreak();
*/
if (a_level == LL_Error)
exit(-1);
}
std::vector<Point> str2pointList(const std::string& str)
{
std::vector<Point> ret;
std::vector<std::string> strs = split(str, ',');
for (size_t i = 0; i < strs.size(); i+=2)
{
Point pnt = { 0, 0 };
pnt.x = (float)atof(trim(strs[i + 0]).c_str());
if ((i + 1) < strs.size())
pnt.y = (float)atof(trim(strs[i + 1]).c_str());
else
WarningMsg("uneven coordinate count in shape, expecting a list of 2d coordinates so should be even count!\n");
ret.push_back(pnt);
}
return ret;
}
std::vector<Point> stringToPolygon(const std::string& str)
{
return str2pointList(str);
}
Point str2pos(const std::string& str)
{
Point ret = { 0, 0 };
std::vector<std::string> strs = split(str, ',');
if (strs.size() != 2)
return ret;
ret.x = (float)atof(trim(strs[0]).c_str());
ret.y = (float)atof(trim(strs[1]).c_str());
return ret;
}
Point stringToPoint(const std::string& str)
{
return str2pos(str);
}
PenStyle str2style(const std::string& str)
{
Choose(str)
{
OptionLower("Solid")
return PS_Solid;
OptionLower("Dashed")
return PS_Dashed;
Defaults
WarningMsg("Bad style\n");
break;
}
Done
return PS_Solid;
}
Align str2align(const std::string& str)
{
if (str.empty())
{
return (Align)(AL_Left | AL_Top);
}
unsigned int alignment = 0;
std::vector<std::string> strs = split(str, ',');
std::string horzStr = trim(strs[0]);
// if both are center, then one is hcenter and other is vcenter
// if only one is center, we can determine if it is a hcenter or vcenter by the other value
// we do this after first attempting to parse it
Choose(horzStr)
{
OptionLower("left") alignment |= AL_Left; break;
OptionLower("right") alignment |= AL_Right; break;
OptionLower("top") alignment |= AL_Top; break;
OptionLower("bottom") alignment |= AL_Bottom; break;
OptionLower("center") alignment |= AL_HCenter; break;
OptionLower("centre") alignment |= AL_HCenter; break;
Defaults
WarningMsg("Bad horizontal alignment\n");
break;
}
Done
if (strs.size() < 2)
{
return (Align)(alignment | AL_Top);
}
std::string vertStr = trim(strs[1]);
Choose(vertStr)
{
OptionLower("left") alignment |= AL_Left; break;
OptionLower("right") alignment |= AL_Right; break;
OptionLower("top") alignment |= AL_Top; break;
OptionLower("bottom") alignment |= AL_Bottom; break;
OptionLower("center") alignment |= AL_VCenter; break;
OptionLower("centre") alignment |= AL_VCenter; break;
Defaults
WarningMsg("Bad vertical alignment\n");
break;
}
Done
// check we have a valid combination
if ((alignment & (AL_Left | AL_HCenter | AL_Right)) && (alignment & (AL_Top | AL_VCenter | AL_Bottom)))
return (Align)alignment;
// for it to still be valid, then one of the values is a center, swap the center types and try again
if (alignment & AL_HCenter)
alignment = (alignment & ~AL_HCenter) | AL_VCenter;
else if (alignment & AL_VCenter)
alignment = (alignment & ~AL_VCenter) | AL_HCenter;
if ((alignment & (AL_Left | AL_HCenter | AL_Right)) && (alignment & (AL_Top | AL_VCenter | AL_Bottom)))
return (Align)alignment;
WarningMsg("Bad alignment values\n");
return (Align)(AL_Top | AL_Left);
}
Align stringToAlignment(const std::string& str)
{
return str2align(str);
}
static std::string dirName(const std::string& fname)
{
size_t pos = fname.find_last_of("\\/");
return (std::string::npos == pos) ? "" : fname.substr(0, pos);
}
void DocTemplate::ReadTemplateFile(const char* a_fileName)
{
m_items.clear();
FILE* f = 0;
fopen_s(&f, a_fileName, "rb");
if ( f )
{
m_currentFile = a_fileName;
char lineBuf[1024];
DocTemplateItem currentItem = m_defaults;
currentItem.m_type = DIT_Unknown;
bool firstFirst = true;
bool first = true;
while ( fgets(lineBuf, sizeof(lineBuf), f) ) {
m_currentLine++;
if (lineBuf[0] == '[') {
// First time through here we haven't read anything yet
if (!firstFirst)
{
// new item - this is now after we have read one section and about to start to read a new section
if (!first) {
if (currentItem.m_type == DIT_Page)
{
m_pageTopLeft.x = currentItem.m_pos.x;
m_pageTopLeft.y = currentItem.m_pos.y;
m_pageBottomRight.x = m_pageTopLeft.x + currentItem.m_size.x;
m_pageBottomRight.y = m_pageTopLeft.y + currentItem.m_size.y;
}
else if (currentItem.m_type == DIT_Defaults)
{
m_defaults = currentItem;
}
else
m_items.push_back(currentItem);
currentItem = m_defaults;
currentItem.m_type = DIT_Unknown;
}
first = false;
}
firstFirst = false;
Choose(trim(std::string(lineBuf)))
{
OptionLower("[Defaults]")
currentItem.m_type = DIT_Defaults;
break;
#define STR_SWITCH_CASE( OPT ) \
OptionLower("[" #OPT "]") \
currentItem.m_type = DIT_##OPT; \
break;
OptionLower("[Text]") // Alias for Label
currentItem.m_type = DIT_Label;
break;
STR_SWITCH_CASE(Label)
STR_SWITCH_CASE(Page)
STR_SWITCH_CASE(Background)
STR_SWITCH_CASE(Image)
STR_SWITCH_CASE(Line)
STR_SWITCH_CASE(Box)
STR_SWITCH_CASE(Circle)
STR_SWITCH_CASE(Ellipse)
STR_SWITCH_CASE(Polygon)
STR_SWITCH_CASE(Spline)
STR_SWITCH_CASE(Shape)
Defaults
currentItem.m_type = DIT_Unknown;
break;
}
Done
} else {
std::vector<std::string> strs = split(lineBuf, '=');
std::string s = (strs.size() > 0) ? trim(strs[0]) : "";
if (strs.size() != 2)
{
if (s != "")
WarningMsg("Error parsing line, no '=' or line is whitespace.\n");
continue;
}
Choose(s)
{
//OptionLower("id")
//OptionLower("flags")// wrap, ignore, clip
OptionLower("role")
currentItem.m_role = stringToTemplateRole(trim(strs[1]));
break;
OptionLower("alpha")
currentItem.m_alpha = str2float(trim(strs[1]));
break;
OptionLower("bgColor")
currentItem.m_bgColor = str2col(trim(strs[1]));
break;
OptionLower("align")
currentItem.m_alignment = str2align(trim(strs[1]));
break;
OptionLower("item-align")
currentItem.m_boundsAlignment = str2align(trim(strs[1]));
break;
OptionLower("pos") // offset
currentItem.m_pos = str2pos(trim(strs[1]));
break;
OptionLower("size")
currentItem.m_size = str2pos(trim(strs[1]));
break;
OptionLower("font")
currentItem.m_font = trim(strs[1]);
break;
OptionLower("fontSize")
currentItem.m_fontSize = str2float(trim(strs[1]));
break;
OptionLower("fillColor")
currentItem.m_fillColor = str2col(trim(strs[1]));
break;
OptionLower("penColor")
currentItem.m_penColor = str2col(trim(strs[1]));
break;
OptionLower("penSize") // alias for penWidth
currentItem.m_penWidth = str2float(trim(strs[1]));
break;
OptionLower("penWidth")
currentItem.m_penWidth = str2float(trim(strs[1]));
break;
OptionLower("penStyle")
currentItem.m_penStyle = str2style(trim(strs[1]));
break;
OptionLower("text")
currentItem.m_text = trim(strs[1]);
break;
OptionLower("points")
currentItem.m_shape = str2pointList(trim(strs[1]));
break;
OptionLower("file")
currentItem.m_imageFile = trim(strs[1]);
break;
OptionLower("cspacing")
m_columnSpacing = str2float(trim(strs[1]));
break;
OptionLower("columns")
m_columns = stringToInt(trim(strs[1]));
break;
Defaults
if (s != "" && strs.size() != 1)
currentItem.m_type = DIT_Unknown;
break;
}
Done
}
/*
Image ( pos={0,0} size={w,h} file="image.jpg" )
Line ( pos={0,0} size={w,h} points={20,20,30,30,40,20} pen={"#000",1.4,"solid"} )
Shape ( pos={0,0} size={w,h} points={20,20,30,30,40,20} fill="#fff" pen={"#000",1.4,"solid"} )
//box, circle, ellipse, poly, spline, label, image
*/
}
if (currentItem.m_type == DIT_Page)
{
m_pageTopLeft.x = currentItem.m_pos.x;
m_pageTopLeft.y = currentItem.m_pos.y;
m_pageBottomRight.x = m_pageTopLeft.x + currentItem.m_size.x;
m_pageBottomRight.y = m_pageTopLeft.y + currentItem.m_size.y;
}
else if (currentItem.m_type == DIT_Defaults)
{
m_defaults = currentItem;
}
else
m_items.push_back(currentItem);
m_items.insert(m_items.begin(), m_defaults);
fclose(f);
}
else
{
WarningMsg("Problem opening file : %s\n", a_fileName);
}
}
void DocTemplate::Save()
{
WriteTemplateFile(m_currentFile.c_str());
}
void SVGTest(const char* a_fileName, double scale, DocOutputPage* outputPage);
void DocTemplate::Apply(DocOutputPage& page)
{
for (size_t i = 0; i < m_items.size(); ++i)
{
float x = m_items[i].m_pos.x;
float y = m_items[i].m_pos.y;
page.setFontSize(m_items[i].m_fontSize);
page.setFillColor(m_items[i].m_fillColor);
page.setPenColor(m_items[i].m_penColor);
page.setPenWidth(m_items[i].m_penWidth);
page.setPenStyle(m_items[i].m_penStyle);
page.setAlpha(m_items[i].m_alpha);
// TODO: handle special text like [PAGENUM], [PAGECOUNT] etc
if (m_items[i].m_type == DIT_Label)
{
// positioning based on alignment flags
// - position based on relative point of the bounds of the text and of the page
//
// eg: the bounds of text have a top,left,right,bottom,hcenter,vcenter values
// the screen has same, top,left,right,bottom,hcenter,vcenter values
//
// The positioning is specified as offsets between these
//
// eg: position the top of the text 5 pixels below the top of the page
// and the left of the text 5 pixels from the left of the page
//
// or, the hcenter of the text at 0 pixels from the hcenter of the page etc.
//
// Must set the font properties first so that getting the textWidth works correctly
page.setFontType(FT_Normal); // TODO: map font name to a font index or make the output accept a string
float pageW = page.width();
float pageH = page.height();
Align pageA = m_items[i].m_alignment;
float pageX = (pageA & AL_Right) ? pageW : ((pageA & AL_HCenter) ? pageW * 0.5f : 0.0f);
float pageY = (pageA & AL_Bottom) ? pageH : ((pageA & AL_VCenter) ? pageH * 0.5f : 0.0f);
float textW = page.textWidth(m_items[i].m_text.c_str());
float textH = m_items[i].m_fontSize;
Align textA = m_items[i].m_boundsAlignment;
float textX = (textA & AL_Right) ? textW : ((textA & AL_HCenter) ? textW * 0.5f : 0.0f);
float textY = (textA & AL_Bottom) ? 0 : ((textA & AL_VCenter) ? -textH * 0.5f : -textH);
x += pageX - textX;
y += pageY - textY;
//if (x < 0.0) x = x + page.width(); // negative numbers are aligned from the right
//if (y < 0.0) y = y + page.height(); // negative numbers are aligned from the bottom
page.drawText(x, y, m_items[i].m_text.c_str());
}
if (m_items[i].m_type == DIT_Polygon)
{
page.drawPolygon((float*)m_items[i].m_shape.data(), m_items[i].m_shape.size());
}
if (m_items[i].m_type == DIT_Background)
{
std::string fileLocation = dirName(m_currentFile) + "/" + m_items[i].m_imageFile;
if (endsWith(fileLocation, ".svg")) {
SVGTest(fileLocation.c_str(), 0.02, &page);
} else {
// Draws image from top-left corner and stretched to the entire page
page.drawImage(fileLocation.c_str(), 0, 0, page.width(), page.height());
}
}
if (m_items[i].m_type == DIT_Image)
{
std::string fileLocation = dirName(m_currentFile) + "/" + m_items[i].m_imageFile;
if (endsWith(fileLocation, ".svg")) {
printf("Looks like an SVG -%s-\n", fileLocation.c_str());
SVGTest(fileLocation.c_str(), 0.02, &page);
} else {
// Draw image at the given position and with the given size
page.drawImage(fileLocation.c_str(), x, y, m_items[i].m_size.x, m_items[i].m_size.y);
}
}
}
page.setAlpha(1.0);
}
std::string stringFromAlignment(Align align)
{
std::string ret;
if (align & AL_Left)
ret = "left";
else if (align & AL_Right)
ret = "right";
else
ret = "center";
ret += ",";
if (align & AL_Top)
ret += "top";
else if (align & AL_Bottom)
ret += "bottom";
else
ret += "center";
return ret;
}
std::string stringFromPoint(const Point& pnt)
{
return stringFromFloat(pnt.x) + "," + stringFromFloat(pnt.y);
}
std::string stringFromPolygon(const std::vector<Point>& poly)
{
std::string ret;
bool first = true;
for (const Point& pnt : poly)
{
if (!first)
ret += ",";
ret += stringFromPoint(pnt);
first = false;
}
return ret;
}
bool operator==(const Point& pntA, const Point& pntB)
{
return (pntA.x == pntB.x && pntA.y == pntB.y);
}
bool operator!=(const Point& pntA, const Point& pntB)
{
return (pntA.x != pntB.x || pntA.y != pntB.y);
}
static void WriteItemToFile(FILE* f, const DocTemplateItem& item, const DocTemplateItem& defaults)
{
fprintf(f, "[%s]\n", stringFromDocItemType(item.m_type).c_str());
#define OUTPUT_ITEM(member, tag, typ) \
if (item.m_##member != defaults.m_##member) \
fprintf(f, tag "=%s\n", stringFrom##typ(item.m_##member).c_str())
OUTPUT_ITEM(role , "role", TemplateRole);
OUTPUT_ITEM(alpha , "alpha", Float );
OUTPUT_ITEM(bgColor , "bgColor", Color );
OUTPUT_ITEM(pos , "pos", Point );
OUTPUT_ITEM(size , "size", Point );
OUTPUT_ITEM(font , "font", String );
OUTPUT_ITEM(fontSize , "fontSize", Float );
OUTPUT_ITEM(fontColor , "fontColor", Color );
OUTPUT_ITEM(fillColor , "fillColor", Color );
OUTPUT_ITEM(penColor , "penColor", Color );
OUTPUT_ITEM(penWidth , "penWidth", Float );
OUTPUT_ITEM(penStyle , "penStyle", PenStyle );
OUTPUT_ITEM(alignment , "align", Alignment);
OUTPUT_ITEM(boundsAlignment , "item-align", Alignment);
OUTPUT_ITEM(text , "text", String );
OUTPUT_ITEM(shape , "points", Polygon );
if (item.m_imageFile != defaults.m_imageFile)
fprintf(f, "file=%s\n", stringFromString(item.m_imageFile).c_str());
}
void DocTemplate::WriteTemplateFile(const char* a_fileName)
{
FILE* f = 0;
fopen_s(&f, a_fileName, "w");
if ( f )
{
// WriteItemToFile(f, m_defaults, m_defaultDefaults);
// m_currentFile = a_fileName;
for (const auto& item : m_items)
{
if (item.m_type == DIT_Defaults)
WriteItemToFile(f, item, m_defaultDefaults);
else
WriteItemToFile(f, item, m_defaults);
fprintf(f, "\n");
}
DocTemplateItem pageItem;
pageItem = m_defaults;
pageItem.m_type = DIT_Page;
pageItem.m_pos = m_pageTopLeft;
pageItem.m_size.x = m_pageBottomRight.x - m_pageTopLeft.x;
pageItem.m_size.y = m_pageBottomRight.y - m_pageTopLeft.y;
WriteItemToFile(f, pageItem, m_defaults);
fprintf(f, "columns=%s\n", stringFromInt(m_columns).c_str());
fprintf(f, "cspacing=%s\n", stringFromFloat(m_columnSpacing).c_str());
fprintf(f, "\n");
fclose(f);
}
}