Skip to content

Instantly share code, notes, and snippets.

@hoshi-sano
Last active August 19, 2023 05:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hoshi-sano/ccdaaa0af5e91c858ed1 to your computer and use it in GitHub Desktop.
Save hoshi-sano/ccdaaa0af5e91c858ed1 to your computer and use it in GitHub Desktop.
display PNG image without using PImage.
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