headless

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
matthew99
Posts: 2
Joined: Tue Dec 19, 2017 2:38 am
antibot: No, of course not.

headless

Post by matthew99 » Wed Dec 20, 2017 2:16 am

Happy Holidays!
Code snippets and advice in posts matching 'headless' have been helpful. Not for lack of trying, I have been unable to generate correctly rendered chart images in headless mode.

Safari: Black with very faint, dark red artifacts of each bar barely visible
Mac Preview: " " " " " " " " " " " "
Firefox: Orange background with Green and Orange bars
GIMP: " " " " " " "
FlickR: " " " " " " "

The output chart images are here: https://www.flickr.com/photos/68694632@ ... 391994689/
The first image at the link above with the Orange background and green bars (busting the shaded 3D effect), as displayed in Firefox, GIMP, and FlickR. The same image file shows as black on Mac using both Mac Preview, and Safari.

The second image at the link above shows a broken bar and is an example of the screen grab failing every few calls, but thats not important as I need only headless.
The third image at the link above is the correctly rendered chart image produced by doing a programmed screen grab in a non-headless environment. (Note: since I generated these images I switched to the JfreeChart 1.5.0 version which resulted in the new 3D cylinder type bar chart, but the the same colour behaviour is present.)

Here are the relevant POM dependancies in my project. I suspect the jcommon artifact version needs to be bumped, advice welcome:
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.0</version> <!-- was 1.0.19 -->
</dependency>

<dependency>
<groupId>org.jfree</groupId>
<artifactId>jcommon</artifactId>
<version>1.0.23</version>
</dependency>

To Test: (after setting CLASSPATH to include jFreeChart jar)
1. Command Line
$ java -Djava.awt.headless=true org.someorg.SkillMatchBar # headless test
$ java org.someorg.SkillMatchBar # non-headless test
Open the generated file testChart.jpg (in current directory) in your browser
OR
2. In Eclipse:
- create two Eclipse run profiles, one with -Djava.awt.headless=true in VM settings, and one with -Djava.awt.headless=false or not present
Open the generated file testChart.jpg (in current directory) in your browser

SkillMatchBar.java

Code: Select all

package org.someorg;

import java.awt.AWTException;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.imageio.ImageIO;
import javax.swing.JFrame;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.encoders.ImageFormat;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.RefineryUtilities;
import org.junit.Assert;
import org.slf4j.Logger;

/**
 * Originally based on bar chart demo, this adds headless support and adapts
 * to my spring microservices demo app
 */
public class SkillMatchBar  
{
	final static String CLSNAME = SkillMatchBar.class.getSimpleName();
	final static Logger logger = org.slf4j.LoggerFactory.getLogger( SkillMatchBar.class );

	private JFreeChart chart;
	private String title;
	private int preferredWidth;
	private int preferredHeight;
	private String filename;

	/**
	 * Creates a new demo instance.
	 */
	public SkillMatchBar(String title) 
	{
		this.title = title;

		preferredHeight = 340; // hard-coded for test example
		preferredWidth = 500; // 		"
		filename = "testChart.jpg"; //	"
	}

	/**
	 * Returns a sample dataset.
	 * 
	 * @return The dataset.
	 */
	private CategoryDataset createDataset() {

		// row keys...
		final String series1 = "Matching Skills Experience";

		// create the dataset...
		final DefaultCategoryDataset dataset = new DefaultCategoryDataset();
		
		String[] skills = new String[] { "java", "spring", "sql" };  // test data set
		int[] skillMonths= new int[] { 80, 18, 120 };
		for ( int i = 0; i < skills.length; i++ )
			dataset.addValue( skillMonths[i], series1, skills[i] );

		return dataset;
	}

	/**
	 * Creates a sample chart.
	 * 
	 * @param dataset
	 *            the dataset.
	 * 
	 * @return The chart.
	 */
	
	private JFreeChart createChart(final CategoryDataset dataset) 
	{
		final String METHOD = CLSNAME + ".createChart(dataset) ";
        boolean headless = GraphicsEnvironment.isHeadless();
        System.out.println( METHOD + "Headless: " + headless );
        Toolkit tk = Toolkit.getDefaultToolkit();
        tk.beep(); // r2d2 lives
        
		// create the chart...        
		chart = ChartFactory.createBarChart( "Matching Skill Experience", // chart title
				"Skill", 	// domain axis label
				"Months",	// range axis label
				dataset, 	// data
				PlotOrientation.HORIZONTAL, // orientation
				true, 		// include legend
				true, 		// tooltips?
				false 		// URLs?
		);
		chart.setBorderVisible(false); 
		 
		// NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART...
		chart.setBackgroundPaint(Color.white);
		//chart.setBackgroundImageAlpha(0.15f);
		
		// get a reference to the plot for further customisation...
		final CategoryPlot plot = chart.getCategoryPlot();
		plot.setBackgroundPaint(Color.lightGray);
		plot.setDomainGridlinePaint(Color.white);
		plot.setRangeGridlinePaint(Color.white);

		// set the range axis to display integers only...
		final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
		rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

		// disable bar outlines...
		final BarRenderer renderer = (BarRenderer) plot.getRenderer();
		renderer.setDrawBarOutline(false);

		// set up gradient paints for series... TODO test if series are used
		final GradientPaint gp0 = new GradientPaint(0.0f, 0.0f, Color.blue, 0.0f, 0.0f, Color.lightGray);
		renderer.setSeriesPaint(0, gp0);
		
		final CategoryAxis domainAxis = plot.getDomainAxis();
		domainAxis.setCategoryLabelPositions(CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 6.0));
		 
		return chart;
	}

	public BufferedImage generateChartImage()
	{
		final String METHOD = CLSNAME + ".generateChartImage() ";
		BufferedImage image = null;
		Rectangle2D rec = new Rectangle( preferredWidth, preferredHeight );
		
		chart = createChart( createDataset() );					// create the JFreeChart chart var
		Assert.assertNotNull( chart );
		
		logger.info( METHOD + "size=(" + preferredWidth + "w," + preferredHeight +"h)" );	
		
		chart.setBorderPaint( Color.white );
		chart.setBorderVisible( false );
		chart.setBackgroundPaint( Color.white );
		
		if( ! GraphicsEnvironment.isHeadless() )					
		{
			// STEP 1 - Render chart in GUI
			Canvas canvas = null;
			JFrame frame = null;
			canvas = new Canvas();								// new canvas with white background 
			canvas.setSize( preferredWidth, preferredHeight );
			canvas.setBackground( Color.white );
			canvas.setVisible( true );
			
			frame = new JFrame();
			frame.setBackground( Color.white );					// new frame with white background 
			Dimension preferredDim = new Dimension( preferredWidth, preferredHeight );
			frame.setSize( preferredDim );
			
			frame.add( canvas );									// canvas added to frame
			frame.setVisible( true );
			
			Graphics2D  g2 = ( Graphics2D )canvas.getGraphics();
			Assert.assertNotNull( g2 );
			chart.draw( g2, rec );								// chart draws on canvas perfectly
			
			frame.pack(); 										// pack frame components layout
			RefineryUtilities.centerFrameOnScreen( frame );		// center, correct chart appears in GUI
			
			logger.debug( METHOD + "frame bounds="+frame.getBounds() );
			g2.dispose(); 
			
			// STEP 2 - screen grab to file
			BufferedImage screenImage = null;
			Robot robot = null;
			Exception ex = null;
			String msgPrefix = "";
	        try
	        {
	        		robot = new Robot();  								
	        		canvas.setVisible( true );
	        		screenImage = robot.createScreenCapture( frame.getBounds() ); // screen grab, but often has defects 
        	        ImageIO.write( screenImage, ImageFormat.JPEG, new File( filename ) ); 
        	        logger.info( METHOD + "step 2 did write: "+ filename );
        	    } 
        	    catch( IOException e )  		{ ex = e; 	 msgPrefix="ImageIO.write(..) "; }
    	        catch( AWTException awte )  	{ ex = awte; msgPrefix="new Robot() "; }
        	    finally
        	    {
        	    		if( ex != null )
        	    			logger.error( METHOD + msgPrefix + "threw Exception: "+ex.getMessage()
        	    				+   " cls: "+ex.getClass().getSimpleName()
        	    				+ " cause: "+ex.getCause() );
        	    }
		}
		else // else headless mode create chart
			image = headlessCreateChart();
		
		return image;
	}
	
	@SuppressWarnings("unused")
	private BufferedImage headlessCreateChart()
	{
		final String METHOD = CLSNAME + ".headlessCreateChart() ";
		BufferedImage chartImage = null, backImage = null;
        
		chartImage = chart.createBufferedImage( preferredWidth, preferredHeight ); // chart with very faint 
			// dark red bars on black entirely black background
	
		/* ****************************************************************************
		 * A failed attempt to fix the problem
		 * [The very faint bar artifacts not visible on my MacBook Pro Retina 
		 * display, but are visible on my 23" LCD, only if viewed from 20-90 degrees 
		 * angle vertically above center]  Below attempt had no effect to fix the problem.
		 */
		if( false )
		{
			backImage = new BufferedImage( preferredWidth, preferredHeight, BufferedImage.TYPE_INT_ARGB );			// 2 - fill a rectangle of white on the whole image
			Graphics2D g = ( Graphics2D )backImage.getGraphics(); 
			
			// 1 - build white backImage
			g.setBackground( Color.white );
			g.clearRect( 0, 0, preferredWidth, preferredHeight );
	        g.setPaint( Color.white );
	        g.setPaintMode();
	        g.fill( new Rectangle( 0, 0, preferredWidth, preferredHeight ) );	       		
	        
	        // 2 - set background into chart
	        chart.setBackgroundImage( backImage );
	        chart.setBackgroundImageAlpha( 0.15f );
		chartImage = chart.createBufferedImage( preferredWidth, preferredHeight ); // create does draw(.)
		g.dispose();

		}
		
		writeChartImage( chartImage );
		
        return chartImage;
	}
	
	private void writeChartImage( BufferedImage chartImage )
	{
		final String METHOD = CLSNAME + ".writeTestImage(chartImage) ";
		Assert.assertNotNull( chartImage );
		
		int numElements  = chartImage.getData().getDataBuffer().getSize();
		IOException ex = null;
		try 
		{
			File imageFile = new File( filename );
			boolean exists = imageFile.exists(); 
			logger.info( METHOD + " did the new File( filename ), exists="+exists+" so over-writing" );					
			OutputStream outStream = new FileOutputStream( filename );		
			//ChartUtilities.writeBufferedImageAsJPEG( outStream, chartImage ); 
	        ImageIO.write( chartImage, ImageFormat.JPEG, outStream );	
	        outStream.close();	     
			logger.info( METHOD + "End wrote filename="+filename+" image.numElements="+numElements
				+ " size=("+chartImage.getWidth()+"w," + chartImage.getHeight()+"h)" );	
		}
		catch( IOException ex2 )  { ex = ex2; }
		finally
		{
			if( ex != null )
				logger.error( METHOD + "IOException: "+ex.getMessage()
					+  " cls: "+ex.getClass().getSimpleName()
					+" cause: "+ex.getCause() );
		}
	}

	/**
	 * Starting point for the demonstration application.
	 *
	 * @param args
	 *            ignored.
	 */
	public static void main(final String[] args) 
	{
		final String METHOD = CLSNAME + ".main(args.length="+args.length+") ";
		
		logger.info( METHOD + "Start" );
		
		final SkillMatchBar demo 	= new SkillMatchBar( "Match Skills Experience" );
		BufferedImage chartImage 	= demo.generateChartImage();
		
		logger.info( METHOD + "End" );
	}
}
The non-headless generated image file looks perfect (image 3), but the screen grab misses a part of a bar every few generates (image 2). That's okay as I only need headless working. The headless behaviour (image 1) is all black with very faint, dark red bar artifacts when viewed in Mac Preview and Safari, but when displayed on Firefox, FlickR, and GIMP it has Orange Background and Green and Orange bars. Any suggestions would be greatly appreciated. (The Developer Manual section 20.2 directed me here.)
JFreeChart ver: 1.5
Java ver: 1.8
Browser: Safari 11.01

matthew99
Posts: 2
Joined: Tue Dec 19, 2017 2:38 am
antibot: No, of course not.

SOLVED Re: headless

Post by matthew99 » Wed Dec 20, 2017 8:32 pm

Switching the generated image file from JPG to PNG format solved the problem.
The FAQ mentioned better results with PNG because of JPG being lossy, which I thought only affected pixel quality.
I learned that JPG is lossy not just in pixel data, but in ColorModel or something else in the image that affects its rendering correct colours.

SUMMARY
========
So that others do not spend days on this as I did, I would suggest adding both an FAQ entry, and an entry in the Developer Manual to the effect:
- If you call createBufferedImage(.) in headless, and save as JPG, your chart will have a broken Color model and render Orange background with Orange and Green bars, and when viewed on Mac Preview or Safari it will appear all black.
- The solution is to save as PNG

[While I read in the Developer Manual that PNG was preferred because it was not lossy, I did not expect the lossy-ness would manifest as a image with significant colour problems, I was expecting very minor defects in detail as is normal with JPG.]

Matthew

John Matthews
Posts: 513
Joined: Wed Sep 12, 2007 3:18 pm

Re: headless

Post by John Matthews » Thu Dec 21, 2017 9:40 pm

It's not clear why you need a frame at all in a headless environment, but Swing GUI objects must be constructed and manipulated only on the event dispatch thread. The extra time for JPG compression may be exposing a latent race condition. ChartUtilities.writeChartAs* may be an alternative.

Locked