Сохранение изображения с помощью libpng

  • Tutorial

Развлекаясь на досуге с OpenGL, решил научиться делать скриншоты средствами программы, а не системы. Довольно быстро нагуглил функцию glReadPixels, но вот с сохранением картинки вышла проблема. Вспомнил былые времена, когда полностью своим кодом сохранял в bmp, нашел функцию сохранения в tga, понял, что все эти варианты попахивают велосипедизмом и решил использовать широко распространенную библиотеку. Выбор пал на libpng.
Дальше пошли грабли.

Оказалось, что никакого описания библиотеки на русском языке нет (собственно поэтому и пишу сейчас этот пост), а английская документация написана не самым удобным образом и не содержит даже простейшего полноценного примера использования.
Ниже я постараюсь объяснить, как средствами libpng сохранить изображение в простейшем случае.


Прежде всего надо подключить заголовочный файл

#include <png.h>


В функции/методе, в которой будем сохранять изображение, откроем файл, в который будем сохранять и создадим структуру png.

void
Renderer::screenshoot(const std::string& name) {

    FILE *fp = fopen(name.c_str(), "wb");
    if (!fp) {
       return;
    }

    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if (!png_ptr) {
        goto close_file;
    }


Теперь нужно создать структуру информации о png, вызвать setjmp, на случай ошибок и инициализировать вывод в файл.

    png_infop png_info;
    if (!(png_info = png_create_info_struct(png_ptr))) {
        goto destroy_write;
    }

    if (setjmp(png_jmpbuf(png_ptr))) {
        goto destroy_write;
    }

    png_init_io(png_ptr, fp);


Дальше нужно установить параметры изображения (размеры, количество цветов, количество битов на канал цвета, черезстрочность и сжатие. А также сформировать изображение и массив указателей на отдельные строки изображения.

Тут начинаются грабли.

Грабли №1: функция glReadPixels отдает изображение, строчки в котором идут снизу вверх, хотя обычно работая с графикой мы подразумеваем обратный порядок.
Грабли №2: если указать функции glReadPixels, что мы хотим получить цвета RGBA, то на самом деле мы получим их в порядке ARGB. Досадно, но на то, чтобы выяснить это, у меня ушел час, в течении которого я не понимал, почему у меня не правильно отображаются цвета на скриншоте.

Объявим массив data, в котором будут данные для libpng, и массив argb_data, в котором будут данные от OpenGL, ну и не забудем про массив указателей rows.

    png_set_IHDR(png_ptr, png_info, width, height, 0, PNG_COLOR_TYPE_RGB,
        PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
        PNG_FILTER_TYPE_DEFAULT);

    unsigned char data[width*height*3], argb_data[width*height*4];
    unsigned char *rows[height];

    render();
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, argb_data);

    for (int i = 0; i < height; ++i) {
        rows[height - i - 1] = data + (i*width*3);
        for (int j = 0; j < width; ++j) {
            int i1 = (i*width+j)*3;
            int i2 = (i*width+j)*4;
            data[i1++] = argb_data[++i2];
            data[i1++] = argb_data[++i2];
            data[i1++] = argb_data[++i2];
        }
    }


Теперь дело за малым — сохранить изображение, завершить ввод и обработать ошибки.

    png_set_rows(png_ptr, png_info, rows);
    png_write_png(png_ptr, png_info, PNG_TRANSFORM_IDENTITY, nullptr);
    png_write_end(png_ptr, png_info);

destroy_write:
    png_destroy_write_struct(&png_str, nullptr);
close_file:
    fclose(fp);
}


Таким, относительно несложным образом можно сохранить изображение в png. Однако, не думайте, что это все, на что способна эта библиотека. Можно следить за процессом сохранения, применять фильтры, использовать черезстрочность, сжатие… Но чтобы разобраться с этим стоит читать оригинальную документацию.

UPD. Спасибо MrGobus за замечание по поводу glReadPixels и порядка строк.

Комментарии 16

    +10
    Дааа, обмельчали статьи на Хабре.
      0
      Я не ставил цель описать полностью работу с libpng — для этого необходимо понимать все настройки изображения, которые можно выставить. Понимая их разобраться в libpng не сложно.

      Цель моего поста — экономия времени тех, кому как и мне нужен минимум. Я потратил на это полтора часа, буду рад, если кто-то благодаря этому посту потратит всего 15 минут. Повторюсь, но толкового примера использования libpng я не встретил, тем более в рунете.
      +6
      glReadPixels returns pixel data from the frame buffer, starting with the pixel whose lower left corner is at location (x, y),

      Это не пнг хранит картинку вверх ногами а OpenGL возвращает ее такой, так как в нем используется такая система координат. Вообще, при работе с OpenGL, рекомендую отказаться от стереотипа, что картинка рисуется от верхнего левого угла и считать все от нижнего левого угла, это избавит вас от огромного числа граблей, например если ваши игры с OpenGL дойдут до уровня фреймбуфера и рендера в текстуру, то вы заметите, что изображение полученное при рендере в текстуру будет вверх ногами, но вам будет казаться что это грабли и баг, более того вы можете начать писать алгоритмы которые бы учитывая, что рендер в текстуру переворачивали бы изображение за счет ortho или еще как…
      В общем както так.

      Все таки знать английский стоит, и также стоит читать документацию которая идет вместе с библиотекой.
      libpng-manual.txt содержит исчерпывающую информацию о библиотеке, решая проблемы с граблями порядка цветовых компанент.

      PNG files store 3 color pixels in red, green, blue order. This code
      would be used if they are supplied as blue, green, red:

      png_set_bgr(png_ptr);

      glReadPixel тоже позволяет решить эту проблему задавая формат GL_RGBA GLBGRA

      format
      Specifies the format of the pixel data. The following symbolic values are accepted: GL_STENCIL_INDEX, GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL, GL_RED, GL_GREEN, GL_BLUE, GL_RGB, GL_BGR, GL_RGBA, and GL_BGRA.

      type
      Specifies the data type of the pixel data. Must be one of GL_UNSIGNED_BYTE, GL_BYTE, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT, GL_HALF_FLOAT, GL_FLOAT, GL_UNSIGNED_BYTE_3_3_2, GL_UNSIGNED_BYTE_2_3_3_REV, GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_5_6_5_REV, GL_UNSIGNED_SHORT_4_4_4_4, GL_UNSIGNED_SHORT_4_4_4_4_REV, GL_UNSIGNED_SHORT_5_5_5_1, GL_UNSIGNED_SHORT_1_5_5_5_REV, GL_UNSIGNED_INT_8_8_8_8, GL_UNSIGNED_INT_8_8_8_8_REV, GL_UNSIGNED_INT_10_10_10_2, GL_UNSIGNED_INT_2_10_10_10_REV, GL_UNSIGNED_INT_24_8, GL_UNSIGNED_INT_10F_11F_11F_REV, GL_UNSIGNED_INT_5_9_9_9_REV, or GL_FLOAT_32_UNSIGNED_INT_24_8_REV.

      Ручная перестановка это както не комильфо. Ну и точно не грабли.
        0
        Спасибо. Сейчас подправлю пост.

        Я знаю английский, просто некоторую документацию бывает очень тяжело читать (вне зависимости от языка).

        С форматом и типом я ковырялся в течении часа, в пост вынес тот способ, который заработал.
        Проблема не сколько в RGB vs. BGR (с этим всё в порядке), проблема в альфа канале, значение которого идет не после значений цветов, а перед ними. Если это не учитывать, то с картинкой творятся странные вещи.
        –1
        А альтернативных способов обработки ошибок в libpng так и не появилось, так и делается все исключительно через setjmp?
          0
          Её можно собрать без поддержки setjmp, но альтернативы не предложено.
            –1
            Что можете предложить для чистого Си, кроме как писать идеальный код и давать на вход идеальные данные для этого? Экзепшинов то нет.
              0
              Ну а как тысячи других C библиотек обходятся без setjmp? Как ядро линукса обходится без setjmp для обработки внутренних ошибок? Как стандартная C library обходится без них? Ответ один, — «случилось что то не так, — верни минус один» (ну или аналогичное значение) ( извиняюсь за рифму, случайно вышло). В нормальном С коде, каждый код возврата, в случае если функция может вернуть ошибку, должен провеняться вызывающей стороной, и если ошибка случилась, она должна быть обработана соответствующим образом, и в большинстве случаев — передана выше, как уже код возврата функции стоящей на одну позицию выше в стеке вызова, итп, итп. И все так и делают, за исключением редких «гениальных библиотек», вроде libpng или исходников lua.
                0
                Такой подход неправилен тем что:
                1)Тип возвращаемого значения не всегда совместим с дескриптором ошибки
                2)Обрабатывать исключения можно только на шаг выше колстека, а екзепшен можно кинуть куда угодно.

                П.С. Отношение плотно работающих с «железом» программистов к исключениям варьируется от «лучше не надо», до «категорически против» — потому в ядре линукс их нет, в недрах епл кста тоже запрещено использовать исключения при работе с компонентами ОС — такие дела, почему не знаю.
                  0
                  Про п1 — приведите пример.

                  Про п2 — С — язык без исключений, и надо с этим либо жить, либо писать на C++ а не на C :) Такие вещи как setjmp порождают куда больше проблем, чем решают, по этому — это костыли. Если нужно «пробросить» ошибку через несколько уровней вызовов, то каждый из вложенных уровней, должен проверить на ошибку, и вернуть свою ошибку, и так всегда и делают.

                  Про Apple — по тому что в ядре очень ограниченный C++ Runtime, там много вещей из «обычного C++» нельзя делать, если вы про IOKit / NKE драйвера говорите.

                  P.S. Про проблемы с setjmp, — представьте такую ситуацию: есть библиотека «libA», написанная на C, содержащая функции X and Y, каждый из которых может «выкинуть» setjmp, а так-же плюс есть некий callback регистрируемый в этой библиотеке. И представьте, что пользуется этой C-библиотекой некая программа на C++ (частый случай). Вот она регистрирует callback, вот он в какой-то момент вызывается, внутри коллбэка C++ код вызывает другой метод библиотеки — скажем «X», там что-то идет не так, и библиотека дергает setjmp. Что имеем? Если внутри коллбэка были какие-то обьекты расположенные на стеке, то мы имеем кучу обьектов, для которых не был вызван деструктор, как следствие — утечки памяти, и прочие глюки C++ рантайма. В итоге, когда из C++ кода приходится (ввиду отсутствия альтернатив) общатся с подобной C библиотекой, приходится городить огромные костыли. (Пример подобного C кода — lua движек например).
                    0
                    п1. Да пожалуйста — математическая функция для которой -1 может быть результатом вычислений, функция возвращающая объект не ввиде указателя, функция имеющая тип void (внезапно) или возвращающая булевый тип — короче на практике зарезервировать -1 как дескриптор на все ошибки та еще задача.
                    Я сам, если честно, против setjmp — на моей практике ни один отладчик брошенное таким образом эрзац исключение корректно не обрабатывал, откуда оно было брошено и почему выяснить удавалось только ручной трассировкой и это печально.
                      0
                      > математическая функция

                      никто не обязывает использовать строго -1, код ошибки может быть совершенно любой. Если речь идет о функции возвращающей float/double, то у этих типов есть специальные значения для сигнализации о невычислимости функции для данных аргументов, — INF и NaN (бесконечность и «Not a Number»). Можно использовать их. Если функция возвращает int или другой целый тип, то у нее часто область значений ограничена все равно, так что можно в качестве кода ошибки использовать какой-нибудь INT_MAX или INT_MIN. Если ничто из этого не применимо, то можно сделать вот так:

                      int err = 0;
                      int result = function(arg1, arg2, ..., &err);
                      if ( err = -1 ) 
                      {
                            // ошибка
                      }
                      


                      > функция возвращающая объект не ввиде указателя

                      Классический паттерн, который все используют для такого случая:

                      
                      object_t obj;
                      
                      int err = createObject( arg1, ... argN,  &obj);
                      
                      if ( err == -1 ) 
                      { 
                         ...
                      


                      > функция имеющая тип void (внезапно)

                      Функция может зафейлится, но возвращает void? За такое молодым программистам по рукам указкой бьют.

                      > возвращающая булевый тип

                      Ну так возвращайте не булевый тип, а enum какой-нибудь трехстейтный, как это делают многие библиотеки.

                      > короче на практике зарезервировать -1 как дескриптор на все ошибки та еще задача.

                      Я не говорил что именно -1 надо использовать, — под каждый случай свой код ошибки, в зависимости от типа возвращаемого значения, и кучи других функторов, -1 — это метафорическое обобщение способа обработки ошибок принятого в С.
            0
            А зачем для скриншота RGBA? Можно просто RGB и тогда не будет проблем с сохранением альфа канала. На самом деле веселуха если изображение 8 битное и есть несколько зон в палитре. :)
              0
              Потому что если указать GL_RGB, то проблема остается — на каждый пиксель все равно идет четыре байта. Разницы нет.
                0
                Что у вас за видео драйвер? И система big-endian или little-endian?
                www.opengl.org/sdk/docs/man/xhtml/glReadPixels.xml — что то я таких странностей в спецификации не нахожу.
                Используйте GL_UNSIGNED_BYTE и будет вам счастье.
                glReadPixels(0,0, SCREEN_WIDTH, SCREEN_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, output);
              0
              По-моему, у вас в примере есть опечатка

              destroy_write:
                  png_destroy_write_struct(&png_str, nullptr);
              close_file:
                  fclose(fp);
              }
              

              В этом фрагменте удаляется png_str, но такой переменной не объявлялось
              По-видимому, имелась в виду переменная png_ptr

              Спасибо за пример, это — как раз то, что я искал (если, конечно, всё заработает) :-)

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое