概要
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);
}
}
}
}
フロントエンドとは。