Skip to content
istinnstudio edited this page Apr 17, 2022 · 6 revisions

Snippets and recipes

*WARNING: Some snippets are still not adapted/optimized/tested for version 2.0 *

Since 2.1.0 the test source (not included in the main jar) has a package ar.com.hjg.pngj.cli which includes some command line utilities based on PNG.

Flip horizontally an image

This is basically the code for SampleMirrorImage class in the samples. Notice that this works for every image model - that's because even for the "packed" formats (low bitdepth: one byte packs several samples of 1-2-4 bits) we always have (if we use the standard ImageLineInt storage) one sample per array element.

void static mirrorPng(String orig,String dest) {
 PngReader pngr = new PngReader(orig);
 PngWriter pngw = new PngWriter(dest, pngr.imgInfo, false);
 pngw.copyChunksFrom(pngr.getChunksList());
 for (int row = 0; row < pngr.imgInfo.rows; row++) {
	ImageLineInt line = (ImageLineInt)pngr.readRow();
	mirrorLine(line, pngr.imgInfo);
	pngw.writeRow(line);
 }
 pngr.end();
 pngw.end();
}

void static mirrorLine(ImageLineInt line, ImageInfo iminfo) {
	int channels = iminfo.channels;
	int[imlinei = imline.getScanline();
	for (int c1 = 0, c2 = iminfo.cols - 1; c1 < c2; c1++, c2--) {
		for (int i = 0; i < channels; i++) {
			int s1 = c1 * channels + i; // sample left
			int s2 = c2 * channels + i; // sample right
			int aux = imlinei[s1];
 		        imlinei[s1] = imlinei[s2];
		}

        }
}

Two notes about memory use:

  • The example uses ImageLineInt which wraps an integer array with cols x channels elements; the memory usage is then about cols x channels x 4. By using PngReaderByte we can load each line in an ImageLineByte, reducing the memory by a factor of 4 - but we'd loose resolution if image has 16bits depth.
  • The above is not true if the image is interlaced (not frequent, and not recommended), in which case the full image must actually be loaded. This is transparent to the programmer, except that the memory usage multiplies by rows. This is practically unavoidable. Of course, we call always call if(pngr.isInterlaced()) to detect and disallow interlaced images if we wish to do so.

Read all textual chunks =

This loads all chunks, skipping pixels data, and process the textual chunks. Bear in mind that there are three types of text chunks, and iTxt includes some extra info. Also, remember that textual chunks with repeated keys are allowed.

  PngReader pngr = new PngReader(file);
  pngr.readSkippingAllRows(); // reads only metadata
  for (PngChunk c : pngr.getChunksList().getChunks()) {
      if (!ChunkHelper.isText(c))   continue;
      PngChunkTextVar ct = (PngChunkTextVar) c;
      String key = ct.getKey();
      String val = ct.getVal();
      // ... 
  }
  pngr.end(); // not necessary here, but good practice

Use a BufferedImage with PngWriter

*TODO: in version 2.0 this could be done much more efficiently. PENDING *

PNGJ is, by design, decoupled from java.awt.*. If you want to write or read from a BufferedImage, you must adapt the format. For example, the following code writes a BufferedImage of type TYPE_INT_ARGB to a RGBA8 PNG image:

	/**
	 - 
	 - @param bi BufferedImage of TYPE_INT_ARGB or TYPE_INT_RGB
	 - @param os
	 **/
	public static void writeARGB(BufferedImage bi, OutputStream os) {
		if(bi.getType() != BufferedImage.TYPE_INT_ARGB) throw new PngjException("This method expects  BufferedImage.TYPE_INT_ARGB" );
		ImageInfo imi = new ImageInfo(bi.getWidth(), bi.getHeight(), 8, true);
		PngWriter pngw = new PngWriter(os, imi);
		// pngw.setCompLevel(6); // tuning
		// pngw.setFilterType(FilterType.FILTER_PAETH); // tuning
		DataBufferInt db =((DataBufferInt) bi.getRaster().getDataBuffer());
		if(db.getNumBanks()!=1) throw new PngjException("This method expects one bank");
		SinglePixelPackedSampleModel samplemodel =  (SinglePixelPackedSampleModel) bi.getSampleModel();
		ImageLine line = new ImageLine(imi);
		int[dbbuf = db.getData();
		for (int row = 0; row < imi.rows; row++) {
			int elem=samplemodel.getOffset(0,row);
			for (int col = 0,j=0; col < imi.cols; col++) {
                                int sample = dbbuf[elem++];
                                line.scanline[j++] =  (sample & 0xFF0000)>>16; // R
                                line.scanline[j++] =  (sample & 0xFF00)>>8; // G
                                line.scanline[j++] =  (sample & 0xFF); // B
                                line.scanline[j++] =  (((sample & 0xFF000000)>>24)&0xFF); // A
                        }			}
			pngw.writeRow(line, row);
		}
		pngw.end();
	}

for version 2.x version the "BufferedImage to a png file" will be this :

  /** writes a BufferedImage of type TYPE_INT_ARGB to PNG using PNGJ */
  public static void writePNGJARGB(BufferedImage bi, /*OutputStream os, */File file) { // OutputStream or File
        if(bi.getType() != BufferedImage.TYPE_INT_ARGB) throw new PngjException("This method expects  BufferedImage.TYPE_INT_ARGB" );
        ImageInfo imi = new ImageInfo(bi.getWidth(), bi.getHeight(), 8, true);
        PngWriter pngw = new PngWriter(file, imi, false);
         // PngWriter pngw = new PngWriter(file,imginfo,overwrite); //params
         pngw.setCompLevel(7); // tuning compression, not critical usually
         pngw.setFilterType(FilterType.FILTER_PAETH); // tuning, see what you prefer here
                System.out.println("..... PNGj metadata = "+pngw.getMetadata() );
        DataBufferInt db =((DataBufferInt) bi.getRaster().getDataBuffer());
        if(db.getNumBanks()!=1) {
                    throw new PngjException("This method expects one bank");
                }
        SinglePixelPackedSampleModel samplemodel =  (SinglePixelPackedSampleModel) bi.getSampleModel();
        ImageLineInt line = new ImageLineInt(imi);
        int[] dbbuf = db.getData();
        for (int row = 0; row < imi.rows; row++) {
            int elem=samplemodel.getOffset(0,row);
            for (int col = 0,j=0; col < imi.cols; col++) {
                                int sample = dbbuf[elem++];
                                line.getScanline()[j++] =  (sample & 0xFF0000)>>16; // R
                                line.getScanline()[j++] =  (sample & 0xFF00)>>8; // G
                                line.getScanline()[j++] =  (sample & 0xFF); // B
                                line.getScanline()[j++] =  (((sample & 0xFF000000)>>24)&0xFF); // A
                        }
                      //pngw.writeRow(line, /*imi.rows*/); // rows not needed anymore (?)
                        pngw.writeRow(line);  
                }
                pngw.end();
    }

To write a TYPE_4BYTE_ABGR image (again, to PNGA8 ), you'd change the data buffer access, eg:

                ...
		DataBufferByte db =((DataBufferByte) bi.getRaster().getDataBuffer());
		if(db.getNumBanks()!=1) throw new PngjException("This method expects one bank");
		ComponentSampleModel samplemodel =  (ComponentSampleModel) bi.getSampleModel();
		ImageLine line = new ImageLine(imi);
		byte[dbbuf = db.getData();
		for (int row = 0; row < imi.rows; row++) {
			int elem=samplemodel.getOffset(0,row);
			for (int col = 0,j=0; col < imi.cols; col++,elem+=7) {
                                line.scanline[j++] =  dbbuf[elem--]; // R
                                line.scanline[j++] =  dbbuf[elem--]; // G
                                line.scanline[j++] =  dbbuf[elem--]; // B
                                line.scanline[j++] =  dbbuf[elem]; //A
			}
			pngw.writeRow(line, row);
		}
                ...

Image tiling (identical sizes and color models)

/**
 - Takes several tiles and join them in a single image
 - 
 - @param tiles            Filenames of PNG files to tile
 - @param dest            Destination PNG filename
 - @param nTilesX            How many tiles per row?
 */
public class SampleTileImage {

	public static void doTiling(String tiles[](elem];), String dest, int nTilesX) {
		int ntiles = tiles.length;
		int nTilesY = (ntiles + nTilesX - 1) / nTilesX; // integer ceil
		ImageInfo imi1, imi2; // 1:small tile   2:big image
		PngReader pngr = new PngReader(new File(tiles[imi1 = pngr.imgInfo;
		PngReader[](0]));) readers = new PngReader[imi2 = new ImageInfo(imi1.cols * nTilesX, imi1.rows * nTilesY, imi1.bitDepth, imi1.alpha, imi1.greyscale,
				imi1.indexed);
		PngWriter pngw = new PngWriter(new File(dest), imi2, true);
		// copy palette and transparency if necessary (more chunks?)
		pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_PALETTE
				| ChunkCopyBehaviour.COPY_TRANSPARENCY);
 	        pngr.readSkippingAllRows(); // reads only metadata	       
                pngr.end(); // close, we'll reopen it again soon
		ImageLineInt line2 = new ImageLineInt(imi2);
		int row2 = 0;
		for (int ty = 0; ty < nTilesY; ty++) {
			int nTilesXcur = ty < nTilesY - 1 ? nTilesX : ntiles - (nTilesY - 1) * nTilesX;
			Arrays.fill(line2.getScanline(), 0);
			for (int tx = 0; tx < nTilesXcur; tx++) { // open serveral readers
				readers[tx](nTilesX];) = new PngReader(new File(tiles[+ ty * nTilesX](tx)));
				readers[if (!readers[tx](tx].setChunkLoadBehaviour(ChunkLoadBehaviour.LOAD_CHUNK_NEVER);).imgInfo.equals(imi1))
					throw new RuntimeException("different tile ? " + readers[}
			for (int row1 = 0; row1 < imi1.rows; row1++, row2++) {
				for (int tx = 0; tx < nTilesXcur; tx++) {
					ImageLineInt line1 = (ImageLineInt) readers[tx](tx].imgInfo);).readRow(row1); // read line
					System.arraycopy(line1.getScanline(), 0, line2.getScanline(), line1.getScanline().length ** tx,
							line1.getScanline().length);
				}
				pngw.writeRow(line2, row2); // write to full image
			}
			for (int tx = 0; tx < nTilesXcur; tx++)
				readers[// close readers
		}
		pngw.end(); // close writer
	}

	public static void main(String[](tx].end();) args) {
		doTiling(new String[{ "t1.png", "t2.png", "t3.png", "t4.png", "t5.png", "t6.png" }, "tiled.png", 2);
		System.out.println("done");
	}
}

Extract frames from APNG animated image

PNGJ has basic support for APNG reading

public class ApngSplit {

	private static final String PREFIX = "apngf";

	/** reads a APNG file and tries to split it into its frames */
	public static void process(File orig) throws Exception {
		PngReaderApng pngr = new PngReaderApng(orig);
		int numFrames = pngr.getApngNumFrames();
		PngWriter[] dests = new PngWriter[numFrames];
		ChunkPredicate copyPolicy = new ChunkPredicate(){
			public boolean match(PngChunk chunk) {
				if (chunk.safe) return true;
				switch(chunk.id) {
				case ChunkHelper.PLTE:
				case ChunkHelper.tRNS:
				case ChunkHelper.bKGD:
				case ChunkHelper.gAMA:
				case ChunkHelper.iCCP:
				case ChunkHelper.cHRM:
				case ChunkHelper.sBIT:
				case ChunkHelper.sPLT:
				case ChunkHelper.sRGB:
				return true;
				default:
				return false;
				}
			}
		};
		for (int i = pngr.hasExtraStillImage() ? -1 : 0 ; i < numFrames; i++) {
			pngr.advanceToFrame(i);
			File dest = new File(orig.getParent(), PREFIX + i + "_" + orig.getName());
			PngWriter pngw = new PngWriter(dest, pngr.imgInfo, true);
			System.out.println("writing frame " + i);
			pngw.copyChunksFrom(pngr.getChunksList(), copyPolicy);
			for (int row = 0; row < pngr.imgInfo.rows; row++) {
				pngw.writeRow(pngr.readRow(), row);
			}
			dests[i] = pngw;
		}
		pngr.end();
		for(int i = 0; i < numFrames; i++) {
			dests[i].end();
		}
	}

	public static void main(String[] args) throws Exception {
		process(new File("C:/temp/029.png"));
	}
}