[C++]zlibを用いたpngの解凍と読み込み

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
いろは
記事: 11
登録日時: 7年前

[C++]zlibを用いたpngの解凍と読み込み

#1

投稿記事 by いろは » 5年前

 こんにちは。後学のためにC++で必要最低限の要素でpngを読み込むことを考えています。
 イメージデータの圧縮部をを自作の関数で解凍することはでき、pngを読み込むことができたのですが、やはり速度が遅いのでzlibを用いて解凍をしようと考えています。
 そこで質問なのですが、何故かzlibの解凍をする関数を用いてもデータがおかしいとのエラーが出るだけで解凍ができません。自作の関数ではできたので、これはおかしいと思い質問させていただきました。

コード:

	//ここまでチャンクデータの読み込み
	//comp_size:圧縮データのサイズ
	//comp_img:圧縮データ配列

	//出力データ分のメモリの確保
	unsigned long output_size = img->h*img->real_w + img->h;
	unsigned char *output = new unsigned char[output_size];
	//zlibのヘッダとフッタ情報は飛ばして読み込む
	switch (uncompress(output, &output_size, &comp_img[2], comp_size - 6)) {
	case Z_OK:
		std::cout << "復号に成功" << std::endl;
		break;
	case Z_MEM_ERROR:
		std::cout << "メモリが不足" << std::endl;
		break;
	case Z_BUF_ERROR:
		std::cout << "出力バッファの不足" << std::endl;
		break;
	case Z_DATA_ERROR:
		std::cout << "入力バッファがおかしい" << std::endl;
		break;
	default:
		std::cout << "その他" << std::endl;
		break;
	}
	//自作関数では解凍できていた(zlib形式のため内部でサムチェック等をしてdeflateを解凍)
	//auto output = zlib::decode(comp_img, comp_size);

	//以後イメージデータのフィルタリングを解く処理
	
 環境はvisual studio 2015でwindows8.1を用いて作成しています。

かずま

Re: [C++]zlibを用いたpngの解凍と読み込み

#2

投稿記事 by かずま » 5年前

いろは さんが書きました:
5年前

コード:

	switch (uncompress(output, &output_size, &comp_img[2], comp_size - 6)) {
&comp_img[2] と comp_size - 6 がおかしいのでは?
2 と 6 の意味を説明してください。

次のプログラムは復号に成功します。

コード:

#include <iostream>
#include <zlib.h>

/*
000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52  >.PNG........IHDR<
000010 00 00 00 10 00 00 00 0c 08 02 00 00 00 e4 85 aa  >................<
000020 d6 00 00 00 01 73 52 47 42 00 ae ce 1c e9 00 00  >.....sRGB.......<
000030 00 04 67 41 4d 41 00 00 b1 8f 0b fc 61 05 00 00  >..gAMA......a...<
000040 00 09 70 48 59 73 00 00 12 74 00 00 12 74 01 de  >..pHYs...t...t..<
000050 66 1f 78 00 00 00 53 49 44 41 54 28 53 63 78 2b  >f.x...SIDAT(Scx+<
000060 a3 42 24 fa ff 89 01 88 40 1a 20 2c fc 88 1a 1a  >.B$.....@. ,....<
000070 70 01 88 2c 76 0d ff 31 00 01 0d 58 01 44 16 8b  >p..,v..1...X.D..<
000080 06 fc 08 45 03 31 48 69 a3 0f 04 a1 18 83 07 01  >...E.1Hi........<
000090 95 42 48 06 86 45 2f 88 41 28 1a e0 c6 e0 42 e8  >.BH..E/.A(....B.<
0000a0 1a 88 41 40 a5 20 b4 d1 07 00 00 e4 25 2e 43 9b  >..A@. ......%.C.<
0000b0 5c 61 00 00 00 00 49 45 4e 44 ae 42 60 82        >\a....IEND.B`.<
0000be
*/

unsigned char png[] = { 
    0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52,
    0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x0c,0x08,0x02,0x00,0x00,0x00,0xe4,0x85,0xaa,
    0xd6,0x00,0x00,0x00,0x01,0x73,0x52,0x47,0x42,0x00,0xae,0xce,0x1c,0xe9,0x00,0x00,
    0x00,0x04,0x67,0x41,0x4d,0x41,0x00,0x00,0xb1,0x8f,0x0b,0xfc,0x61,0x05,0x00,0x00,
    0x00,0x09,0x70,0x48,0x59,0x73,0x00,0x00,0x12,0x74,0x00,0x00,0x12,0x74,0x01,0xde,
    0x66,0x1f,0x78,0x00,0x00,0x00,0x53,0x49,0x44,0x41,0x54,0x28,0x53,0x63,0x78,0x2b,
    0xa3,0x42,0x24,0xfa,0xff,0x89,0x01,0x88,0x40,0x1a,0x20,0x2c,0xfc,0x88,0x1a,0x1a,
    0x70,0x01,0x88,0x2c,0x76,0x0d,0xff,0x31,0x00,0x01,0x0d,0x58,0x01,0x44,0x16,0x8b,
    0x06,0xfc,0x08,0x45,0x03,0x31,0x48,0x69,0xa3,0x0f,0x04,0xa1,0x18,0x83,0x07,0x01,
    0x95,0x42,0x48,0x06,0x86,0x45,0x2f,0x88,0x41,0x28,0x1a,0xe0,0xc6,0xe0,0x42,0xe8,
    0x1a,0x88,0x41,0x40,0xa5,0x20,0xb4,0xd1,0x07,0x00,0x00,0xe4,0x25,0x2e,0x43,0x9b,
    0x5c,0x61,0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,0x60,0x82,
    };  

struct Img { int real_w, h; };

Img data = { 16, 12 }; // IHDR の次の 0x00000010 と 0x0000000c
Img *img = &data;

int main()
{
	//comp_size:圧縮データのサイズ
    unsigned long comp_size = 0x53;  // IDAT の直前の 0x00000053
	//comp_img:圧縮データ配列
    unsigned char *comp_img = png + 0x5b; // IDAT の直後のデータ

	// IHDR のビット深度が 8、カラータイプが 2 だから、RGB 3バイト
    unsigned long output_size = (img->h*img->real_w + img->h) * 3;
    unsigned char *output = new unsigned char[output_size];

    switch (uncompress(output, &output_size, comp_img, comp_size)) {
    case Z_OK:
        std::cout << "復号に成功" << std::endl;
        break;
    case Z_MEM_ERROR:
        std::cout << "メモリが不足" << std::endl;
        break;
    case Z_BUF_ERROR:
        std::cout << "出力バッファの不足" << std::endl;
        break;
    case Z_DATA_ERROR:
        std::cout << "入力バッファがおかしい" << std::endl;
        break;
    default:
        std::cout << "その他" << std::endl;
        break;
    }   
}
成功した自作の関数を見せてもらえませんか?

いろは
記事: 11
登録日時: 7年前

Re: [C++]zlibを用いたpngの解凍と読み込み

#3

投稿記事 by いろは » 5年前

 すみません、どうやらzlibでやる圧縮方法を勘違いしていたみたいです。とりあえず、勘違いしていた部分が以下のコードです。

コード:

namespace zlib {
	//zlib形式のデコード
	auto decode(const unsigned char* binary, size_t byte) {

		//圧縮方式がdeflateであることのチェック
		if ((binary[0] & 0x0F) != 0x08) throw std::runtime_error("deflate以外無理");
		//スライド窓サイズ(256(2^8) ~ 32768(2^15))
		const unsigned int slide_window = (1 << (((binary[0] & 0xF0) >> 4) + 8));
		if (slide_window > 32768) throw std::runtime_error("スライド窓が大きすぎる");

		//zlibのヘッダチェック
		//結果が31の倍数にならなければならない
		if (unsigned_short_cast(&binary[0]) % 31 != 0) throw std::runtime_error("zlibのヘッダがおかしい");
		//プリセット辞書には非対応
		if (binary[1] & 0x20) throw std::runtime_error("プリセット辞書は無理");

		//deflateの解凍(ヘッダーとフッターの和の6byte分引く)
		auto result = deflate::decode(&binary[2], byte - 6, slide_window);
		//↑この部分がzlibのライブラリでやっている部分だと思っていた

		//フッターのAdler32のチェックサム
		if (unsigned_int_cast(&binary[byte - 4]) != adler_32(result)) throw std::runtime_error("圧縮データが壊れている");
		
		return result;
	}
}
 画像データを扱うための構造体は以下の通りです。

コード:

//画像の構造体(ビット深度は8のみをサポート)
struct image {
private:
	//4の倍数に切り上げるの関数
	static size_t ceil4(size_t n) {
		return (n & 3) ? (((n >> 2) + 1) << 2) : (n);
	}
public:
	const size_t w, h;				//幅、高さ
	size_t real_w;					//実際の幅のビット長(4の倍数)
	size_t color;						//色数(3~で最初の3色はRGBで基本的に3か4のみ)
	unsigned char* pixels;				//データ配列(左上から右下へ[R,G,B,...])

	image(size_t width, size_t height, size_t n)
		: w(width), real_w(ceil4(width*n)), h(height), color(n), pixels(nullptr) {}
	~image() { if (pixels != nullptr) delete[] pixels; }

	//メモリ確保(成功でtrue)
	bool reserve() {
		pixels = new unsigned char[real_w*h]{};
		return (pixels != nullptr);
	}

	//任意のピクセルに対するアクセサ
	const unsigned char* operator()(size_t width, size_t height) const { return pixels + height*(real_w) + width*color; }
	unsigned char* operator()(size_t width, size_t height) { return pixels + height*(real_w) + width*color; }
};
 かずまさんの用意してくださったコードを私の環境で実行してみようとしましたが、なぜか"zlibwapi.dllでアクセス違反"が発生しました。
 流石におかしいと思ったのでpngの読み込みと含めて以下のコードを実行してみました。

コード:

//PNGの読み込み(p:ビット配列)
void load_png(const unsigned char* p) {
	const unsigned char* img_p;
	unsigned int chunk_size;
	size_t comp_size = 0;			//圧縮データのサイズ
	unsigned char *comp_img;		//圧縮画像データ配列
	unsigned char interlace;		//インターレース手法
	unsigned char filter;			//フィルター手法
	//image* img;
	std::list<std::pair<unsigned int, const unsigned char*>> img_index;	//画像データのチャンクが複数ある場合を想定したインデックス

	//reinterpret_castではメモリ配置が逆になるときのuintへのキャスト的なやつ
	static const auto unsigned_int_cast = [](const unsigned char* p) -> unsigned int {
		return (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + (p[3]);
	};

	//PNGシグネチャのチェック
	if (!((p[0] == 0x89) && (p[1] == 'P') && (p[2] == 'N') && (p[3] == 'G')
		&& (p[4] == '\r') && (p[5] == '\n') && (p[6] == 0x1A) && (p[7] == '\n'))) return;
	img_p = &p[8];
	//イメージヘッダ
	//img = new image(unsigned_int_cast(&img_p[8])
	//	, unsigned_int_cast(&img_p[12]), 3 + ((img_p[17] & 4) == 4));
	filter = img_p[19];
	interlace = img_p[20];
	img_p += 25;

	//チャンク読み込み
	while (1) {
		chunk_size = unsigned_int_cast(&img_p[0]);
		if ((img_p[4] == 'I') && (img_p[5] == 'E') && (img_p[6] == 'N') && (img_p[7] == 'D')) break;
		//画像データ(複数ある可能性がある(このとき連続で存在))
		else if ((img_p[4] == 'I') && (img_p[5] == 'D') && (img_p[6] == 'A') && (img_p[7] == 'T')) {
			img_index.push_back(std::pair<unsigned int, const unsigned char*>(chunk_size, &img_p[8]));
			comp_size += chunk_size;
		}

		//次のチャンクを読み込むために進める
		img_p += chunk_size + 12;
	}
	//圧縮画像データを結合
	comp_img = new unsigned char[comp_size]; comp_size = 0;
	for (auto itr = img_index.begin(); itr != img_index.end(); ++itr) {
		iml::copy_order(&comp_img[comp_size], itr->second, itr->first);
		comp_size += itr->first;
	}

	//圧縮データの復元
	/*unsigned long decode_size = img->h*img->real_w + img->h;
	std::unique_ptr<unsigned char> decode(new unsigned char[decode_size]);
	switch (uncompress(decode.get(), &decode_size, comp_img, comp_size)) {
	case Z_OK:
		std::cout << "復号に成功" << std::endl;
		break;
	case Z_MEM_ERROR:
		std::cout << "メモリが不足" << std::endl;
		break;
	case Z_BUF_ERROR:
		std::cout << "出力バッファの不足" << std::endl;
		break;
	case Z_DATA_ERROR:
		std::cout << "入力バッファがおかしい" << std::endl;
		break;
	default:
		std::cout << "その他" << std::endl;
		break;
	}*/
	//ここでの復号は成功している
	auto decode = iml::zlib::decode(comp_img, comp_size);

	//再圧縮して戻せるか
	{
		std::cout << "再圧縮してみる" << std::endl;

		unsigned long encode_size = comp_size * 2;			//この程度で十分?
		std::unique_ptr<unsigned char> encode(new unsigned char[encode_size]);
		switch (compress(encode.get(), &encode_size, &decode[0], decode.size())) {
		case Z_OK:
			std::cout << "暗号化に成功" << std::endl;
			break;
		case Z_MEM_ERROR:
			std::cout << "メモリが不足" << std::endl;
			break;
		case Z_BUF_ERROR:
			std::cout << "出力バッファの不足" << std::endl;
			break;
		case Z_DATA_ERROR:
			std::cout << "入力バッファがおかしい" << std::endl;
			break;
		default:
			std::cout << "その他" << std::endl;
			break;
		}

		std::cout << "戻せるはず" << std::endl;

		unsigned long rdecode_size = encode_size;
		std::unique_ptr<unsigned char> rdecode(new unsigned char[rdecode_size]);
		switch (uncompress(rdecode.get(), &rdecode_size, encode.get(), encode_size)) {
		case Z_OK:
			std::cout << "再復号に成功" << std::endl;
			break;
		case Z_MEM_ERROR:
			std::cout << "メモリが不足" << std::endl;
			break;
		case Z_BUF_ERROR:
			std::cout << "出力バッファの不足" << std::endl;
			break;
		case Z_DATA_ERROR:
			std::cout << "入力バッファがおかしい" << std::endl;
			break;
		default:
			std::cout << "その他" << std::endl;
			break;
		}
	}

	return;
}
 このコードでの出力結果が、

コード:

再圧縮してみる
暗号化に成功
戻せるはず
 で再復号中に"zlibwapi.dllでアクセス違反"が発生しました。つまりzlib側がおかしいのでは?とも思いましたが、適当に実装した以下のコードは正常に動きました。

コード:

	unsigned char original[] = "aaaaaabbbbbbbaaaaaaaaaccccccaaaaaaaaaaaaaaaadddddaaaaaaaaaaaaaaaaa";
	unsigned long original_size = sizeof(original) + 1;
	unsigned long encode_size = original_size * 2;			//これだけあれば十分
	unsigned char* encode = new unsigned char[encode_size];

	switch (compress(encode, &encode_size, original, original_size)) {
	case Z_OK:
		std::cout << "暗号化に成功" << std::endl;
		std::cout << "暗号化前サイズ:" << original_size << ",暗号化後サイズ:" << encode_size << std::endl;
		std::cout << "暗号化データ:" << encode << std::endl;
		break;
	case Z_MEM_ERROR:
		std::cout << "メモリが不足" << std::endl;
		break;
	case Z_BUF_ERROR:
		std::cout << "出力バッファの不足" << std::endl;
		break;
	case Z_DATA_ERROR:
		std::cout << "入力バッファがおかしい" << std::endl;
		break;
	default:
		std::cout << "その他" << std::endl;
		break;
	}

	//復号してみる

	unsigned long decode_size = original_size + 100;
	unsigned char* decode = new unsigned char[decode_size];

	switch (uncompress(decode, &decode_size, encode, encode_size)) {
	case Z_OK:
		std::cout << "復号に成功" << std::endl;
		std::cout << "復号前サイズ:" << encode_size << ",復号後サイズ:" << decode_size << std::endl;
		std::cout << "復号データ:" << decode << std::endl;
		break;
	case Z_MEM_ERROR:
		std::cout << "メモリが不足" << std::endl;
		break;
	case Z_BUF_ERROR:
		std::cout << "出力バッファの不足" << std::endl;
		break;
	case Z_DATA_ERROR:
		std::cout << "入力バッファがおかしい" << std::endl;
		break;
	default:
		std::cout << "その他" << std::endl;
		break;
	}

	delete[] decode;
	delete[] encode;
 出力

コード:

暗号化に成功
暗号化前サイズ:68,暗号化後サイズ:29
暗号化データ:x廳L Hпd0HD) €.佇タ
復号に成功
復号前サイズ:29,復号後サイズ:68
復号データ:aaaaaabbbbbbbaaaaaaaaaccccccaaaaaaaaaaaaaaaadddddaaaaaaaaaaaaaaaaa
 多分私の方でのzlibがおかしいのではと思いましたが、上記のように動くコードもあるのでちょっと訳がわからないくなってきました。
 zlibのバージョンのせいかvisual studioでのzlibのビルドがおかしいのかと考えましたが、あまりそういうのには自信がないため、誰かアドバイス等をいただけるとありがたいです。
 zlibのバージョンは1.2.11で"/contrib/vstudio/vc14/zlibvc.sln"を開いてReleaseでビルドしたものを使っています。

 私が書いたzlibのコードは申し訳ありませんが整理がついていないためお見せすることはできません。

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

Re: [C++]zlibを用いたpngの解凍と読み込み

#4

投稿記事 by みけCAT » 5年前

いろは さんが書きました:
5年前

コード:

	//圧縮画像データを結合
	comp_img = new unsigned char[comp_size]; comp_size = 0;
	for (auto itr = img_index.begin(); itr != img_index.end(); ++itr) {
		iml::copy_order(&comp_img[comp_size], itr->second, itr->first);
		comp_size += itr->first;
	}
これはいけませんね。
comp_imgにポインタを代入しているバッファはcomp_sizeバイトしか確保していないのに、
いきなりcomp_sizeバイト目から書き込んでいるので、範囲外への書き込みが発生します。
このことにより何らかのデータが破壊される可能性があり、危険です。

comp_sizeを使い回さず、「どこからコピーするか」を管理する変数を別に用意して使うと改善するでしょう。
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

いろは
記事: 11
登録日時: 7年前

Re: [C++]zlibを用いたpngの解凍と読み込み

#5

投稿記事 by いろは » 5年前

 みけCATさんの指摘した点について。
 一応"comp_size"はこの分のメモリ確保した直後に0を代入しているのでメモリ範囲外の書き込みについては大丈夫だと思います。でもやはり、変数の使いまわしはどうも汚い感じがしますので改善したいと思います。
 あと、指摘で気づきましたが、"iml::copy_order"という関数は私自身が作って使っているライブラリの関数で、イテレータ範囲やサイズ指定の配列を順オーダーで代入するといったもので、ソースは以下のようになります。

コード:

//配列のコピー(順)
template <class OutputIterator, class InputIterator>
OutputIterator copy_order(OutputIterator first1, InputIterator first2, size_t n) {

	//アサート処理は省略

	for (size_t i = 0; i < n; ++i, ++first1, ++first2)
		*first1 = *first2;
	return first1;
}
 もう少しわかりやすいコードを書いていきたいと思います。

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

Re: [C++]zlibを用いたpngの解凍と読み込み

#6

投稿記事 by みけCAT » 5年前

いろは さんが書きました:
5年前
一応"comp_size"はこの分のメモリ確保した直後に0を代入しているのでメモリ範囲外の書き込みについては大丈夫だと思います。
ごめんなさい、見落としました。
現状で大丈夫そうですね。
考え直してみます。
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

Re: [C++]zlibを用いたpngの解凍と読み込み

#7

投稿記事 by みけCAT » 5年前

データ構造によってはデータが連続せず、
何も考えずに配列として先頭要素へのポインタを渡してしまうと変な所にアクセスしておかしくなってしまうかもしれません。
iml::zlib::decodeの返り値の型(deflate::decodeの返り値の型?)は何ですか?
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

Re: [C++]zlibを用いたpngの解凍と読み込み

#8

投稿記事 by みけCAT » 5年前

ValgrindやAddressSanitizerなどで範囲外アクセスなどを起こしていないかチェックしてみるのもいいかもしれないですね。
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

返信

“C言語何でも質問掲示板” へ戻る