Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Glitch PNG Generator/Displayer
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