LoginSignup
0
2

More than 5 years have passed since last update.

JavaFXで読み込めるデータをPNGでセーブしてみる

Last updated at Posted at 2018-08-23

文章書く時間が無いのでポイントだけ。

(追記:誤字脱字は勘弁してください)

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());
    }

}
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2