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;
static final int BPP = 3;
// フィルタータイプ
static final int FT_NONE = 0;
static final int FT_SUB = 1;
static final int FT_UP = 2;
static final int FT_AVRG = 3;
static final int FT_PAETH = 4;
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 * BPP + 1));
// 行の先頭は色の持ち方
if (j == 0) {
filterType = readAsInt(imageData, rowHead, 1);
}
int p = rowHead + (j * BPP + 1);
color c = color(readAsInt(imageData, p, 1),
readAsInt(imageData, p + 1, 1),
readAsInt(imageData, p + 2, 1));
c = calculateUnfilteredColor(filterType, c, colors, j, i, imageInfo);
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();
}
/**
* 引数に渡された2つのcolorを加算する。加算の結果、RGBいずれかの値が
* 255を越えた場合、その値から256を引いた値を使用する。
*
* @param c1 加算対象のcolor1
* @param c2 加算対象のcolor2
* @return {color} 加算後のcolor
*/
color colorAdditionWithOverFlow(color c1, color c2) {
float r = ( red(c1) + red(c2)) % 256;
float g = (green(c1) + green(c2)) % 256;
float b = ( blue(c1) + blue(c2)) % 256;
return color(r, g, b);
}
/**
* PAETH フィルター計算に利用
*/
float paethPredictor(float a, float b, float c) {
float p = a + b - c;
float pa = abs(p - a);
float pb = abs(p - b);
float pc = abs(p - c);
if (pa <= pb && pa <= pc) {
return a;
} else if (pb <= pc) {
return b;
} else {
return c;
}
}
/**
* IDATから取得した色データを逆フィルタリングして生の色値に変換する
*
* @param filterType フィルターのタイプ(0..4)
* @param c 逆フィルタリング対象のcolor
* @param rowColors 逆フィルタリング済のcolorの配列
* @param x 逆フィルタリング対象のcolorの、画像上におけるX座標
* @param y 逆フィルタリング対象のcolorの、画像上におけるY座標
* @param imageInfo 画像情報
* @return {color} 逆フィルタリング後のcolor
*/
color calculateUnfilteredColor(int filterType, color c, color[] rawColors,
int x, int y, HeaderInfo imageInfo) {
color left = color(0), above = color(0), upperLeft = color(0);
switch (filterType) {
case FT_NONE:
// do nothing.
break;
case FT_SUB:
if (x > 0) left = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
c = colorAdditionWithOverFlow(c, left);
break;
case FT_UP:
if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
c = colorAdditionWithOverFlow(c, above);
break;
case FT_AVRG:
if (x > 0) left = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
int avrgR = floor(( red(left) + red(above)) / 2);
int avrgG = floor((green(left) + green(above)) / 2);
int avrgB = floor(( blue(left) + blue(above)) / 2);
color avrg = color(avrgR, avrgG, avrgB);
c = colorAdditionWithOverFlow(c, avrg);
break;
case FT_PAETH:
if (x > 0) left = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
if (x > 0 && y > 0) {
upperLeft = rawColors[((y - 1) * imageInfo.imageWidth) + (x - 1)];
}
float paethR = paethPredictor(red(left), red(above), red(upperLeft));
float paethG = paethPredictor(green(left), green(above), green(upperLeft));
float paethB = paethPredictor(blue(left), blue(above), blue(upperLeft));
c = colorAdditionWithOverFlow(c, color(paethR, paethG, paethB));
break;
default:
break;
}
return c;
}
/**
* 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