Created
August 21, 2013 15:52
-
-
Save hoshi-sano/6296296 to your computer and use it in GitHub Desktop.
Glitch PNG Generator/Displayer
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.Deflater; | |
import java.util.zip.Inflater; | |
import java.util.zip.CRC32; | |
import javax.xml.bind.DatatypeConverter; | |
// フィルタータイプ | |
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; | |
// ---------- 変更可能な設定値 ここから ---------- // | |
// 入力ファイル名 | |
static final String INTIAL_INPUT_FILE_NAME = "Lenna.png"; | |
// 出力ファイル名に使用する接頭語 | |
static final String OUTPUT_FILE_NAME_PREFIX = "out_"; | |
// フレームレート | |
// MEMO: 負荷が高くなるのでなるべく大きくしない | |
static final int APP_FRAME_RATE = 1; | |
// ファイルを生成する間隔(sec) | |
static final int INTERVAL = 2; | |
// フィルターを変更するラインの間隔(px) | |
static final int FILTER_CHANGE_FREQUENCY = 20; | |
// フィルターを変更する確率(%) | |
static final int FILTER_CHANGE_PROBABILITY = 10; | |
// ランダムで選択されるフィルターの候補 | |
static final int[] FILTER_CANDIDATES = { | |
FT_NONE, | |
FT_SUB, | |
FT_UP, | |
FT_AVRG, | |
FT_PAETH | |
}; | |
//---------- 変更可能な設定値 ここまで ---------- // | |
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; | |
// Deflate(圧縮)する単位(byte) | |
static final int COMPRESS_UNIT = 1024; | |
static final int BPP = 3; | |
static final int IHDR_CHANK_SIZE = | |
LENGTH_SIZE + CHANK_TYPE_SIZE + IHDR_DATA_SIZE + CRC_SIZE; | |
static final int IEND_CHANK_SIZE = | |
LENGTH_SIZE + CHANK_TYPE_SIZE + CRC_SIZE; | |
byte[] signature = new byte[FILE_SIGNATURE_SIZE]; | |
byte[] ihdr = new byte[IHDR_CHANK_SIZE]; | |
byte[] idat = null; | |
byte[] iend = new byte[IEND_CHANK_SIZE]; | |
HeaderInfo imageInfo = null; | |
PImage dispImg = null; | |
String currentFileName = ""; | |
void setup() { | |
byte b[] = loadBytes(INTIAL_INPUT_FILE_NAME); | |
int idx = 0; | |
arrayCopy(b, idx, signature, 0, FILE_SIGNATURE_SIZE); | |
// シグネチャをチェック | |
String str = readAsHexString(signature, 0, 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; | |
// データの末尾までチャンク単位で読み込む | |
while (idx >= 0) { | |
length = readChankDataLength(b, idx); | |
iChankType = readChankType(b, idx + LENGTH_SIZE); | |
switch (iChankType) { | |
case IHDR: | |
// 画像ヘッダから画像情報(幅、高さ、...etc)を取得 | |
arrayCopy(b, idx, ihdr, 0, IHDR_CHANK_SIZE); | |
imageInfo = readIHDR(ihdr, 0); | |
break; | |
case IDAT: | |
// 画像データ | |
if (idat == null) { | |
idat = readIDAT(b, idx, length); | |
} else { | |
// IDAT チャンクは複数存在する可能性がある | |
idat = concat(idat, readIDAT(b, idx, length)); | |
} | |
break; | |
case IEND: | |
// 画像終端 | |
arrayCopy(b, idx, iend, 0, IEND_CHANK_SIZE); | |
idx = -1; | |
break; | |
default: | |
break; | |
} | |
// IEND まで到達していない場合は idx を加算してループ | |
if (idx >= 0) { | |
int chankSize = LENGTH_SIZE + CHANK_TYPE_SIZE + length + CRC_SIZE; | |
idx += chankSize; | |
} | |
} | |
size(imageInfo.imageHeight, imageInfo.imageWidth); | |
frameRate(APP_FRAME_RATE); | |
createPNG(idat); | |
background(0); | |
dispImg = loadImage(currentFileName); | |
} | |
void draw() { | |
if ((frameCount % (APP_FRAME_RATE * INTERVAL)) == 0) { | |
glitch(); | |
dispImg = loadImage(currentFileName); | |
} | |
image(dispImg, 0, 0); | |
} | |
/** | |
* 引数に渡した数値を4byteのbyte配列に変換して返す | |
* | |
* @param i 変換対象の数値 | |
* @return {byte[]} | |
*/ | |
byte[] to4Bytes(int i) { | |
byte[] b = new byte[4]; | |
b[3] = (byte) (0x000000ff & (i)); | |
b[2] = (byte) (0x000000ff & (i >>> 8)); | |
b[1] = (byte) (0x000000ff & (i >>> 16)); | |
b[0] = (byte) (0x000000ff & (i >>> 24)); | |
return b; | |
} | |
/** | |
* 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列 | |
* @return {long} 算出したCRC値 | |
*/ | |
long calculateChankCRC(byte[] typeBytes, byte[] data) { | |
CRC32 crc32 = new CRC32(); | |
crc32.update(typeBytes); | |
crc32.update(data); | |
return crc32.getValue(); | |
} | |
/** | |
* チャンクのCRCを検証する | |
* | |
* @param typeBytes チャンクタイプを表すbyte列 | |
* @param data チャンクのデータ部であるbyte列 | |
* @param crc チャンクのCRC部から取得した実数値 | |
* @return {boolean} 検証の結果、正しいか否かを返す | |
*/ | |
boolean verifyCRC(byte[] typeBytes, byte[] data, long crc) { | |
long calculated = calculateChankCRC(typeBytes, data); | |
return (calculated == crc); | |
} | |
/** | |
* deflateで圧縮されたデータを展開して返す | |
* | |
* @param bytes 圧縮されたデータのbyte列 | |
* @return {byte[]} 展開後のデータのbyte列 | |
*/ | |
byte[] decompress(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(); | |
} | |
/** | |
* Inflateで展開されたデータを圧縮して返す | |
* | |
* @param bytes 展開されたデータのbyte列 | |
* @return {byte[]} 圧縮後のデータのbyte列 | |
*/ | |
byte[] compress(byte[] bytes) { | |
Deflater compresser = new Deflater(); | |
compresser.setInput(bytes); | |
compresser.finish(); | |
ByteArrayOutputStream result = new ByteArrayOutputStream(); | |
while(!compresser.finished()) | |
{ | |
byte[] resultBuf = new byte[DECOMPRESS_UNIT]; | |
int resultLength = compresser.deflate(resultBuf); | |
result.write(resultBuf, 0, resultLength); | |
} | |
return result.toByteArray(); | |
} | |
void createPNG(byte[] bIdatData) { | |
byte[] bIdatLength = to4Bytes(bIdatData.length); | |
byte[] bIdatType = DatatypeConverter.parseHexBinary(HEX_IDAT); | |
byte[] bIdatCRC = to4Bytes((int)calculateChankCRC(bIdatType, bIdatData)); | |
byte[] idatChank = concat(concat(concat(bIdatLength, bIdatType), | |
bIdatData), bIdatCRC); | |
byte[] output = concat(concat(concat(signature, ihdr), idatChank), iend); | |
String outputFileName = "data/" + OUTPUT_FILE_NAME_PREFIX + frameCount + ".png"; | |
saveBytes(outputFileName, output); | |
currentFileName = outputFileName; | |
} | |
void glitch() { | |
// IDAT から読んだデータは圧縮されているため展開する | |
byte[] imageData = decompress(idat); | |
int filterType = 0; | |
int changedFilterType = 0; | |
for (int i = 0; i < imageInfo.imageHeight; i++) { | |
// フィルタータイプを取得 | |
int rowHead = (i * (imageInfo.imageWidth * BPP + 1)); | |
filterType = readAsInt(imageData, rowHead, 1); | |
// フィルタータイプを何らかのロジックで変更 | |
// ----------- 変更するなら ここから --------------- // | |
if ((i % FILTER_CHANGE_FREQUENCY) == 0) { | |
changedFilterType = FILTER_CANDIDATES[(int)random(FILTER_CANDIDATES.length)]; | |
} | |
filterType = (random(100) < FILTER_CHANGE_PROBABILITY) ? | |
changedFilterType : filterType; | |
// ----------- 変更するなら ここまで --------------- // | |
imageData[rowHead] = ((Integer)filterType).byteValue(); | |
} | |
// 圧縮して IDAT のデータ部に使用可能にする | |
byte[] glitchedImageData = compress(imageData); | |
createPNG(glitchedImageData); | |
} | |
/** | |
* 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