Skip to main content

Loading Images at Runtime

In this section we will discuss a method for loading images from a file in your TouchGFX application.

The standard procedure for using bitmaps in TouchGFX application is to include the bitmaps in the application. The PNG files are converted to binary data in .cpp files and compiled, and linked into the application. This is an easy way of making the bitmaps available for the application at runtime.

In some application this is not possible or practical. For example when the images are not available at compile-time (maybe they are downloaded through an internet connection later), or when you simply have too many bitmaps to fit in your application flash space.

The Dynamic bitmaps (see link below) is a RAM-based solution to this problem. The Dynamic bitmaps are created at runtime, and the application can freely decide the number of bitmaps, the format, and the width and height.

As the dynamic bitmaps are stored in RAM (inside the bitmap cache), the application must copy pixels values from a source into the dynamic bitmap after creating the dynamic bitmaps. After creation, the dynamic bitmap contains uninitialized pixels.

This section describes how dynamic bitmaps can be used to create applications where some of the graphic content is read from BMP files. A simple use-case is an application that shows BMP files from an SD-card.

Note
Read first about Dynamic Bitmaps.

Recall that standard bitmaps are compiled into the application and therefore must be available at compile time. The Dynamic Bitmap feature allows you to read images from files at runtime, or even download images through an internet connection.

Loading BMP file Example

Here we will see how to use a BMP loader to load pixels from a Windows BMP file. The code for the loader is later in the article.

Insert first an Image widget in the view. This widget will show the BMP:

class TemplateView : public View
{
private:
Image image;
}

Load the image date in setupScreen:

void TemplateView::setupScreen()
{
FILE* f = fopen("image.jpg", "rb");
uint16_t width, height;

//Get the image dimensions from the BMP file
BMPFileLoader::getBMP24Dimensions(f, width, height);
BitmapId bmpId;

//Create (16bit) dynamic bitmap of same dimension
bmpId = Bitmap::dynamicBitmapCreate(width, height, Bitmap::RGB565);

//Load pixels from BMP file to dynamic bitmap
BMPFileLoader::readBMP24File(Bitmap(bmpId), f);

//Make Image show the loaded bitmap
image.setBitmap(Bitmap(bmpId));

//Position image and add to View
image.setXY(20, 20);
add(image);
...
}

The BMP loader

Here is the code for a simple BMP file loader. It only supports 24bpp BMP files. You may have to adjust the file system calls to match your system.

BMPFileLoader.hpp
#include <touchgfx/hal/Types.hpp>
#include <touchgfx/Bitmap.hpp>

using namespace touchgfx;

class BMPFileLoader
{
public:
typedef void* FileHdl;

static void getBMP24Dimensions(FileHdl fileHandle, uint16_t& width, uint16_t& height);
static void readBMP24File(Bitmap bitmap, FileHdl fileHandle);
private:
static int readFile(FileHdl hdl, uint8_t* const buffer, uint32_t length);
static void seekFile(FileHdl hdl, uint32_t offset);
};
BMPFileLoader.cpp
#include <gui/common/BMPFileLoader.hpp>
#include <platform/driver/lcd/LCD16bpp.hpp>
#include <touchgfx/Color.hpp>

int BMPFileLoader::readFile(FileHdl hdl, uint8_t* const buffer, uint32_t length)
{
uint32_t r = fread(buffer, 1, length, (FILE*)hdl);
if (r != length)
{
return 1;
}
return 0;
}

void BMPFileLoader::seekFile(FileHdl hdl, uint32_t offset)
{
fseek((FILE*)hdl, offset, SEEK_SET);
}

void BMPFileLoader::getBMP24Dimensions(FileHdl fileHandle, uint16_t& width, uint16_t& height)
{
uint8_t data[50];
seekFile(fileHandle, 0);
readFile(fileHandle, data, 26); //read first part of header.

width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24);
height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24);
}

void BMPFileLoader::readBMP24File(Bitmap bitmap, FileHdl fileHandle)
{
uint8_t data[50];
seekFile(fileHandle, 0);
readFile(fileHandle, data, 26); //read first part of header.

const uint32_t offset = data[10] | (data[11] << 8) | (data[12] << 16) | (data[12] << 24);
const uint32_t width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24);
const uint32_t height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24);

readFile(fileHandle, data, offset - 26); //read rest of header.

//get dynamic bitmap boundaries
const uint32_t buffer_width = bitmap.getWidth();
const uint32_t buffer_height = bitmap.getHeight();

const uint32_t rowpadding = (4 - ((width * 3) % 4)) % 4;

const Bitmap::BitmapFormat format = bitmap.getFormat();
uint8_t* const buffer8 = Bitmap::dynamicBitmapGetAddress(bitmap.getId());
uint16_t* const buffer16 = (uint16_t*)buffer8;

for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
if (x % 10 == 0) //read data every 10 pixels = 30 bytes
{
if (x + 10 <= width) //read 10
{
readFile(fileHandle, data, 10 * 3); //10 pixels
}
else
{
readFile(fileHandle, data, (width - x) * 3 + rowpadding); //rest of line
}
}
//insert pixel, if within dynamic bitmap boundaries
if (x < buffer_width && ((height - y - 1) < buffer_height))
{
switch (format)
{
case Bitmap::RGB565:
buffer16[x + (height - y - 1) * buffer_width] =
LCD16bpp::getNativeColorFromRGB(data[(x % 10) * 3 + 2], data[(x % 10) * 3 + 1], data[(x % 10) * 3]);
break;
case Bitmap::RGB888:
{
//24 bit framebuffer
const uint32_t inx = 3 * (x + (height - y - 1) * buffer_width);
buffer8[inx + 0] = data[(x % 10) * 3 + 0];
buffer8[inx + 1] = data[(x % 10) * 3 + 1];
buffer8[inx + 2] = data[(x % 10) * 3 + 2];
break;
}
case Bitmap::ARGB8888:
{
//24 bit framebuffer
const uint32_t inx = 4 * (x + (height - y - 1) * buffer_width);
buffer8[inx + 0] = data[(x % 10) * 3 + 0];
buffer8[inx + 1] = data[(x % 10) * 3 + 1];
buffer8[inx + 2] = data[(x % 10) * 3 + 2];
buffer8[inx + 3] = 255; //solid
break;
}
default:
assert(!"Unsupported bitmap format in BMPFileLoader!");
}
}
}
}
}

This code is for illustrative purposes. A more optimal reader for RGB888 can read directly from the file to the dynamic bitmap memory (remember to skip the row padding). The reader above reads 10 pixels from the BMP file to a temporary buffer. The pixels are then copied to the bitmap while converting to the correct format.

Configure memory for dynamic bitmaps

Before you can create and use dynamic bitmaps you must configure TouchGFX. It is a prerequisite to provide a buffer and the maximum number of dynamic bitmaps (also for the simulator).

Here is an example for STM32F7xx where we allocate a buffer in external RAM: We wish to load and show a 24-bit bitmap of size 320x240. The memory requirement is thus 320x240x3 = 230400. We also need a little space for bookkeeping, so we allocate 232000 bytes for the buffer.

FrontendApplication.cpp (extract)
#include <gui/common/FrontendApplication.hpp>
#include <touchgfx/Bitmap.hpp>

FrontendApplication::FrontendApplication(Model& m, FrontendHeap& heap)
: FrontendApplicationBase(m, heap)
{
static uint32_t bmpCache = (uint32_t)(0xC00C0000); // SDRAM
Bitmap::setCache((uint16_t*)bmpCache, 232000, 1);
}

The final argument is the maximum number of dynamic bitmaps, so adjust this according to your needs. Be aware that on many platforms, the framebuffers are also stored in external RAM. Make sure that you don't put the bitmap cache in overlapping memory regions.

Note
Note that in case of insufficient memory, the BitmapId returned by dynamicBitmapCreate will be BITMAP_INVALID.

Loading JPEG files

A JPEG File Loader example can be found here which shows how to use LibJPEG to use JPEG-files. It uses a JPEGLoader class similar to the above BMPFileLoader.