一番大変なのは、大量のメモリとそれなりの計算リソースが必用な点でしょう。
例えばPNGを読み込む場合は、Deflateを展開するのに約32kByteのバッファが必要になります(計算に使うメモリがさらに必用です)。また、PNGは隣のピクセルの輝度を参照する場合があるので、少なくとも上1行部のピクセルデータも必用です。ピクセルのバッファは、通常は横解像度の4倍程度で足ります。マイコンで使う画像ならせいぜい横数百ピクセルとして、約1KiBです。STM32F4ならこの程度は問題ないですが、計算リソースが必用なことに変わりはありません。
ところで、PNGには他のピクセルを参照しないフィルタリングモードがあります。また、Deflateも無圧縮データを入れるモードがあります。ではこれらを組み合わせればどうなるかというと、非圧縮画像の出来上がりです。もちろん、PNGデータの仕様内ですから、通常のビューアで表示可能ですし、圧縮データではないので、任意のピクセルを直接取り出すことができます。
シークする際は、ピクセル位置のちょっと面倒な計算以外に、Deflateの仕様による詰め物が有るので、その計算が面倒ですが、ピクセル間の計算や圧縮データの展開に比べれば微々たるものでしょう。
今回、Deflateにかなりつまずきました。
PNGはビッグエンディアンですが、Deflateはリトルエンディアンです。コレに気がつくのにかなり時間がかかりました。また、Deflateはビットストリームを下位ビットファーストで詰め込んでいくので、例えば「最初の1bitが1の8bitデータ」は0x01が正解となります。
Deflateに無圧縮データを入れる場合、4バイトのヘッダを入れます。内訳は16bitの長さデータと、それをビット反転した16bit分です。つまりデータ長は65535バイトが最大になります。これを超える長さの場合は、都度4バイトのデータ長情報をもたせる必要があります。
さらにDeflate(を格納したzlib)を格納するPNGチャンクは32bitのデータ長を持たせ、これに収まらない場合はチャンクを分ける必要があります。しかし、32bitでは4GB程度を詰め込むことができ、1ピクセル4バイトとしても3万px四方くらいになります。この面積をマイコンで処理することはありえないでしょうし、PCでデータを処理するにしても、そんなに大きな画像はめったにないので、今回は32bit制限は気にしないことにしています。
ということで、マイコンでも扱いやすいPNGデータを作ることができるようになりました。あとはHUB75を制御するマイコンで、非圧縮PNGに対応したローダーを作れば画像を表示できるようになります。画像が表示できるようになれば、APNGに対応させてアニメーションが表示できるようになります。拡張チャンクにPCMデータを入れれば、音声も再生できるかもしれません。
とりあえず簡単にDecompPNGをロードするコードを書いてみましたが、結構面倒ですね。例えばBitmapならファイルヘッダにイメージへのオフセットが書いてあるので、データの先頭54バイトを読んで、いくつかの情報をチェックし、あとは画像データを直接取り出すことができます。
あと将来的にAPNGに対応したいので、決め打ちで画像を読み込むことができません。そのあたりもどうにかしないと。
ガーッと全部データをメモリに読み込んで置ける、PCのプログラムは楽だなぁ、と改めて実感。
using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text; using System.Windows.Forms; namespace PNGDecompress { class Program { [STAThread] static void Main(string[] args) { string srcPath, dstPath; #region 入力ファイルと出力ファイルのダイアログ #if true using (OpenFileDialog ofd = new OpenFileDialog()) { ofd.Filter = "PNG|*.png|All|*.*"; if (ofd.ShowDialog() != DialogResult.OK) { return; } srcPath = ofd.FileName; } using (SaveFileDialog sfd = new SaveFileDialog()) { sfd.Filter = "PNG|*.png|All|*.*"; if (sfd.ShowDialog() != DialogResult.OK) { return; } dstPath = sfd.FileName; } #else #endif #endregion byte[] srcData; #region バイト列に読み込み using (FileStream fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read)) { srcData = new byte[fs.Length]; fs.Read(srcData, 0, srcData.Length); } #endregion Listlist = new List (); Chunk_IHDR IHDR = null; #region チャンクで分割 foreach (byte[] chunk in ParseChunks(srcData)) { Chunk c = new Chunk() { Type = Encoding.ASCII.GetString(chunk, 4, 4), Data = chunk, }; list.Add(c); switch (c.Type) { case "IHDR": IHDR = new Chunk_IHDR(c); break; } } #endregion #region ヘッダを確認 if (IHDR == null || IHDR.Compress != 0 || IHDR.Filter != 0 || IHDR.Interlace != 0) { Console.WriteLine("不明なデータ形式です"); Console.WriteLine("終了します"); Console.Write("..."); Console.ReadLine(); return; } #endregion using (MemoryStream ms = new MemoryStream()) { for (int i = 0; i < list.Count; i++) { Chunk c = list[i]; int start = 0; switch (c.Type) { case "IDAT": break; case "fdAT": start = 4; break; default: ms.Write(list[i].Data, 0, list[i].Data.Length); continue; } int len; #region 連続するチャンクを探す for (len = 0; i + len < list.Count && list[i + len].Type == c.Type; len++) ; #endregion #region 連続するチャンクをマージ、展開、フィルタリング解除、圧縮を行う using (MemoryStream msSrc = new MemoryStream()) { // マージ for (int j = 0; j < len; j++) { msSrc.Write(list[i + j].Data, 8, list[i + j].Data.Length - 12); } msSrc.Position = start; byte[] buff = new byte[msSrc.Length - start]; msSrc.Read(buff, 0, buff.Length); buff = zlibDecomp(buff); //展開 Unfiltered(buff, IHDR); // フィルタリング解除 buff = zlibPack(buff, Adler32(buff)); // 圧縮 zlibDecomp(buff); ms.Write((UInt32)(buff.Length + start)); long pos = ms.Position; ms.Write(list[i].Data, 4, 4); if (start > 0) { byte[] tmp = new byte[start]; msSrc.Position = 0; msSrc.Read(tmp, 0, tmp.Length); ms.Write(tmp, 0, tmp.Length); } ms.Write(buff, 0, buff.Length); WriteLastCRC(ms, -(ms.Position - pos)); } #endregion i += len - 1; } #region ファイルのCRCをチェック { byte[] buff = new byte[ms.Length]; ms.Position = 0; ms.Read(buff, 0, buff.Length); ParseChunks(buff); } #endregion #region ファイルに書き出し ms.Position = 0; using (FileStream fs = new FileStream(dstPath, FileMode.Create, FileAccess.Write)) { ms.CopyTo(fs); } #endregion } } static byte[] zlibDecomp(byte[] src) { if ((src[0] << 8 | src[1]) % 31 != 0) throw (new Exception()); using (MemoryStream msSrc = new MemoryStream()) using (MemoryStream msDst = new MemoryStream()) using (DeflateStream ds = new DeflateStream(msSrc, CompressionMode.Decompress)) { msSrc.Write(src, 2, src.Length - 6); msSrc.Position = 0; ds.CopyTo(msDst); byte[] decomp = new byte[msDst.Length]; msDst.Position = 0; msDst.Read(decomp, 0, decomp.Length); return (decomp); } } static byte[] zlibPack(byte[] src, UInt32 Adler) { using (MemoryStream ms = new MemoryStream()) { ms.Write((UInt16)0x081D); for (int i = 0, j; i < src.Length; i += j) { byte type = 0x00; j = src.Length - i; if (j > 0xFFFF) j = 0xFFFF; else type = 0x01; ms.Write(new byte[]{ type, (byte)(j >> 0 & 0xFF), (byte)(j >> 8 & 0xFF), (byte)(~j >> 0 & 0xFF), (byte)(~j >> 8 & 0xFF), }, 0, 5); ms.Write(src, i, j); } ms.Write(Adler); byte[] compress = new byte[ms.Length]; ms.Position = 0; ms.Read(compress, 0, compress.Length); return (compress); } } static void Unfiltered(byte[] src, Chunk_IHDR IHDR) { for (int y = 1; y < IHDR.BytesPerLine * IHDR.Height; y += IHDR.BytesPerLine) { byte ftype = src[y - 1]; src[y - 1] = 0; switch (ftype) { case 1: for (int x = IHDR.BytesPerPixel; x < IHDR.Width * IHDR.BytesPerPixel; x++) { src[y + x] += src[y + x - IHDR.BytesPerPixel]; } break; case 2: for (int x = 0; x < IHDR.Width * IHDR.BytesPerPixel; x++) { src[y + x] += src[y - IHDR.BytesPerLine + x]; } break; case 3: for (int x = 0; x < IHDR.Width * IHDR.BytesPerPixel; x++) { byte a = 0, b = 0; if (x >= IHDR.BytesPerPixel) a = src[y + x - IHDR.BytesPerPixel]; if (y - 1 > IHDR.BytesPerLine) b = src[y - IHDR.BytesPerLine + x]; src[y + x] += (byte)((a + b) / 2); } break; case 4: for (int x = 0; x < IHDR.Width * IHDR.BytesPerPixel; x++) { byte a = 0, b = 0, c = 0; if (x > IHDR.BytesPerPixel) a = src[y + x - IHDR.BytesPerPixel]; if (y > IHDR.BytesPerLine) b = src[y - IHDR.BytesPerLine + x]; if (y > IHDR.BytesPerLine && x > IHDR.BytesPerPixel) c = src[y - IHDR.BytesPerLine + x - IHDR.BytesPerPixel]; int p = a + b - c; int pa = Math.Abs(p - a); int pb = Math.Abs(p - b); int pc = Math.Abs(p - c); src[y + x] += pa <= pb && pa <= pc ? a : pb <= pc ? b : c; } break; } } } static UInt32 Adler32(byte[] data) { UInt32 a = 1, b = 0; foreach (byte d in data) { a = (a + d) % 65521; b = (b + a) % 65521; } return ((b << 16) | a); } static byte[][] ParseChunks(byte[] ByteData) { if (ByteData[0] != 0x89 || Encoding.UTF8.GetString(ByteData, 1, 7) != "PNG\r\n\x1A\n") { throw (new Exception()); } List Chunks = new List (); { byte[] buff = new byte[8]; Array.Copy(ByteData, 0, buff, 0, 8); Chunks.Add(buff); } for (int pos = 8; pos < ByteData.Length;) { int len = (int)ToUInt32BE(ByteData, pos); string type = Encoding.ASCII.GetString(ByteData, pos + 4, 4); UInt32 crc = ToUInt32BE(ByteData, pos + 4 + 4 + len); if (crc != CRC.Calc(ByteData, pos + 4, len + 4)) { throw (new Exception()); } byte[] buff = new byte[4 + 4 + len + 4]; Array.Copy(ByteData, pos, buff, 0, buff.Length); Chunks.Add(buff); pos += buff.Length; } return (Chunks.ToArray()); } static UInt32 ToUInt32BE(byte[] value, int startIndex) { return ( (UInt32)value[startIndex + 0] << 24 | (UInt32)value[startIndex + 1] << 16 | (UInt32)value[startIndex + 2] << 8 | (UInt32)value[startIndex + 3]); } static void WriteLastCRC(Stream stream, long startIndex) { if (startIndex < 0) { startIndex = stream.Position + startIndex; } stream.Position = startIndex; byte[] buff = new byte[stream.Length - stream.Position]; stream.Read(buff, 0, buff.Length); stream.Write(CRC.Calc(buff, 0, buff.Length)); } class CRC { // [PNGで使うCRC32を計算する - Qiita #テーブルを導入する](http://qiita.com/mikecat_mixc/items/e5d236e3a3803ef7d3c5#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B) static readonly UInt32[] Table; static CRC() { UInt32 magic = 0xEDB88320; Table = new UInt32[256]; for (int i = 0; i < 256; i++) { UInt32 value = (UInt32)i; for (int j = 0; j < 8; j++) { bool b = (value & 1) != 0; value >>= 1; if (b) { value ^= magic; } } Table[i] = value; } } public static UInt32 Calc(byte[] data, int start, int length) { UInt32 crc = 0xFFFFFFFF; for (int i = start; i < start + length; crc = Table[(crc ^ data[i++]) & 0xFF] ^ (crc >> 8)) ; return (~crc); } } class Chunk { public string Type; public byte[] Data; public Chunk() { } public Chunk(Chunk c) { Type = c.Type; Data = c.Data; } public override string ToString() { return (Type + " " + (Data.Length - 12)); } } class Chunk_IHDR : Chunk { public Chunk_IHDR(Chunk c) : base(c) { } public UInt32 Width { get { return (ToUInt32BE(Data, 8)); } } public UInt32 Height { get { return (ToUInt32BE(Data, 12)); } } public byte Depth { get { return (Data[16]); } } public byte ColorType { get { return (Data[17]); } } public byte Compress { get { return (Data[18]); } } public byte Filter { get { return (Data[19]); } } public byte Interlace { get { return (Data[20]); } } public int BytesPerPixel { get { switch (ColorType) { case 2: if (Depth == 8) return (3); if (Depth == 16) return (6); break; case 3: if (Depth == 8) return (1); break; case 6: if (Depth == 8) return (4); if (Depth == 16) return (8); break; } throw (new Exception()); } } public int BytesPerLine { get { return ((int)(BytesPerPixel * Width + 1)); } } } } static class MyExt { public static void Write(this Stream stream, UInt32 value) { stream.Write(new byte[] { (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)(value), }, 0, 4); } public static void Write(this Stream stream, UInt16 value) { stream.Write(new byte[] { (byte)(value >> 8), (byte)(value), }, 0, 2); } public static void Write(this Stream stream, string value, Encoding encoding) { byte[] buff = encoding.GetBytes(value); stream.Write(buff, 0, buff.Length); } } }
0 件のコメント:
コメントを投稿