ПрограммированиеСтатьиОбщее

Работа с файлами форматов ZIP, JPEG и PNG.

Автор:

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

И так, о чем же пойдет речь? Начиная свой новый проект, мы решили, что хорошо бы спрятать все игровые данные от глаз любопытных пользователей. Да и с эстетической точки зрения, чем меньше файлов, тем нагляднее структура. Дабы не изобретать велосипед, в качестве ресурсного файла был взят обычный zip-архив. И все бы ничего, да вот для чтения текстур из файлов (*.jpg и *.png) используются сторонние библиотеки, с ними то и возникло большинство проблем. Вот о них и пойдет разговор.

  Что нам понадобится?
Чтение ZIP-архивов
Чтение PNG-формата
Чтение JPEG-формата
Заключение

Что нам понадобится?

1. Для чтения из ZIP-архива понадобиться библиотека unzip. Найти ее можно по этому адресу:
http://www.winimage.com/zLibDll/unzip.html. Дома у меня версия 1.00, за 10 сентября 2003 г.
2. Читать PNG формат будем при помощи библиотечки libpng. Домашняя страничка здесь:
http://www.libpng.org/pub/png/libpng.html. Я пользовался версией 1.2.5, от 3 октября 2002 г.
3. Картинки в JPEG-формате понимает libjpeg. Последние версии можно найти по этому адресу:
ftp://ftp.uu.net/graphics/jpeg/. Моя версия библиотеки значится под номером 6 бета, и датируется 27 марта 1998 г. Страница этой библиотеки здесь: http://www.ijg.org/.
4. Пример, поставляемый с этой статьей.
5. Ко всему этому комплекту желательно иметь какую-нибудь визуальную студию с компилятором «С++».

Стоит отметить, что все вышеперечисленные библиотеки и примеры работы с ними можно найти в «nVidia SDK».

Чтение ZIP-архивов

Наша игра общается с винчестером через класс-оболочку cFile, который умеет писать и читать как обычные файлы, так и файлы, запакованные в zip (только чтение). Описан он в файле «filemngr.h», а реализован соответственно в «filemngr.cpp». Для унификации работы, параметры, принимаемые методами этого класса, и возвращаемые значения совпадают с параметрами функций из заголовочного файла <stdio.h>. Дабы не отвлекаться по мелочам, в статье рассмотрим только те части кода, которые непосредственно необходимы для чтения ZIP-архивов.

Для работы с архивом, понадобится две переменных. Первая – указатель на ZIP-файл, подобная FILE* в стандартных потоках ввода/вывода. Вторая представляет собой структуру, содержащую информацию о файле в архиве.

  // данные для сжатых ZIP'ом файлов
  // указатель на ZIP файл
  unzFile    m_pZFile;
  // информация о зазипованном файле (интересен размер распакованного файла)
  unz_file_info  m_ZInfo;

Открытие файлов — метод Open.

  // смотрим, откуда юзверь хочет достать файл
  if ( szZipName ) {  // из зипа
    // определяем тип файла (запись/чтение), сейчас доступно только чтение
    switch ( szMode[0] ) {
      case 'r': m_pData->m_Type = cFileData::e_ZipFile; break;
    }
    if ( m_pData->m_Type == cFileData::e_Unknown ) return( cFile::cError::WrongMode );

    //-------------------------------------------------------------------
    // пытаемся открыть файл и распаковать его

    try {
      // открываем zip файл
      m_pData->m_pZFile = unzOpen(szZipName);
      if ( !m_pData->m_pZFile ) 
        throw cZipCatch("^7File Manager:^6 ZIP file %s ^4not found^6\n", cFile::cError::ZipNotFound);
      // ищем в нем наш файл
      if ( unzLocateFile(m_pData->m_pZFile, szFileName, 0) !=  UNZ_OK ) 
        throw cZipCatch("^7File Manager:^6 File %s ^4not found^6 in ZIP file\n", 
          cFile::cError::FileNotFound);
      // если мы его нашли, то получаем о нем информацию
      if ( unzGetCurrentFileInfo(m_pData->m_pZFile, &m_pData->m_ZInfo, 0,0, 0,0, 0,0) != UNZ_OK )
        throw cZipCatch("^7File Manager:^6 ^4Internal error^6 in ZIP file - couldn't get file info\n",
          cFile::cError::DamageZip);
      // открываем наш файл для чтения
      if ( unzOpenCurrentFile(m_pData->m_pZFile) != UNZ_OK )
        throw cZipCatch("^7File Manager:^6 ^4Internal error^6 in ZIP file - couldn't open file\n",
          cFile::cError::DamageZip);
    }
    catch ( cZipCatch err ) {
      // сначала выведем ошибку в консоль
      // ...
      // удаляем промежуточные данные (те которые в блоке try)
      if ( m_pData->m_pZFile ) unzClose( m_pData->m_pZFile );
      // теперь удалим основные данные и вернем ошибку
      SAFE_DELETE( m_pData );
      return( err.m_iError );
    }
  }

Метод поиска и открытия файла в архиве, очень похож на стандартный сишный стиль. Сначала, как и с обычным I/O потоком открываем архив функцией unzOpen(). В случае отсутствия файла или некорректного архива она вернет 0, если же все прошло гладко, то результатом будет указатель, используемый в других zip-функциях. Следующий наш шаг — поиск необходимого файла в архиве при помощи функции unzLocateFile(), помимо этого она сделает его файлом по умолчанию. В качестве параметров ей передается само имя файла, а также метод сравнения имен, зависящий от регистра букв или нет. В случае успешного поиска результат функции будет равен UNZ_OK, в противном случае  — UNZ_END_OF_LIST_OF_FILE. В процессе работы с файлом, часто приходится запрашивать его размер, к сожалению, ничего подобного fseek() моя старая версия библиотеки не поддерживала, поэтому, и по сей день, приходится брать информацию о файле непосредственно у zip-архива. Для этих целей служит функция unzGetCurrentFileInfo(). Помимо этого она позволяет узнать: версию zip’а, необходимого для распаковки, метод компрессии, дату последней модификации файла, размер файла в сжатом виде и пр. Так как пишем не архиватор, то об этих вещах, я рассказывать не буду. И последний шаг — это открытие текущего файла для чтения при помощи функции unzOpenCurrentFile(). В случае успеха, он вернет UNZ_OK.

Закрытие запакованного файла (метод Close()) происходит в 2 этапа: сначала мы закрываем файл внутри архива при помощи функции unzCloseCurrentFile(), а затем сам архив, используя unzClose().

  // смотрим, как открывали файл
  if ( m_pData->m_Type == cFileData::e_ZipFile ) {  // открывали из зипа
    if ( m_pData->m_pZFile ) unzCloseCurrentFile( m_pData->m_pZFile );
    if ( m_pData->m_pZFile ) unzClose( m_pData->m_pZFile );
  }

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

Получение данных из запакованного файла происходит при помощи метода Read():

  // читаем из зип-файла
  if ( m_pData->m_Type == cFileData::e_ZipFile ) {
    int err = unzReadCurrentFile(m_pData->m_pZFile, pBuffer, (unsigned int)(uiSize * uiCount));
    if ( err < 0 ) {
      err = 0; // была ошибка - вернем 0
    }
    return( err / uiSize ); // возвращаем как и fread кол-во полностью прочитанных элементов (а не байт)
  }

Функция unzReadCurrentFile() заполняет буфер pBuffer, массивом байт из уже распакованного файла, последний параметр указывает на размер буфера. В случае успеха, функция возвращает: число байт занесенное в буфер, 0 если достигнут конец файла и отрицательное число если произошла какая-то ошибка. Так как метод Read() я старался сделать похожим на стандартную функцию fread(), то возвращается не кол-во прочитанных байт, а кол-во «полных» элементов, в связи с этим и происходит деление на размер элемента.

Чтение PNG-формата

Теперь, когда разобрались с чтением запакованных файлов, можно приступить к главному — чтению графических форматов. Благо все библиотеки предоставляют возможность работы с пользовательскими I/O потоками. Для этого, библиотеке нужно описать функцию чтения данных из файла:

  void PNGReadFunction(png_structp png_ptr, png_bytep data, png_size_t length)
  {
    cFile* pFile = (cFile*)png_get_io_ptr(png_ptr);
    pFile->Read(data, 1, length);
  }

На вход подается структура, содержащая информацию о библиотеке, массив байт, в который нужно занести прочитанную информацию, и длина этого массива. Первой строкой определяется указатель на поток данных. Как настроить на него PNG-библиотеку будет сказано ниже. Вторая строка — побайтовое чтение из файла в массив. На этом написание вспомогательных функций закончено, можно приступать к чтению файла.

Первым делом, откроем файл и считаем из него сигнатуру, которая позволит узнать тип файла. Обычно для PNG-формата считывают первые восемь байт:

  // проверяем сигнатуру файла (первые number байт)
  png_byte sig[number] = {0};
  file.Read(sig, sizeof(png_byte), number);
  if ( !png_check_sig(sig, number) ) { 
    file.Close();
    return( cDataManager::cError::UnknownFormat );
  }
  // проверка прошла успешно - это png-файл

Тестируется сигнатура при помощи функции png_check_sig(), на вход которой поступает массив из считанных байт и его длина. На выходе в случае несовпадения сигнатур, будет 0, в противном случае можно считать, что файл правильный. Следующий шаг — создание структур, содержащих информацию о библиотеке и файле:

  // создаем внутреннюю структуру png для работы с файлом
  // последние параметры - структура, для функции обработки ошибок и варнинга (последн. 2 параметра)
  png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
  // создаем структуру с информацией о файле
  png_infop info_ptr = png_create_info_struct(png_ptr);

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

  // говорим библиотеке, что мы уже прочли number байт, когда проверяли сигнатуру
  png_set_sig_bytes(png_ptr, number);
  // читаем всю информацию о файле
  png_read_info(png_ptr, info_ptr);
  // Эта функция возвращает инфу из info_ptr
  png_uint_32 width = 0, height = 0;  // размер картинки в пикселях
  int bit_depth = 0;      // глубина цвета (одного из каналов, может быть 1, 2, 4, 8, 16)
  int color_type = 0;      // описывает какие каналы присутствуют:
       // PNG_COLOR_TYPE_GRAY, PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE,
       // PNG_COLOR_TYPE_RGB, PNG_COLOR_TYPE_RGB_ALPHA...
  // последние 3 параметра могут быть нулями и обозначают: тип фильтра, тип компрессии и тип смещения
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0);

Получив необходимые данные, можно проверить, подходят они нам или нет. В случае неудовлетворительного ответа, библиотеке можно сказать, чтобы она на этапе считывания массива данных, конвертировала их в удобный для нас формат:

  // png формат может содержать 16 бит на канал, но нам нужно только 8, поэтому сужаем канал
  if (bit_depth == 16) png_set_strip_16(png_ptr);
  // преобразуем файл если он содержит палитру в нормальный RGB
  if (color_type == PNG_COLOR_TYPE_PALETTE && bit_depth <= 8) png_set_palette_to_rgb(png_ptr);
  // если в грэйскейле меньше бит на канал чем 8, то конвертим к нормальному 8-битному
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_gray_1_2_4_to_8(png_ptr);
  // и добавляем полный альфа-канал
  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png_ptr);

В нашей игре допустимы картинки, содержащие информацию только об оттенках серого цвета (grayscale картинки). Если же, необходимо преобразование к RGB формату, то это можно сделать таким образом:

  if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    png_set_gray_to_rgb(png_ptr);

Теперь осталось только настроить гамму и можно будет приступить к чтению данных. Гамма складывается из двух компонент, так называемой гаммы монитора (screen gamma) и гаммы картинки. В документации к библиотеке указано, какое значение гаммы лучше подбирать для различных типов платформ и мониторов: 2.2 подходит для PC мониторов в освещенной комнате, 2.0 – в комнате с тусклым освещением и от 1.7 до 1.0 – для MacOS платформ. Гамма картинки может быть указана в самом файле, в случае если эта информация отсутствует, желательно присвоить стандартное значение гаммы, равное 0.45455.

  double gamma = 0.0f;
  // если есть информация о гамме в файле, то устанавливаем на 2.2
  if ( png_get_gAMA(png_ptr, info_ptr, &gamma) ) png_set_gamma(png_ptr, 2.2, gamma);
  // иначе ставим дефолтную гамму для файла в 0.45455 (good guess for GIF images on PCs)
  else png_set_gamma(png_ptr, 2.2, 0.45455);

Теперь обновим информацию в библиотеке и заново перечитаем данные о картинке — они должны совпасть с теми, которые мы запросили:

  // после всех трансформаций, апдейтим информацию в библиотеке
  png_read_update_info(png_ptr, info_ptr);
  // опять получаем все размеры и параметры обновленной картинки
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0);

Предварительные приготовления закончены, можно приступать к чтению картинки. Оно будет происходить построчно, от нижней строки к верхней.

  // определяем кол-во байт нужных для того чтобы вместить строку
  png_uint_32 row_bytes = png_get_rowbytes(png_ptr, info_ptr);
  // теперь, мы можем выделить память чтобы вместить картинку
  png_byte* data = new png_byte[row_bytes * height];
  // выделяем память, для указателей на каждую строку
  png_byte **row_pointers = new png_byte * [height];
  // сопоставляем массив указателей на строчки, с выделенными в памяти (res)
  // т.к. изображение перевернутое, то указатели идут снизу вверх
  for (unsigned int i = 0; i < height; i++)
    row_pointers[height - i - 1] = data + i * row_bytes;
  // все, читаем картинку
  png_read_image(png_ptr, row_pointers);

В приведенном выше коде определяется размер строки в байтах. Затем выделяется память для массива пикселей (размер строки, помноженный на количество строк). Чтобы после получения информации не заниматься перестановкой строк, просто выделяем еще один массив, в который занесем указатели на строки в обратном порядке, их и будем подсовывать библиотечной функции чтения всей картинки за раз png_read_image. Теперь картинка в памяти и остается только освободить память от указателей, закрыть библиотеку и файл:

  // освобождаем память от указателей на строки
  delete [] row_pointers;
  // освобождаем память выделенную для библиотеки libpng
  png_destroy_read_struct(&png_ptr, 0, 0);
  // закрываем файл
  file.Close();

На этом рассказ о PNG-формате можно считать завершенным.

Страницы: 1 2 Следующая »

#JPEG, #PNG, #ZIP, #файлы

12 июля 2004 (Обновление: 9 июня 2009)

Комментарии [32]