一番大変なのは、大量のメモリとそれなりの計算リソースが必用な点でしょう。
例えば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
List list = 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 件のコメント:
コメントを投稿