Try to save the data that can be read by JavaFX as PNG

I don't have time to write sentences, so only the points.

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

Header format

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.

Chunk format

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)

Required chunks for saving full-color images

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

Create an IDAT.

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) *

Write END.

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.

Sample code

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

Try to save the data that can be read by JavaFX as PNG
How to make a key pair of ecdsa in a format that can be read by Java
Try the lightweight JavaScript engine “QuickJS” that can be incorporated into C / C ++
An active hash that can be treated as data even if it is not in the database
Android development-WEB access (GET) Try to get data by communicating with the outside. ~
Object-oriented that can be understood by fairies
Continuation ・ Active hash that can be handled as data even if it is not in the database ~ Display
About the matter that hidden_field can be used insanely
I tried a puzzle that can only be solved by the bottom 10% of bad engineers
Four-in-a-row with gravity that can be played on the console
You can solve the problem by referring to the two articles !!!
Introduction to Rakefile that can be done in about 10 minutes
Find a Switch statement that can be converted to a Switch expression
A collection of patterns that you want to be aware of so as not to complicate the code
Let's create a Docker container that can connect to CentOS 8 with the minimum configuration by SSH
[Swift5] How to create a .gitignore file and the code that should be written by default
Read the data of Shizuoka point cloud DB with Java and try to detect the tree height.