JavaScriptでpngファイルを解析する

# 概要
PNGファイルについて、バイナリ解析もしたいけど、気軽に表示もしたい、という状況に対処する
※フロントエンド

# FileAPI

FileAPIを用いて、
`.readAsDataURI()`
を用いると、DataURIとして取得できるので、imgタグのsrcにセットするだけで表示できる。

ただ、解析も同時に行いたいので、以降
`.readAsArrayBuffer()`
を用いる。

# DataURIとして表示する

ArrayBufferのままでは表示すらできないので、とりあえず表示したい。

```js
let DataURIFromArrayBuffer = (ab) => {return "data:image/png;base64," + btoa(Array.from(new Uint8Array(ab), e => String.fromCharCode(e)).join(""));}
```

DataURIについても、詳しい説明はリンク先がとても詳しく解説されている。
これで晴れて解析と表示が同時にできるようになった。

参考：[nmm実験室：Blob, ArrayBuffer, Uint8Array, DataURI の変換](http://var.blog.jp/archives/62330155.html)

# PngファイルのArrayBufferを解析する

まず全体の構造として、
PNGのシグネチャがあり、イメージヘッダが続く。その後ろに画像データが続く。

参考→[http://www.setsuki.com/hsp/ext/png.htm](http://www.setsuki.com/hsp/ext/png.htm)

まずイメージヘッダを解析したい。
http://www.setsuki.com/hsp/ext/chunk/IHDR.htm

エンディアンについて、[ここ](http://hakuhin.jp/js/array_buffer.html#ARRAY_BUFFER_05)を見ると、高速化のため？CPU のエンディアン方式などを考慮しているようだが、速度を求めているわけではないので、DataViewを用いてlittleEndianで解析する。

```js
const SIZE_SIGNATURE = 0x08;

let reader = new FileReader();
reader.onload = (e) => {
    let dataView = new DataView(e.target.result, SIZE_SIGNATURE);
    let width = dataView.getUint32(0x08, false);
    let height = dataView.getUint32(0x0C, false);
}
reader.readAsArrayBuffer(file);
```

できた。fileは`<input type="file">`とかで適当にとってくればよい。

# png-parser

```js
const SIZE_SIGNATURE = 0x08;
const IS_LITTLE_ENDIAN = false;
// const COLOR_TYPE_MAP = { 3: 'PNG-8', 2: 'PNG-24', 6: 'PNG-32' };

export default class PngParser {
    constructor(arrayBuffer) {
      this.arrayBuffer = arrayBuffer;
      this.dataView = new DataView(arrayBuffer, SIZE_SIGNATURE);
    }
    getDataURI() {
      return `data:image/png;base64,${btoa(Array.from(new Uint8Array(this.arrayBuffer), e => String.fromCharCode(e)).join(''))}`;
    }
    getUint32(offset) {
      return this.dataView.getUint32(offset, IS_LITTLE_ENDIAN);
    }
    getUint16(offset) {
      return this.dataView.getUint16(offset, IS_LITTLE_ENDIAN);
    }
    getUint8(offset) {
        return this.dataView.getUint8(offset);
    }
    getChar(offset) {
      return String.fromCharCode(this.getUint8(offset));
    }
    getString(offset, number) {
      return Array(number).fill(0).map((v, i) => this.getChar(offset + i)).join('');
    }
    getWidth() {
      return this.getUint32(0x08);
    }
    getHeight() {
      return this.getUint32(0x0C);
    }
    getBitDepth() {
      return this.getUint8(0x10);
    }
    getColorType() {
      return this.getUint8(0x11);
    }
    getCompression() {
      return this.getUint8(0x12);
    }
    getFilter() {
      return this.getUint8(0x13);
    }
    getInterlace() {
      return this.getUint8(0x14);
    }
    getCrc() {
      return this.getUint32(0x15);
    }
    readIHDRChunkData(offset) {
      return {
        width: this.getUint32(offset),
        height: this.getUint32(offset + 4),
        bitDepth: this.getUint8(offset + 8),
        colorType: this.getUint8(offset + 9),
        compression: this.getUint8(offset + 10),
        filter: this.getUint8(offset + 11),
        interlace: this.getUint8(offset + 12),
        crc: this.getUint32(offset + 13),
      };
    }
    readtRNSChunkData(offset, size, colorType) {
      if (colorType === 3) {
        return Array(size).fill(0).map((v, i) => this.getUint8(offset + i));
      } else {
        return Array(size / 2).fill(0).map((v, i) => this.getUint16(offset + (i * 2)));
      }
    }
    readChunk(offset) {
      let currentOffset = offset;
      const size = this.getUint32(currentOffset);
      currentOffset += 4;
      const type = this.getString(currentOffset, 4);
      currentOffset += 4;
      // データを読み飛ばす
      const dataOffset = currentOffset;
      currentOffset += size;
      // CRC
      const crc = this.getUint32(currentOffset);
      currentOffset += 4;
      return {
        size: size,
        type: type,
        crc: crc,
        dataOffset: dataOffset,
        endOffset: currentOffset,
      };
    }
    getChunks() {
      const chunks = [];
      let chunk = { size: 0, type: '', crc: 0, endOffset: 0x00 };
      while (chunk.type !== 'IEND') {
        chunk = this.readChunk(chunk.endOffset);
        chunks.push(chunk);
      }
      return chunks;
    }
    showInformation() {
      const chunks = this.getChunks();
      for (const chunk of chunks) {
        console.log(chunk);
        if (chunk.type === 'IHDR') {
          const ihdr = this.readIHDRChunkData(chunk.dataOffset);
          console.log(ihdr);
        }
      }
    }
}
```

フロントエンドとは。