런타임에서 이미지 로드
이 섹션에서는 TouchGFX 애플리케이션의 파일에서 이미지를 로딩하는 방법에 대해 설명할 것입니다.
TouchGFX 애플리케이션에서 비트맵을 사용하기 위한 표준 절차로 애플리케이션에 비트맵을 포함시킵니다. PNG 파일은 .cpp 파일의 바이너리 데이터로 변환되고 컴파일되어 애플리케이션에 연결됩니다. 이렇게 하면 런타임에서 애플리케이션에서 비트맵을 손쉽게 사용할 수 있습니다.
일부 애플리케이션에서는 이것이 불가능하거나 실용적이지 않습니다. 예를 들어, 컴파일 시 이미지를 사용할 수 없는 경우(아마도 나중에 인터넷 연결을 통해 다운로드)나 비트맵이 너무 많아서 애플리케이션 플래시 공간에 맞지 않는 경우가 여기에 해당됩니다.
동적 비트맵(아래 링크 참조)은 이 문제를 해결할 수 있는 RAM 기반 솔루션입니다. 동적 비트맵은 런타임에서 생성되며, 애플리케이션은 비트맵 개수, 형식, 너비 및 높이를 자유롭게 결정할 수 있습니다.
동적 비트맵은 RAM(비트맵 캐시 내부)에 저장되기 때문에 애플리케이션은 동적 비트맵을 생성한 이후에 소스에서 동적 데이터로 픽셀 값을 복사해야 합니다. 생성된 동적 비트맵에는 초기화되지 않은 픽셀이 포함됩니다.
이 섹션에서는 동적 비트맵을 사용하여 BMP 파일에서 일부 그래픽 콘텐츠를 읽어오는 애플리케이션을 생성하는 방법에 대해 알아보겠습니다. 간단한 사용 사례로 SD 카드에서 BMP 파일을 표시하는 애플리케이션을 들 수 있습니다.
Note
먼저 애플리케이션으로 컴파일되는 것은 표준 비트맵이기 때문에 컴파일할 때는 표준 비트맵을 사용해야 합니다. 하지만 동적 비트맵 기능을 이용하면 런타임 시 파일에서 이미지를 읽어올 수 있으며, 인터넷 연결을 통해 이미지를 다운로드할 수도 있습니다.
BMP 파일 로딩 예
여기서는 BMP 로더를 사용해 Windows BMP 파일에서 픽셀을 로드하는 방법을 알아보겠습니다. 로더 코드에 대한 설명은 글 후반부에 나와 있습니다.
먼저 뷰에 이미지 위젯을 삽입합니다. 그러면 이 위젯에서 BMP가 다음과 같이 표시됩니다.
class TemplateView : public View
{
private:
Image image;
}
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);
...
}
BMP 로더
다음은 단순한 BMP 파일 로더에 사용되는 코드로, 24bpp BMP 파일만 지원합니다. 따라서 사용 중인 시스템에 맞게 파일 시스템 호출을 조정해야 할 수도 있습니다.
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!");
}
}
}
}
}
위 코드는 설명을 위한 예시입니다. 따라서 RGB888에 더욱 최적화된 리더라면 파일에서 동적 비트맵 메모리로 직접 읽어올 수 있습니다(단, 이 경우 row padding을 건너뛰어야 합니다). 위의 리더는 BMP 파일에서 임시 버퍼로 10개의 픽셀을 읽어옵니다. 그러면 픽셀이 올바른 형식으로 변환되면서 비트맵으로 복사됩니다.
동적 비트맵에 적합한 메모리 구성
동적 비트맵을 생성하여 사용하려면 먼저 TouchGFX를 구성해야 합니다. 전제 조건으로, 버퍼 1개와 최대한 많은 동적 비트맵(시뮬레이터에도 사용해야 함)이 필요합니다.
다음은 STM32F7xx 예제입니다. 여기서는 외부 RAM에서 버퍼를 할당합니다. 또한 320x240 크기의 24비트 비트맵을 로드하여 표시할 것이기 때문에 필요한 메모리 요건은 320x240x3 = 230400입니다. 그 밖에도 기록을 위해 약간의 공간이 필요하므로 버퍼에 232000바이트를 할당합니다.
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);
}
마지막으로, 동적 비트맵을 최대한 많이 입력해야 하므로 요건에 따라 조정합니다. 단, 프레임버퍼는 대부분의 플랫폼에서 외부 RAM에도 저장됩니다. 따라서 비트맵 캐시를 중복되는 메모리 영역에 저장하지 않도록 주의해야 합니다.
Note
JPEG 파일 로드
LibJPEG에서 JPEG 파일을 사용하는 JPEG 파일 로더 예제는 여기에서 찾아볼 수 있습니다. 이 로더는 위의 BMPFileLoader와 유사한 JPEGLoader 클래스를 사용합니다.