GFX 第二部分:设备无关位图





5.00/5 (2投票s)
GFX IoT 图形库第二部分 - 设备无关位图。
引言
更新: 修复了许多错误。您确实应该更新代码。
在本系列上一篇文章中,我介绍了设备无关像素,它在编译时使用了一些高级 C++ 特性,以提供一种高效且极其灵活的像素,其通道和分辨率完全由您(亲爱的读者)定义。您可以在此处跳转到下一篇文章。
就其本身而言,这些并不是非常有用,特别是因为位图不一定只是像素数组。
我们需要一种在内存中表示矩形像素区域的方法。这听起来并不简单。在某些情况下,您可能有 1 位像素、18 位像素或 7 位像素!由于这些像素不是字节对齐的,您不能简单地将它们存储在数组中,而是需要将它们存储为连续的位流,不带填充。
此代码正是提供了这一点。除了用于读写单个像素的访问器之外,位图还提供了一种将区域 blt 到另一个位图以及填充或清除位图区域的方法。
注意:这些是内存中的位图,而不是*图像*。图像是 JPEG 或 PNG 等。它们可能经过压缩并嵌入了额外的数据。它们也不一定在内存中。目前还没有图像模板类,但将在未来的版本中提供。
构建这个大杂烩
此代码已使用 clang 11 和 gcc 10 进行测试,使用 -std=c++17
最新版本的 MSVC 不会编译此代码。坦率地说,我不在乎,因为我从未遇到过与 MSVC 交叉编译的代码,而且此代码的主要目的是用于无法通过 MSVC 轻松定位的 IoT 设备。此代码中没有任何 Linux 特定的内容,我认为它只是符合标准的 C++,但我也可能在使用一些 gcc 和 clang 特定的功能而没有意识到。
使用这个烂摊子
在编写图形库时我意识到的一件事是依赖关系往往会滚雪球。我的意思是,如果您将不同的功能区域分解到不同的头文件中,您很快就会陷入一种情况,即所有高级别的东西都依赖于低级别的*所有*东西。这很不幸,并提出了一个问题,如果它们都是相互依赖的,为什么一开始还要分离头文件。主要原因只是为了让代码稍微更容易导航。
在这种情况下,bitmap<>
不仅需要 pixel<>
,还需要表示各种操作的位置和大小信息的数据结构。我们首先介绍位置和大小。
定位
本质上,我们需要表示 2D 点、表示 2D 大小以及表示矩形的方法。核心图形代码主要依赖这些元素的 16 位无符号表示,但您可以在实例化它们时指定任何所需的数字类型(带符号整数、无符号整数或浮点数)。通常,我们将使用 point16
、size16
和 rect16
,它们使用 16 位无符号整数,原因很简单,因为图形代码通常就是这样期望的。在某些情况下,图形方法可能会采用这些元素之一的带符号变体,即 spoint16
、ssize16
或 srect16
。
Point
point<>
模板类有两个数据成员,x
和 y
,它们简单地指示 2D 笛卡尔平面上点的位置。该模板接受一个数字类型。
大小
size<>
模板类也有两个数据成员,在本例中是 width
和 height
,它们指示 2D 区域的大小。这与 point<>
非常相似,但拥有一个具有更适当命名字段的独立数据结构可以更清楚地表达意图。
矩形
rect<>
模板类有四个数据成员和几个方法。我们需要花一些时间来研究它。
首先,有 x1
、y1
、x2
和 y2
字段。每个 rect<>
都由这两个点定义,指示一条通常为对角线的线,其边界矩形就是该矩形。请注意,不保证第一个点位于最后一个点的左上方。
除了这些访问器字段,还有几个方法用于对 rect<>
执行各种操作
left()
- 指示矩形最左侧的位置top()
- 指示矩形最顶部的位置right()
- 指示矩形最右侧的位置bottom()
- 指示矩形最底部的位置location()
- 指示矩形左上角的位置dimensions()
- 指示矩形的尺寸intersects()
- 指示点或矩形是否与此矩形相交inflate()
- 返回一个矩形,其边界按指定的x
和y
值增加或减少。矩形以中心为锚点,有效width
和height
增加或减少x
或y
值的两倍。normalize()
- 返回矩形的副本,其第一个点是左上角,最后一个点是右下角。crop()
- 返回矩形的副本,由指定的边界矩形裁剪。split()
- 通过另一个矩形分割一个矩形,返回最多out_count
或 4(取较小者)个矩形,这些矩形表示排除了传入矩形的新矩形。首先,返回top()
上方的任何空间,如果存在,则从left()
到right()
。接下来,返回传入矩形left()
正左侧的任何空间,如果存在。之后,返回right()
正右侧的任何空间,如果存在。最后,返回bottom()
下方剩余的任何空间,如果存在。
位图
位图控制所提供的内存缓冲区,并提供该缓冲区的视图,允许您 blt、填充、清除以及读写单个像素。您负责管理自己的内存。位图接受缓冲区并简单地使用它。它不处理分配或删除。这是为了实现最大的灵活性,特别是考虑到许多设备需要使用特殊分配的内存进行位图的 DMA 传输。同样,您管理内存,因此它可以按需工作。
要实例化 bitmap<>
模板类,您必须将其 pixel<>
模板类的实例化作为其唯一模板参数传递。有关如何使用像素的信息,请参阅上一篇文章。
一旦您确定了位图*类型*,您就可以开始创建位图。无论何时这样做,您都需要一个足够大的缓冲区来容纳它包含的所有像素。计算这个有点棘手,主要基于每个像素以位为单位的大小,但是 static
方法 sizeof_buffer()
会为您计算它,以字节为单位。您可以将返回的值作为数组维度说明符传递,当然也可以传递给您喜欢的内存分配例程。该例程接受一个 width
和一个 height
,它们共同指示位图的大小。
接下来,您应该根据需要创建缓冲区,并将其指针以及您传递给上一个例程的大小一起传递。
就是这样。现在您有了一个位图。您可以使用 operator[]
来获取或设置单个像素。它接受一个 point16
并返回一个访问器,该访问器可用于获取或设置指定位置的像素值。因为它在某些情况下返回访问器,您可能需要显式转换为位图类型的 ::pixel_type
以获取像素。
除了上述功能,位图还提供了一些基本操作
dimensions()
- 返回位图的大小bounds()
- 便捷方法,返回以 (0,0) 为原点且尺寸为dimensions()
的边界矩形。size_pixels()
- 返回位图中的像素数量size_bytes()
- 返回位图的大小,以字节为单位begin()
- 返回指向缓冲区开头的字节指针end()
- 返回指向缓冲区末尾之后的字节指针clear()
- 清除由指定边界矩形表示的内存。这通常将像素设置为黑色。fill()
- 使用指定的像素填充由指定边界矩形表示的内存。请注意,如果您想保证像素设置为黑色,这是一个比clear()
更健壮的替代方案。对于某些颜色模型,归零的内存可能不是黑色,在这种情况下,此方法是正确的,但速度也较慢。blt()
- 将位图中指定的矩形区域复制到目标位图的指定点。如果需要,源矩形和结果会被裁剪以适应其各自的缓冲区。
所有这些都很好,但是有没有例子呢?我们可以做到,但事情复杂化的是我们目前没有任何显示驱动。我们将自己制作一个原始版本,它只使用 ASCII 字符表( .%#)提供 4 种颜色的“灰度”,并以这种方式渲染位图。
让我们从代码开始,然后我将努力解释它
#define HTCW_LITTLE_ENDIAN
#include <stdio.h>
// include the relevant GFX header
#include "include/gfx_bitmap.hpp"
// ... and namespace
using namespace gfx;
// displays an ascii bitmap
template <typename BitmapType>
void dump_bitmap(const BitmapType& bmp) {
static const char *col_table = " .%#";
using gsc2 = pixel<channel_traits<channel_name::L,2>>;
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)];
char sz[2];
sz[1]=0;
const auto px2 = px.template convert<gsc2>();
size_t i =px2.template channel<0>();
sz[0] = col_table[i];
printf("%s",sz);
}
printf("\r\n");
}
}
int main() {
// predefine many different pixel types you can try
// Note that this generates compile warnings for
// unused instantiations
using bgr888 = pixel<
channel_traits<channel_name::B,8>,
channel_traits<channel_name::G,8>,
channel_traits<channel_name::R,8>
>;
using rgb565 = pixel<
channel_traits< channel_name::R,5>,
channel_traits<channel_name::G,6>,
channel_traits<channel_name::B,5>
>;
using gsc8 = pixel<
channel_traits<channel_name::L,8>
>;
using mono1 = pixel<
channel_traits<channel_name::L,1>
>;
using gsc2 = pixel<
channel_traits<channel_name::L,2>
>;
using gsc4 = pixel<
channel_traits<channel_name::L,4>
>;
using rgb888 = pixel<
channel_traits<channel_name::R,8>,
channel_traits<channel_name::G,8>,
channel_traits<channel_name::B,8>
>;
using rgb666 = pixel<
channel_traits<channel_name::R,6>,
channel_traits<channel_name::G,6>,
channel_traits<channel_name::B,6>
>;
using rgb232 = pixel<
channel_traits<channel_name::R,2>,
channel_traits<channel_name::G,3>,
channel_traits<channel_name::B,2>
>;
using rgb333 = pixel<
channel_traits<channel_name::R,3>,
channel_traits<channel_name::G,3>,
channel_traits<channel_name::B,3>
>;
using rgb101210 = pixel<
channel_traits<channel_name::R,10>,
channel_traits<channel_name::G,12>,
channel_traits<channel_name::B,10>
>;
using rgb212221 = pixel<
channel_traits<channel_name::R,21>,
channel_traits<channel_name::G,22>,
channel_traits<channel_name::B,21>
>;
using yuv888 = pixel<
channel_traits<channel_name::Y,8>,
channel_traits<channel_name::U,8>,
channel_traits<channel_name::V,8>
>;
// set your pixel type to any one of the above
// or make your own pixel format
using bmp_type = bitmap<rgb888>;
// we declare this to make it easier to change
const size16 bmp_size(15,15);
// declare our buffer
uint8_t buf[bmp_type::sizeof_buffer(bmp_size)];
// declare our bitmap, using the bmp_size and the buffer
bmp_type bmp(bmp_size,buf);
// for each pixel in the bitmap, working from top to bottom,
// left to right, if the pixel falls on an edge, make it white
// otherwise, alternate the colors between dark_blue and purple
bool col = false;
for(int y=0;y<bmp.dimensions().height;++y) {
for(int x=0;x<bmp.dimensions().width;++x) {
if(x==0||y==0||x==bmp.bounds().right()||y==bmp.bounds().bottom()) {
bmp[point16(x,y)]=color<typename bmp_type::pixel_type>::white;
} else {
bmp[point16(x,y)]=col?
color<typename bmp_type::pixel_type>::dark_blue:
color<typename bmp_type::pixel_type>::purple;
}
col = !col;
}
}
// display our initial bitmap
dump_bitmap(bmp);
printf("\r\n");
// create rect for our inner square
rect16 r = bmp.bounds().inflate(-3,-3);
// clear it
bmp.clear(r);
// now fill a rect inside that
bmp.fill(r.inflate(-1,-1),color<bmp_type::pixel_type>::medium_aquamarine);
// display the bitmap
dump_bitmap(bmp);
printf("\r\n");
// create a second bitmap 4 times the size
const size16 bmp2_size(bmp_size.width*2,bmp_size.height*2);
uint8_t buf2[bmp_type::sizeof_buffer(bmp2_size)];
bmp_type bmp2(bmp2_size,buf2);
// clear it
bmp2.clear(bmp2.bounds());
// now blt portions of the first bitmap to it to create a tile
// effect
bmp.blt(rect16(point16(8,8),size16(7,7)),bmp2,point16(0,0));
bmp.blt(rect16(point16(0,8),size16(7,7)),bmp2,point16(22,0));
bmp.blt(bmp.bounds(),bmp2,point16(7,7));
bmp.blt(rect16(point16(8,0),size16(7,7)),bmp2,point16(0,22));
bmp.blt(rect16(point16(0,0),size16(7,7)),bmp2,point16(22,22));
// display the bitmap
dump_bitmap(bmp2);
return 0;
}
我们做的第一件事是定义我们的字节序。由于此代码主要用于交叉编译到 IoT 平台,所以您几乎总是需要它,但几乎所有机器都是小端。请注意,我尚未在大端架构上测试过此代码。我非常确定我刚拿到的小型 ARM Cortex-M 可以处理大端,但工具链一团糟,我必须在测试之前彻底研究它。
接下来,我们包含 gfx_bitmap.hpp 头文件并使用 gfx
命名空间。
您会看到一个辅助例程,它只是从上到下、从左到右遍历位图,将每个像素转换为 2 位(4 色)灰度,然后将其用作 ASCII“颜色”表的索引,并逐个字符地打印到控制台。
在 main()
中,有一系列像素定义。这些只是为了给您提供一些尝试的东西。请注意,颜色模型之间的转换有时会(但不总是)有损,因此例如从 RGB 到 Y'UV 的转换以及反向转换时,您的颜色可能会略微改变,从而显着改变您的输出,因为颜色使用 ASCII 进行非常粗略的近似。
之后,我们声明一个位图类型,其中包含我们想要的任何像素类型。然后我们创建一个 size16
来保存我们的 bmp_size
和一个缓冲区,其大小我们按照前面描述的方式计算。
接下来,如注释中所述,我们逐像素创建模式位图,从上到下,从左到右。
现在我们把它输出到控制台。
接着,我们进行一些矩形操作,以便从位图中心清除一个正方形。在再次缩小矩形后,我们用纯色填充内部正方形。
然后我们把*那个*输出到控制台。
最后,我们创建了第二个位图,其面积是初始位图的四倍,然后执行一系列 blt()
调用,在刚创建的位图上创建交替的瓦片效果。
作为我们最后的行动,我们将其转储到控制台。
希望这能澄清如何使用位图。
下一步
现在我们已经介绍了一些低级图形 I/O,我们有机会接下来探索绘图类,它提供对位图选定部分的绘图服务。我们还将介绍字体。
历史
- 2021 年 4 月 16 日 - 初次提交
- 2021 年 4 月 17 日 - 更新 1,错误修复