Introduction
I have written java code for generating indexed png using pngj version 0.94.
Now pngj version 2.1.1 has been released on 2014/6/1.
So I have refined the code for generating indexed png so that the code suits with pngj version 2.1.1.
Code
package com.dukesoftware.utils.image.pngj; import ar.com.hjg.pngj.IImageLine; import ar.com.hjg.pngj.ImageInfo; import ar.com.hjg.pngj.ImageLineInt; import ar.com.hjg.pngj.PngReader; import ar.com.hjg.pngj.PngWriter; import ar.com.hjg.pngj.chunks.ChunkCopyBehaviour; import ar.com.hjg.pngj.chunks.PngChunkSingle; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public final class PngUtils { private static final int _1K_BYTES = 1024; public static void toIndexed256Colors(File input, File output) throws IOException { PngReader png1 = new PngReader(input); ImageInfo pnginfo1 = png1.imgInfo; if (pnginfo1.indexed) { // already indexed, skip copy(new FileInputStream(input), new FileOutputStream(output)); return; } // 1st step: create color palette from source png and also create indexed color pixel lines used by the palette final PngChunkPLTEBuilder builder = new PngChunkPLTEBuilder(); int[][] indexedLine = new int[pnginfo1.rows][pnginfo1.cols]; final int channel = pnginfo1.channels; if (channel == 1) { for (int row = 0, rows = pnginfo1.rows; row < rows; row++) { IImageLine l1 = png1.readRow(); int[] scanline = ((ImageLineInt) l1).getScanline(); // to save typing for (int col = 0, len = scanline.length / channel, offset = 0; col < len; col++, offset = col * channel) { indexedLine[row][col] = builder.getIndex(scanline[offset], 0, 0); } } } else if (channel == 2) { for (int row = 0, rows = pnginfo1.rows; row < rows; row++) { IImageLine l1 = png1.readRow(); int[] scanline = ((ImageLineInt) l1).getScanline(); for (int col = 0, len = scanline.length / channel, offset = 0; col < len; col++, offset = col * channel) { indexedLine[row][col] = builder.getIndex(scanline[offset], scanline[offset + 1], 0); } } } else if (channel == 3) { for (int row = 0, rows = pnginfo1.rows; row < rows; row++) { IImageLine l1 = png1.readRow(); int[] scanline = ((ImageLineInt) l1).getScanline(); for (int col = 0, len = scanline.length / channel, offset = 0; col < len; col++, offset = col * channel) { indexedLine[row][col] = builder.getIndex(scanline[offset], scanline[offset + 1], scanline[offset + 2]); } } } else if (channel == 4) { for (int row = 0, rows = pnginfo1.rows; row < rows; row++) { IImageLine l1 = png1.readRow(); int[] scanline = ((ImageLineInt) l1).getScanline(); for (int col = 0, len = scanline.length / channel, offset = 0; col < len; col++, offset = col * channel) { if(scanline[offset + 3] != 255) { throw new IllegalArgumentException("sorry png image which has alpha value is not supported"); } indexedLine[row][col] = builder.getIndex(scanline[offset], scanline[offset + 1], scanline[offset + 2]); } } } png1.end(); int bitDepth = determineBitDepth(builder.getNumberOfColors()); // 2nd step: write the palette and the indexed color pixel lines to output png ImageInfo pnginfo2 = new ImageInfo(png1.imgInfo.cols, png1.imgInfo.rows, bitDepth, false, false, true); PngWriter png2 = new PngWriter(output, pnginfo2); png2.setCompLevel(9); PngChunkSingle palette = builder.buildPngChunkPaletteFromCurrentMap(pnginfo2); png2.getMetadata().queueChunk(palette); png2.copyChunksFrom(png1.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE); ImageLineInt l2 = new ImageLineInt(pnginfo2); for (int row = 0, rows = png1.imgInfo.rows; row < rows; row++) { System.arraycopy(indexedLine[row], 0, l2.getScanline(), 0, indexedLine[row].length); png2.writeRow(l2, row); } png2.end(); } private static int determineBitDepth(int numberOfColors) { if (numberOfColors <= 1) return 1; else if(numberOfColors <= 16) return 4; else if(numberOfColors <= 256) return 8; else throw new IllegalStateException("Number of colors should be less than 256"); } private final static void copy(InputStream is, OutputStream os) throws IOException { copy(is, os, new byte[_1K_BYTES]); } private final static void copy(InputStream is, OutputStream os, byte[] buffer) throws IOException { try { for (int bytes = 0; (bytes = is.read(buffer)) != -1;) { os.write(buffer, 0, bytes); } os.flush(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // quietly close anyway } } } } }
package com.dukesoftware.utils.image.pngj; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import ar.com.hjg.pngj.ImageInfo; import ar.com.hjg.pngj.chunks.PngChunkPLTE; public final class PngChunkPLTEBuilder { private final Map<Integer, Integer> map = new HashMap<Integer, Integer>(); private int index = -1; public int getIndex(int r, int g, int b) { Integer key = (r << 16) | (g << 8) | b; Integer indexInMap = map.get(key); if (indexInMap != null) { return indexInMap; } index++; map.put(key, index); return index; } public PngChunkPLTE buildPngChunkPaletteFromCurrentMap(ImageInfo pnginfo) { PngChunkPLTE palette = new PngChunkPLTE(pnginfo); palette.setNentries(map.size()); for (Entry<Integer, Integer> entry : map.entrySet()) { int key = entry.getKey(); palette.setEntry( entry.getValue(), key >> 16, (key & 0xFF00) >> 8, key & 0x00FF ); } return palette; } public int getNumberOfColors() { return map.size(); } }
コメント
For my application I require that there be a maximum of 64 colors. Could you recommend some changes to this code that would output an 8 bit indexed image with a max of 64 colors?
Thanks.
Thank you for your comment.
If image has max of 64 colors, 8 bit depth (256 colors) png image can only support your image.
According to this png specification, the valid bit depth values are 1, 2, 4, and 8.
If png support any number for bit depth, you can specify 6 though ;)
The code example posted above calculates proper bit depth based on input image by determineBitDepth method.
The method throws exception if number of colors is more than 256 (8bit).
So if you would like to always create image which has max of 64 colors, I think you just specify constant value 8 as bit depth around here in the example code.
ImageInfo pnginfo2 = new ImageInfo(png1.imgInfo.cols, png1.imgInfo.rows, bitDepth, false, false, true);
Hope this helps.
Many thanks.