/**
 * Copyright (c) 2014, the Railo Company Ltd.
 * Copyright (c) 2015, Lucee Assosication Switzerland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package lucee.runtime.img;

import java.awt.AWTException;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.QuadCurve2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.PackedColorModel;
import java.awt.image.PixelGrabber;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.ParameterBlock;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.text.AttributedString;
import java.util.Iterator;
import java.util.Locale;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.media.jai.BorderExtender;
import javax.media.jai.BorderExtenderConstant;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.operator.ShearDir;
import javax.media.jai.operator.TransposeType;
import javax.swing.ImageIcon;

import lucee.commons.io.IOUtil;
import lucee.commons.io.res.Resource;
import lucee.commons.lang.CFTypes;
import lucee.commons.lang.ExceptionUtil;
import lucee.commons.lang.StringUtil;
import lucee.commons.lang.font.FontUtil;
import lucee.runtime.PageContext;
import lucee.runtime.dump.DumpData;
import lucee.runtime.dump.DumpProperties;
import lucee.runtime.dump.DumpTable;
import lucee.runtime.engine.ThreadLocalPageContext;
import lucee.runtime.exp.CasterException;
import lucee.runtime.exp.ExpressionException;
import lucee.runtime.exp.PageException;
import lucee.runtime.exp.PageRuntimeException;
import lucee.runtime.functions.image.ImageGetEXIFMetadata;
import lucee.runtime.img.filter.QuantizeFilter;
import lucee.runtime.img.gif.GifEncoder;
import lucee.runtime.interpreter.VariableInterpreter;
import lucee.runtime.op.Caster;
import lucee.runtime.op.Constants;
import lucee.runtime.op.Decision;
import lucee.runtime.text.xml.XMLUtil;
import lucee.runtime.type.Array;
import lucee.runtime.type.ArrayImpl;
import lucee.runtime.type.Collection;
import lucee.runtime.type.ObjectWrap;
import lucee.runtime.type.Struct;
import lucee.runtime.type.StructImpl;
import lucee.runtime.type.UDFPlus;
import lucee.runtime.type.dt.DateTime;
import lucee.runtime.type.util.ArrayUtil;
import lucee.runtime.type.util.KeyConstants;
import lucee.runtime.type.util.ListUtil;
import lucee.runtime.type.util.MemberUtil;
import lucee.runtime.type.util.StructSupport;

import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Image extends StructSupport implements Cloneable,Struct {
	private static final long serialVersionUID = -2370381932689749657L;


	public static final int BORDER_TYPE_CONSTANT=-1;

	
	public static final int INTERPOLATION_NONE=0;
	public static final int INTERPOLATION_NEAREST=1;
	public static final int INTERPOLATION_BILINEAR=2;
	public static final int INTERPOLATION_BICUBIC=3;

	public static final int IP_NONE=0;
	
	public static final int IPC_NEAREST=1;
	public static final int IPC_BILINEAR=2;
	public static final int IPC_BICUBIC=3;
	public static final int IPC_MAX=3;
	
	public static final int IP_HIGHESTQUALITY=100;
	public static final int IP_HIGHQUALITY=101;
	public static final int IP_MEDIUMQUALITY=102;
	public static final int IP_HIGHESTPERFORMANCE=103;
	public static final int IP_HIGHPERFORMANCE=104;
	public static final int IP_MEDIUMPERFORMANCE=105;
	
	public static final int IP_BESSEL=109;
	public static final int IP_BLACKMAN=110;
	public static final int IP_HAMMING=111;
	public static final int IP_HANNING=112;
	public static final int IP_HERMITE=113;
	public static final int IP_LANCZOS=114;
	public static final int IP_MITCHELL=115;
	public static final int IP_QUADRATIC=116;
	public static final int IP_TRIANGLE=117;

	private static final int ANTI_ALIAS_NONE=0;
	private static final int ANTI_ALIAS_ON=1;
	private static final int ANTI_ALIAS_OFF=2;


	private static final String FORMAT = "javax_imageio_1.0";
	
	private BufferedImage _image;
	private Resource source=null;
	private String format;

	private Graphics2D graphics;

	private Color bgColor;
	private Color fgColor;
	private Color xmColor;

	private float tranparency=-1;
	private int antiAlias=ANTI_ALIAS_NONE;

	private Stroke stroke;

	private Struct sctInfo;


	private float alpha=1;


	private Composite composite;
	private static Object sync=new Object();

	
	static {
		ImageIO.scanForPlugins();
	}

	public Image(byte[] binary) throws IOException {
		this(binary, null); 
	}
	
	public Image(byte[] binary, String format) throws IOException {
		if(StringUtil.isEmpty(format))format=ImageUtil.getFormat(binary,null);
		this.format=format;
		_image=ImageUtil.toBufferedImage(binary,format);
		if(_image==null) throw new IOException("can not read in image");
	}

	public Image(Resource res) throws IOException {
		this(res,null);
	}
	public Image(Resource res, String format) throws IOException {
		if(StringUtil.isEmpty(format))format=ImageUtil.getFormat(res);
		this.format=format;
		
		_image=ImageUtil.toBufferedImage(res,format);
		this.source=res;
		if(_image==null) throw new IOException("can not read in file "+res);
	}


	public Image(BufferedImage image) {
		this._image=image;
		this.format=null;
	}
	

	public Image(String b64str) throws IOException {
		this(b64str,null);
	}
	
	
	public Image(String b64str, String format) throws IOException {
		// load binary from base64 string and get format
		StringBuilder mimetype=new StringBuilder();
		byte[] binary = ImageUtil.readBase64(b64str,mimetype);
		if(StringUtil.isEmpty(format) && !StringUtil.isEmpty(mimetype)) {
			format=ImageUtil.getFormatFromMimeType(mimetype.toString());
		}

		if(StringUtil.isEmpty(format))format=ImageUtil.getFormat(binary,null);
		this.format=format;
		_image=ImageUtil.toBufferedImage(binary,format);
		if(_image==null) throw new IOException("can not read in image");
	}
	
	

	public Image(int width, int height, int imageType, Color canvasColor) throws ExpressionException {
		_image = new BufferedImage(width, height, imageType);
		if(!StringUtil.isEmpty(canvasColor)){
			
			setBackground(canvasColor);
			clearRect(0, 0, width, height);
		}
		this.format=null;
	}

	public Image() {
		this.format=null;
	}

		
	

	/**
	 * add a border to image
	 * @param thickness
	 * @param color
	 * @param borderType 
	 */
	public void addBorder(int thickness, Color color, int borderType)  throws ExpressionException{
		ColorModel cm = image().getColorModel();
		if (((cm instanceof IndexColorModel)) && (cm.hasAlpha()) && (!cm.isAlphaPremultiplied())) {
			image(paletteToARGB(image()));
			cm = image().getColorModel();
		}

		BufferedImage alpha = null;
		if ((cm.getNumComponents() > 3) && (cm.hasAlpha())) {
			alpha = getAlpha(image());
			image(removeAlpha(image()));
		}
		if (alpha != null) {
			ParameterBlock params1 = new ParameterBlock();
			params1.addSource(alpha);
			
			params1.add(thickness); // left
			params1.add(thickness); // right
			params1.add(thickness); // top
			params1.add(thickness); // bottom
			params1.add(new BorderExtenderConstant(new double[] { 255D }));
			
			RenderingHints hints = new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
			hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE));
			alpha = JAI.create("border", params1, hints).getAsBufferedImage();
		}

	    ParameterBlock params = new ParameterBlock();
	    params.addSource(image());
	    params.add(thickness); // left
		params.add(thickness); // right
		params.add(thickness); // top
		params.add(thickness); // bottom
	    params.add(toBorderExtender(borderType,color));

	    image(JAI.create("border", params).getAsBufferedImage());

	    if (alpha != null) {
	      image(addAlpha(image(), alpha, thickness, thickness));
	    }
	}
	
	private Object toBorderExtender(int borderType, Color color) {
		if (borderType==Image.BORDER_TYPE_CONSTANT) {
			double[] colorArray = { color.getRed(), color.getGreen(), color.getBlue() };
			return new BorderExtenderConstant(colorArray);
		}
		return BorderExtender.createInstance(borderType);
	}

	public void blur(int blurFactor)  throws ExpressionException{
	    ParameterBlock params = new ParameterBlock();
		params.addSource(image());
		params.add(blurFactor);
		RenderingHints hint= new RenderingHints(JAI.KEY_BORDER_EXTENDER,BorderExtender.createInstance(1));
		image(JAI.create("boxfilter", params, hint).getAsBufferedImage());
	}

	public void clearRect(int x, int y, int width, int height)  throws ExpressionException{
		getGraphics().clearRect(x, y, width, height);
	}
	

	public Struct info()  throws PageException {
		if(sctInfo!=null) return sctInfo;
		
		Struct sctInfo=new StructImpl(),sct;
		ImageMetaDrew.addInfo(format,source,sctInfo);
		sctInfo=ImageGetEXIFMetadata.flatten(sctInfo);
		
		sctInfo.setEL(KeyConstants._height,new Double(getHeight()));
		sctInfo.setEL(KeyConstants._width,new Double(getWidth()));
		sctInfo.setEL(KeyConstants._source,source==null?"":source.getAbsolutePath());
		//sct.setEL("mime_type",getMimeType());
		
		ColorModel cm = image().getColorModel();
		sct=new StructImpl();
		sctInfo.setEL("colormodel",sct);
		
		sct.setEL("alpha_channel_support",Caster.toBoolean(cm.hasAlpha()));
		sct.setEL("alpha_premultiplied",Caster.toBoolean(cm.isAlphaPremultiplied()));
		sct.setEL("transparency",toStringTransparency(cm.getTransparency()));
		sct.setEL("pixel_size",Caster.toDouble(cm.getPixelSize()));
		sct.setEL("num_components",Caster.toDouble(cm.getNumComponents()));
		sct.setEL("num_color_components",Caster.toDouble(cm.getNumColorComponents()));
		sct.setEL("colorspace",toStringColorSpace(cm.getColorSpace()));
		
	    //bits_component
		int[] bitspercomponent = cm.getComponentSize();
		Array arr=new ArrayImpl();
		Double value;
	    for (int i = 0; i < bitspercomponent.length; i++) {
	    	sct.setEL("bits_component_" + (i + 1),value=new Double(bitspercomponent[i]));
	    	arr.appendEL(value);
	    }
		sct.setEL("bits_component",arr);
		
	    // colormodel_type
		if (cm instanceof ComponentColorModel)		sct.setEL("colormodel_type", "ComponentColorModel");
		else if (cm instanceof IndexColorModel)		sct.setEL("colormodel_type", "IndexColorModel");
		else if (cm instanceof PackedColorModel)	sct.setEL("colormodel_type", "PackedColorModel");
		else sct.setEL("colormodel_type", ListUtil.last(cm.getClass().getName(), '.'));

		
		getMetaData(sctInfo);
		//Metadata.addInfo(format,source,sctInfo);
		Metadata.addExifInfo(format,source,sctInfo);
		this.sctInfo=sctInfo;
		return sctInfo;
	}

	public IIOMetadata getMetaData(Struct parent) {
        InputStream is=null;
        javax.imageio.stream.ImageInputStreamImpl iis=null;
    	try {
        	
        	if(source instanceof File) { 
				iis=new FileImageInputStream((File) source);
			}
			else if(source==null)iis=new MemoryCacheImageInputStream(new ByteArrayInputStream(getImageBytes(format,true)));
			else iis=new MemoryCacheImageInputStream(is=source.getInputStream());
			
            Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
            if (readers.hasNext()) {
            	// pick the first available ImageReader
                ImageReader reader = readers.next();
                IIOMetadata meta=null;
                synchronized (sync) {
	                // attach source to the reader
	                reader.setInput(iis, true);
	
	                // read metadata of first image
	                meta = reader.getImageMetadata(0);
	                meta.setFromTree(FORMAT, meta.getAsTree(FORMAT));
	                reader.reset();
                }
                // generating dump
                if(parent!=null){
	                String[] formatNames = meta.getMetadataFormatNames();
					for(int i=0;i<formatNames.length;i++) {
						Node root = meta.getAsTree(formatNames[i]);
						//print.out(XMLCaster.toString(root));
						addMetaddata(parent,"metadata",root);
					}
                }
                return meta;
            }
        }
        catch(Throwable t) {ExceptionUtil.rethrowIfNecessary(t);}
        finally{
        	ImageUtil.closeEL(iis);
			IOUtil.closeEL(is);
        }
        return null;
    }

	private void addMetaddata(Struct parent, String name, Node node) {
		
		
		// attributes
		NamedNodeMap attrs = node.getAttributes();
		Attr attr;
		int len=attrs.getLength();
		if(len==1 && "value".equals(attrs.item(0).getNodeName())) {
			parent.setEL(name, attrs.item(0).getNodeValue());
		}
		else {
			Struct sct=metaGetChild(parent,name);
			for(int i=attrs.getLength()-1;i>=0;i--) {
				attr=(Attr) attrs.item(i);
				sct.setEL(attr.getName(), attr.getValue());
			}
		}
		
		
		// child nodes
		NodeList children = XMLUtil.getChildNodes(node, Node.ELEMENT_NODE);
		Element el;
		for(int i=children.getLength()-1;i>=0;i--) {
			el=(Element) children.item(i);
			Struct sct = metaGetChild(parent,name);
			addMetaddata(sct, el.getNodeName(),children.item(i));
		}
	}

	private Struct metaGetChild(Struct parent, String name) {
		Object child=parent.get(name,null);
		if(child instanceof Struct) return (Struct) child;
		Struct sct=new StructImpl();
		parent.setEL(name, sct);
		return sct;
	}

	public void sharpen(float gain)  throws ExpressionException{
		ParameterBlock params = new ParameterBlock();
		params.addSource(image());
		params.add((Object) null);
		params.add(new Float(gain));
		image(JAI.create("unsharpmask", params).getAsBufferedImage());
	}
	
	public void setTranparency(float percent)  throws ExpressionException{
		if(percent==-1)return;
		tranparency=percent;
		AlphaComposite rule = AlphaComposite.getInstance(3, 1.0F-(percent/100.0F));
		getGraphics().setComposite(rule);
	}

	 public void invert()  throws ExpressionException{
		ParameterBlock params = new ParameterBlock();
		params.addSource(image());
		image(JAI.create("invert", params).getAsBufferedImage());
	}

    public Image copy(float x, float y, float width, float height)  throws ExpressionException{
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.add(x);
    	params.add(y);
    	params.add(width);
    	params.add(height);
    	//image(JAI.create("crop", params).getAsBufferedImage());
    	return new Image(JAI.create("crop", params).getAsBufferedImage());
    }
    
    public Image copy(float x, float y, float width, float height, float dx,float dy)  throws ExpressionException{
    	Image img = copy(x, y, width, height);
		img.getGraphics().copyArea((int)x, (int)y, (int)width, (int)height, (int)(dx-x), (int)(dy-y));
		return img;
    }

    public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle, boolean filled)  throws ExpressionException{
    	if (filled)
    		getGraphics().fillArc(x, y, width, height, startAngle, arcAngle);
    	else
    		getGraphics().drawArc(x, y, width, height, startAngle, arcAngle);
    }
    

    public void draw3DRect(int x, int y, int width, int height, boolean raised, boolean filled)  throws ExpressionException{
    	if (filled)
    		getGraphics().fill3DRect(x, y, width+1, height+1, raised);
    	else
    		getGraphics().draw3DRect(x, y, width, height, raised);
    }
    
    public void drawCubicCurve(double ctrlx1, double ctrly1, double ctrlx2, double ctrly2,double x1, double y1, double x2, double y2)  throws ExpressionException{
    	CubicCurve2D curve = new CubicCurve2D.Double(x1,y1,ctrlx1,ctrly1,ctrlx2,ctrly2,x2,y2);
		getGraphics().draw(curve);
    }

    public void drawPoint(int x, int y) throws ExpressionException {
    	drawLine(x, y, x + 1, y);
    }
    
    public void drawQuadraticCurve(double x1, double y1, double ctrlx, double ctrly, double x2, double y2)  throws ExpressionException {
    	QuadCurve2D curve = new QuadCurve2D.Double(x1, y1, ctrlx, ctrly, x2, y2);
    	getGraphics().draw(curve);
    }
    
    public void drawRect(int x, int y, int width, int height, boolean filled)  throws ExpressionException {
    	if (filled)
    	    getGraphics().fillRect(x, y, width + 1, height + 1);
    	else
    		getGraphics().drawRect(x, y, width, height);
    }
    
    public void drawRoundRect(int x, int y, int width, int height,int arcWidth, int arcHeight, boolean filled)  throws ExpressionException{
    	if (filled)
    		getGraphics().fillRoundRect(x, y, width + 1, height + 1, arcWidth,arcHeight);
    	else
    		getGraphics().drawRoundRect(x, y, width, height, arcWidth, arcHeight);
    }
    


	public void drawLine(int x1, int y1, int x2, int y2)  throws ExpressionException{
    	getGraphics().drawLine(x1, y1, x2, y2);
    }

	public void drawImage(Image img,int x, int y)  throws ExpressionException{
    	getGraphics().drawImage(img.image(), x, y,null);
    }

	public void drawImage(Image img,int x, int y, int width, int height)  throws ExpressionException{
    	getGraphics().drawImage(img.image(), x, y,width,height,null);
    }
	
    public void drawLines(int[] xcoords, int[] ycoords, boolean isPolygon,boolean filled)  throws ExpressionException{
    	if (isPolygon) {
    		if (filled)	getGraphics().fillPolygon(xcoords, ycoords, xcoords.length);
    		else 		getGraphics().drawPolygon(xcoords, ycoords, xcoords.length);
    	} 
    	else {
    					getGraphics().drawPolyline(xcoords, ycoords, xcoords.length);
    	}
    }
    public void drawOval(int x, int y, int width, int height, boolean filled) throws ExpressionException {
    	if (filled)	getGraphics().fillOval(x, y, width, height);
    	else getGraphics().drawOval(x, y, width, height);
	}
    
    public void drawString(String text, int x, int y, Struct attr) throws PageException {
    	
    	if (attr != null && attr.size()>0) {

       	 // font
       		String font=StringUtil.toLowerCase(Caster.toString(attr.get("font",""))).trim();
       	    if(!StringUtil.isEmpty(font)) {
   	    	    font=FontUtil.getFont(font).getFontName();
       	    }
       	    else font = "Serif";
       	    
    	 // alpha
    		//float alpha=Caster.toFloatValue(attr.get("alpha",null),1F);
    	    
    	 // size
    	    int size=Caster.toIntValue(attr.get(KeyConstants._size, Constants.INTEGER_10));

    	 // style
    	    int style=Font.PLAIN;
    	    String strStyle=StringUtil.toLowerCase(Caster.toString(attr.get("style","")));
    	    strStyle=StringUtil.removeWhiteSpace(strStyle);
    	    if(!StringUtil.isEmpty(strStyle)) {
	    	    if("plain".equals(strStyle)) style=Font.PLAIN;
	    	    else if("bold".equals(strStyle)) style=Font.BOLD;
	    	    else if("italic".equals(strStyle)) style=Font.ITALIC;
	    	    else if("bolditalic".equals(strStyle)) style=Font.BOLD+Font.ITALIC;
	    	    else if("bold,italic".equals(strStyle)) style=Font.BOLD+Font.ITALIC;
	    	    else if("italicbold".equals(strStyle)) style=Font.BOLD+Font.ITALIC;
	    	    else if("italic,bold".equals(strStyle)) style=Font.BOLD+Font.ITALIC;
	    	    else throw new ExpressionException(
	    	    		"key style of argument attributeCollection has an invalid value ["+strStyle+"], valid values are [plain,bold,italic,bolditalic]");
    	    }

    	 // strikethrough
    	    boolean strikethrough = Caster.toBooleanValue(attr.get("strikethrough",Boolean.FALSE));
    	    
    	 // underline
    	    boolean underline = Caster.toBooleanValue(attr.get("underline",Boolean.FALSE));
    	    
    	    AttributedString as = new AttributedString(text);
    	    as.addAttribute(TextAttribute.FONT, new Font(font, style, size));
    	    if(strikethrough)	as.addAttribute(TextAttribute.STRIKETHROUGH,TextAttribute.STRIKETHROUGH_ON);
    	    if(underline)		as.addAttribute(TextAttribute.UNDERLINE,TextAttribute.UNDERLINE_ON);
    	    Graphics2D g = getGraphics();
    	    //if(alpha!=1D) setAlpha(g,alpha);
    	    
    	    g.drawString(as.getIterator(), x, y);
    	} 
    	else getGraphics().drawString(text, x, y);
        
    }
    
    
    /*private void setAlpha(Graphics2D graphics,float alpha) {
    	//Composite originalComposite = graphics.getComposite();
    	
    	AlphaComposite alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
        
    	graphics.setComposite(alphaComposite);
    	//graphics.setComposite(originalComposite);	
	}*/

	public void setDrawingStroke(Struct attr) throws PageException {
    	
    	// empty 
    	if(attr==null || attr.size()==0) {
    		setDrawingStroke(new BasicStroke());
    		return;
    	}
    	
    	// width
    	float width=Caster.toFloatValue(attr.get("width",new Float(1F)));
    	if(width<0) throw new ExpressionException("key [width] should be a none negativ number");
    	
    	// endcaps
    	String strEndcaps=Caster.toString(attr.get("endcaps","square"));
    	strEndcaps=strEndcaps.trim().toLowerCase();
    	int endcaps;
    	if("square".equals(strEndcaps))		endcaps = BasicStroke.CAP_SQUARE;
    	else if("butt".equals(strEndcaps))	endcaps = BasicStroke.CAP_BUTT;
    	else if("round".equals(strEndcaps))	endcaps = BasicStroke.CAP_ROUND;
    	else throw new ExpressionException("key [endcaps] has an invalid value ["+strEndcaps+"], valid values are [square,round,butt]");
    	
    	// linejoins
    	String strLinejoins=Caster.toString(attr.get("linejoins","miter"));
    	strLinejoins=strLinejoins.trim().toLowerCase();
    	int linejoins;
    	if("bevel".equals(strLinejoins))		linejoins = BasicStroke.JOIN_BEVEL;
    	else if("miter".equals(strLinejoins))	linejoins = BasicStroke.JOIN_MITER;
    	else if("round".equals(strLinejoins))	linejoins = BasicStroke.JOIN_ROUND;
    	else throw new ExpressionException("key [linejoins] has an invalid value ["+strLinejoins+"], valid values are [bevel,miter,round]");
    	
    	// miterlimit
    	float miterlimit = 10.0F;
    	if(linejoins==BasicStroke.JOIN_MITER) {
    		miterlimit=Caster.toFloatValue(attr.get("miterlimit",new Float(10F)));
        	if(miterlimit<1F) throw new ExpressionException("key [miterlimit] should be greater or equal to 1");
    	}
    	
    	// dashArray
    	Object oDashArray=attr.get("dashArray",null);
    	float[] dashArray=null;
    	if(oDashArray!=null) {
    		dashArray=ArrayUtil.toFloatArray(oDashArray);
    	}
    	
    	// dash_phase
    	float dash_phase=Caster.toFloatValue(attr.get("dash_phase",new Float(0F)));
    	
    	
    	
    	setDrawingStroke(width, endcaps, linejoins, miterlimit, dashArray, dash_phase);
    }
        
    public void setDrawingStroke(float width, int endcaps, int linejoins,float miterlimit, float[] dash,float dash_phase)  throws ExpressionException {
    	setDrawingStroke(new BasicStroke(width, endcaps, linejoins, miterlimit, dash, dash_phase));
	}
	    
	public void setDrawingStroke(Stroke stroke) throws ExpressionException {
		if(stroke==null) return;
		this.stroke=stroke;
	    getGraphics().setStroke(stroke);
	}
    
    
    public void flip(TransposeType transpose) throws ExpressionException {
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.add(transpose);
    	image(JAI.create("transpose", params).getAsBufferedImage());
    }

    public void grayscale() throws ExpressionException {
    	BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_BYTE_GRAY);
    	Graphics2D graphics = img.createGraphics();
    	graphics.drawImage(image(),new AffineTransformOp(AffineTransform.getTranslateInstance(0.0, 0.0),1),0, 0);
    	graphics.dispose();
    	image(img);
    }

    public void rgb() throws ExpressionException {
    	BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_BYTE_INDEXED);
    	Graphics2D graphics = img.createGraphics();
    	graphics.drawImage(image(),new AffineTransformOp(AffineTransform.getTranslateInstance(0.0, 0.0),1),0, 0);
    	graphics.dispose();
    	image(img);
    	
    }
    public void threeBBger() throws ExpressionException {
    	BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_3BYTE_BGR);
    	Graphics2D graphics = img.createGraphics();
    	graphics.drawImage(image(),new AffineTransformOp(AffineTransform.getTranslateInstance(0.0, 0.0),1),0, 0);
    	graphics.dispose();
    	image(img);
    }
    
    public void overlay(Image topImage) throws ExpressionException {
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.addSource(topImage.image());
    	image(JAI.create("overlay", params).getAsBufferedImage());
    }
    
    public void paste(Image topImage, int x, int y) throws ExpressionException {
    	RenderingHints interp = new RenderingHints(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    	BorderExtender extender = BorderExtender.createInstance(1);
    	Graphics2D g = getGraphics();
    	g.addRenderingHints(new RenderingHints(JAI.KEY_BORDER_EXTENDER,extender));
    	g.drawImage(topImage.image(), (new AffineTransformOp(AffineTransform.getTranslateInstance(x,y),interp)), 0, 0);
    	
    }
    
    public void setXorMode(Color color) throws ExpressionException {
    	if(color==null) return;
    	xmColor=color;
    	getGraphics().setXORMode(color);
    }
    

    public void translate(int xtrans, int ytrans, Object interpolation) throws ExpressionException {
    	
    	RenderingHints hints = new RenderingHints(RenderingHints.KEY_INTERPOLATION,interpolation);
    	if(interpolation!=RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
    		hints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(1)));
    	}
    	
    	ParameterBlock pb = new ParameterBlock();
    	pb.addSource(image());
    	BufferedImage img = JAI.create("translate", pb).getAsBufferedImage();
    	Graphics2D graphics = img.createGraphics();
    	graphics.clearRect(0, 0, img.getWidth(), img.getHeight());
    	AffineTransform at = new AffineTransform();
    	at.setToIdentity();
    	graphics.drawImage(image(), new AffineTransformOp(at, hints), xtrans, ytrans);
    	graphics.dispose();
    	image(img);
    }
    
    public void translateAxis(int x, int y) throws ExpressionException {
    	getGraphics().translate(x, y);
    }

    public void rotateAxis(double angle) throws ExpressionException {
    	getGraphics().rotate(Math.toRadians(angle));
    }
        
    public void rotateAxis(double angle, double x, double y) throws ExpressionException {
    	getGraphics().rotate(Math.toRadians(angle), x, y);
    }
        
    public void shearAxis(double shx, double shy) throws ExpressionException {
    	getGraphics().shear(shx, shy);
    }
    
    public void shear(float shear, ShearDir direction, Object interpolation) throws ExpressionException {
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.add(shear);
    	params.add(direction);
    	params.add(0.0F);
    	params.add(0.0F);
    	RenderingHints hints = null;
    	
    	if (interpolation==RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)
    	    params.add(Interpolation.getInstance(0));
    	else if (interpolation==RenderingHints.VALUE_INTERPOLATION_BILINEAR) {
    	    params.add(Interpolation.getInstance(1));
    	    BorderExtender extender = BorderExtender.createInstance(1);
    	    hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);
    	} 
    	else if (interpolation==RenderingHints.VALUE_INTERPOLATION_BICUBIC) {
    	    params.add(Interpolation.getInstance(2));
    	    BorderExtender extender = BorderExtender.createInstance(1);
    	    hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);
    	}
    	// TODO
    	Color bg = getGraphics().getBackground();
    	params.add(new double[]{bg.getRed(),bg.getGreen(),bg.getBlue()});
    	image(JAI.create("shear", params, hints).getAsBufferedImage());
    }
    
	public BufferedImage getBufferedImage() throws ExpressionException {
		return image();
	}
	public BufferedImage image() throws ExpressionException {
		if(_image==null) throw (new ExpressionException("image is not initialized"));
		return _image;
	}
	public void image(BufferedImage image) {
		this._image=image;
		graphics=null;
		
		sctInfo=null;
	}
	
    private Graphics2D getGraphics() throws ExpressionException {
		if(graphics==null) {
			graphics=image() .createGraphics();
			// reset all properties
			if(antiAlias!=ANTI_ALIAS_NONE)	setAntiAliasing(antiAlias==ANTI_ALIAS_ON);
			if(bgColor!=null)	setBackground(bgColor);
			if(fgColor!=null)	setColor(fgColor);
			if(alpha!=1)		setAlpha(alpha);
			if(tranparency!=-1)	setTranparency(tranparency);
			if(xmColor!=null)	setXorMode(xmColor);
			if(stroke!=null)	setDrawingStroke(stroke);
		}
    	return graphics;
	}
    
    
	private String toStringColorSpace(ColorSpace colorSpace) {
		switch (colorSpace.getType()) {
	    case 0: return "Any of the family of XYZ color spaces";
	    case 1: return "Any of the family of Lab color spaces";
	    case 2: return "Any of the family of Luv color spaces";
	    case 3: return "Any of the family of YCbCr color spaces";
	    case 4:return "Any of the family of Yxy color spaces";
	    case 5: return "Any of the family of RGB color spaces";
	    case 6: return "Any of the family of GRAY color spaces";
	    case 7: return "Any of the family of HSV color spaces";
	    case 8: return "Any of the family of HLS color spaces";
	    case 9: return "Any of the family of CMYK color spaces";
	    case 11: return "Any of the family of CMY color spaces";
	    case 12: return "Generic 2 component color space.";
	    case 13: return "Generic 3 component color space.";
	    case 14: return "Generic 4 component color space.";
	    case 15: return "Generic 5 component color space.";
	    case 16: return "Generic 6 component color space.";
	    case 17: return "Generic 7 component color space.";
	    case 18: return "Generic 8 component color space.";
	    case 19: return "Generic 9 component color space.";
	    case 20: return "Generic 10 component color space.";
	    case 21: return "Generic 11 component color space.";
	    case 22: return "Generic 12 component color space.";
	    case 23: return "Generic 13 component color space.";
	    case 24: return "Generic 14 component color space.";
	    case 25: return "Generic 15 component color space.";
	    case 1001: return "CIEXYZ";
	    case 1003: return "GRAY";
	    case 1004: return "LINEAR_RGB";
	    case 1002: return "PYCC";
	    case 1000: return "sRGB";
	    }
		
		return "Unknown ColorSpace" + colorSpace;
	}

	private Object toStringTransparency(int transparency) {
		if(Transparency.OPAQUE==transparency) 		return "OPAQUE";
		if(Transparency.BITMASK==transparency) 		return "BITMASK";
		if(Transparency.TRANSLUCENT==transparency)	return "TRANSLUCENT";
		return "Unknown type of transparency";
	}
	
	public String writeBase64(Resource destination, String format, boolean inHTMLFormat) throws PageException, IOException {
		// destination
		if(destination==null) {
			if(source!=null)destination=source;
			else throw new IOException("missing destination file");
		}
		
		String content = getBase64String(format);
		if(inHTMLFormat) content="data:image/" + format + ";base64,"+content;
		IOUtil.write(destination, content, (Charset)null, false);
		return content;
	}
	
	public String getBase64String(String format) throws PageException {
		byte[] imageBytes = getImageBytes(format);
		return new String(Base64.encodeBase64(imageBytes));
	}
	
	public void writeOut(Resource destination, boolean overwrite, float quality) throws IOException, ExpressionException {
		String format = ImageUtil.getFormatFromExtension(destination,null);
		if(format==null) format = ImageUtil.getFormat(destination);
		writeOut(destination, format, overwrite, quality);
	}
	
	public void writeOut(Resource destination, final String format,boolean overwrite, float quality) throws IOException, ExpressionException {
		if(destination==null) {
			if(source!=null)destination=source;
			else throw new IOException("missing destination file");
		}
		
		if(destination.exists()) {
			if(!overwrite)throw new IOException("can't overwrite existing image");
		}

    	if(JAIUtil.isSupportedWriteFormat(format)){
    		JAIUtil.write(getBufferedImage(),destination,format);
    		return;
    	}
		OutputStream os=null;
		ImageOutputStream ios = null;
		try {
			os=destination.getOutputStream();
			ios = ImageIO.createImageOutputStream(os);
			_writeOut(ios, format, quality,false);
		}
		finally {
			ImageUtil.closeEL(ios);
			IOUtil.closeEL(os);
		}		
	}

	
	public static void writeOutGif(BufferedImage src, OutputStream os) throws IOException {
		BufferedImage dst = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB);
		QuantizeFilter filter=new QuantizeFilter();
		filter.setSerpentine(true);
		filter.setDither(true);
		//filter.setNumColors(8);
		filter.filter(src, dst);
		

		//image(Quantizer.quantize(image(), 8));
		try {
			GifEncoder enc = new GifEncoder(dst);
			enc.Write(os);
			os.flush();
		} catch (AWTException e) {
			throw new IOException(e.getMessage());
		}
	}
	
	public void writeOut(OutputStream os, String format,float quality, boolean closeStream) throws IOException, ExpressionException {
		ImageOutputStream ios = ImageIO.createImageOutputStream(os);
		try{
			_writeOut(ios, format, quality,false);
		}
		finally{
			IOUtil.closeEL(ios);
		}
	}

	private void _writeOut(ImageOutputStream ios, String format,float quality,boolean noMeta) throws IOException, ExpressionException {
		if(quality<0 || quality>1)
			throw new IOException("quality has an invalid value ["+quality+"], value has to be between 0 and 1");
		if(StringUtil.isEmpty(format))	format=this.format;
		if(StringUtil.isEmpty(format))	throw new IOException("missing format");
		
		BufferedImage im = image();
		
		//IIOMetadata meta = noMeta?null:metadata(format);
		IIOMetadata meta = noMeta?null:getMetaData(null);
		
		ImageWriter writer = null;
    	ImageTypeSpecifier type =ImageTypeSpecifier.createFromRenderedImage(im);
    	Iterator<ImageWriter> iter = ImageIO.getImageWriters(type, format);
    	
    	
    	if (iter.hasNext()) {
    		writer = iter.next();
    	}
    	if (writer == null) throw new IOException("no writer for format ["+format+"] available, available writer formats are ["+ListUtil.arrayToList(ImageUtil.getWriterFormatNames(), ",")+"]");
    	
    	
		ImageWriteParam iwp=getImageWriteParam(im,writer,quality,this.format,format);
		
    	
		writer.setOutput(ios);
    	
		
		try {
    		writer.write(meta, new IIOImage(im, null, meta), iwp);
    	}
    	finally {
    		writer.dispose();
    		ios.flush();
    		this.format=format;
    	}
	}
	
	private static ImageWriteParam getImageWriteParam(BufferedImage im, ImageWriter writer, float quality, String srcFormat, String trgFormat) {
		ImageWriteParam iwp;
		if("jpg".equalsIgnoreCase(srcFormat)) {
    		ColorModel cm = im.getColorModel();
    		if(cm.hasAlpha())im=jpgImage(im);
    		JPEGImageWriteParam jiwp = new JPEGImageWriteParam(Locale.getDefault());
    		jiwp.setOptimizeHuffmanTables(true);
    		iwp = jiwp;
    	}
		else
			iwp = writer.getDefaultWriteParam();
    	
    	setCompressionModeEL(iwp,ImageWriteParam.MODE_EXPLICIT);
    	setCompressionQualityEL(iwp,quality);
    	
    	return iwp;
	}

	private static BufferedImage jpgImage(BufferedImage src) {
        int w = src.getWidth();
        int h = src.getHeight();
        SampleModel srcSM = src.getSampleModel();
        WritableRaster srcWR = src.getRaster();
        java.awt.image.DataBuffer srcDB = srcWR.getDataBuffer();
        
        ColorModel rgb = new DirectColorModel(32, 0xff0000, 65280, 255);
        int[] bitMasks = new int[]{0xff0000, 65280, 255};
        
        SampleModel csm = new SinglePixelPackedSampleModel(3, w, h, bitMasks);
        int data[] = new int[w * h];
        for(int i = 0; i < h; i++) {
            for(int j = 0; j < w; j++) {
                int pix[] = null;
                int sample[] = srcSM.getPixel(j, i, pix, srcDB);
                if(sample[3] == 0 && sample[2] == 0 && sample[1] == 0 && sample[0] == 0)
                    data[i * w + j] = 0xffffff;
                else
                    data[i * w + j] = sample[0] << 16 | sample[1] << 8 | sample[2];
            }

        }

        java.awt.image.DataBuffer db = new DataBufferInt(data, w * h * 3);
        WritableRaster wr = Raster.createWritableRaster(csm, db, new Point(0, 0));
        return new BufferedImage(rgb, wr, false, null);
    }

	private static void setCompressionModeEL(ImageWriteParam iwp, int mode) {
		try {
			iwp.setCompressionMode(mode);
		}
		catch(Throwable t) {ExceptionUtil.rethrowIfNecessary(t);}
	}

	private static void setCompressionQualityEL(ImageWriteParam iwp, float quality) {
		try {
			iwp.setCompressionQuality(quality);
		}
		catch(Throwable t) {ExceptionUtil.rethrowIfNecessary(t);}
	}

	/*public void convertX(String format) {
		this.format=format;
	}*/

	public void scaleToFit(String fitWidth, String fitHeight,String interpolation, double blurFactor) throws PageException {
		if (StringUtil.isEmpty(fitWidth) || StringUtil.isEmpty(fitHeight))	
			resize(fitWidth, fitHeight, interpolation, blurFactor);
		else {
			float width = Caster.toFloatValue(fitWidth) / getWidth();
			float height= Caster.toFloatValue(fitHeight) / getHeight();
			if (width < height)	resize(fitWidth, "", interpolation, blurFactor);
			else				resize("", fitHeight, interpolation, blurFactor);
		}
	}
	
	
	
	/**
     * Convenience method that returns a scaled instance of the
     * provided {@code BufferedImage}.
     *
     * @param img the original image to be scaled
     * @param targetWidth the desired width of the scaled instance,
     *    in pixels
     * @param targetHeight the desired height of the scaled instance,
     *    in pixels
     * @param hint one of the rendering hints that corresponds to
     *    {@code RenderingHints.KEY_INTERPOLATION} (e.g.
     *    {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
     *    {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
     *    {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
     * @param higherQuality if true, this method will use a multi-step
     *    scaling technique that provides higher quality than the usual
     *    one-step technique (only useful in downscaling cases, where
     *    {@code targetWidth} or {@code targetHeight} is
     *    smaller than the original dimensions, and generally only when
     *    the {@code BILINEAR} hint is specified)
     * @return a scaled version of the original {@code BufferedImage}
     */
    private BufferedImage getScaledInstance(BufferedImage img,
                                           int targetWidth,
                                           int targetHeight,
                                           Object hint,
                                           boolean higherQuality)
    {
        // functionality not supported in java 1.4
    	int transparency=Transparency.OPAQUE;
    	try {
			transparency=img.getTransparency();
		} 
    	catch(Throwable t) {ExceptionUtil.rethrowIfNecessary(t);}
    	int type = (transparency == Transparency.OPAQUE) ?BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        
    	
    	
    	BufferedImage ret = img;
        int w, h;
        if (higherQuality) {
            // Use multi-step technique: start with original size, then
            // scale down in multiple passes with drawImage()
            // until the target size is reached
            w = img.getWidth();
            h = img.getHeight();
        } else {
            // Use one-step technique: scale directly from original
            // size to target size with a single drawImage() call
            w = targetWidth;
            h = targetHeight;
        }
        
        do {
            if (higherQuality && w > targetWidth) {
                w /= 2;
                if (w < targetWidth) {
                    w = targetWidth;
                }
            }

            if (higherQuality && h > targetHeight) {
                h /= 2;
                if (h < targetHeight) {
                    h = targetHeight;
                }
            }

            BufferedImage tmp = new BufferedImage(w, h, type);
            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
            g2.drawImage(ret, 0, 0, w, h, null);
            g2.dispose();

            ret = tmp;
        } while (w != targetWidth || h != targetHeight);

        return ret;
    }

	
	
	
	
    public void resize(int scale, String interpolation, double blurFactor) throws PageException {
    	if (blurFactor <= 0.0 || blurFactor > 10.0)
			throw new ExpressionException("blurFactor must be between 0 and 10");
    	
		float width=getWidth()/100F*scale;
		float height=getHeight()/100F*scale;
			
		resize((int)width, (int)height, toInterpolation(interpolation), blurFactor);
    }
	
    public void resize(String strWidth, String strHeight, String interpolation, double blurFactor) throws PageException {
		if (StringUtil.isEmpty(strWidth,true) && StringUtil.isEmpty(strHeight,true))
			throw new ExpressionException("you have to define width or height");
		if (blurFactor <= 0.0 || blurFactor > 10.0)
			throw new ExpressionException("blurFactor must be between 0 and 10");
		int w = getWidth();
		int h = getHeight();
		float height=resizeDimesion("height",strHeight, h);
		float width=resizeDimesion("width",strWidth, w);
		
		if(height==-1)	height=h*(width/w);
		if(width==-1)	width=w*(height/h);
			
		resize((int)width, (int)height, toInterpolation(interpolation), blurFactor);
    }
	
    public void resizeImage2(int width, int height) throws ExpressionException{
    	image(getScaledInstance(image(),width,height,RenderingHints.VALUE_INTERPOLATION_BILINEAR,false));
    }
	
    public void resizeImage(int width, int height, int interpolation) throws ExpressionException{
    	Object ip;
    	if(interpolation==IPC_NEAREST)		ip=RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
    	else if(interpolation==IPC_BICUBIC)	ip=RenderingHints.VALUE_INTERPOLATION_BICUBIC;
    	else if(interpolation==IPC_BILINEAR)	ip=RenderingHints.VALUE_INTERPOLATION_BILINEAR;
    	else throw new ExpressionException("invalid interpoltion definition");
    	
    	BufferedImage dst = new BufferedImage(width,height,image().getType());
        Graphics2D graphics = dst.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,ip); 
        graphics.drawImage(image(), 0, 0, width, height, null);
        graphics.dispose();
        image(dst);
        
    }
	
	private float resizeDimesion(String label,String strDimension, float originalDimension) throws PageException {
		if (StringUtil.isEmpty(strDimension,true)) return -1;
		strDimension=strDimension.trim();
		
		if (StringUtil.endsWith(strDimension, '%')) {
			float p = Caster.toFloatValue(strDimension.substring(0,(strDimension.length()- 1))) / 100.0F;
			return originalDimension*p;
		}
		float dimension = Caster.toFloatValue(strDimension);
		if (dimension <= 0F)
			throw new ExpressionException(label+" has to be a none negative number");
		return dimension;
	}
	
	

    public void resize(int width, int height, int interpolation, double blurFactor) throws ExpressionException {
    	
		ColorModel cm = image().getColorModel();
		
	    if (interpolation==IP_HIGHESTPERFORMANCE)	{
	    	interpolation = IPC_BICUBIC;
	    }
	    
	    if (cm.getColorSpace().getType() == ColorSpace.TYPE_GRAY && cm.getComponentSize()[0] == 8) {
	    	if (interpolation==IP_HIGHESTQUALITY || interpolation==IP_HIGHPERFORMANCE || interpolation==IP_HIGHQUALITY || interpolation==IP_MEDIUMPERFORMANCE || interpolation==IP_MEDIUMQUALITY)	{
	    		interpolation = IPC_BICUBIC;
	    	}
	    	if (interpolation!=IPC_BICUBIC && interpolation!=IPC_BILINEAR && interpolation!=IPC_NEAREST)	{
	    		throw new ExpressionException("invalid grayscale interpolation");
	    	}
	    }
	    
	    if (interpolation<=IPC_MAX)	{
	    	resizeImage(width, height, interpolation);
	    }
	    else {
	    	image(ImageResizer.resize(image(), width, height, interpolation, blurFactor));
			
	    }
	}
    
    
    
    /*private BufferedImage resizeImageWithJAI(float scaleWidth, float scaleHeight, int interpolation) throws ExpressionException {
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
		params.add(scaleWidth);
		params.add(scaleHeight);
		params.add(0.0F);
		params.add(0.0F);
		RenderingHints hints = null;
		if (interpolation != IP_NONE) {
		    if (interpolation==IP_NEAREST)	{
		    	params.add(Interpolation.getInstance(0));
		    }
		    else if (interpolation==IP_BILINEAR) {
		    	params.add(Interpolation.getInstance(1));
		    	BorderExtender extender = BorderExtender.createInstance(1);
		    	hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);
		    } 
		    else if (interpolation==IP_BICUBIC) {
		    	params.add(Interpolation.getInstance(2));
		    	BorderExtender extender = BorderExtender.createInstance(1);
		    	hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);
		    } 
		    else	{
		    	throw new ExpressionException("invalid interpolation definition");
		    }
		}
		return JAI.create("scale", params, hints).getAsBufferedImage();
    }*/
	
    private double toScale(int src, int dst) {
    	double tmp = Math.round((int)((Caster.toDoubleValue(dst)/Caster.toDoubleValue(src))*100D));
    	return tmp/100D;
	}

	public void rotate(float x, float y, float angle, int interpolation) throws ExpressionException {
    	if(x==-1)x = (float)getWidth() / 2;
    	if(y==-1)y = (float)getHeight() / 2;
		
    	angle = (float) Math.toRadians(angle);
    	ColorModel cmSource = image().getColorModel();
    	
    	if (cmSource instanceof IndexColorModel && cmSource.hasAlpha() && !cmSource.isAlphaPremultiplied()) {
    		image(paletteToARGB(image()));
    	    cmSource = image().getColorModel();
    	}
    	
    	BufferedImage alpha = null;
    	if (cmSource.hasAlpha() && !cmSource.isAlphaPremultiplied()) {
    	    alpha = getAlpha(image());
    	    image(removeAlpha(image()));
    	}
    	
    	Interpolation interp = Interpolation.getInstance(0);
    	if (INTERPOLATION_BICUBIC==interpolation)	interp = Interpolation.getInstance(1);
    	else if (INTERPOLATION_BILINEAR==interpolation)		interp = Interpolation.getInstance(2);
    	
    	if (alpha != null) {
    	    ParameterBlock params = new ParameterBlock();
    	    params.addSource(alpha);
    	    params.add(x);
    	    params.add(y);
    	    params.add(angle);
    	    params.add(interp);
    	    params.add(new double[] { 0.0 });
    	    RenderingHints hints= new RenderingHints(RenderingHints.KEY_INTERPOLATION,(RenderingHints.VALUE_INTERPOLATION_BICUBIC));
    	    hints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER,new BorderExtenderConstant(new double[] { 255.0 })));
    	    hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL,Boolean.TRUE));
    	    alpha = JAI.create("rotate", params, hints).getAsBufferedImage();
    	}
    	
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.add(x);
    	params.add(y);
    	params.add(angle);
    	params.add(interp);
    	params.add(new double[] { 0.0 });
    	BorderExtender extender= new BorderExtenderConstant(new double[] { 0.0 });
    	RenderingHints hints= new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender);
    	hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE));
    	image(JAI.create("rotate", params, hints).getAsBufferedImage());
    	if (alpha != null)image(addAlpha(image(), alpha, 0, 0));
    }
    
    private static BufferedImage paletteToARGB(BufferedImage src) {
    	IndexColorModel icm = (IndexColorModel) src.getColorModel();
    	int bands = icm.hasAlpha()?4:3;
    	
    	byte[][] data = new byte[bands][icm.getMapSize()];
    	if (icm.hasAlpha()) icm.getAlphas(data[3]);
    	icm.getReds(data[0]);
    	icm.getGreens(data[1]);
    	icm.getBlues(data[2]);
    	LookupTableJAI rtable = new LookupTableJAI(data);
    	return JAI.create("lookup", src, rtable).getAsBufferedImage();
    }

    
    private static BufferedImage getAlpha(BufferedImage src) {
	return JAI.create("bandselect", src, new int[] { 3 }).getAsBufferedImage();
    }
    
    private static BufferedImage removeAlpha(BufferedImage src) {
    	return JAI.create("bandselect", src, new int[] { 0, 1, 2 }).getAsBufferedImage();
    }
    
    private static BufferedImage addAlpha(BufferedImage src, BufferedImage alpha, int x, int y) {
    	int w = src.getWidth();
    	int h = src.getHeight();
    	BufferedImage bi = new BufferedImage(w, h, 2);
    	WritableRaster wr = bi.getWritableTile(0, 0);
    	WritableRaster wr3 = wr.createWritableChild(0, 0, w, h, 0, 0, new int[] { 0, 1, 2 });
    	WritableRaster wr1 = wr.createWritableChild(0, 0, w, h, 0, 0, new int[] { 3 });
    	wr3.setRect(src.getData());
    	wr1.setRect(alpha.getData());
    	bi.releaseWritableTile(0, 0);
    	return bi;
    }

	

	public void _rotate(float x, float y, float angle, String interpolation) throws ExpressionException {

		float radiansAngle = (float)Math.toRadians(angle);

		// rotation center
		float centerX = (float)getWidth() / 2;
		float centerY = (float)getHeight() / 2;
		
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(image());
		pb.add(centerX);
		pb.add(centerY);
		pb.add(radiansAngle);
		pb.add(new javax.media.jai.InterpolationBicubic(10));
		
		// create a new, rotated image
		image(JAI.create("rotate", pb).getAsBufferedImage());

	}

	public static Image toImage(Object obj) throws PageException {
		return toImage(ThreadLocalPageContext.get(), obj, true);
	}
	
	// used in bytecode
	public static Image toImage(Object obj,PageContext pc) throws PageException {
		return toImage(pc, obj, true);
	}
	

	public static Image toImage(PageContext pc,Object obj) throws PageException {
		return toImage(pc, obj, true);
	}
	
	public static Image toImage(PageContext pc,Object obj, boolean checkForVariables) throws PageException {
		if(obj instanceof Image) return (Image) obj;
		if(obj instanceof ObjectWrap) return toImage(pc,((ObjectWrap)obj).getEmbededObject(),checkForVariables);
		
		if(obj instanceof BufferedImage) return new Image((BufferedImage)obj);
		
		
		
		// try to load from binary
		if(Decision.isBinary(obj)) {
			try {
				return new Image(Caster.toBinary(obj),null);
			}
			catch (IOException e) {
				throw Caster.toPageException(e);
			}
		}
		// try to load from String (base64)
		if(Decision.isString(obj)) {
			String str=Caster.toString(obj);
			if(checkForVariables && pc!=null) {
				Object o = VariableInterpreter.getVariableEL(pc, str, null);
				if(o!=null) return toImage(pc, o, false);
			}
			try {
				return new Image(str);
			}
			catch (IOException e) {
				throw Caster.toPageException(e);
			}
		}
		
		throw new CasterException(obj,"Image");
	}

	public static Image toImage(PageContext pc,Object obj, boolean checkForVariables, Image defaultValue) {
		try {
			return toImage(pc, obj, checkForVariables);
		}
		catch(Throwable t) {
			ExceptionUtil.rethrowIfNecessary(t);
			return defaultValue;
		}
	}

	public static boolean isImage(Object obj) {
		if(obj instanceof Image) return true;
		if(obj instanceof ObjectWrap) return isImage(((ObjectWrap)obj).getEmbededObject(""));
		return false;
	}
	
	@Override
	public Object call(PageContext pc, Key methodName, Object[] args) throws PageException {
		Object obj = get(methodName,null);
		if(obj instanceof UDFPlus) {
			return ((UDFPlus)obj).call(pc,methodName,args,false);
		}
		return MemberUtil.call(pc, this, methodName, args, new short[]{CFTypes.TYPE_IMAGE}, new String[]{"image"});
	}

    @Override
	public Object callWithNamedValues(PageContext pc, Key methodName, Struct args) throws PageException {
		Object obj = get(methodName,null);
		if(obj instanceof UDFPlus) {
			return ((UDFPlus)obj).callWithNamedValues(pc,methodName,args,false);
		}
		return MemberUtil.callWithNamedValues(pc,this,methodName,args, CFTypes.TYPE_IMAGE, "image");
	}

	public static boolean isCastableToImage(PageContext pc,Object obj) {
		if(isImage(obj)) return true;
		return toImage(pc, obj, true, null)!=null;
	}

	public static Image createImage(PageContext pc,Object obj, boolean check4Var, boolean clone, boolean checkAccess, String format) throws PageException {
		try {
			if(obj instanceof String || obj instanceof Resource || obj instanceof File) {
				try {
					Resource res = Caster.toResource(pc,obj,true);
					pc.getConfig().getSecurityManager().checkFileLocation(res);
					return new Image(res,format);
				} 
				catch (ExpressionException ee) {
					if(check4Var && Decision.isVariableName(Caster.toString(obj))) {
						try {
							return createImage(pc, pc.getVariable(Caster.toString(obj)), false,clone,checkAccess,format);
						}
						catch(Throwable t) {
							ExceptionUtil.rethrowIfNecessary(t);
							throw ee;
						}
					}
					try {
						return new Image(Caster.toString(obj),format);
					}
					catch(Throwable t) {
						ExceptionUtil.rethrowIfNecessary(t);
						throw ee;
					}
				}
			}
			if(obj instanceof Image)	{
				if(clone)return (Image) ((Image)obj).clone();
				return (Image)obj;
			}
			if(Decision.isBinary(obj))			return new Image(Caster.toBinary(obj),format);
			if(obj instanceof BufferedImage)	return new Image(((BufferedImage) obj));
			if(obj instanceof java.awt.Image)	return new Image(toBufferedImage((java.awt.Image) obj));
			
		} catch(Throwable t) {
			ExceptionUtil.rethrowIfNecessary(t);
			throw Caster.toPageException(t);
		}
		throw new CasterException(obj,"Image");
	}

	@Override
	public Collection duplicate(boolean deepCopy) {
		try {
			//if(_image!=null) return new Image(getBufferedImage());
			return new Image(getImageBytes(null));
			
		} catch (Exception e) {
			throw new PageRuntimeException(e.getMessage());
		}
	}
	
	
	

	public ColorModel getColorModel() throws ExpressionException {
		return image().getColorModel();
	}
    
    public void crop(float x, float y, float width, float height) throws ExpressionException {
    	ParameterBlock params = new ParameterBlock();
    	params.addSource(image());
    	params.add(x);
    	params.add(y);

    	float w = getWidth();
    	float h = getHeight();
    	
    	if (w < x + width) params.add(w - x);
    	else params.add(width);
    	
    	if (h < y + height) params.add(h - y);
    	else params.add(height);
    	
    	image(JAI.create("crop", params).getAsBufferedImage());
    }
    
    public int getWidth() throws ExpressionException {
    	return image().getWidth();
    }
    
    public int getHeight() throws ExpressionException {
    	return image().getHeight();
    }

	public String getFormat() {
		return format;
	}

	public byte[] getImageBytes(String format) throws PageException{
		return getImageBytes(format,false);
	}
	public byte[] getImageBytes(String format,boolean noMeta) throws PageException {
		
		
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		
		if(JAIUtil.isSupportedWriteFormat(format)){
    		try {
				JAIUtil.write(getBufferedImage(),baos,format);
			}catch (IOException e) {
				throw Caster.toPageException(e);
			}
    	}
		else {
			ImageOutputStream ios = null;
			try {
				ios = ImageIO.createImageOutputStream(baos);
				_writeOut(ios, format, 1,noMeta);
			} catch (IOException e) {
				throw Caster.toPageException(e);
			}
			finally {
				IOUtil.closeEL(ios);
			}
		}
		return baos.toByteArray();
	}

	public void setColor(Color color) throws ExpressionException {
		if(color==null) return;
		fgColor=color;
		getGraphics().setColor(color);
	}
	
	public void setAlpha(float alpha) throws ExpressionException {
		this.alpha=alpha;
		Graphics2D g = getGraphics();
		
		Composite alphaComposite;
		if(composite==null) {
			if(alpha==1) return;
			composite = g.getComposite();
		}
		if(alpha==1) alphaComposite=composite;
		else alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
        
    	g.setComposite(alphaComposite);
    	//graphics.setComposite(originalComposite);	
	}	
	
	public void setBackground(Color color) throws ExpressionException {
		if(color==null) return;
		bgColor=color;
		getGraphics().setBackground(color);
	}
	
	public void setAntiAliasing(boolean antiAlias) throws ExpressionException {
		this.antiAlias=antiAlias?ANTI_ALIAS_ON:ANTI_ALIAS_OFF;
		Graphics2D graphics = getGraphics();
		if(antiAlias) {
		    graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		    graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		}
		else {
		    graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		    graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
		} 
	}
	
	private Struct _info() {
		try {
			return info();
		} catch (PageException e) {
			throw new PageRuntimeException(e);
		}
	}

	@Override
	public void clear() {
		throw new RuntimeException("can't clear struct, struct is readonly");
	}

	@Override
	public boolean containsKey(Key key) {
		return _info().containsKey(key);
	}

	@Override
	public Object get(Key key) throws PageException {
		return info().get(key);
	}

	@Override
	public Object get(Key key, Object defaultValue) {
		return _info().get(key, defaultValue);
	}

	@Override
	public Key[] keys() {
		return _info().keys();
	}

	@Override
	public Object remove(Key key) throws PageException {
		throw new ExpressionException("can't remove key ["+key.getString()+"] from struct, struct is readonly");
	}

	@Override
	public Object removeEL(Key key) {
		throw new PageRuntimeException("can't remove key ["+key.getString()+"] from struct, struct is readonly");
	}

	@Override
	public Object set(Key key, Object value) throws PageException {
		throw new ExpressionException("can't set key ["+key.getString()+"] to struct, struct is readonly");
	}

	@Override
	public Object setEL(Key key, Object value) {
		throw new PageRuntimeException("can't set key ["+key.getString()+"] to struct, struct is readonly");
	}

	@Override
	public int size() {
		return _info().size();
	}

	@Override
	public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
		DumpData dd = _info().toDumpData(pageContext, maxlevel,dp);
		if(dd instanceof DumpTable) {
			DumpTable dt = ((DumpTable)dd);
			dt.setTitle("Struct (Image)");
			try {
				dt.setComment("<img style=\"margin:5px\" src=\"data:image/png;base64,"+getBase64String("png")+"\">");
			}
			catch (PageException e) {}
			
		}
		
		return dd;
	}
	
	@Override
	public String castToString() throws PageException {
		return "<img src=\"data:image/png;base64,"+getBase64String("png")+"\">";
	}
	@Override
	public String castToString(String defaultValue) {
		try {
			return castToString();
		} catch (PageException e) {
			return defaultValue;
		}
	}

	@Override
	public Iterator<Collection.Key> keyIterator() {
		return _info().keyIterator();
	}
    
    @Override
	public Iterator<String> keysAsStringIterator() {
    	return _info().keysAsStringIterator();
    }
	
	@Override
	public Iterator<Entry<Key, Object>> entryIterator() {
		return _info().entryIterator();
	}
	
	@Override
	public Iterator<Object> valueIterator() {
		return _info().valueIterator();
	}

	@Override
	public boolean castToBooleanValue() throws PageException {
		return info().castToBooleanValue();
	}

	@Override
	public Boolean castToBoolean(Boolean defaultValue) {
		try {
			return info().castToBoolean(defaultValue);
		} catch (PageException e) {
			return defaultValue;
		}
	}

	@Override
	public DateTime castToDateTime() throws PageException {
		return info().castToDateTime();
	}
    
    @Override
    public DateTime castToDateTime(DateTime defaultValue) {
        try {
			return info().castToDateTime(defaultValue);
		} catch (PageException e) {
			return defaultValue;
		}
    }

	@Override
	public double castToDoubleValue() throws PageException {
		return info().castToDoubleValue();
	}
    
    @Override
    public double castToDoubleValue(double defaultValue) {
        try {
			return info().castToDoubleValue(defaultValue);
		} catch (PageException e) {
			return defaultValue;
		}
    }

	

	@Override
	public int compareTo(String str) throws PageException {
		return info().compareTo(str);
	}

	@Override
	public int compareTo(boolean b) throws PageException {
		return info().compareTo(b);
	}

	@Override
	public int compareTo(double d) throws PageException {
		return info().compareTo(d);
	}

	@Override
	public int compareTo(DateTime dt) throws PageException {
		return info().compareTo(dt);
	}

	public static int toInterpolation(String strInterpolation) throws ExpressionException {
		if(StringUtil.isEmpty(strInterpolation))
			throw new ExpressionException("interpolation definition is empty");
		strInterpolation=strInterpolation.trim().toLowerCase();
		
		if("highestquality".equals(strInterpolation))			return IP_HIGHESTQUALITY;
		else if("highquality".equals(strInterpolation)) 		return IP_HIGHQUALITY;
		else if("mediumquality".equals(strInterpolation)) 		return IP_MEDIUMQUALITY;
		else if("highestperformance".equals(strInterpolation)) 	return IP_HIGHESTPERFORMANCE;
		else if("highperformance".equals(strInterpolation)) 	return IP_HIGHPERFORMANCE;
		else if("mediumperformance".equals(strInterpolation)) 	return IP_MEDIUMPERFORMANCE;
		else if("nearest".equals(strInterpolation)) 			return IPC_NEAREST;
		else if("bilinear".equals(strInterpolation)) 			return IPC_BILINEAR;
		else if("bicubic".equals(strInterpolation)) 			return IPC_BICUBIC;
		else if("bessel".equals(strInterpolation)) 				return IP_BESSEL;
		else if("blackman".equals(strInterpolation)) 			return IP_BLACKMAN;
		else if("hamming".equals(strInterpolation)) 			return IP_HAMMING;
		else if("hanning".equals(strInterpolation)) 			return IP_HANNING;
		else if("hermite".equals(strInterpolation)) 			return IP_HERMITE;
		else if("lanczos".equals(strInterpolation)) 			return IP_LANCZOS;
		else if("mitchell".equals(strInterpolation)) 			return IP_MITCHELL;
		else if("quadratic".equals(strInterpolation)) 			return IP_QUADRATIC;

		throw new ExpressionException("interpolation definition ["+strInterpolation+"] is invalid");
	}

	/**
	 * @return the source
	 */
	public Resource getSource() {
		return source;
	}

	@Override
	public boolean containsValue(Object value) {
		try {
			return info().containsValue(value);
		} 
		catch (PageException e) {
			return false;
		}
	}

	@Override
	public java.util.Collection values() {
		try {
			return info().values();
		} catch (PageException e) {
			throw new PageRuntimeException(e);
		}
	}

	/**
	 * This method returns true if the specified image has transparent pixels
	 * @param image
	 * @return
	 */
	public static boolean hasAlpha(java.awt.Image image) {
	    // If buffered image, the color model is readily available
	    if (image instanceof BufferedImage) {
	        BufferedImage bimage = (BufferedImage)image;
	        return bimage.getColorModel().hasAlpha();
	    }

	    // Use a pixel grabber to retrieve the image's color model;
	    // grabbing a single pixel is usually sufficient
	     PixelGrabber pg = new PixelGrabber(image, 0, 0, 1, 1, false);
	    try {
	        pg.grabPixels();
	    } catch (InterruptedException e) {
	    }

	    // Get the image's color model
	    ColorModel cm = pg.getColorModel();
	    return cm.hasAlpha();
	}
	
	// This method returns a buffered image with the contents of an image
	public static BufferedImage toBufferedImage(java.awt.Image image) {
	    if (image instanceof BufferedImage) {
	        return (BufferedImage)image;
	    }

	    // This code ensures that all the pixels in the image are loaded
	    image = new ImageIcon(image).getImage();

	    // Determine if the image has transparent pixels; for this method's
	    boolean hasAlpha = hasAlpha(image);

	    // Create a buffered image with a format that's compatible with the screen
	    BufferedImage bimage = null;
	    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
	    try {
	        // Determine the type of transparency of the new buffered image
	        int transparency = Transparency.OPAQUE;
	        if (hasAlpha) {
	            transparency = Transparency.BITMASK;
	        }

	        // Create the buffered image
	        GraphicsDevice gs = ge.getDefaultScreenDevice();
	        GraphicsConfiguration gc = gs.getDefaultConfiguration();
	        bimage = gc.createCompatibleImage(
	            image.getWidth(null), image.getHeight(null), transparency);
	    } catch (HeadlessException e) {
	        // The system does not have a screen
	    }

	    if (bimage == null) {
	        // Create a buffered image using the default color model
	        int type = BufferedImage.TYPE_INT_RGB;
	        if (hasAlpha) {
	            type = BufferedImage.TYPE_INT_ARGB;
	        }
	        bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), type);
	    }

	    // Copy image to buffered image
	    Graphics g = bimage.createGraphics();

	    // Paint the image onto the buffered image
	    g.drawImage(image, 0, 0, null);
	    g.dispose();

	    return bimage;
	}

	@Override
	public int getType() {
		if(_info() instanceof StructSupport) return ((StructSupport)_info()).getType();
		return Struct.TYPE_REGULAR;
	}
	
}