LoginSignup
20
23

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-09-02

概要

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

FileAPI

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

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

DataURIとして表示する

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

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 の変換

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

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

参考→http://www.setsuki.com/hsp/ext/png.htm

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

エンディアンについて、ここを見ると、高速化のため?CPU のエンディアン方式などを考慮しているようだが、速度を求めているわけではないので、DataViewを用いてlittleEndianで解析する。

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

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);
        }
      }
    }
}

フロントエンドとは。

20
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
23