-
-
Save hoshi-sano/ccdaaa0af5e91c858ed1 to your computer and use it in GitHub Desktop.
display PNG image without using PImage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.ByteArrayOutputStream; | |
import java.util.zip.DataFormatException; | |
import java.util.zip.Inflater; | |
import java.util.zip.CRC32; | |
import javax.xml.bind.DatatypeConverter; | |
static final String PNG_FILE_SIGNATURE = "89504e470d0a1a0a"; | |
static final String HEX_IHDR = "49484452"; | |
static final String HEX_IDAT = "49444154"; | |
static final String HEX_IEND = "49454e44"; | |
static final int IHDR = 1; | |
static final int IDAT = 2; | |
static final int IEND = 3; | |
static final HashMap CHANK_TYPE_MAP = new HashMap() {{ | |
put(HEX_IHDR, IHDR); | |
put(HEX_IDAT, IDAT); | |
put(HEX_IEND, IEND); | |
}}; | |
static final int FILE_SIGNATURE_SIZE = 8; | |
// チャンクの構造に共通する定数 | |
static final int LENGTH_SIZE = 4; | |
static final int CHANK_TYPE_SIZE = 4; | |
static final int CRC_SIZE = 4; | |
// IHDR チャンクに関連する定数 | |
static final int IHDR_DATA_SIZE = 13; | |
static final int IHDR_IW_SIZE = 4; | |
static final int IHDR_IH_SIZE = 4; | |
static final int IHDR_BD_SIZE = 1; | |
static final int IHDR_CT_SIZE = 1; | |
static final int IHDR_CM_SIZE = 1; | |
static final int IHDR_FM_SIZE = 1; | |
static final int IHDR_IM_SIZE = 1; | |
// Inflate(解凍)する単位(byte) | |
static final int DECOMPRESS_UNIT = 1024; | |
void setup() { | |
byte b[] = loadBytes("Lenna.png"); | |
int idx = 0; | |
// シグネチャをチェック | |
String str = readAsHexString(b, idx, FILE_SIGNATURE_SIZE); | |
if (!isPng(str)) { | |
println("ERROR: This file is broken, or not PNG."); | |
return; | |
} else { | |
idx += FILE_SIGNATURE_SIZE; | |
} | |
int length = -1; | |
int iChankType = -1; | |
HeaderInfo imageInfo = null; | |
byte[] idat = null; | |
// データの末尾までチャンク単位で読み込む | |
while (idx >= 0) { | |
length = readChankDataLength(b, idx); | |
iChankType = readChankType(b, idx + LENGTH_SIZE); | |
switch (iChankType) { | |
case IHDR: | |
// 画像ヘッダから画像情報(幅、高さ、...etc)を取得 | |
imageInfo = readIHDR(b, idx); | |
break; | |
case IDAT: | |
// 画像データ | |
if (idat == null) { | |
idat = readIDAT(b, idx, length); | |
} else { | |
// IDAT チャンクは複数存在する可能性がある | |
idat = concat(idat, readIDAT(b, idx, length)); | |
} | |
break; | |
case IEND: | |
// 画像終端 | |
idx = -1; | |
break; | |
default: | |
break; | |
} | |
// IEND まで到達していない場合は idx を加算してループ | |
if (idx >= 0) { | |
int chankSize = LENGTH_SIZE + CHANK_TYPE_SIZE + length + CRC_SIZE; | |
idx += chankSize; | |
} | |
} | |
// IDAT から読んだデータは圧縮されているため展開する | |
byte[] imageData = getByteArrayFromDeflatedData(idat); | |
// 展開したデータを color に変換 | |
color[] colors = new color[imageInfo.imageWidth * imageInfo.imageHeight]; | |
for (int i = 0; i < imageInfo.imageHeight; i++) { | |
int filterType = 0; | |
for (int j = 0; j < imageInfo.imageWidth; j++) { | |
int rowHead = (i * (imageInfo.imageWidth * 3 + 1)); | |
// 行の先頭は色の持ち方 | |
if (j == 0) { | |
filterType = readAsInt(imageData, rowHead, 1); | |
} | |
// TODO: filterType によって処理は変更する必要がある | |
int p = rowHead + (j * 3 + 1); | |
color c = color(readAsInt(imageData, p, 1), | |
readAsInt(imageData, p + 1, 1), | |
readAsInt(imageData, p + 2, 1)); | |
colors[(i * imageInfo.imageWidth) + j] = c; | |
} | |
} | |
// 最後に画像を表示 | |
size(imageInfo.imageWidth, imageInfo.imageHeight); | |
for (int i = 0; i < imageInfo.imageHeight; i++) { | |
for (int j = 0; j < imageInfo.imageWidth; j++) { | |
set(j, i, colors[i * imageInfo.imageWidth + j]); | |
} | |
} | |
} | |
void draw() { | |
} | |
/** | |
* byte列を指定した位置から指定した長さ分読み込んで16進数へ変換する | |
* | |
* @param bytes 読み込むbyte列 | |
* @param init_idx 読み込み開始位置 | |
* @param length 読み込む長さ | |
* @return {String} 読み込んだbyte列を16進数変換した文字列 | |
*/ | |
String readAsHexString(byte[] bytes, int init_idx, int length) { | |
StringBuffer strbuf = new StringBuffer(length); | |
for (int i = init_idx; i < (init_idx + length); i++) { | |
// バイト値を自然数に変換 | |
int bt = bytes[i] & 0xff; | |
if (bt < 0x10) { | |
// 0x10以下の場合、文字列バッファに0を追加 | |
strbuf.append("0"); | |
} | |
strbuf.append(Integer.toHexString(bt)); | |
} | |
return strbuf.toString(); | |
} | |
/** | |
* byte列の指定した位置から指定した長さ分読み込んで整数へ変換する | |
* | |
* @param bytes 読み込むbyte列 | |
* @param init_idx 読み込み開始位置 | |
* @param length 読み込む長さ | |
* @return {int} 読み込んだbyte列を整数に変換したもの | |
*/ | |
int readAsInt(byte[] bytes, int init_idx, int length) { | |
String hexStr = readAsHexString(bytes, init_idx, length); | |
return Integer.parseInt(hexStr, 16); | |
} | |
/** | |
* byte列の指定した位置から指定した長さ分読み込んで実数へ変換する | |
* | |
* @param bytes 読み込むbyte列 | |
* @param init_idx 読み込み開始位置 | |
* @param length 読み込む長さ | |
* @return {long} 読み込んだbyte列を実数に変換したもの | |
*/ | |
long readAsLong(byte[] bytes, int init_idx, int length) { | |
String hexStr = readAsHexString(bytes, init_idx, length); | |
return Long.parseLong(hexStr, 16); | |
} | |
/** | |
* ファイルシグネチャからPNG画像かどうかを判定する | |
* | |
* @param signatureHexStr 16進数文字列化したファイルシグネチャ | |
* @return {boolean} | |
*/ | |
boolean isPng(String signatureHexStr) { | |
return signatureHexStr.equals(PNG_FILE_SIGNATURE); | |
} | |
/** | |
* byte列の指定した位置からチャンクデータの長さを読み込んで返す | |
* | |
* @param bytes 読み込むbyte列 | |
* @param idx 読み込み開始位置 | |
* @return {int} チャンクデータの長さ | |
*/ | |
int readChankDataLength(byte[] bytes, int idx) { | |
return readAsInt(bytes, idx, LENGTH_SIZE); | |
} | |
/** | |
* byte列の指定した位置からチャンクタイプを読み込んで返す | |
* | |
* @param bytes 読み込むbyte列 | |
* @param idx 読み込み開始位置 | |
* @return {int} チャンクタイプを表す整数値(IHDR(1),IDAT(2),...) | |
*/ | |
int readChankType(byte[] bytes, int idx) { | |
String sChankType = readAsHexString(bytes, idx, CHANK_TYPE_SIZE); | |
int res = -1; | |
try { | |
res = (Integer)CHANK_TYPE_MAP.get(sChankType); | |
} catch (NullPointerException e) { | |
// CHANK_TYPE_MAP に存在しない chank type の場合。 | |
// 最終的には NullPointerException が発生しないようにするのが望まし | |
// いが、とりあえずは catch して何もしない。 | |
} | |
return res; | |
} | |
/** | |
* チャンクのデータ部の開始位置を返す | |
* | |
* @param idx チャンクの開始位置 | |
* @return {int} データ部の開始位置 | |
*/ | |
int getChankDataPosition(int idx) { | |
return idx + LENGTH_SIZE + CHANK_TYPE_SIZE; | |
} | |
/** | |
* IHDRチャンクを解析して画像情報を返す | |
* | |
* @param bytes 読み込むbyte列 | |
* @param idx チャンクの開始位置 | |
* @return {HeaderInfo} 画像情報 | |
*/ | |
HeaderInfo readIHDR(byte[] bytes, int idx) { | |
byte[] chankData = new byte[IHDR_DATA_SIZE]; | |
int dataPos = getChankDataPosition(idx); | |
arrayCopy(bytes, dataPos, chankData, 0, IHDR_DATA_SIZE); | |
// CRCのチェック | |
long crc = readAsLong(bytes, dataPos + IHDR_DATA_SIZE, CRC_SIZE); | |
boolean valid = | |
verifyCRC(DatatypeConverter.parseHexBinary(HEX_IHDR), chankData, crc); | |
if (!valid) { println("WARN: IHDR CRC is not valid."); } | |
return new HeaderInfo(chankData); | |
} | |
/** | |
* IDATチャンクを抽出してbyte列を返す | |
* | |
* @param bytes 読み込むbyte列 | |
* @param idx チャンクの開始位置 | |
* @param length 対象のIDATチャンクのデータ部のサイズ | |
* @return {byte[]} IDATチャンクのデータ部 | |
*/ | |
byte[] readIDAT(byte[] bytes, int idx, int length) { | |
byte[] chankData = new byte[length]; | |
int dataPos = getChankDataPosition(idx); | |
arrayCopy(bytes, dataPos, chankData, 0, length); | |
// CRCのチェック | |
long crc = readAsLong(bytes, dataPos + length, CRC_SIZE); | |
boolean valid = | |
verifyCRC(DatatypeConverter.parseHexBinary(HEX_IDAT), chankData, crc); | |
if (!valid) { println("WARN: IDAT CRC is not valid."); } | |
return chankData; | |
} | |
/** | |
* チャンクのCRCを検証する | |
* | |
* @param typeBytes チャンクタイプを表すbyte列 | |
* @param data チャンクのデータ部であるbyte列 | |
* @param crc チャンクのCRC部から取得した実数値 | |
* @return {boolean} 検証の結果、正しいか否かを返す | |
*/ | |
boolean verifyCRC(byte[] typeBytes, byte[] data, long crc) { | |
CRC32 crc32 = new CRC32(); | |
crc32.update(typeBytes); | |
crc32.update(data); | |
long calculated = crc32.getValue(); | |
return (calculated == crc); | |
} | |
/** | |
* deflateで圧縮されたデータを展開して返す | |
* | |
* @param bytes 圧縮されたデータのbyte列 | |
* @return {byte[]} 展開後のデータのbyte列 | |
*/ | |
byte[] getByteArrayFromDeflatedData(byte[] bytes) { | |
ByteArrayOutputStream result = new ByteArrayOutputStream(); | |
Inflater decompresser = new Inflater(); | |
decompresser.setInput(bytes); | |
while(!decompresser.finished()) { | |
try { | |
byte[] resultBuf = new byte[DECOMPRESS_UNIT]; | |
int resultLength = decompresser.inflate(resultBuf); | |
result.write(resultBuf, 0, resultLength); | |
} catch (DataFormatException e) { | |
println("ERROR: Decompress error occurred."); | |
} | |
} | |
decompresser.end(); | |
return result.toByteArray(); | |
} | |
/** | |
* IHDR チャンクから取得した画像情報を解析・保持するためのクラス | |
*/ | |
class HeaderInfo { | |
int imageWidth; | |
int imageHeight; | |
int bitDepth; | |
int colorType; | |
int compressionMethod; | |
int filterMethod; | |
int interlaceMethod; | |
HeaderInfo(byte[] data) { | |
int idx = 0; | |
imageWidth = readAsInt(data, idx, IHDR_IW_SIZE); | |
imageHeight = readAsInt(data, idx += IHDR_IW_SIZE, IHDR_IH_SIZE); | |
bitDepth = readAsInt(data, idx += IHDR_IH_SIZE, IHDR_BD_SIZE); | |
colorType = readAsInt(data, idx += IHDR_BD_SIZE, IHDR_CT_SIZE); | |
compressionMethod = readAsInt(data, idx += IHDR_CT_SIZE, IHDR_CM_SIZE); | |
filterMethod = readAsInt(data, idx += IHDR_CM_SIZE, IHDR_FM_SIZE); | |
interlaceMethod = readAsInt(data, idx += IHDR_FM_SIZE, IHDR_IM_SIZE); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment