/* ===============================================================================
*
* Part of the InfoGlue Content Management Platform (www.infoglue.org)
*
* ===============================================================================
*
* Copyright (C)
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License version 2, as published by the
* Free Software Foundation. See the file LICENSE.html for more information.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY, including the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc. / 59 Temple
* Place, Suite 330 / Boston, MA 02111-1307 / USA.
*
* ===============================================================================
*/
package org.infoglue.deliver.util.graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.imageio.ImageIO;
import org.apache.log4j.Logger;
import org.infoglue.cms.util.CmsPropertyHandler;
import com.jhlabs.image.MarbleFilter;
import com.jhlabs.image.TwirlFilter;
/**
* Renders images and saves them.
* @author Per Jonsson - per.jonsson@it-huset.se
*
* @version 1.1 fixed reading properties from file, some optimizations and added a imageFileFormat,
*/
public class AdvancedImageRenderer
{
private static final long serialVersionUID = -1377395059993980530L;
private final static Logger logger = Logger.getLogger( AdvancedImageRenderer.class.getName() );
// type of image, colordepth etc.
private int imageType = BufferedImage.TYPE_4BYTE_ABGR;
// An template image to get the right rendering attributes for the font renderer
// don't change this in runtime.
private static BufferedImage templateImage = null;
// the rendered image
private BufferedImage renderedImage = null;
// Fontname
private String fontName = "Dialog";
// style of font
private int fontStyle = Font.PLAIN;
// size of font
private int fontSize = 18;
// font to render
private Font font = null;
// font color
private Color fgColor = new Color( 0, 0, 0, 255 ); // black
//background color
private Color bgColor = new Color( 255, 255, 255, 255 ); // white
// width of the rendered image, maxwidth if used with trimedges
private int renderWidth = 200;
// the textalign
private int align = 0; // 0 = left, 1 = right , 2 = center
// top padding in pixels
private int padTop = 4;
// bottom padding in pixels
private int padBottom = 4;
// left padding in pixels
private int padLeft = 4;
// right padding in pixels
private int padRight = 4;
// maximum number of textrows
private int maxRows = 20;
// default imageFormatName
private String imageFormatName = "png";
// 0 = notrim, 1 = left, 2 = right, 3 = left and right
private int trimEdges = 0;
// an url to for the background
private String backgroundImageUrl = null;
// just for caching
private BufferedImage backgroundImage = null;
// 0 = no, 1 = horizontal, 2 = vertical, 3 = both
private int tileBackgroundImage = 0;
//Capctha-params
private float twirlAspect = 1.1f;
private float marbleXScale = 2.0f;
private float marbleYScale = 2.0f;
private float marbleTurbulence = 0.9f;
private float marbleAmount = 0.9f;
private static Map renderHints = null;
// cached map of the methods
private static Map methodMap = null;
// if config is read from propertyfile these are stored here.
private static Map defaultConfigMap = new HashMap();
/**
* Creates a new instance of tne NewImageRenderer and reads in properties
* from the property file if exists. The propertieas must have the suffix of
* "rendertext" ie. rendertext.fontname.
*/
public AdvancedImageRenderer()
{
// precalc some setters for faster seach
if ( methodMap == null )
{
methodMap = new HashMap();
Method[] methods = this.getClass().getDeclaredMethods();
String name = null;
for ( int i = 0; i < methods.length; i++ )
{
name = methods[ i ].getName().toLowerCase();
if ( name.startsWith( "set" ) && methods[ i ].getParameterTypes().length == 1 )
{
name = name.substring( "set".length() );
methodMap.put( name , methods[ i ] );
// Add the default config from properties to the defaultConfig map if exists.
String propVal = CmsPropertyHandler.getProperty( "rendertext." + name );
if ( propVal != null && propVal.trim().length() > 0 )
{
//this.setAttribute( name, propVal.toLowerCase() );
defaultConfigMap.put( name, propVal.toLowerCase() );
}
}
}
logger.debug( defaultConfigMap );
}
if ( renderHints == null )
{
renderHints = new HashMap();
renderHints.put( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
renderHints.put( RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY );
renderHints.put( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
renderHints.put( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
renderHints.put( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY );
renderHints.put( RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE );
}
}
/**
* Write the rendered image to a file.
* @param file the file of the image to create.
* @return true if success, false if error
*/
public boolean writeImage( File file )
{
boolean success = false;
try
{
success = ImageIO.write( this.renderedImage, this.imageFormatName , file );
}
catch ( Exception e )
{
logger.error( "Couldn't write Image file : " + file, e );
}
return success;
}
/**
* Renders a text returnes the rendered picture.
* @param text The text to be rendered.
* @return an rendered image
*/
public BufferedImage renderImage( CharSequence text, Map renderAttributes )
{
AttributedString attributedString = new AttributedString( text.toString() );
return this.renderImage( attributedString, renderAttributes );
}
/**
* Renders a text returnes the rendered picture.
* @param attributedString an attributed string, to enable multicolored or
* similar texts.
* @return an rendered image
*/
public BufferedImage renderImage( AttributedString attributedString, Map renderAttributes )
{
// Copy the defaultconfig and merge with the supplied render attributes.
Map tempMap = new HashMap( defaultConfigMap );
if ( renderAttributes != null )
{
tempMap.putAll( renderAttributes );
}
renderAttributes = tempMap;
if ( renderAttributes != null && renderAttributes.size() > 0 )
{
Iterator keyIter = renderAttributes.entrySet().iterator();
while ( keyIter.hasNext() )
{
Map.Entry entry = (Map.Entry)keyIter.next();
String key = entry.getKey().toString().trim().toLowerCase();
if ( hasAttribute( key ) && entry.getValue() != null )
{
setAttribute( key, entry.getValue().toString() );
}
}
}
// Set TemplateImage
if ( templateImage == null )
{
templateImage = new BufferedImage( 8, 8, imageType );
}
font = new Font( fontName, fontStyle, fontSize );
// renderWidth = getSize().width; // temp when testing
float wrappingWidth = renderWidth - ( padLeft + padRight );
if ( wrappingWidth < 0 )
{
throw new IllegalArgumentException( "The renderwidth (" + renderWidth + ") is lesser than the total padding ("
+ ( padLeft + padRight ) + "), modify your settings.");
}
Graphics2D g2d = templateImage.createGraphics();
g2d.setRenderingHints( renderHints );
attributedString.addAttribute( TextAttribute.FONT, font );
attributedString.addAttribute( TextAttribute.FOREGROUND, fgColor );
FontRenderContext context = g2d.getFontRenderContext();
AttributedCharacterIterator iterator = attributedString.getIterator();
LineBreakMeasurer measurer = new LineBreakMeasurer( iterator, context );
TextLayout layout = null;
// precalculating the render pictureheight
double renderHeight = padTop + padBottom;
int numRows = 0;
while ( measurer.getPosition() < iterator.getEndIndex() )
{
if ( ( layout = measurer.nextLayout( wrappingWidth ) ) == null || ( numRows >= maxRows ) )
{
break;
}
numRows++;
renderHeight += layout.getAscent() + layout.getDescent() + layout.getLeading();
}
renderedImage = new BufferedImage( renderWidth, (int)( renderHeight + 0.5 ), templateImage.getType() );
Graphics2D img2d = renderedImage.createGraphics();
img2d.setRenderingHints( renderHints );
img2d.setColor( fgColor );
checkAndSetBackground();
Point2D.Float pen = new Point2D.Float( padLeft, padTop );
context = img2d.getFontRenderContext();
iterator = attributedString.getIterator();
measurer = new LineBreakMeasurer( iterator, context );
numRows = 0;
while ( measurer.getPosition() < iterator.getEndIndex() )
{
if ( ( layout = measurer.nextLayout( wrappingWidth ) ) == null || ( numRows >= maxRows ) )
{
break;
}
numRows++;
pen.y += layout.getAscent();
float dx = 0.0f;
if ( align == 1 || !layout.isLeftToRight() ) // align right
{
dx = ( wrappingWidth - layout.getVisibleAdvance() );
}
else if ( align == 2 ) // align center
{
dx = ( wrappingWidth - layout.getVisibleAdvance() ) / 2;
}
layout.draw( img2d, pen.x + dx, pen.y );
pen.y += layout.getDescent() + layout.getLeading();
}
// check and trim
renderedImage = horizontalTrim();
return renderedImage;
}
/**
* Checks the attributes and set the correct background.
*/
private void checkAndSetBackground()
{
Graphics2D img2d = renderedImage.createGraphics();
img2d.setBackground( bgColor );
img2d.clearRect( 0, 0, renderedImage.getWidth(), renderedImage.getHeight() );
if ( backgroundImageUrl != null )
{
try
{
if ( backgroundImage == null )
{
URLConnection connection = new URL( backgroundImageUrl ).openConnection();
// Hmm... Only 1.5 these below.
//connection.setConnectTimeout( 1000 * 5 ); // set the timeout to 5 seconds.
//connection.setReadTimeout( 1000 * 5 ); // set the timeout to 5 seconds.
InputStream is = connection.getInputStream();
backgroundImage = ImageIO.read( is );
is.close();
}
if ( tileBackgroundImage == 1 && backgroundImage.getWidth() < renderedImage.getWidth() ) // horizontal
{
int xnum = (int)( renderedImage.getWidth() / backgroundImage.getWidth() + 0.5 ) + 1;
while ( xnum-- >= 0 )
{
img2d.drawImage( backgroundImage, backgroundImage.getWidth() * xnum, 0, null );
}
}
if ( tileBackgroundImage == 2 && backgroundImage.getHeight() < renderedImage.getHeight() ) // vertical
{
int ynum = (int)( renderedImage.getHeight() / backgroundImage.getHeight() + 0.5 ) + 1;
while ( ynum-- >= 0 )
{
img2d.drawImage( backgroundImage, 0, backgroundImage.getHeight() * ynum, null );
}
}
if ( tileBackgroundImage == 3 && backgroundImage.getHeight() < renderedImage.getHeight() ) // vertical
{
int ynum = (int)( renderedImage.getHeight() / backgroundImage.getHeight() + 0.5 ) + 1;
while ( ynum-- >= 0 )
{
int xnum = (int)( renderedImage.getWidth() / backgroundImage.getWidth() + 0.5 ) + 1;
while ( xnum-- >= 0 )
{
img2d.drawImage( backgroundImage, backgroundImage.getWidth() * xnum, backgroundImage.getHeight() * ynum, null );
}
}
}
if ( tileBackgroundImage == 0 )
{
img2d.drawImage( backgroundImage, 0, 0, null );
}
}
catch ( IOException ioe )
{
logger.error( "Error in reading backgoundImageUrl: " + backgroundImageUrl, ioe );
}
}
}
/**
* Trims the edges of the image.
* @return a new trimmed image from the original.
*/
private BufferedImage horizontalTrim()
{
if ( trimEdges == 0 )
{
return renderedImage;
}
int imgHeight = renderedImage.getHeight();
int imgWidth = renderedImage.getWidth();
int bgRGB = bgColor.getRGB(); // get the background color
// check and trim left side
int w = 0;
int leftPos = 0, rightPos = 0;
if ( this.trimEdges == 1 || this.trimEdges == 3 )
{
loop: for ( w = 0; w < imgWidth; w++ )
{
int imgRGB = 0;
for ( int y = 0; y < imgHeight; y++ )
{
imgRGB = renderedImage.getRGB( w, y );
if ( imgRGB != bgRGB )
{
break loop;
}
}
}
leftPos = ( w > 0 ) ? w - 1 : 0;
leftPos -= padLeft;
// ensure none negative numbers
leftPos = ( leftPos <= 0 ) ? 0 : leftPos;
}
// check and trim right side
if ( this.trimEdges == 2 || this.trimEdges == 3 )
{
loop: for ( w = ( imgWidth - 1 ); w >= 0; w-- )
{
int imgRGB = 0;
for ( int y = 0; y < imgHeight; y++ )
{
imgRGB = renderedImage.getRGB( w, y );
if ( imgRGB != bgRGB )
{
break loop;
}
}
}
rightPos = w + 1;
rightPos += padRight;
// ensure not outside
rightPos = ( rightPos > imgWidth ) ? imgWidth - 1 : rightPos;
}
else
{
rightPos = imgWidth - 1;
}
return renderedImage.getSubimage( leftPos, 0, rightPos - leftPos, imgHeight - 1 );
}
public void distortImage()
{
// twirl filter
TwirlFilter tf = new TwirlFilter();
float angle = this.twirlAspect;
tf.setAngle( angle );
tf.filter(renderedImage, renderedImage);
// Marble Filter
MarbleFilter mf = new MarbleFilter();
mf.setXScale( this.marbleXScale );
mf.setYScale( this.marbleYScale );
mf.setTurbulence( this.marbleTurbulence );
mf.setAmount( this.marbleAmount );
mf.filter(renderedImage, renderedImage);
}
/**
* Check if this class has a specific attribute, name of attribute is case
* insensitive. ie. "fontname", "fontsize", "bgcolor"
* @param attributeName name of the attribute to check
* @return true if attribute exisist, false otherwise
*/
public boolean hasAttribute( CharSequence attributeName )
{
return methodMap.containsKey( attributeName.toString().toLowerCase() );
}
/**
* Using reflection to set the fields corresponing to the attribute. tries
* to convert to the right object. The attribute is caseinsesitive. <br>
* If it's a color value it has to be a string in the format
* "252:123:133:255" where they are "R:G:B:A" values from 0-255.
* @param attribute the field/property to set
* @param value the value to set.
*/
public void setAttribute( CharSequence attribute, CharSequence value )
{
logger.debug("set attribute: " + attribute + " = " + value );
Method method = (Method)methodMap.get( attribute );
if ( method != null )
{
try
{
Class[] params = method.getParameterTypes();
Class param = params[ 0 ];
if ( param.isPrimitive() )
{
if ( param.getName().equals( "int" ) )
{
method.invoke( this, new Object[]
{ new Integer(Integer.parseInt( value.toString() )) } );
}
else if ( param.getName().equals( "float" ) )
{
method.invoke( this, new Object[]
{ new Float(Float.parseFloat( value.toString() )) } );
}
else if ( param.getName().equals( "double" ) )
{
method.invoke( this, new Object[]
{ new Double(Double.parseDouble( value.toString() )) } );
}
else if ( param.getName().equals( "boolean" ) )
{
method.invoke( this, new Object[]
{ new Boolean(Boolean.parseBoolean( value.toString() )) } );
}
}
else if ( param.equals( String.class ) )
{
method.invoke( this, new Object[]
{ value.toString() } );
}
else if ( param.equals( Color.class ) )
{
method.invoke( this, new Object[]
{ ColorHelper.getColor( value.toString() ) } );
}
}
catch ( Exception e )
{
logger.warn( "Error in setting properties: " + attribute + " = " + value, e );
}
}
else
{
logger.warn( "No attribut, named: " + attribute + " found, value =" + value );
}
}
/**
* @param align The align to set.
*/
public void setAlign( int align )
{
this.align = align;
}
/**
* @param backgroundImage The backgroundImage to set.
*/
public void setBackgroundImage( BufferedImage backgroundImage )
{
this.backgroundImage = backgroundImage;
}
/**
* @param backgroundImageUrl The backgroundImageUrl to set.
*/
public void setBackgroundImageURL( String backgroundImageUrl )
{
this.backgroundImageUrl = backgroundImageUrl;
}
/**
* @param bgColor The bgColor to set.
*/
public void setBgColor( Color bgColor )
{
this.bgColor = bgColor;
}
/**
* @param fgColor The fgColor to set.
*/
public void setFgColor( Color fgColor )
{
this.fgColor = fgColor;
}
/**
* @param fontName The fontName to set.
*/
public void setFontName( String fontName )
{
this.fontName = fontName;
}
/**
* @param fontSize The fontSize to set.
*/
public void setFontSize( int fontSize )
{
this.fontSize = fontSize;
}
/**
* @param fontStyle The fontStyle to set.
*/
public void setFontStyle( int fontStyle )
{
this.fontStyle = fontStyle;
}
/**
* @param imageType The imageType to set.
*/
public void setImageType( int imageType )
{
this.imageType = imageType;
}
/**
* @param padBottom The padBottom to set.
*/
public void setPadBottom( int padBottom )
{
this.padBottom = padBottom;
}
/**
* @param padLeft The padLeft to set.
*/
public void setPadLeft( int padLeft )
{
this.padLeft = padLeft;
}
/**
* @param padRight The padRight to set.
*/
public void setPadRight( int padRight )
{
this.padRight = padRight;
}
/**
* Sets all paddings to the same value.
* @param pad The padRight, padLeft, padTop andpadBottom to set.
*/
public void setPad( int pad )
{
this.padRight = pad;
this.padLeft = pad;
this.padTop = pad;
this.padBottom = pad;
}
/**
* @param padTop The padTop to set.
*/
public void setPadTop( int padTop )
{
this.padTop = padTop;
}
/**
* @param renderHints The renderHints to set.
*/
public void setRenderHints( Map renderHints )
{
this.renderHints = renderHints;
}
/**
* @param renderWidth The renderWidth to set.
*/
public void setRenderWidth( int renderWidth )
{
this.renderWidth = renderWidth;
}
/**
* @param templateImage The templateImage to set.
*/
public void setTemplateImage( BufferedImage templateImage )
{
this.templateImage = templateImage;
}
/**
* @param tileBackgroundImage The tileBackgroundImage to set.
*/
public void setTileBackgroundImage( int tileBackgroundImage )
{
this.tileBackgroundImage = tileBackgroundImage;
}
/**
* @param backgroundImageUrl The backgroundImageUrl to set.
*/
public void setBackgroundImageUrl( String backgroundImageUrl )
{
this.backgroundImageUrl = backgroundImageUrl;
}
/**
* @param maxRows The maxRows to set.
*/
public void setMaxRows( int maxRows )
{
this.maxRows = maxRows;
}
/**
* @param trimEdges The trimEdges to set.
*/
public void setTrimEdges( int trimEdges )
{
this.trimEdges = trimEdges;
}
/**
* @param imageFormatName the format of the image. ie ( PNG, GIF);
*/
public void setImageFormatName( String imageFormatName )
{
this.imageFormatName = imageFormatName;
}
/**
* Get the image format name, default is "png" if none is set.
* @return a string with the image format name used by the renderer.
*/
public String getImageFormatName()
{
return this.imageFormatName;
}
public void setTwirlAspect(float twirlAspect)
{
this.twirlAspect = twirlAspect;
}
public void setMarbleXScale(float marbleXScale)
{
this.marbleXScale = marbleXScale;
}
public void setMarbleYScale(float marbleYScale)
{
this.marbleYScale = marbleYScale;
}
public void setMarbleTurbulence(float marbleTurbulence)
{
this.marbleTurbulence = marbleTurbulence;
}
public void setMarbleAmount(float marbleAmount)
{
this.marbleAmount = marbleAmount;
}
}