'Write PNG file with less disk size in Java

I have a BufferedImage:

BufferedImage bi = new BufferedImage(14400, 14400, BufferedImage.TYPE_INT_ARGB);

I have saved this image to a PNG file using the following code:

public static void saveGridImage(BufferedImage sourceImage, int DPI,
            File output) throws IOException {
        output.delete();

        final String formatName = "png";

        for (Iterator<ImageWriter> iw = ImageIO
                .getImageWritersByFormatName(formatName); iw.hasNext();) {
            ImageWriter writer = iw.next();
            ImageWriteParam writeParam = writer.getDefaultWriteParam();
            ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier
                    .createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
            IIOMetadata metadata = writer.getDefaultImageMetadata(
                    typeSpecifier, writeParam);
            if (metadata.isReadOnly()
                    || !metadata.isStandardMetadataFormatSupported()) {
                continue;
            }

            setDPI(metadata, DPI);

            final ImageOutputStream stream = ImageIO
                    .createImageOutputStream(output);
            try {
                writer.setOutput(stream);
                writer.write(metadata,
                        new IIOImage(sourceImage, null, metadata), writeParam);
            } finally {
                stream.close();
            }
            break;
        }
    }

    public static void setDPI(IIOMetadata metadata, int DPI)
            throws IIOInvalidTreeException {

        double INCH_2_CM = 2.54;

        // for PNG, it's dots per millimeter
        double dotsPerMilli = 1.0 * DPI / 10 / INCH_2_CM;

        IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
        horiz.setAttribute("value", Double.toString(dotsPerMilli));

        IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
        vert.setAttribute("value", Double.toString(dotsPerMilli));

        IIOMetadataNode dim = new IIOMetadataNode("Dimension");
        dim.appendChild(horiz);
        dim.appendChild(vert);

        IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
        root.appendChild(dim);

        metadata.mergeTree("javax_imageio_1.0", root);
    }

When the code executes it creates an PNG file with 400 DPI and Disk Size of 168 MB; this is too much.

Is there any way or parameters I can use to save a smaller PNG?

Before, I had a 1.20 GB TIFF file, and when I converted it to PNG using imagemagick at 400 DPI, the resulting file size was only 700 KB.

So, I think I might be able to save the above file smaller.

Can pngj help me? Because I now have a png file which I can read in pngj library.



Solution 1:[1]

A 14400x14400 ARGB8 image has a raw (uncompressed) size of 791MB. It will compress more or less according to its nature (has uniform or smooth zones) and according (less important) to the PNG compression parameters.

when i convert it using imagemagic to PNG using 400 DPI , the resulting file size is only 700 KB.

(I don't understand why you speak of DPI, that has nothing to do, what matters is the size in pixels) Are you saying that you are getting a 14400x14400 ARGB of 700KB? That would represent a compression of 1/1000, hard to believe unless the image is practically flat. You should first understand what is going on here.

Anyway, here's a sample code with PNGJ

/** writes a BufferedImage of type TYPE_INT_ARGB to PNG using PNGJ */
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(9);// maximum compression, not critical usually
    pngw.setFilterType(FilterType.FILTER_AGGRESSIVE); // see what you prefer here
    DataBufferInt db =((DataBufferInt) bi.getRaster().getDataBuffer());
    SinglePixelPackedSampleModel samplemodel =  (SinglePixelPackedSampleModel) bi.getSampleModel();
    if(db.getNumBanks()!=1) 
        throw new PngjException("This method expects one bank");
    ImageLine line = new ImageLine(imi);
    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 = db.getElem(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();
}

Solution 2:[2]

I would attempt to fiddle with the settings on the writeParam object you're creating. Currently you're calling getDefaultWriteParam(); which gives you a basic writeParam object. My guess is the default would be NO compression.

After doing that, you can probably set some of the compression modes to reduce the file size.

writeParam.setCompressionMode(int mode);
writeParam.setCompressionQuality(float quality);
writeParam.setCompressionType(String compressionType);

See http://docs.oracle.com/javase/6/docs/api/javax/imageio/ImageWriteParam.html And specifically http://docs.oracle.com/javase/6/docs/api/javax/imageio/ImageWriteParam.html#setCompressionMode(int)

Solution 3:[3]

sample code for pngj that works for 2.x versions of the leonbloy's pngj library

      /** writes a BufferedImage of type TYPE_INT_ARGB to PNG using PNGJ */
  public static void writePNGJARGB(BufferedImage bi, /*OutputStream os, */File file) {
      
      System.out.println(".....entering PNGj alternative image file save mode....." );
        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*/);
                        pngw.writeRow(line);  
                }
                pngw.end();
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Jazzepi
Solution 3