Draw points, lines, shapes, and text to bitmaps using the GFX drawing facilities.
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 blt
s 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:
#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
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);
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) {
static const size_t bit_depth = 24;
using bmp_type = bitmap<rgb_pixel<bit_depth>>;
using color = color<typename bmp_type::pixel_type>;
static const size16 bmp_size(32,32);
uint8_t bmp_buf[bmp_type::sizeof_buffer(bmp_size)];
bmp_type bmp(bmp_size,bmp_buf);
bmp.clear(bmp.bounds());
srect16 bounds(0,0,bmp_size.width-1,(bmp_size.height-1)/(4/3.0));
rect16 ubounds(0,0,bounds.x2,bounds.y2);
draw::filled_ellipse(bmp,bounds,color::yellow);
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);
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);
srect16 mouth_bounds=bounds.inflate(-bounds.width()/7,-bounds.height()/8).normalize();
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);
const size_t count =3; 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);
bmp2.clear(bmp2.bounds());
srect16 r = bounds;
spoint16 shrink(-bounds.width()/8,-bounds.height()/8);
for(size_t i = 0;i<count;++i) {
draw::bitmap(bmp2,r,bmp,ubounds,bitmap_flags::resize);
r=r.offset(r.width(),0);
r=r.inflate(shrink.x,shrink.y);
r=r.flip_vertical();
}
int er = r.right();
const font& f =
terminal_fon ;
const char* str = "Have a nice day!";
r=srect16(0,bounds.height(),er+1,bmp2_size.height);
r=srect16(r.top_left(),f.measure_text(r.dimensions(),str));
int16_t s = (er+1)-r.width();
if(0>s)
s=0;
r=r.offset(s/2,0);
draw::text(bmp2,r,str,f,color::white);
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