文章書く時間が無いのでポイントだけ。
(追記:誤字脱字は勘弁してください)
8/24 クリティカルなバグがあったので追記
encoder.setInput(buffer);
encoder.finish(); // これ必須
詳細はPNGの仕様書はどこかで参照してください。
256色のパレットつき画像やグレイスケールにも対応していますが、実際に使うのはほとんどRGBフルカラー 24bit画像(なお48bitにも対応してます)なので今回は無視します。
なお画像はjavafx.scene.image.Imageで読み込んでます(楽なので)
ヘッダの形式
最初に6文字のシグネチャが入っています。
static public byte[] getPNGSignature () {
return new byte[]{(byte)0x89, (byte)0x50, (byte)0x4E, (byte)0x47,
(byte)0x0D, (byte)0x0A, (byte)0x1A, (byte)0x0A};
}
無理矢理開くと .PNG... などと読めます。まずこいつをファイルに書き込みます。
チャンクの形式
PNGは、チャンクという箱の中にデータ分けてを入れます。データはBig Endianで格納されます(Intel CPUはLittel Endianなので逆になります)
以下の形式になってます。
4byte チャンクサイズ
4byte チャンクネーム(4文字の英数字)
.....
..... チャンクサイズ分のデータ
.....
4byte CRC32(チャンクネーム+チャンクデータまで)
フルカラー画像を保存する場合必須なチャンク
仕様書を読んでいると沢山出てきますが、ほとんど無視して構わないので、必須なのは3つ。
IHDR 画像のヘッダ
IDAT 画像本体のデータ
IEND ファイルの終わりに付ける
PLTEはパレットが無いので要りません。
IHDRはチャンクサイズが13、IENDは0で固定されています。IENDはデータが無いのでIENDはCRCはつねに同じになります。
IHDRは、以下の様になります
4byte 画像の横サイズ(width)
4byte 画像の縦サイズ(height)
1byte bitdepth (フルカラーなら8か16)
1byte colortype (フルカラーなら2か6 6=アルファチャンネル付き)
1byte 圧縮アルゴリズム (0 = 1種類しかないから)
1byte フィルター (無しなら 0)
1byte インタレース (無しなら 0)
今回は、アルファチャンネルもフィルターもインタレースをサポートする気がないので、width,height,8,2,0,0,0 となります。
HEXで書くとこんな感じです。
00 00 00 0D 49 48 44 52 13 IHDR
XX XX XX XX YY YY YY YY width height
08 02 00 00 00 CC CC CC 8bit フルカラー CC=CRC
CC
IDATを作成する。
先に圧縮用のデータを作成します。24bitフルカラーなら
FRGBRGBRGB ...
FRGBRGBRGB ...
となります。最初のFはなんぞやですが、仕様書を見るとラインの先頭にフィルター用に1byte必要と書いてあります。フィルターかけないので0を埋めておきます。
アルファチャンネル付きは FRGBARGBA... であってるんでしょうか……
それで作ったバッファを deflaterに突っ込むだけです。サイズを取得したらヘッダに加えて、CRCを計算して書き出します。
deflaterはLZWより圧縮率が悪いと嘘が書いてあるサイトがありましたが、LZ77をハフマン符号や算術符号化と組み合わせることで改善したもでLZWより平均3割以上圧縮率が高いです(その代わり遅い)
ENDを書き込む。
固定値なので何も考える必要はありません。
ヘッダ作成が簡単で、圧縮は、Defraterを呼ぶだけなので、下手にライブラリを叩くより楽です(あのImageWriterのヘッダ編集の面倒さと行ったら……)。CRCは仕様書にあるサンプルコードを少し書き直すだけです。
サンプルコード
PNGとPNGSaverにクラスをわけた意味があるのかよく分かりません。後からなにか加えようかと思ったからの様な。
import java.util.zip.Deflater;
import javafx.scene.image.Image;
public interface PNG {
static public byte[] getPNGSignature () {
return new byte[]{(byte)0x89, (byte)0x50, (byte)0x4E, (byte)0x47,
(byte)0x0D, (byte)0x0A, (byte)0x1A, (byte)0x0A};
}
static public final int HEADER_SIZE = 4;
public static PNGChunk createIHDR(int width,int height,int bitdepth,int colortype,int filter,int interlace) {
PNGChunk header = new PNGChunk(ChunkTYPE.IHDR);
byte[] buffer = header.getBuffer();
buffer[0] = (byte) (width >>> 24 & 0xff);
buffer[1] = (byte) (width >>> 16 & 0xff);
buffer[2] = (byte) (width >>> 8 & 0xff);
buffer[3] = (byte) (width >>> 0 & 0xff);
buffer[4] = (byte) (height >>> 24 & 0xff);
buffer[5] = (byte) (height >>> 16 & 0xff);
buffer[6] = (byte) (height >>> 8 & 0xff);
buffer[7] = (byte) (height >>> 0 & 0xff);
buffer[8] = (byte) bitdepth;
buffer[9] = (byte) colortype;
buffer[10] = 0; //compress type a
buffer[11] = (byte) filter;
buffer[12] = (byte) interlace;
return header;
}
public static PNGChunk createIHDR(int width,int height) {
return createIHDR(width,height,8,2,0,0);
}
public static PNGChunk createIEND() {
return new PNGChunk(ChunkTYPE.IEND);
}
public static PNGChunk createIDAT(Image img) {
int width = (int) img.getWidth();
int height = (int) img.getHeight();
PNGChunk data = new PNGChunk(ChunkTYPE.IDAT);
int raw = width * 3 + 1; // add Filter Byte(1byte)
byte[] buffer = new byte[raw * height ];
byte[] outbuffer = new byte[raw * height ];
for (int y = 0 ; y < height ; y++) {
int offset = y * raw ;
buffer[offset++] = 0; // scan line first byte is Filter Byte(1byte) /zero because no use filter
for (int x = 0 ; x < width ; x++) {
int color = img.getPixelReader().getArgb(x, y);
buffer[offset++] = (byte) ((color >>> 16) & 0xff); //R
buffer[offset++] = (byte) ((color >>> 8) & 0xff); //G
buffer[offset++] = (byte) ((color >>> 0) & 0xff); //B
}
}
Deflater encoder = new Deflater();
encoder.setInput(buffer);
encoder.finish(); // add Aug 24,2018 MUST USE
int compresslength = encoder.deflate(outbuffer);
data.setBuffer(outbuffer);
data.setLength(compresslength);
return data;
}
}
public class PNGsaver implements PNG {
static public void PNGWriteFile (String outpath,javafx.scene.image.Image img) throws IOException {
File outFile = new File(outpath);
if (! outFile.exists()) {
FileOutputStream outStream = ( new FileOutputStream(outpath));
outStream.write(PNG.getPNGSignature());
PNGChunk header = PNG.createIHDR((int)img.getWidth(),(int)img.getHeight());
header.writeChunk(outStream);
PNGChunk data =PNG.createIDAT(img);
data.writeChunk(outStream);
PNGChunk eof = PNG.createIEND();
eof.writeChunk(outStream);
outStream.close();
} else {
System.err.println("File is already exist.");
}
}
}
Chunk管理用
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
public class PNGChunk {
private ChunkTYPE type;
private int length;
private boolean crcClucFlag = true;
private long crc;
private byte[] buffer = null;
private CRC crcCulc = new CRC();
public PNGChunk(ChunkTYPE chunkType) {
this.setType(chunkType);
switch (chunkType) {
//Must need
case IHDR: // HEADER
this.setLength(13); // always 13
break;
case PLTE: // Color pallet
break;
case IDAT: // Image Data
break;
case IEND: // END of FILE
this.setLength(0); // always ZERO
//0xAE 0x42 0x60 0x82
this.crc = crcCulc.createCRC(null, 0,this.type);
crcClucFlag = false;
break;
// APNG Chunks
case acTL: // Animation Control
break;
case fcTL: // Frame Control
break;
case fdAT: // Frame Data
break;
default:
// must before PLTE and IDAT
// cHRM,
// tRNS,
// gAMA, // Gamma scale
// sRGB, // sRPG
// between PLTE and IDAT
// iCCP,
// bKGD,
// before IDAT
// pHYs,
// hIST,
// non constrains
// tIME, // modify time - only single chunk
// Multiple chunk OK
// sPLT,
// tEXt, // TEXT
// iTXt, // i18n TEXT
// zTXt, // Archived TEXT
}
}
private void setCRC(long crc) {
this.crc = crc;
}
private void setType(ChunkTYPE chunkType) {
this.type = chunkType;
}
public ChunkTYPE getType() {
return type;
}
public void culcCRC() {
this.crc = crcCulc.createCRC(this.getBuffer(), this.getLength(),this.type);
}
public long getCRC() {
if (crcClucFlag) {
this.crc = crcCulc.createCRC(this.getBuffer(), this.getLength(),this.type);
}
return crc;
}
public int getLength() {
return length;
}
public void setLength(int length) {
// cannot SET FIXED SIZE HEADER
this.length = length;
if (this.buffer == null ) {
this.buffer = new byte [(int)length];
}
}
public byte[] getBuffer() {
return buffer;
}
public void setBuffer(byte[] buffer) {
this.buffer = buffer;
}
public byte[] getChunkText() {
return this.type.toString().getBytes();
}
public void writeChunk(OutputStream out) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(4);
out.write(buf.putInt((int)getLength()).array());
out.write(getChunkText());
out.write(getBuffer(), 0, (int)getLength());
buf = ByteBuffer.allocate(4);
out.write(buf.putInt((int)getCRC()).array());
}
}