Битый BMP при сохранении растрового изображения с помощью SOIL. Область скриншота

Это продолжение моего последнего вопроса о сохранении снимка экрана в 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.:

Сломанный BMP1

Сломанный BMP2

Дважды проверял любую конвертацию и код, но мне кажется причина в моих неправильных блит функциях. Функция преобразования 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;
}

person hardCode    schedule 27.05.2017    source источник


Ответы (1)


Поврежденные растровые изображения являются результатом несоответствия расположения данных между растровыми изображениями Windows и того, что ожидает библиотека SOIL1. Буфер пикселей, возвращенный из CreateDIBSection, соответствует правилам Windows (см. Типы заголовков растровых изображений):

Строки сканирования выровнены по типу DWORD [...]. Они должны быть дополнены шириной строки сканирования в байтах, которая не делится без остатка на четыре [...].

Другими словами: ширина каждой строки развертки в байтах равна (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 после преобразования, хотя исходные данные можно реконструировать, даже если это требует немного больше усилий.

person IInspectable    schedule 28.05.2017
comment
В любом случае спасибо за помощь, но я пробую разные варианты вашего кода (перед редактированием) и ничего не меняется. :(( Каждый раз, когда widthPart не делится на 4 (независимо от того, что такое heightPart), я получаю BMP выше. Я обновляю вопрос вашей частью. - person hardCode; 28.05.2017
comment
@hardCode: Пожалуйста, не принимайте ответ, который не решает вашу проблему, и не редактируйте свой вопрос, чтобы сделать его другим. Вместо этого оставляйте комментарии, чтобы попросить разъяснений. Предположительно, я бы предположил, что проблема все еще связана с выравниванием строки сканирования в растровых изображениях. Возможно, SOIL_save_image не ожидает, что буфер будет выровнен по типу DWORD. Можем ли мы увидеть документацию для SOIL_save_image? - person IInspectable; 28.05.2017
comment
Я обновил вопрос. В документации SOIL ничего не говорится о написании BMP: lonesock.net/soil.html - person hardCode; 28.05.2017