(Addition: Please forgive typographical errors)
** 8/24 Added because there was a critical bug **
encoder.setInput(buffer);
encoder.finish(); //This is required
See the PNG specification somewhere for details. It also supports images with a 256-color palette and grayscale, but since most of the images actually used are RGB full-color 24-bit images (which also support 48-bit), I will ignore them this time.
The image is read by javafx.scene.image.Image (because it is easy)
First contains a 6-character signature.
static public byte[] getPNGSignature () {
return new byte[]{(byte)0x89, (byte)0x50, (byte)0x4E, (byte)0x47,
(byte)0x0D, (byte)0x0A, (byte)0x1A, (byte)0x0A};
}
If you forcibly open it, you can read .PNG ... etc. First write this to a file.
PNG puts data separately in a box called chunk. Data is stored in Big Endian (Intel CPU is Littel Endian, so the opposite is true)
It has the following format.
4byte chunk size 4byte chunk name (4 alphanumeric characters) ..... ..... chunk size data ..... 4byte CRC32 (up to chunk name + chunk data)
When you read the specifications, you will see a lot of them, but you can almost ignore them, so three are essential.
IHDR image header IDAT image body data Attached to the end of the IEND file
PLTE is not needed as it does not have a pallet. IHDR has a fixed chunk size of 13 and IEND has a fixed chunk size of 0. Since IEND has no data, IEND will always have the same CRC.
IHDR is as follows Horizontal size (width) of 4byte image Vertical size of 4byte image (height) 1byte bitdepth (8 or 16 for full color) 1byte colortype (2 or 6 6 = with alpha channel for full color) 1byte compression algorithm (because there is only 0 = 1 type) 1byte filter (0 without) 1byte interlace (0 without)
This time, neither the alpha channel nor the filter is willing to support interlacing, so it's width, height, 8,2,0,0,0.
Writing in HEX looks like this.
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 Full Color CC = CRC CC
First, create the data for compression. 24-bit full color FRGBRGBRGB ... FRGBRGBRGB ...
It will be. The first F is awkward, but when I look at the specifications, it says that 1 byte is required for the filter at the beginning of the line. Since it is not filtered, fill in 0.
All you have to do is insert the buffer created with it into the deflater. After getting the size, in addition to the header, calculate and write the CRC.
There was a site that lied that * deflater had a worse compression rate than LZW, but it was improved by combining LZ77 with Huffman coding and arithmetic coding, and the compression rate is higher than LZW by 30% or more on average (that). Instead slow) *
It's a fixed value so you don't have to think about anything.
Header creation is easy, and compression is easier than hitting the library poorly because it only calls Defrater (if you go with the trouble of editing the header of that ImageWriter ...). CRC is just a small rewrite of the sample code in the spec.
I'm not sure if it makes sense to separate the classes into PNG and PNG Saver. It seems that I thought I'd add something later.
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.");
}
}
}
For Chunk management
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());
}
}
Recommended Posts