claudegex
claudegex

Reputation: 295

GraphicsMagick TTF Font Performance

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:

Upvotes: 3

Views: 703

Answers (1)

emcconville
emcconville

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

Related Questions