通过 SOIL 保存位图时 BMP 损坏。截图区
Broken BMP when save bitmap by SOIL. Screenshot area
这是我上一个关于将屏幕截图保存到 SOIL 的问题的延续。 现在我想知道,如何制作屏幕 部分 的屏幕截图并消除奇怪的行为。我的代码:
bool saveTexture(string path, glm::vec2 startPos, glm::vec2 endPos)
{
const char *charPath = path.c_str();
GLuint widthPart = abs(endPos.x - startPos.x);
GLuint heightPart = abs(endPos.y - startPos.y);
BITMAPINFO bmi;
auto& hdr = bmi.bmiHeader;
hdr.biSize = sizeof(bmi.bmiHeader);
hdr.biWidth = widthPart;
hdr.biHeight = -1.0 * heightPart;
hdr.biPlanes = 1;
hdr.biBitCount = 24;
hdr.biCompression = BI_RGB;
hdr.biSizeImage = 0;
hdr.biXPelsPerMeter = 0;
hdr.biYPelsPerMeter = 0;
hdr.biClrUsed = 0;
hdr.biClrImportant = 0;
unsigned char* bitmapBits = (unsigned char*)malloc(3 * widthPart * heightPart);
HDC hdc = GetDC(NULL);
HDC hBmpDc = CreateCompatibleDC(hdc);
HBITMAP hBmp = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, (void**)&bitmapBits, nullptr, 0);
SelectObject(hBmpDc, hBmp);
BitBlt(hBmpDc, 0, 0, widthPart, heightPart, hdc, startPos.x, startPos.y, SRCCOPY);
//UPDATE:
- int bytes = widthPart * heightPart * 3;
- // invert R and B chanels
- for (unsigned i = 0; i< bytes - 2; i += 3)
- {
- int tmp = bitmapBits[i + 2];
- bitmapBits[i + 2] = bitmapBits[i];
- bitmapBits[i] = tmp;
- }
+ unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
+ // invert R and B chanels
+ for (unsigned row = 0; row < heightPart; ++row) {
+ for (unsigned col = 0; col < widthPart; ++col) {
+ // Calculate the pixel index into the buffer, taking the
alignment into account
+ const size_t index{ row * stride + col * hdr.biBitCount / 8 };
+ std::swap(bitmapBits[index], bitmapBits[index + 2]);
+ }
+ }
int texture = SOIL_save_image(charPath, SOIL_SAVE_TYPE_BMP, widthPart, heightPart, 3, bitmapBits);
return texture;
}
当我 运行 如果 widthPart 和 heightPart 是偶数,那效果很好。但是,如果这是奇数,我会得到这个 BMP。:
我检查了两次转换和代码,但在我看来原因是我错误的 blit 函数。转换 RGB 的功能不影响问题。可能是什么原因?这是 BitBlt 中区域块传输的正确方法吗?
更新偶数和奇数没有区别。当这个数字相等时,正确的图片就会产生。不知道哪里出了问题。((
更新2
SOIL_save_image 函数检查参数是否有错误并发送到 stbi_write_bmp:
int stbi_write_bmp(char *filename, int x, int y, int comp, void *data)
{
int pad = (-x*3) & 3;
return outfile(filename,-1,-1,x,y,comp,data,0,pad,
"11 4 22 4" "4 44 22 444444",
'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header
40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header
}
outfile 函数:
static int outfile(char const *filename, int rgb_dir, int vdir, int x, int
y, int comp, void *data, int alpha, int pad, char *fmt, ...)
{
FILE *f = fopen(filename, "wb");
if (f) {
va_list v;
va_start(v, fmt);
writefv(f, fmt, v);
va_end(v);
write_pixels(f,rgb_dir,vdir,x,y,comp,data,alpha,pad);
fclose(f);
}
return f != NULL;
}
损坏的位图图像是 Windows 位图与 SOIL 库期望的数据布局不一致的结果1。从 CreateDIBSection
返回的像素缓冲区遵循 Windows 规则(参见 Bitmap Header Types):
The scan lines are DWORD aligned [...]. They must be padded for scan line widths, in bytes, that are not evenly divisible by four [...].
换句话说:每条扫描线的宽度(以字节为单位)为(biWidth * (biBitCount / 8) + 3) & ~3
。另一方面,SOIL 库不期望像素缓冲区是 DWORD 对齐的。
要解决此问题,像素数据需要在传递给 SOIL 之前进行转换,方法是去除(潜在的)填充并交换 R 和 B 颜色通道。以下代码就地执行 2:
unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
for (unsigned row = 0; row < heightPart; ++row) {
for (unsigned col = 0; col < widthPart; ++col) {
// Calculate the source pixel index, taking the alignment into account
const size_t index_src{ row * stride + col * hdr.biBitCount / 8 };
// Calculate the destination pixel index (no alignment)
const size_t index_dst{ (row * width + col) * (hdr.biBitCount / 8) };
// Read color channels
const unsigned char b{ bitmapBits[index_src] };
const unsigned char g{ bitmapBits[index_src + 1] };
const unsigned char r{ bitmapBits[index_src + 2] };
// Write color channels switching R and B, and remove padding
bitmapBits[index_dst] = r;
bitmapBits[index_dst + 1] = g;
bitmapBits[index_dst + 2] = b;
}
}
使用此代码,index_src
是像素缓冲区的索引,其中包括填充以强制执行正确的 DWORD 对齐。 index_dst
是没有应用任何填充的索引。将像素从 index_src
移动到 index_dst
会移除(潜在的)填充。
1 标志是扫描线向左或向右移动一个或两个像素(或以不同速度移动的各个颜色通道)。这通常是一个安全的指示,表明扫描线对齐存在分歧。
2 此操作是破坏性的,即像素缓冲区一旦转换就不能再传递给 Windows GDI 函数,尽管原始数据可以重建,即使涉及更多。
这是我上一个关于将屏幕截图保存到 SOIL 的问题的延续。
bool saveTexture(string path, glm::vec2 startPos, glm::vec2 endPos)
{
const char *charPath = path.c_str();
GLuint widthPart = abs(endPos.x - startPos.x);
GLuint heightPart = abs(endPos.y - startPos.y);
BITMAPINFO bmi;
auto& hdr = bmi.bmiHeader;
hdr.biSize = sizeof(bmi.bmiHeader);
hdr.biWidth = widthPart;
hdr.biHeight = -1.0 * heightPart;
hdr.biPlanes = 1;
hdr.biBitCount = 24;
hdr.biCompression = BI_RGB;
hdr.biSizeImage = 0;
hdr.biXPelsPerMeter = 0;
hdr.biYPelsPerMeter = 0;
hdr.biClrUsed = 0;
hdr.biClrImportant = 0;
unsigned char* bitmapBits = (unsigned char*)malloc(3 * widthPart * heightPart);
HDC hdc = GetDC(NULL);
HDC hBmpDc = CreateCompatibleDC(hdc);
HBITMAP hBmp = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, (void**)&bitmapBits, nullptr, 0);
SelectObject(hBmpDc, hBmp);
BitBlt(hBmpDc, 0, 0, widthPart, heightPart, hdc, startPos.x, startPos.y, SRCCOPY);
//UPDATE:
- int bytes = widthPart * heightPart * 3;
- // invert R and B chanels
- for (unsigned i = 0; i< bytes - 2; i += 3)
- {
- int tmp = bitmapBits[i + 2];
- bitmapBits[i + 2] = bitmapBits[i];
- bitmapBits[i] = tmp;
- }
+ unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
+ // invert R and B chanels
+ for (unsigned row = 0; row < heightPart; ++row) {
+ for (unsigned col = 0; col < widthPart; ++col) {
+ // Calculate the pixel index into the buffer, taking the
alignment into account
+ const size_t index{ row * stride + col * hdr.biBitCount / 8 };
+ std::swap(bitmapBits[index], bitmapBits[index + 2]);
+ }
+ }
int texture = SOIL_save_image(charPath, SOIL_SAVE_TYPE_BMP, widthPart, heightPart, 3, bitmapBits);
return texture;
}
当我 运行 如果 widthPart 和 heightPart 是偶数,那效果很好。但是,如果这是奇数,我会得到这个 BMP。:
我检查了两次转换和代码,但在我看来原因是我错误的 blit 函数。转换 RGB 的功能不影响问题。可能是什么原因?这是 BitBlt 中区域块传输的正确方法吗?
更新偶数和奇数没有区别。当这个数字相等时,正确的图片就会产生。不知道哪里出了问题。((
更新2
SOIL_save_image 函数检查参数是否有错误并发送到 stbi_write_bmp:
int stbi_write_bmp(char *filename, int x, int y, int comp, void *data)
{
int pad = (-x*3) & 3;
return outfile(filename,-1,-1,x,y,comp,data,0,pad,
"11 4 22 4" "4 44 22 444444",
'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header
40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header
}
outfile 函数:
static int outfile(char const *filename, int rgb_dir, int vdir, int x, int
y, int comp, void *data, int alpha, int pad, char *fmt, ...)
{
FILE *f = fopen(filename, "wb");
if (f) {
va_list v;
va_start(v, fmt);
writefv(f, fmt, v);
va_end(v);
write_pixels(f,rgb_dir,vdir,x,y,comp,data,alpha,pad);
fclose(f);
}
return f != NULL;
}
损坏的位图图像是 Windows 位图与 SOIL 库期望的数据布局不一致的结果1。从 CreateDIBSection
返回的像素缓冲区遵循 Windows 规则(参见 Bitmap Header Types):
The scan lines are DWORD aligned [...]. They must be padded for scan line widths, in bytes, that are not evenly divisible by four [...].
换句话说:每条扫描线的宽度(以字节为单位)为(biWidth * (biBitCount / 8) + 3) & ~3
。另一方面,SOIL 库不期望像素缓冲区是 DWORD 对齐的。
要解决此问题,像素数据需要在传递给 SOIL 之前进行转换,方法是去除(潜在的)填充并交换 R 和 B 颜色通道。以下代码就地执行 2:
unsigned stride = (widthPart * (hdr.biBitCount / 8) + 3) & ~3;
for (unsigned row = 0; row < heightPart; ++row) {
for (unsigned col = 0; col < widthPart; ++col) {
// Calculate the source pixel index, taking the alignment into account
const size_t index_src{ row * stride + col * hdr.biBitCount / 8 };
// Calculate the destination pixel index (no alignment)
const size_t index_dst{ (row * width + col) * (hdr.biBitCount / 8) };
// Read color channels
const unsigned char b{ bitmapBits[index_src] };
const unsigned char g{ bitmapBits[index_src + 1] };
const unsigned char r{ bitmapBits[index_src + 2] };
// Write color channels switching R and B, and remove padding
bitmapBits[index_dst] = r;
bitmapBits[index_dst + 1] = g;
bitmapBits[index_dst + 2] = b;
}
}
使用此代码,index_src
是像素缓冲区的索引,其中包括填充以强制执行正确的 DWORD 对齐。 index_dst
是没有应用任何填充的索引。将像素从 index_src
移动到 index_dst
会移除(潜在的)填充。
1 标志是扫描线向左或向右移动一个或两个像素(或以不同速度移动的各个颜色通道)。这通常是一个安全的指示,表明扫描线对齐存在分歧。
2 此操作是破坏性的,即像素缓冲区一旦转换就不能再传递给 Windows GDI 函数,尽管原始数据可以重建,即使涉及更多。