Reputation: 295
I am using GraphicsMagick in a C++ library in order to create rasterized output, which mainly consists out of text.
I am doing something like this:
void gfx_writer::add_text(Magick::Image& img) const
{
using namespace Magick;
const unsigned x = // just a position;
const unsigned y_title = // just a position;
const unsigned y_heading = // just a position;
const unsigned y_value = // just a position;
img.strokeColor("transparent");
img.fillColor("black");
img.font(font_title_);
img.fontPointsize(font_size_title_);
img.draw(DrawableText{static_cast<double>(x), static_cast<double>(y_title), "a text"});
img.font(font_heading_);
img.fontPointsize(font_size_heading_);
img.draw(DrawableText{static_cast<double>(x), static_cast<double>(y_heading), "another text"});
img.font(font_value_);
img.fontPointsize(font_size_value_);
img.draw(DrawableText{static_cast<double>(x), static_cast<double>(y_value), "third text"});
}
Whereas font_title_
, font_heading_
and font_value_
are paths to the TTF files.
This is done more than once and I experience rather bad performance. When I have a look at what happens using Sysinternals Process Monitor I see that the TTF files are read over and over again. So my questions are:
img.font(...)
is called?Upvotes: 3
Views: 703
Reputation: 24419
Note: This answer uses ImageMagick's Magick++
library, and may have minor portability issues with GraphicsMagick, but the underlying solution is the same.
are my observations correct, that the TTF files are read each time img.font(...) is called?
Yes, the TTF font is reloaded each time. One option is to install the fonts in the system, and call the font-family constructor.
DrawableFont ( const std::string &family_,
StyleType style_,
const unsigned long weight_,
StretchType stretch_ );
Most systems have some sort of font caching system that would allow quicker access, but not really noticeable on modern hardware.
any other thing I am missing?
Try building a graphical context, and only call Magick::Image.draw
once. Remember that the Drawable...
calls are only wrapping MVG statements, and creating a std::list<Drawable>
allows you to build complex vectors. Only when the draw
method consumes the draw commands is when the TTF will be loaded, so its key to prepare all the drawing commands ahead of time.
Let's start by rewriting the code you provided (and I'm taking a degree of liberty here).
#include <Magick++.h>
const char * font_title_ = "fonts/OpenSans-Regular.ttf";
const char * font_heading_ = "fonts/LiberationMono-Regular.ttf";
const char * font_value_ = "fonts/sansation.ttf";
double font_size_title_ = 32;
double font_size_heading_ = 24;
double font_size_value_ = 16;
void gfx_writer_add_text(Magick::Image& img)
{
using namespace Magick;
double x = 10.0;
double y_title = 10;
double y_heading = 20.0;
double y_value = 30.0;
img.strokeColor("transparent");
img.fillColor("black");
img.font(font_title_);
img.fontPointsize(font_size_title_);
img.draw(DrawableText{x, y_title, "a text"});
img.font(font_heading_);
img.fontPointsize(font_size_heading_);
img.draw(DrawableText{x, y_heading, "another text"});
img.font(font_value_);
img.fontPointsize(font_size_value_);
img.draw(DrawableText{x, y_value, "third text"});
}
int main()
{
Magick::Image img("wizard:");
gfx_writer_add_text(img);
gfx_writer_add_text(img);
gfx_writer_add_text(img);
img.write("output.png");
}
I can compile and benchmark the run time. I get the following times:
$ time ./original.o
real 0m5.061s
user 0m0.094s
sys 0m0.029s
Refactoring the code to use a drawing context, and only call Magick::Image.draw
once.
#include <Magick++.h>
#include <list>
const char * font_title_ = "fonts/OpenSans-Regular.ttf";
const char * font_heading_ = "fonts/LiberationMono-Regular.ttf";
const char * font_value_ = "fonts/sansation.ttf";
double font_size_title_ = 32;
double font_size_heading_ = 24;
double font_size_value_ = 16;
void gfx_writer_add_text(Magick::Image& img)
{
using namespace Magick;
double x = 10.0;
double y_title = 10;
double y_heading = 20.0;
double y_value = 30.0;
std::list<Drawable> ctx;
ctx.push_back(DrawableStrokeColor("transparent"));
ctx.push_back(DrawableFillColor("black"));
/* TITLE */
ctx.push_back(DrawablePushGraphicContext());
ctx.push_back(DrawableFont(font_title_);
ctx.push_back(DrawablePointSize(font_size_title_));
ctx.push_back(DrawableText{x, y_title, "a text"});
ctx.push_back(DrawablePopGraphicContext());
/* HEADING */
ctx.push_back(DrawablePushGraphicContext());
ctx.push_back(DrawableFont(font_heading_));
ctx.push_back(DrawablePointSize(font_size_heading_));
ctx.push_back(DrawableText{x, y_heading, "another text"});
ctx.push_back(DrawablePopGraphicContext());
/* Value */
ctx.push_back(DrawablePushGraphicContext());
ctx.push_back(DrawableFont(font_value_));
ctx.push_back(DrawablePointSize(font_size_value_));
ctx.push_back(DrawableText{x, y_value, "third text"});
ctx.push_back(DrawablePopGraphicContext());
img.draw(ctx);
}
int main()
{
Magick::Image img("wizard:");
gfx_writer_add_text(img);
gfx_writer_add_text(img);
gfx_writer_add_text(img);
img.write("output2.png");
}
And the benchmark times are slightly better.
$ time ./with_context.o
real 0m0.106s
user 0m0.090s
sys 0m0.012s
This is done more than once and I experience rather bad performance.
Worth taking a step back, and asking: "How can a refactor my solution to only draw at the last possible moment?".
Upvotes: 4