PNG-Encoder Performance

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Simon
Posts: 7
Joined: Mon Feb 16, 2004 11:06 am

PNG-Encoder Performance

Post by Simon » Tue Mar 02, 2004 2:28 pm

Hi,
I started generating large PNG - images with the com.keypoint.PngEncoder @version 1.1, 11 Nov 1999.
No I updated to more recent jfreechart-libraries and switched to PngEncoder @version 1.5, 19 Oct 2003.
I recognized a significate increase in encoding time. With the old encoder the encoding took about 1.5 sec the new encoder needs about 3.4 sec to encode the same image.
While tracing the lost seconds I found that changes to the code where made by David Gilbert. ("19-Nov-2002 : CODING STYLE CHANGES ONLY (by David Gilbert for Object Refinery Limited)")

David, can you imagine why the encoding lasts this long? Did you make similar experiences?

Best regards,
Simon

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Tue Mar 02, 2004 4:58 pm

That sounds quite slow, but it depends on what you're measuring and how fast your machine is etc. etc. Here is a chart showing timings on my machine:

Image

Each bar represents a compression level (1 = fastest, 9 = best compression, 0 = no compression). I'm not sure why the 0 case is so much slower, but it is.

Here is the code I ran to produce this chart:

Code: Select all

import java.awt.Color;
import java.awt.image.BufferedImage;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.CategoryDataset;
import org.jfree.data.DefaultCategoryDataset;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;

import com.keypoint.PngEncoder;

public class PNGPerformance extends ApplicationFrame {
    
    private JFreeChart chart;
    
    private DefaultCategoryDataset dataset;
    
    private long[][] times = new long[10][40];
    
    public PNGPerformance(String title) {
        super(title);   
        this.dataset = new DefaultCategoryDataset();
        this.chart = createChart(dataset);
        ChartPanel chartPanel = new ChartPanel(chart);
        chartPanel.setPreferredSize(new java.awt.Dimension(500, 270));
        setContentPane(chartPanel);
    }
    
    /**
     * Creates a chart.
     * 
     * @param dataset  a dataset.
     * 
     * @return The chart.
     */
    private JFreeChart createChart(CategoryDataset dataset) {
        JFreeChart chart = ChartFactory.createBarChart(
            "PNG Performance", 
            "Compression Level", 
            "Milliseconds to Encode", 
            dataset, 
            PlotOrientation.VERTICAL, 
            true, 
            false, 
            false
        );
        chart.setBackgroundPaint(Color.white);
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.setBackgroundPaint(Color.lightGray);
        plot.setDomainGridlinePaint(Color.white);
        plot.setRangeGridlinePaint(Color.white);
        return chart;
    }
    
    public double calculateMean(long[] values, int count) {
    	double result = 0.0;
        if (values.length > 0) {
            double sum = 0.0;
            for (int i = 0; i < count; i++) {
                sum = sum + values[i];
            }
            result = sum / count;
        }
        return result;
    }
    
    public void run() {
        for (int i = 0; i < 40; i++) {
            for (int j = 0; j < 10; j++) {
                BufferedImage image = this.chart.createBufferedImage(350, 500);
                PngEncoder encoder = new PngEncoder(image, false, PngEncoder.FILTER_NONE, j);
                long start = System.currentTimeMillis();
                byte[] data = encoder.pngEncode();
                long end = System.currentTimeMillis();
                this.times[j][i] = end - start;
                data = null;
                encoder = null;
                System.gc();
                double mean = calculateMean(this.times[j], i + 1);
                this.dataset.setValue(mean, "PngEncoder", "C = " + j);
                System.out.println("i = " + i + ", " + (end - start) + " (" + Runtime.getRuntime().freeMemory() + " / " + Runtime.getRuntime().totalMemory() + ")");
            }
        }
    }
    
    /**
     * The starting point for the demo.
     * 
     * @param args  ignored.
     */
    public static void main(String[] args) {
        
        PNGPerformance demo = new PNGPerformance("PNG Performance");
        demo.pack();
        RefineryUtilities.centerFrameOnScreen(demo);
        demo.setVisible(true);
        demo.run();
        
    }
    
}
Try it out on your own machine and see how it compares (I'm running JDK 1.4.2, 256MB, 2.6GHz CPU, and Windows XP right at this moment).
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

Guest

Post by Guest » Wed Mar 03, 2004 11:21 am

Hi,
I tried the benchmark on my AMD Athlon 1400, 1GB Ram, Win2000, JDK 1.4.2 and got the following results:

Image

Image

Image

Image

It seems, that the old encoder is very fast while encoding big image-sizes but looses a lot of time creating smaller PNGs.
Can You imagine why the new encoder gets this slow while images get bigger?

Regards,
Simon

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Wed Mar 03, 2004 11:42 am

That is quite interesting. I've sent an e-mail to the original author of the encoder to see if he has any time to look at it. When I get a chance, I'll see if I can spot anything that could be improved...
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

Simon
Posts: 7
Joined: Mon Feb 16, 2004 11:06 am

Post by Simon » Wed Mar 03, 2004 12:12 pm

Hi ,
i forgot to mention that the old encoder does not have the feature to set a compression. So all bars should be indicating the same time...


Best regards
Simon
Last edited by Simon on Sun May 01, 2005 1:30 pm, edited 2 times in total.

J David Eisenberg

Haven't looked at code in a while...

Post by J David Eisenberg » Wed Mar 03, 2004 6:03 pm

I wrote the original PNG encoder, and I haven't looked at the code in ages. (I figured that everyone was using the PNG encoder library that comes with the new Java SDK.) I just did a diff on the two files, and I don't see any code differences at all between 1.4 and 1.5; it really appears to be all stylistic.

If someone can email me (catcode@catcode.com) a copy of version 1.1, which I don't have any more, I can take a look.

I know that one big difference between the original and current versions was the addition of these lines:

nRows = Math.min(32767 / (width * (bytesPerPixel + 1)), rowsLeft);
nRows = Math.max( nRows, 1 );

to fix a bug when you had large images; you'd get nRows = 0, and that would cause problems.
You might want to expand these into "if" statements rather than function calls, and see if that makes a difference. Or change the code to take the invariant calculation out of the loop [sorry,I can't do indenting properly here]

int rowSize = 32767 / (width * (bytesPerPixel + 1));
while (rowsLeft > 0) {
nRows = Math.min(rowSize, rowsLeft);
nRows = Math.max( nRows, 1 );

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Wed Mar 03, 2004 6:48 pm

I notice in the writeBytes() method there is some array resizing being done which uses a constant value of '1000'. I changed that to '2000' and ran the numbers again:

Image

It looks promising, but I want to spend some more time understanding what the code is doing before I jump to conclusions. :wink:
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

scot

other encoders

Post by scot » Fri Mar 12, 2004 1:42 pm

http://www.objectplanet.com/PngEncoder/

i am not affiliated with this company in any way.

Simon
Posts: 7
Joined: Mon Feb 16, 2004 11:06 am

Post by Simon » Mon Mar 15, 2004 10:24 am

Hi,

i benchmarked the objectplanet encoder and it is in deed very fast. But it costs 100 USD an I also want to keep the jfree-package and its opportunities.
Are there any other news on this topic?

Ciao,
Simon

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Mon Mar 15, 2004 5:36 pm

Simon wrote:Are there any other news on this topic?
I'm pretty confident that we can squeeze some extra performance out of the PNG encoder, but I haven't had much time to work on it.
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

Tzaphkiel
Posts: 44
Joined: Tue May 13, 2003 1:37 pm
Location: Belgium

PngEncoder + resolution and OutOfMemoryError

Post by Tzaphkiel » Wed Feb 16, 2005 10:32 am

Hi,

I'm also using the PngEncoder (modified to write the resolution).

I'm using it to output some really big chart images, 17cm*12cm*600dpi which should gime me (with no compression) an image size of 34,14 Mb...
I get an OutOfMemoryError on the the image array creation... it seems that allocating 32Mb as a variable is not working too well for some reason:

Code: Select all

public byte[] pngEncode(boolean encodeAlpha) {
(...)
    // this line below throws the OutOfMemoryError !
    pngBytes = new byte[((width + 1) * height * 3) + 200];
(...)
I've got a pc running XP with 1Gb ram, I've tried increasing the VM size to 1500m using the -X switches... but nothing does it !

Does someone have an idea of how to solve this problem ?
Is there a way to increase the stack size ?
it does not seam that 32Mb as a byte array is much... ?
I calculated the aproximate sizes using the following formulae:

Code: Select all

byte size: ((width + 1) * height * 3) + 200
=> ((4015+1) * 2834 * 3) + 200 = 34144232
=> (/(2*1024)) 32.56Mb!!!

I calculated the size of the image 17cm*12cm*2.54*600dpi
2.54 is the cm to inch
600 is the dpi
giving 190485979 pixels... at 8bit per pixels
=> 1523887833.6 bytes
=> (/1024) 1488171.7 Kb
=> (/1024) 1453.29 Mb
=> (/1024) 1.42Gb in memory
If this can help...
Tzaphkiel
Image

richard_atkinson
Posts: 115
Joined: Fri Mar 14, 2003 3:13 pm
Location: London, England
Contact:

SVG?

Post by richard_atkinson » Thu Feb 17, 2005 8:24 pm

Can you use SVG instead? The vector nature of the format would make it a lot easier.

Regards,
Richard...

Tzaphkiel
Posts: 44
Joined: Tue May 13, 2003 1:37 pm
Location: Belgium

Post by Tzaphkiel » Mon Feb 21, 2005 7:51 am

Hi, I can't, I have to generate some PNGs with resolution information in them.
The images are used for publications using QuickSilver... I don't think it reads SVGs... I'll have a look at the SVG's import in QS (I know it would be better to use it, but...).

It seems strange that allocating 32Mb in a var is so "hard" for the java VM ! ?

If I have to use SVG, I suppose I have to use Batik libraries ?

Regards
Tzaphkiel
Image

Tzaphkiel
Posts: 44
Joined: Tue May 13, 2003 1:37 pm
Location: Belgium

PNG, JPEG, GIF encoding using ImageIO

Post by Tzaphkiel » Wed Feb 23, 2005 9:02 am

Hi all,

I've been looking throught the forum to solve my pngEncoder problems (see the PNGEncoder performance post).
I came up with a solution that not content with reducing the encoding time (4 images in 36 seconds (disk writes included) to 15 seconds !!!) it also reduces the file size by 44 times ( of a 17cmx12cmx300dpi image => 8.14Mb to 186.61Kb without loosing image definition) !!!

:?: I have a question nonetheless: (I've not been able to take the time and find out for myself) why is there such a big file size difference ??? If anyone finds out the why, please post a reply, I really would like to find out!!! :?:

I've based my solution on:
JpegWrite, Java Forums - how to set PNG dpi and RFE [ 777515 ] High resolution images saving

I wanted to encode pngs of my graphics from a batch application and to load them into a publication application such as QuickSilver.
I thus needed a way to encode the file quickly and with resolution information in them! (ATT: Gif does not provide resolution information !)

Here is the code I've used to do so:

Code: Select all

	private void encodeAndWritePreviewImage(String filePath, ChartParameter _chartParams)
	{
		double outputWidth = _chartParams.getWidth(); //cm
		double outputHeight = _chartParams.getHeight(); //cm
		float resolution = _chartParams.getResolution();//dpi
		String fileName = filePath.substring(0, filePath.lastIndexOf("."));
		String extension = filePath.substring(filePath.lastIndexOf("."));
		String file = fileName + "_" + outputWidth + "x" + outputHeight + "x" + resolution + "dpi" + extension;

		try
		{
			// old code
			//		FileOutputStream fos = null;
			//			fos = new FileOutputStream(file);
			//			PngEncoder encoder = new
			// PngEncoder(_chart.getHiResChartImage(resolution, outputWidth,
			// outputHeight), false, 0, 0);
			//			encoder.setDpi((int) resolution, (int) resolution);
			//			encoder.pngEncodeToStream(fos);
			//			fos.close();
			RenderedImage rendImage = _chart.getHiResChartImage(resolution, outputWidth, outputHeight);
			PNGMetadata png = new PNGMetadata();
			int resX =  (int)Math.ceil(resolution / 0.0254f);
			png.pHYs_pixelsPerUnitXAxis = resX;
			png.pHYs_pixelsPerUnitYAxis = resX;
			png.pHYs_unitSpecifier = 1;
			png.pHYs_present = true;

			IIOImage iioImage = new IIOImage(rendImage, null, png);
			
			//Write generated image to a file
			// Save as PNG
			File f = new File(file);
			ImageWriter writer = (ImageWriter) ImageIO.getImageWritersByFormatName("png").next();
			ImageOutputStream ios = ImageIO.createImageOutputStream(f);
			writer.setOutput(ios);

			// Set the compression quality and other header information
			ImageWriteParam iwparam = new ImageWriteParam()
			{
				public int getProgressiveMode()
				{
					return MODE_COPY_FROM_METADATA;
				}
			};
			//			writer.write(png, iioImage, iwparam);
			writer.write(iioImage);
			writer.dispose();
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}
Edit: png.pHYs_unitSpecifier = 1; must be specified otherwise windows does not recognize the dpi correctly in the file (it sees 96 dpi whilst IrfanView sees the 300dpi) !
Also int resX = (int)Math.ceil(resolution / 0.0254f); is "ceilled" because otherwise, windows sees 299 dpi...

NB: to change the format, just ask for another writer using the ImageIO.getImageWritersByFormatName !

The getHiResChartImage is taken from the solution provided with the PNGEncoder... It actually re-sizes the image according to the ratio between "screen res" and "wanted image res" ! (ATT: on windows, the screen resolution is not 72dpi as one could expect but is often 96dpi !!!)
Here is the code:

Code: Select all

	public BufferedImage getHiResChartImage(double resolution, double width, double height)
	{
		width = width / CM_TO_INCH * _screenResolution;
		height = height / CM_TO_INCH * _screenResolution;
		double scaleRatio = resolution / _screenResolution;
		int rasterWidth = (int) (width * scaleRatio);
		int rasterHeight = (int) (height * scaleRatio);
		BufferedImage image = new BufferedImage(rasterWidth, rasterHeight, BufferedImage.TYPE_INT_RGB);
		Graphics2D g2 = image.createGraphics();
		g2.transform(AffineTransform.getScaleInstance(scaleRatio, scaleRatio));
		_chart.draw(g2, new Rectangle2D.Double(0, 0, width, height), null);
		g2.dispose();
		return image;
	}
Hope this post helps everyone out there and I do believe that including such code in a next release of JFreeChart would be benefic to the library (ImageIO comes with 1.4+ JDKs) !!!

Regards
Last edited by Tzaphkiel on Thu Feb 24, 2005 12:03 pm, edited 1 time in total.
Tzaphkiel
Image

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Wed Feb 23, 2005 10:28 am

Thanks for your post. We need to maintain compatibility with JDK 1.3 still as there are a good number of users that need it.
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

Locked