2017年10月10日火曜日

非圧縮PNG

 PNG推しな僕ですが、マイコンからするとPNGはかなりつらいフォーマットです。
 一番大変なのは、大量のメモリとそれなりの計算リソースが必用な点でしょう。
 例えば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 件のコメント:

コメントを投稿