Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / IoT

GFX Part 3: Drawing Primitives

5.00/5 (2 votes)
25 Apr 2021MIT14 min read 5.5K   83  
Explore the basic drawing functionality provided by the GFX IoT library
Draw points, lines, shapes, and text to bitmaps using the GFX drawing facilities.

gfx 3

Introduction

Building on the previous articles, now we move on to drawing on those bitmaps we explored previously. Using the drawing features of this library, we can draw all kinds of things in all kinds of colors, and clip them to rectangular windows we define.

Update: Now will build with C++14, which makes it much easier to use with certain frameworks and toolchains.

Building this Mess

This code was tested with clang 11 and gcc 10 using -std=c++17 and -std=c++14

Since C++ does not define a value for the __cplusplus macro that indicates the C++17 standard, there are two headers, gfx_color.hpp and gfx_color_cpp14.hpp. The former is for C++17 and above. The latter is for C++14. They each define the color<> template differently because the two standards are incompatible in this regard. Using them is the same, but the declaring code is different for each. Include the appropriate header to gain access to the color<> template.

The latest version of MSVC does not want to compile this code. Frankly, I don't care, as nothing I've encountered cross compiles with MSVC anyway, and the primary purpose of this code is for IoT devices that can't be readily targeted with MSVC. There's nothing Linux specific in this code, and I believe it's simply standards compliant C++, but it's possible that I am using some features specific to gcc and clang without realizing it.

The main file to build is demo.cpp under /reference.

Under /tools is fontgen.cpp you can use for generating fonts.

Using this Mess

The drawing primitives are extremely straightforward. You can draw over a bitmap<> template type or any type with compatible methods. The meat of everything is the draw class, which exposes several template methods that perform basic drawing operations:

  • point<>() - draws a point at the specified location, with the specified color and optionally using the specified clipping rectangle
  • line<>() - draws a line from rect's (x1,y1) to (x2,y2), with the specified color and optionally using the specified clipping rectangle
  • rectangle<>() - draws a rectangle with the specified bounds using the specified color, and optionally using the specified clipping rectangle
  • filled_rectangle<>() - draws a filled rectangle with the specified bounds , using the specified color, and optionally using the specified clipping rectangle
  • ellipse<>() - draws an ellipse with the specified bounds, using the specified color, and optionally using the specified clipping rectangle
  • filled_ellipse<>() - draws a filled ellipse with the specified bounds, using the specified color, and optionally using the specified clipping rectangle
  • arc<>() - draws a 90 degree arc with the specified bounds, which also indicate orientation, using the specified color and optionally using the specified clipping rectangle
  • filled_arc<>() - draws a filled 90 degree arc with the specified bounds, which also indicate orientation, using the specified color and optionally using the specified clipping rectangle
  • rounded_rectangle<>() - draws a rectangle that has rounded corners with the specified bounds, the specified x and y corner ratios, using the specified color, and optionally using the specified clipping rectangle
  • filled_rounded_rectangle<>() - draws a filled rectangle that has rounded corners with the specified bounds, the specified x and y corner ratios, using the specified color, and optionally using the specified clipping rectangle
  • bitmap<>() - draws a selected portion of a bitmap to the destination, using the specified destination bounds, the source bitmap (or compatible type), the source rectangle, options and a clipping rectangle. Internally, there's a "fast mode" that uses raw blts when possible which depends on the copying options
  • text<>() - draws text with the specified bounds, font, color, and optional backcolor, along with an optional clipping rectangle

Rectangles

I'm going to cover rectangles again, because they're of primary importance to these drawing operations. The rectx<> template allows you to declare rectangles of any numeric element type but you'll typically use rect16 or srect16 for the signed version.

Most of the drawing operations take rectangles. Even line<>() does to specify the line endpoints. The reason for this is that rectangles expose an extensive interface with which to manipulate them. Therefore, it's more flexible to take a rectangle vs. two endpoints because you can do things like flip_horizontal() an the rectangle, changing its orientation. Let's talk about why you might want to do things like flip a rectangle.

When you're drawing lines, you can easily draw one, flip your rectangle along either axis, and then draw again using the new flipped rectangle to make an "X".

When you're drawing arcs or bitmaps, the orientation of the rectangle dictates the orientation of the drawing operation, so for example, if a rectangle is flipped vertically, the bitmap will be drawn upside down. The same goes for arcs. Also, the same property applies to flipping horizontally, of course.

Clipping

Originally, I had planned on a scheme whereby you would create a canvas which could contain other canvases, clip all drawing to a canvas and clip the child canvases to their parent, if any. The issue with this is these devices don't really have a lot of RAM and while this doesn't take a lot for each canvas, if you had a lot of them it would add up. It also takes a bit of CPU to clip. Beyond that, such a facility is more useful for complicated dynamic and composable layouts like you might find in HTML or WPF. That isn't exactly appropriate for an IoT device.

That being said, some facility for clipping would be immensely useful, even if we're not using canvas composition as above. The compromise I settled on was to allow for a clipping rectangle to be specified with each drawing primitive. Combined with rectx<> features like crop(), you can readily create compositions of drawing areas (canvases) yourself if you need them. The final parameter for each of these drawing operations is called clip and it's a pointer to an srect16 structure that defines the clipping rectangle, or nullptr for no clipping.

Bitmaps

The bitmap<>() method encompasses quite a bit of functionality. You can draw all or portions of a bitmap, while optionally stretching, shrinking, flipping it, and/or converting it to a different pixel format, all with one method. It should be noted that taking advantage of the flipping, conversion, and resizing is much slower than if you don't need it. Resizing currently uses a nearest neighbor algorithm but I intend to add a (slower) bilinear interpolation down the road for less "choppy" resizing. Essentially, this drawing operation will use the bitmap<>'s blt() method if it's possible. Otherwise, it must draw the bitmap pixel by pixel. Note that the orientation of the destination rectangle dictates the orientation of the bitmap, so by flipping that rectangle, you can flip the bitmap, just like by resizing it, you can resize or crop the bitmap, depending on options.

Text and Fonts

This is easily the largest section. In fact, this is most of the article. Drawing text opens up a big can of worms. For starters, you need fonts.

The actual drawing is simple once you have a font. Note that common whitespace like tabs and newline are respected, and text will be wrapped, but not word wrapped.

That leaves fonts. The font class has a lot of members. Before we get to them, let's back up and talk about how to get your fonts.

This library allows you to import .FON files, which are really old font files often used with Windows 3.1. There are a number of reasons I chose this format, after eschewing TrueType and other more modern options. The reasons are because I'm stuck with raster fonts unless I want to dramatically increase the code size - I don't. The problem with TTF fonts and all that is even if they're raster, the file formats are more complicated, and even the raster fonts have a lot of features like kerning and advanced box placement that IoT decides don't really need or have the cycles for. The FON files were originally devised for 16-bit systems which makes them perfect for our uses here. They can be hard to find, but I've included for testing some of the files from here and one from here.

Credit Where It's Due: Simon Tatham wrote the two Python scripts I've included with the source for converting .FON files to and from text files. I derived some of my font loading code from the Python code he wrote, although it's not very recognizable at this point, because the underlying mechanisms they're built on work completely differently. For example, the script works on an array, mine works on a stream, and the way I store the font data is different as well, but his code gave me the bones for parsing these files - information that was difficult to find anywhere else, and so he deserves credit for that. His website is here.

Disclaimers: Fonts are limited to 64 pixels wide on 32-bit and 64-bit capable systems, or 32 pixels wide on 8-bit and 16-bit systems. Fonts are embedded as resources in Windows executables that are simply a stub with resources. They are basically 16-bit .exe files with the extension renamed. They may occasionally come embedded in a 32-bit executable format instead of the older 16-bit format. I have not implemented the 32-bit format yet, simply because I haven't found a font file that uses it, so I haven't been able to test it. Therefore, this code will not load those fonts. This code should support variable width raster fonts, and I took pains to ensure that, but there's some drawing code for it I desperately need to test, and I don't have a variable width font to try it with yet. I also need to test this on big-endian systems. Because Microsoft decided to distribute fonts embedded in executables this way it's possible they may flag a virus scanner. There are no viruses in any of the fonts I've distributed. There is no meaningful executable code in those font files, just stubs. You'd have to rename them to try to run them anyway, and you'd have to do it on Windows 3.1 because they won't run on your system. They're just fonts, I swear.

Retrieving Fonts

Before you do anything else really, you need to get some font data. There are two ways to do that depending on what you need, and depending on the constraints of your target system. The first way is to load a .FON file from a seekable, readable stream (like io::file_stream). The second way is to embed the font data directly into your source code. The former way is more flexible in some ways. You can load different fonts at different times from a multitude of different sources, but it requires heap memory in order to use it this way. The latter way requires no heap memory on systems that support "PROGMEM" which keeps static data embedded in flash instead of loading it into RAM. Access to the data is slightly slower, but the RAM savings can be critical, especially for the lowest end devices. Using the latter method, the font data is compiled directly into your binary. To generate font data, you can use the included fontgen.cpp utility and pass it the name of a font file. You may need to slightly modify the output - namely changing the #include <gfx_font.hpp> line to reflect your environment, and possibly adding #include <Arduino.h> if using PROGMEM with the Arduino framework. The font it creates is declared static const and its name is derived from the name of the file.

Font Information

The font class provides quite a lot of information about the font it represents.

Most of the data here you will not need, but some of it is pretty critical.

  • height() - indicates the height of the font, in pixels
  • resolution() - indicates the horizontal and vertical resolution in dots per inch
  • external_leading() - indicates the amount leading inside the bounds dictated by height(), in pixels. Accent marks can appear in this area. May be zero.
  • external_leading() - indicates the count of pixels between rows of the font. Currently, this isn't used. May be zero.
  • ascent() - indicates the baseline of the font
  • point_size() - indicates the size of the font, in points
  • style() - indicates 3 bit fields specifying italic, underline, and/or strikeout
  • weight() - indicates the weight of the font. It defaults to 400 but may be higher for bold fonts.
  • first_char() - indicates the first character represented by the font.
  • last_char() - indicates the final character represented by the font.
  • default_char() - indicates the character to map if no other character can be mapped to the input.
  • break_char() - indicates the character that is used for word breaking. Not currently used, and probably never will be, even if word breaking is implemented.
  • charset() - indicates the character set code
  • average_width() - indicates the average width of the characters in this font. This is used to compute tab spacing.
  • width() - indicates the width of a given character.
  • operator[]() - retrieves the width() and data() for a given character.
  • read() - reads a font from a stream, given an optional font index within the font set in the file, the first character hint, and last character hint. You can also use the constructor for this, but this method returns an error result you can check. The constructor does not.
  • measure_text() - measures the effective size of a bounding box that would surround the given text, when the given text is laid out within the specified dimensions. Basically, you pass some text, and some dimensions. The dimensions represent the entire available space that your text can use. The text will be laid out in that space, and wrapped as necessary to fit, and the dimensions of a bounding box that surrounds the text will be returned. The returned dimensions will be equal to or smaller than the passed in dimensions. This can be useful for doing things like centering text.

Demonstration

Let's get to some actual code. The following has had its embedded font data omitted because it's long:

C++
#define HTCW_LITTLE_ENDIAN
#include <stdio.h>
#include <string.h>
#include "../src/gfx_bitmap.hpp"
#include "../src/gfx_drawing.hpp"
#include "../src/gfx_color.hpp"

using namespace gfx;

#ifndef PROGMEM
        #define PROGMEM
#endif

// embedded terminal font data
// generated with the fontgen tool.
static const uint8_t terminal_fon_char_data[] PROGMEM = {
        0x06, 0x00, 0x00, 0x00, ...
        };

static const ::gfx::font terminal_fon(
        13,
        6,
        12,
        11,
        ::gfx::point16(100, 100),
        '\0',
        '\xFF',
        '\0',
        ' ',
        { 0, 0, 0 },
        400,
        255,
        0,
        0,
        terminal_fon_char_data);

// prints a bitmap as 4-bit grayscale ASCII
template <typename BitmapType>
void dump_bitmap(const BitmapType& bmp) {
    static const char *col_table = " .,-~;*+!=1%O@$#";
    using gsc4 = pixel<channel_traits<channel_name::L,4>>;
    for(int y = 0;y<bmp.dimensions().height;++y) {
        for(int x = 0;x<bmp.dimensions().width;++x) {
            const typename BitmapType::pixel_type px = bmp[point16(x,y)];
            const auto px2 = px.template convert<gsc4>();
            size_t i =px2.template channel<0>();
            printf("%c",col_table[i]);
        }
        printf("\r\n");
    }
}

int main(int argc, char** argv) {

    // use whatever bit depth you like.
    // the only difference really is that
    // it might fudge the colors a bit
    // if you go down really low
    // and that can change the ascii
    // output, although with what
    // we draw below it can be as
    // low as 3 with what we draw
    // below without impacting
    // anything. Byte aligned things
    // are quicker, so multiples of 8
    // are good.
    static const size_t bit_depth = 24;

    // our type definitions
    // this makes things easier
    using bmp_type = bitmap<rgb_pixel<bit_depth>>;
    using color = color<typename bmp_type::pixel_type>;

    static const size16 bmp_size(32,32);

    // declare the bitmap
    uint8_t bmp_buf[bmp_type::sizeof_buffer(bmp_size)];
    bmp_type bmp(bmp_size,bmp_buf);

    // draw stuff
    bmp.clear(bmp.bounds()); // comment this out and check out the uninitialized RAM.
                             // It looks neat.

    // bounding info for the face
    srect16 bounds(0,0,bmp_size.width-1,(bmp_size.height-1)/(4/3.0));
    rect16 ubounds(0,0,bounds.x2,bounds.y2);

    // draw the face
    draw::filled_ellipse(bmp,bounds,color::yellow);

    // draw the left eye
    srect16 eye_bounds_left(spoint16(bounds.width()/5,bounds.height()/5),
                            ssize16(bounds.width()/5,bounds.height()/3));
    draw::filled_ellipse(bmp,eye_bounds_left,color::black);

    // draw the right eye
    srect16 eye_bounds_right(
        spoint16(
            bmp_size.width-eye_bounds_left.x1-eye_bounds_left.width(),
            eye_bounds_left.y1
        ),eye_bounds_left.dimensions());
    draw::filled_ellipse(bmp,eye_bounds_right,color::black);

    // draw the mouth
    srect16 mouth_bounds=bounds.inflate(-bounds.width()/7,-bounds.height()/8).normalize();
    // we need to clip part of the circle we'll be drawing
    srect16 mouth_clip(mouth_bounds.x1,mouth_bounds.y1+
            mouth_bounds.height()/(float)1.6,mouth_bounds.x2,mouth_bounds.y2);
    draw::ellipse(bmp,mouth_bounds,color::black,&mouth_clip);

    // now blt the bitmaps
    const size_t count  =3; // 3 faces
    // our second bitmap. Not strictly necessary because one
    // can draw to the same bitmap they copy from
    // but doing so can cause garbage if the regions
    // overlap. This way is safer but takes more RAM
    using bmp2_type = bitmap<typename bmp_type::pixel_type>;
    static const size16 bmp2_size(128,64);
    uint8_t buf2[bmp2_type::sizeof_buffer(bmp2_size)];
    bmp2_type bmp2(bmp2_size,buf2);

    // if we don't do the following, we'll get uninitialized garbage.
    // it looks neat though:
    bmp2.clear(bmp2.bounds());
    srect16 r = bounds;
    // how much to shrink each iteration:
    spoint16 shrink(-bounds.width()/8,-bounds.height()/8);
    for(size_t i = 0;i<count;++i) {
        // draw the bitmap
        draw::bitmap(bmp2,r,bmp,ubounds,bitmap_flags::resize);
        // move the rect, flip and shrink it
        r=r.offset(r.width(),0);
        r=r.inflate(shrink.x,shrink.y);
        // rect orientation dictates bitmap orientation
        r=r.flip_vertical();
    }
    // we can load fonts from a file, but that requires heap
    // while PROGMEM arrays do not (at least on devices
    // that use flash memory)
    // io::file_stream dynfs("./fonts/Bm437_ToshibaSat_8x8.FON");
    // font dynf(&dynfs);
    // store the rightmost extent
    // so we have something to
    // center by
    int er = r.right();
    // now let's draw some text
    // choose the font you want to use below:
    const font& f =
        terminal_fon // use embedded font
        // dynf // use dynamic font
    ;

    const char* str = "Have a nice day!";
    // create the bounding rectangle for our
    // proposed text
    r=srect16(0,bounds.height(),er+1,bmp2_size.height);
    // now "shrink" the bounding rectangle to our
    // actual text size:
    r=srect16(r.top_left(),f.measure_text(r.dimensions(),str));
    // center the bounding rect:
    int16_t s = (er+1)-r.width();
    if(0>s)
        s=0;
    r=r.offset(s/2,0);

    // now draw
    draw::text(bmp2,r,str,f,color::white);

    // display our mess
    dump_bitmap(bmp2);
    return 0;
}

The first thing we do is define the endianness. It defaults to little endian but I like to make it explicit because when cross compiling it's a good reminder to set it. Then we include some stuff, notably gfx_bitmap.hpp and gfx_drawing.hpp. You might think the drawing code would depend on the bitmap code bit, it doesn't. draw has no special knowledge of bitmaps, and it will use anything that exposes similar members. Next, we add a using keyword for the gfx namespace to save typing.

Now we have a PROGMEM sentinel, basically to shut the compiler up if this environment doesn't support the PROGMEM attribute.

Next there's a really big array which I've omitted most of from the text above. That's our font data and it was generated with using ./fontgen ./fonts/terminal.fon 0 from the command line. Fontgen is used by compiling fontgen.cpp and then running it. After that is a font declaration which was generated by the same tool. This is the font we'll be using by default, although you can load others, or run fontgen to create embedded C++ code for them.

After that, there's dump_bitmap<>() which renders a bitmap to 4-bit grayscale which is then mapped to ASCII and printed to the console. This is so we don't have to write actual device drivers or use an IoT device with it's long build cycles. I promise you we'll be rendering to actual graphics displays in the next installment.

In our main() routine, first up, we have some consts and some type aliases to set up our bitmap and pixel "shapes".

Following that, we initialize the bitmap with a byte buffer we've calculated the size of.

Clearing the bitmap is next, although if you don't do it, the output looks kind of neat, and Matrix-like because of the uninitialized memory being mapped to pixel data.

Now we do a series of rectangle manipulations and ellipse drawing to draw a happy face. The only kind of strange bit is how the mouth is drawn. Basically, we use the clipping rectangle over an ellipse so that we only draw roughly the bottom half of it.

Once we've drawn the face onto the initial bitmap, we get up to some monkey business. We declare a 2nd, larger bitmap, wherein we take the bitmap we just made for the face and blt it, then shrink it, flip it vertically, and scoot it to the right, and blt it again, which we repeat one more time.

After that, we measure our text and then center it with some rectangle manipulation, before drawing it to the destination.

Your result should look like this:

           @@@@@@@@@@
        @@@@@@@@@@@@@@@@
      @@@@@@@@@@@@@@@@@@@@
     @@@@@@@@@@@@@@@@@@@@@@               @@@@@@@@@@@@
   @@@@    @@@@@@@@@@    @@@@           @@@@@@@@@@@@@@@
  @@@@      @@@@@@@@      @@@@          @@@@@      @@@@@
  @@@@      @@@@@@@@      @@@@        @@@@@  @@@@@@  @@@@@          @@@@@@@@
 @@@@@      @@@@@@@@      @@@@@      @@@  @@@@@@@@@@@@ @@@         @@@@@@@@@@@
 @@@@@      @@@@@@@@      @@@@@      @@@@@@@@@@@@@@@@@@ @@@      @@   @@@@   @@
@@@@@@      @@@@@@@@      @@@@@@     @@@@@@@@@@@@@@@@@@ @@@      @@   @@@@   @@@
@@@@@@      @@@@@@@@      @@@@@@    @@@@@@@@@@@@@@@@@@@@@@@     @@@   @@@@   @@@
@@@@@@@    @@@@@@@@@@    @@@@@@@    @@@@@@@@@@@@@@@@@@@@@@@     @@@@  @@@@@  @@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@@   @@@@@@@@   @@@@     @@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    @@@@     @@@@@@    @@@@      @@@@@@@@@@@@ @@
@@@@ @@@@@@@@@@@@@@@@@@@@@@ @@@@     @@@     @@@@@@    @@@@      @@ @@@@@@@@ @@
 @@@@ @@@@@@@@@@@@@@@@@@@@ @@@@      @@@     @@@@@@    @@@@       @@@ @@@@  @@@
 @@@@ @@@@@@@@@@@@@@@@@@@@ @@@@      @@@     @@@@@@    @@@         @@@@@@@@@@
  @@@@  @@@@@@@@@@@@@@@@  @@@@        @@@   @@@@@@@@   @@@          @@@@@@@@
  @@@@@@ @@@@@@@@@@@@@@ @@@@@@          @@@@@@@@@@@@@@@@
   @@@@@@   @@@@@@@@   @@@@@@             @@@@@@@@@@@@
     @@@@@@@        @@@@@@@                 @@@@@@@@
      @@@@@@@@@@@@@@@@@@@@
        @@@@@@@@@@@@@@@@
           @@@@@@@@@@


#   #                                                                       #               #
#   #                                             #                         #               #
#   #                                                                       #               #
#   #  ###  #   #  ###         ###        # ##   ##    ###   ###         ####  ###  #   #   #
#####     # #   # #   #           #       ##  #   #   #   # #   #       #   #     # #   #   #
#   #  #### #   # #####        ####       #   #   #   #     #####       #   #  #### #   #   #
#   # #   #  # #  #           #   #       #   #   #   #     #           #   # #   # #  ##   #
#   # #  ##  # #  #   #       #  ##       #   #   #   #   # #   #       #   # #  ##  ## #
#   #  ## #   #    ###         ## #       #   #  ###   ###   ###         ####  ## #     #   #
                                                                                    #   #
                                                                                     ###      

And I sincerely hope you do.

What's Next

In the next installment, we finally get to some actual display drivers, which enable us to draw on TFT, LED and OLED screens instead of just bitmaps and ASCII.

History

  • 25th April, 2021 - Initial submission
  • 25th April, 2021 - Made work with C++14

License

This article, along with any associated source code and files, is licensed under The MIT License