blob: b665f562e25dcaf7767642a9e4c6c26358a93fce [file] [log] [blame]
/* CairoGraphics2D.java --
Copyright (C) 2006 Free Software Foundation, Inc.
This file is part of GNU Classpath.
GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
GNU Classpath 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
General Public License for more details.
You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING. If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA.
Linking this library statically or dynamically with other modules is
making a combined work based on this library. Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module. An independent module is a module which is not derived from
or based on this library. If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so. If you do not wish to do so, delete this
exception statement from your version. */
package gnu.java.awt.peer.gtk;
import gnu.java.awt.ClasspathToolkit;
import java.awt.AWTPermission;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.awt.image.ImagingOpException;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.text.AttributedCharacterIterator;
import java.util.HashMap;
import java.util.Map;
/**
* This is an abstract implementation of Graphics2D on Cairo.
*
* It should be subclassed for different Cairo contexts.
*
* Note for subclassers: Apart from the constructor (see comments below),
* The following abstract methods must be implemented:
*
* Graphics create()
* GraphicsConfiguration getDeviceConfiguration()
* copyArea(int x, int y, int width, int height, int dx, int dy)
*
* Also, dispose() must be overloaded to free any native datastructures
* used by subclass and in addition call super.dispose() to free the
* native cairographics2d structure and cairo_t.
*
* @author Sven de Marothy
*/
public abstract class CairoGraphics2D extends Graphics2D
{
static
{
System.loadLibrary("gtkpeer");
}
/**
* Important: This is a pointer to the native cairographics2d structure
*
* DO NOT CHANGE WITHOUT CHANGING NATIVE CODE.
*/
long nativePointer;
// Drawing state variables
/**
* The current paint
*/
Paint paint;
/**
* The current stroke
*/
Stroke stroke;
/*
* Current foreground and background color.
*/
Color fg, bg;
/**
* Current clip shape.
*/
Shape clip;
/**
* Current transform.
*/
AffineTransform transform;
/**
* Current font.
*/
Font font;
/**
* The current compositing context, if any.
*/
Composite comp;
/**
* Rendering hint map.
*/
private RenderingHints hints;
/**
* Some operations (drawing rather than filling) require that their
* coords be shifted to land on 0.5-pixel boundaries, in order to land on
* "middle of pixel" coordinates and light up complete pixels.
*/
private boolean shiftDrawCalls = false;
/**
* Keep track if the first clip to be set, which is restored on setClip(null);
*/
private boolean firstClip = true;
private Shape originalClip;
/**
* Stroke used for 3DRects
*/
private static BasicStroke draw3DRectStroke = new BasicStroke();
static ColorModel rgb32 = new DirectColorModel(32, 0xFF0000, 0xFF00, 0xFF);
static ColorModel argb32 = new DirectColorModel(32, 0xFF0000, 0xFF00, 0xFF,
0xFF000000);
/**
* Constructor does nothing.
*/
public CairoGraphics2D()
{
}
/**
* Sets up the default values and allocates the native cairographics2d structure
* @param cairo_t_pointer, a native pointer to a cairo_t of the context.
*/
public void setup(long cairo_t_pointer)
{
nativePointer = init(cairo_t_pointer);
setRenderingHints(new RenderingHints(getDefaultHints()));
font = new Font("SansSerif", Font.PLAIN, 12);
setColor(Color.black);
setBackground(Color.white);
setPaint(Color.black);
setStroke(new BasicStroke());
setTransform(new AffineTransform());
}
/**
* Same as above, but copies the state of another CairoGraphics2D.
*/
public void copy(CairoGraphics2D g, long cairo_t_pointer)
{
nativePointer = init(cairo_t_pointer);
paint = g.paint;
stroke = g.stroke;
setRenderingHints(g.hints);
Color foreground;
if (g.fg.getAlpha() != -1)
foreground = new Color(g.fg.getRed(), g.fg.getGreen(), g.fg.getBlue(),
g.fg.getAlpha());
else
foreground = new Color(g.fg.getRGB());
if (g.bg != null)
{
if (g.bg.getAlpha() != -1)
bg = new Color(g.bg.getRed(), g.bg.getGreen(), g.bg.getBlue(),
g.bg.getAlpha());
else
bg = new Color(g.bg.getRGB());
}
clip = g.getClip();
if (g.transform == null)
transform = null;
else
transform = new AffineTransform(g.transform);
font = g.font;
setColor(foreground);
setBackground(bg);
setPaint(paint);
setStroke(stroke);
setTransformImpl(transform);
setClip(clip);
}
/**
* Generic destructor - call the native dispose() method.
*/
public void finalize()
{
dispose();
}
/**
* Disposes the native cairographics2d structure, including the
* cairo_t and any gradient stuff, if allocated.
* Subclasses should of course overload and call this if
* they have additional native structures.
*/
public void dispose()
{
disposeNative(nativePointer);
nativePointer = 0;
}
/**
* Allocate the cairographics2d structure and set the cairo_t pointer in it.
* @param pointer - a cairo_t pointer, casted to a long.
*/
private native long init(long pointer);
/**
* These are declared abstract as there may be context-specific issues.
*/
public abstract Graphics create();
public abstract GraphicsConfiguration getDeviceConfiguration();
protected abstract void copyAreaImpl(int x, int y,
int width, int height, int dx, int dy);
protected abstract Rectangle2D getRealBounds();
////// Native Methods ////////////////////////////////////////////////////
/**
* Dispose of allocate native resouces.
*/
public native void disposeNative(long pointer);
/**
* Draw pixels as an RGBA int matrix
* @param w, h - width and height
* @param stride - stride of the array width
* @param i2u - affine transform array
*/
private native void drawPixels(long pointer, int[] pixels, int w, int h,
int stride, double[] i2u, double alpha);
private native void setGradient(long pointer, double x1, double y1,
double x2, double y2,
int r1, int g1, int b1, int a1, int r2,
int g2, int b2, int a2, boolean cyclic);
private native void setTexturePixels(long pointer, int[] pixels, int w,
int h, int stride);
/**
* Set the current transform matrix
*/
private native void cairoSetMatrix(long pointer, double[] m);
/**
* Scaling method
*/
private native void cairoScale(long pointer, double x, double y);
/**
* Set the compositing operator
*/
private native void cairoSetOperator(long pointer, int cairoOperator);
/**
* Sets the current color in RGBA as a 0.0-1.0 double
*/
private native void cairoSetRGBAColor(long pointer, double red, double green,
double blue, double alpha);
/**
* Sets the current winding rule in Cairo
*/
private native void cairoSetFillRule(long pointer, int cairoFillRule);
/**
* Set the line style, cap, join and miter limit.
* Cap and join parameters are in the BasicStroke enumerations.
*/
private native void cairoSetLine(long pointer, double width, int cap,
int join, double miterLimit);
/**
* Set the dash style
*/
private native void cairoSetDash(long pointer, double[] dashes, int ndash,
double offset);
/*
* Draws a Glyph Vector
*/
native void cairoDrawGlyphVector(long pointer, GdkFontPeer font,
float x, float y, int n,
int[] codes, float[] positions);
private native void cairoRelCurveTo(long pointer, double dx1, double dy1,
double dx2, double dy2, double dx3,
double dy3);
/**
* Appends a rectangle to the current path
*/
private native void cairoRectangle(long pointer, double x, double y,
double width, double height);
/**
* Appends an arc to the current path
*/
private native void cairoArc(long pointer, double x, double y,
double radius, double angle1, double angle2);
/**
* Save / restore a cairo path
*/
private native void cairoSave(long pointer);
private native void cairoRestore(long pointer);
/**
* New current path
*/
private native void cairoNewPath(long pointer);
/**
* Close current path
*/
private native void cairoClosePath(long pointer);
/** moveTo */
private native void cairoMoveTo(long pointer, double x, double y);
/** relative moveTo */
private native void cairoRelMoveTo(long pointer, double dx, double dy);
/** lineTo */
private native void cairoLineTo(long pointer, double x, double y);
/** relative lineTo */
private native void cairoRelLineTo(long pointer, double dx, double dy);
/** Cubic curve-to */
private native void cairoCurveTo(long pointer, double x1, double y1,
double x2, double y2,
double x3, double y3);
/**
* Stroke current path
*/
private native void cairoStroke(long pointer);
/**
* Fill current path
*/
private native void cairoFill(long pointer, double alpha);
/**
* Clip current path
*/
private native void cairoClip(long pointer);
/**
* Save clip
*/
private native void cairoPreserveClip(long pointer);
/**
* Save clip
*/
private native void cairoResetClip(long pointer);
/**
* Set interpolation types
*/
private native void cairoSurfaceSetFilter(long pointer, int filter);
/**
* Draws a line from (x1,y1) to (x2,y2).
*
* @param pointer the native pointer
*
* @param x1 the x coordinate of the starting point
* @param y1 the y coordinate of the starting point
* @param x2 the x coordinate of the end point
* @param y2 the y coordinate of the end point
*/
private native void cairoDrawLine(long pointer, double x1, double y1,
double x2, double y2);
/**
* Draws a rectangle at starting point (x,y) and with the specified width
* and height.
*
* @param pointer the native pointer
* @param x the x coordinate of the upper left corner
* @param y the y coordinate of the upper left corner
* @param w the width of the rectangle
* @param h the height of the rectangle
*/
private native void cairoDrawRect(long pointer, double x, double y, double w,
double h);
/**
* Fills a rectangle at starting point (x,y) and with the specified width
* and height.
*
* @param pointer the native pointer
* @param x the x coordinate of the upper left corner
* @param y the y coordinate of the upper left corner
* @param w the width of the rectangle
* @param h the height of the rectangle
*/
private native void cairoFillRect(long pointer, double x, double y, double w,
double h);
///////////////////////// TRANSFORMS ///////////////////////////////////
/**
* Set the current transform
*/
public void setTransform(AffineTransform tx)
{
// Transform clip into target space using the old transform.
updateClip(transform);
// Update the native transform.
setTransformImpl(tx);
// Transform the clip back into user space using the inverse new transform.
try
{
updateClip(transform.createInverse());
}
catch (NoninvertibleTransformException ex)
{
// TODO: How can we deal properly with this?
ex.printStackTrace();
}
if (clip != null)
setClip(clip);
}
private void setTransformImpl(AffineTransform tx)
{
transform = tx;
if (transform != null)
{
double[] m = new double[6];
transform.getMatrix(m);
cairoSetMatrix(nativePointer, m);
}
}
public void transform(AffineTransform tx)
{
if (transform == null)
transform = new AffineTransform(tx);
else
transform.concatenate(tx);
if (clip != null)
{
try
{
AffineTransform clipTransform = tx.createInverse();
updateClip(clipTransform);
}
catch (NoninvertibleTransformException ex)
{
// TODO: How can we deal properly with this?
ex.printStackTrace();
}
}
setTransformImpl(transform);
}
public void rotate(double theta)
{
transform(AffineTransform.getRotateInstance(theta));
}
public void rotate(double theta, double x, double y)
{
transform(AffineTransform.getRotateInstance(theta, x, y));
}
public void scale(double sx, double sy)
{
transform(AffineTransform.getScaleInstance(sx, sy));
}
/**
* Translate the system of the co-ordinates. As translation is a frequent
* operation, it is done in an optimised way, unlike scaling and rotating.
*/
public void translate(double tx, double ty)
{
if (transform != null)
transform.translate(tx, ty);
else
transform = AffineTransform.getTranslateInstance(tx, ty);
if (clip != null)
{
// FIXME: this should actuall try to transform the shape
// rather than degrade to bounds.
if (clip instanceof Rectangle2D)
{
Rectangle2D r = (Rectangle2D) clip;
r.setRect(r.getX() - tx, r.getY() - ty, r.getWidth(),
r.getHeight());
}
else
{
AffineTransform clipTransform =
AffineTransform.getTranslateInstance(-tx, -ty);
updateClip(clipTransform);
}
}
setTransformImpl(transform);
}
public void translate(int x, int y)
{
translate((double) x, (double) y);
}
public void shear(double shearX, double shearY)
{
transform(AffineTransform.getShearInstance(shearX, shearY));
}
///////////////////////// DRAWING STATE ///////////////////////////////////
public void clip(Shape s)
{
// Do not touch clip when s == null.
if (s == null)
{
// The spec says this should clear the clip. The reference
// implementation throws a NullPointerException instead. I think,
// in this case we should conform to the specs, as it shouldn't
// affect compatibility.
setClip(null);
return;
}
// If the current clip is still null, initialize it.
if (clip == null)
{
clip = getRealBounds();
}
// This is so common, let's optimize this.
if (clip instanceof Rectangle2D && s instanceof Rectangle2D)
{
Rectangle2D clipRect = (Rectangle2D) clip;
Rectangle2D r = (Rectangle2D) s;
Rectangle2D.intersect(clipRect, r, clipRect);
setClip(clipRect);
}
else
{
Area current;
if (clip instanceof Area)
current = (Area) clip;
else
current = new Area(clip);
Area intersect;
if (s instanceof Area)
intersect = (Area) s;
else
intersect = new Area(s);
current.intersect(intersect);
clip = current;
// Call setClip so that the native side gets notified.
setClip(clip);
}
}
public Paint getPaint()
{
return paint;
}
public AffineTransform getTransform()
{
return (AffineTransform) transform.clone();
}
public void setPaint(Paint p)
{
if (paint == null)
return;
paint = p;
if (paint instanceof Color)
{
setColor((Color) paint);
}
else if (paint instanceof TexturePaint)
{
TexturePaint tp = (TexturePaint) paint;
BufferedImage img = tp.getImage();
// map the image to the anchor rectangle
int width = (int) tp.getAnchorRect().getWidth();
int height = (int) tp.getAnchorRect().getHeight();
double scaleX = width / (double) img.getWidth();
double scaleY = height / (double) img.getHeight();
AffineTransform at = new AffineTransform(scaleX, 0, 0, scaleY, 0, 0);
AffineTransformOp op = new AffineTransformOp(at, getRenderingHints());
BufferedImage texture = op.filter(img, null);
int[] pixels = texture.getRGB(0, 0, width, height, null, 0, width);
setTexturePixels(nativePointer, pixels, width, height, width);
}
else if (paint instanceof GradientPaint)
{
GradientPaint gp = (GradientPaint) paint;
Point2D p1 = gp.getPoint1();
Point2D p2 = gp.getPoint2();
Color c1 = gp.getColor1();
Color c2 = gp.getColor2();
setGradient(nativePointer, p1.getX(), p1.getY(), p2.getX(), p2.getY(),
c1.getRed(), c1.getGreen(), c1.getBlue(), c1.getAlpha(),
c2.getRed(), c2.getGreen(), c2.getBlue(), c2.getAlpha(),
gp.isCyclic());
}
else
throw new java.lang.UnsupportedOperationException();
}
public Stroke getStroke()
{
return stroke;
}
public void setStroke(Stroke st)
{
stroke = st;
if (stroke instanceof BasicStroke)
{
BasicStroke bs = (BasicStroke) stroke;
cairoSetLine(nativePointer, bs.getLineWidth(), bs.getEndCap(),
bs.getLineJoin(), bs.getMiterLimit());
float[] dashes = bs.getDashArray();
if (dashes != null)
{
double[] double_dashes = new double[dashes.length];
for (int i = 0; i < dashes.length; i++)
double_dashes[i] = dashes[i];
cairoSetDash(nativePointer, double_dashes, double_dashes.length,
(double) bs.getDashPhase());
}
else
cairoSetDash(nativePointer, new double[0], 0, 0.0);
}
}
public void setPaintMode()
{
setComposite(AlphaComposite.SrcOver);
}
public void setXORMode(Color c)
{
// FIXME: implement
}
public void setColor(Color c)
{
if (c == null)
c = Color.BLACK;
fg = c;
paint = c;
updateColor();
}
/**
* Set the current fg value as the cairo color.
*/
void updateColor()
{
if (fg == null)
fg = Color.BLACK;
cairoSetRGBAColor(nativePointer, fg.getRed() / 255.0,
fg.getGreen() / 255.0,fg.getBlue() / 255.0,
fg.getAlpha() / 255.0);
}
public Color getColor()
{
return fg;
}
public void clipRect(int x, int y, int width, int height)
{
if (clip == null)
setClip(new Rectangle(x, y, width, height));
else if (clip instanceof Rectangle)
{
computeIntersection(x, y, width, height, (Rectangle) clip);
setClip(clip);
}
else
clip(new Rectangle(x, y, width, height));
}
public Shape getClip()
{
if (clip == null)
return null;
else if (clip instanceof Rectangle2D)
return clip.getBounds2D(); //getClipInDevSpace();
else
{
GeneralPath p = new GeneralPath();
PathIterator pi = clip.getPathIterator(null);
p.append(pi, false);
return p;
}
}
public Rectangle getClipBounds()
{
if (clip == null)
return null;
else
return clip.getBounds();
}
protected Rectangle2D getClipInDevSpace()
{
Rectangle2D uclip = clip.getBounds2D();
if (transform == null)
return uclip;
else
{
Point2D pos = transform.transform(new Point2D.Double(uclip.getX(),
uclip.getY()),
(Point2D) null);
Point2D extent = transform.deltaTransform(new Point2D.Double(uclip
.getWidth(),
uclip
.getHeight()),
(Point2D) null);
return new Rectangle2D.Double(pos.getX(), pos.getY(), extent.getX(),
extent.getY());
}
}
public void setClip(int x, int y, int width, int height)
{
if( width < 0 || height < 0 )
return;
setClip(new Rectangle2D.Double(x, y, width, height));
}
public void setClip(Shape s)
{
// The first time the clip is set, save it as the original clip
// to reset to on s == null. We can rely on this being non-null
// because the constructor in subclasses is expected to set the
// initial clip properly.
if( firstClip )
{
originalClip = s;
firstClip = false;
}
clip = s;
cairoResetClip(nativePointer);
if (clip != null)
{
cairoNewPath(nativePointer);
if (clip instanceof Rectangle2D)
{
Rectangle2D r = (Rectangle2D) clip;
cairoRectangle(nativePointer, r.getX(), r.getY(), r.getWidth(),
r.getHeight());
}
else
walkPath(clip.getPathIterator(null), false);
cairoClip(nativePointer);
}
}
public void setBackground(Color c)
{
if (c == null)
c = Color.WHITE;
bg = c;
}
public Color getBackground()
{
return bg;
}
/**
* Return the current composite.
*/
public Composite getComposite()
{
if (comp == null)
return AlphaComposite.SrcOver;
else
return comp;
}
/**
* Sets the current composite context.
*/
public void setComposite(Composite comp)
{
this.comp = comp;
if (comp instanceof AlphaComposite)
{
AlphaComposite a = (AlphaComposite) comp;
cairoSetOperator(nativePointer, a.getRule());
}
else
{
// FIXME: this check is only required "if this Graphics2D
// context is drawing to a Component on the display screen".
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(new AWTPermission("readDisplayPixels"));
// FIXME: implement general Composite support
throw new java.lang.UnsupportedOperationException();
}
}
///////////////////////// DRAWING PRIMITIVES ///////////////////////////////////
public void draw(Shape s)
{
if ((stroke != null && ! (stroke instanceof BasicStroke))
|| (comp instanceof AlphaComposite && ((AlphaComposite) comp).getAlpha() != 1.0))
{
// Cairo doesn't support stroking with alpha, so we create the stroked
// shape and fill with alpha instead
fill(stroke.createStrokedShape(s));
return;
}
createPath(s);
cairoStroke(nativePointer);
}
public void fill(Shape s)
{
createPath(s);
double alpha = 1.0;
if (comp instanceof AlphaComposite)
alpha = ((AlphaComposite) comp).getAlpha();
cairoFill(nativePointer, alpha);
}
private void createPath(Shape s)
{
cairoNewPath(nativePointer);
// Optimize rectangles, since there is a direct Cairo function
if (s instanceof Rectangle2D)
{
Rectangle2D r = (Rectangle2D) s;
cairoRectangle(nativePointer, shifted(r.getX(), shiftDrawCalls),
shifted(r.getY(), shiftDrawCalls), r.getWidth(),
r.getHeight());
}
// We can optimize ellipses too; however we don't bother optimizing arcs:
// the iterator is fast enough (an ellipse requires 5 steps using the
// iterator, while most arcs are only 2-3)
else if (s instanceof Ellipse2D)
{
Ellipse2D e = (Ellipse2D) s;
double radius = Math.min(e.getHeight(), e.getWidth()) / 2;
// Cairo only draws circular shapes, but we can use a stretch to make
// them into ellipses
double xscale = 1, yscale = 1;
if (e.getHeight() != e.getWidth())
{
cairoSave(nativePointer);
if (e.getHeight() < e.getWidth())
xscale = e.getWidth() / (radius * 2);
else
yscale = e.getHeight() / (radius * 2);
if (xscale != 1 || yscale != 1)
cairoScale(nativePointer, xscale, yscale);
}
cairoArc(nativePointer,
shifted(e.getCenterX() / xscale, shiftDrawCalls),
shifted(e.getCenterY() / yscale, shiftDrawCalls), radius, 0,
Math.PI * 2);
if (xscale != 1 || yscale != 1)
cairoRestore(nativePointer);
}
// All other shapes are broken down and drawn in steps using the
// PathIterator
else
walkPath(s.getPathIterator(null), shiftDrawCalls);
}
/**
* Note that the rest of the drawing methods go via fill() or draw() for the drawing,
* although subclasses may with to overload these methods where context-specific
* optimizations are possible (e.g. bitmaps and fillRect(int, int, int, int)
*/
public void clearRect(int x, int y, int width, int height)
{
if (bg != null)
cairoSetRGBAColor(nativePointer, bg.getRed() / 255.0,
bg.getGreen() / 255.0, bg.getBlue() / 255.0, 1.0);
fillRect(x, y, width, height);
updateColor();
}
public void draw3DRect(int x, int y, int width, int height, boolean raised)
{
Stroke tmp = stroke;
setStroke(draw3DRectStroke);
super.draw3DRect(x, y, width, height, raised);
setStroke(tmp);
}
public void drawArc(int x, int y, int width, int height, int startAngle,
int arcAngle)
{
draw(new Arc2D.Double((double) x, (double) y, (double) width,
(double) height, (double) startAngle,
(double) arcAngle, Arc2D.OPEN));
}
public void drawLine(int x1, int y1, int x2, int y2)
{
// The coordinates being pairwise identical means one wants
// to draw a single pixel. This is emulated by drawing
// a one pixel sized rectangle.
if (x1 == x2 && y1 == y2)
cairoFillRect(nativePointer, x1, y1, 1, 1);
else
cairoDrawLine(nativePointer, x1 + 0.5, y1 + 0.5, x2 + 0.5, y2 + 0.5);
}
public void drawRect(int x, int y, int width, int height)
{
cairoDrawRect(nativePointer, shifted(x, shiftDrawCalls),
shifted(y, shiftDrawCalls), width, height);
}
public void fillArc(int x, int y, int width, int height, int startAngle,
int arcAngle)
{
fill(new Arc2D.Double((double) x, (double) y, (double) width,
(double) height, (double) startAngle,
(double) arcAngle, Arc2D.OPEN));
}
public void fillRect(int x, int y, int width, int height)
{
cairoFillRect(nativePointer, x, y, width, height);
}
public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints)
{
fill(new Polygon(xPoints, yPoints, nPoints));
}
public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints)
{
draw(new Polygon(xPoints, yPoints, nPoints));
}
public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints)
{
draw(new Polygon(xPoints, yPoints, nPoints));
}
public void drawOval(int x, int y, int width, int height)
{
drawArc(x, y, width, height, 0, 360);
}
public void drawRoundRect(int x, int y, int width, int height, int arcWidth,
int arcHeight)
{
draw(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
}
public void fillOval(int x, int y, int width, int height)
{
fillArc(x, y, width, height, 0, 360);
}
public void fillRoundRect(int x, int y, int width, int height, int arcWidth,
int arcHeight)
{
fill(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight));
}
/**
* CopyArea - performs clipping to the native surface as a convenience
* (requires getRealBounds). Then calls copyAreaImpl.
*/
public void copyArea(int ox, int oy, int owidth, int oheight,
int odx, int ody)
{
Point2D pos = transform.transform(new Point2D.Double(ox, oy),
(Point2D) null);
Point2D dim = transform.transform(new Point2D.Double(ox + owidth,
oy + oheight),
(Point2D) null);
Point2D p2 = transform.transform(new Point2D.Double(ox + odx, oy + ody),
(Point2D) null);
int x = (int)pos.getX();
int y = (int)pos.getY();
int width = (int)(dim.getX() - pos.getX());
int height = (int)(dim.getY() - pos.getY());
int dx = (int)(p2.getX() - pos.getX());
int dy = (int)(p2.getY() - pos.getY());
Rectangle2D r = getRealBounds();
if( width < 0 || height < 0 )
return;
// Return if outside the surface
if( x + dx > r.getWidth() || y + dy > r.getHeight() )
return;
if( x + dx + width < r.getX() || y + dy + height < r.getY() )
return;
// Clip edges if necessary
if( x + dx < r.getX() ) // left
{
width = x + dx + width;
x = (int)r.getX() - dx;
}
if( y + dy < r.getY() ) // top
{
height = y + dy + height;
y = (int)r.getY() - dy;
}
if( x + dx + width >= r.getWidth() ) // right
width = (int)r.getWidth() - dx - x;
if( y + dy + height >= r.getHeight() ) // bottom
height = (int)r.getHeight() - dy - y;
copyAreaImpl(x, y, width, height, dx, dy);
}
///////////////////////// RENDERING HINTS ///////////////////////////////////
/**
* FIXME- support better
*/
public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue)
{
hints.put(hintKey, hintValue);
if (hintKey.equals(RenderingHints.KEY_INTERPOLATION)
|| hintKey.equals(RenderingHints.KEY_ALPHA_INTERPOLATION))
{
if (hintValue.equals(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
cairoSurfaceSetFilter(nativePointer, 0);
else if (hintValue.equals(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
cairoSurfaceSetFilter(nativePointer, 1);
else if (hintValue.equals(RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED))
cairoSurfaceSetFilter(nativePointer, 2);
else if (hintValue.equals(RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY))
cairoSurfaceSetFilter(nativePointer, 3);
else if (hintValue.equals(RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT))
cairoSurfaceSetFilter(nativePointer, 4);
}
shiftDrawCalls = hints.containsValue(RenderingHints.VALUE_STROKE_NORMALIZE)
|| hints.containsValue(RenderingHints.VALUE_STROKE_DEFAULT);
}
public Object getRenderingHint(RenderingHints.Key hintKey)
{
return hints.get(hintKey);
}
public void setRenderingHints(Map hints)
{
this.hints = new RenderingHints(getDefaultHints());
this.hints.add(new RenderingHints(hints));
if (hints.containsKey(RenderingHints.KEY_INTERPOLATION))
{
if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
cairoSurfaceSetFilter(nativePointer, 0);
else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
cairoSurfaceSetFilter(nativePointer, 1);
}
if (hints.containsKey(RenderingHints.KEY_ALPHA_INTERPOLATION))
{
if (hints.containsValue(RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED))
cairoSurfaceSetFilter(nativePointer, 2);
else if (hints.containsValue(RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY))
cairoSurfaceSetFilter(nativePointer, 3);
else if (hints.containsValue(RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT))
cairoSurfaceSetFilter(nativePointer, 4);
}
shiftDrawCalls = hints.containsValue(RenderingHints.VALUE_STROKE_NORMALIZE)
|| hints.containsValue(RenderingHints.VALUE_STROKE_DEFAULT);
}
public void addRenderingHints(Map hints)
{
this.hints.add(new RenderingHints(hints));
}
public RenderingHints getRenderingHints()
{
return hints;
}
///////////////////////// IMAGE. METHODS ///////////////////////////////////
protected boolean drawImage(Image img, AffineTransform xform,
Color bgcolor, ImageObserver obs)
{
if (img == null)
return false;
if (xform == null)
xform = new AffineTransform();
// In this case, xform is an AffineTransform that transforms bounding
// box of the specified image from image space to user space. However
// when we pass this transform to cairo, cairo will use this transform
// to map "user coordinates" to "pixel" coordinates, which is the
// other way around. Therefore to get the "user -> pixel" transform
// that cairo wants from "image -> user" transform that we currently
// have, we will need to invert the transformation matrix.
AffineTransform invertedXform;
try
{
invertedXform = xform.createInverse();
}
catch (NoninvertibleTransformException e)
{
throw new ImagingOpException("Unable to invert transform "
+ xform.toString());
}
// Unrecognized image - convert to a BufferedImage
// Note - this can get us in trouble when the gdk lock is re-acquired.
// for example by VolatileImage. See ComponentGraphics for how we work
// around this.
if( !(img instanceof BufferedImage) )
{
ImageProducer source = img.getSource();
if (source == null)
return false;
img = Toolkit.getDefaultToolkit().createImage(source);
}
BufferedImage b = (BufferedImage) img;
DataBuffer db;
double[] i2u = new double[6];
int width = b.getWidth();
int height = b.getHeight();
// If this BufferedImage has a BufferedImageGraphics object,
// use the cached CairoSurface that BIG is drawing onto
if( BufferedImageGraphics.bufferedImages.get( b ) != null )
db = (DataBuffer)BufferedImageGraphics.bufferedImages.get( b );
else
db = b.getRaster().getDataBuffer();
invertedXform.getMatrix(i2u);
double alpha = 1.0;
if (comp instanceof AlphaComposite)
alpha = ((AlphaComposite) comp).getAlpha();
if(db instanceof CairoSurface)
{
((CairoSurface)db).drawSurface(nativePointer, i2u, alpha);
updateColor();
return true;
}
if( bgcolor != null )
{
// Fill a rectangle with the background color
// to composite the image onto.
Paint oldPaint = paint;
AffineTransform oldTransform = transform;
setPaint( bgcolor );
setTransform( invertedXform );
fillRect(0, 0, width, height);
setTransform( oldTransform );
setPaint( oldPaint );
}
int[] pixels = b.getRGB(0, 0, width, height, null, 0, width);
drawPixels(nativePointer, pixels, width, height, width, i2u, alpha);
// Cairo seems to lose the current color which must be restored.
updateColor();
return true;
}
public void drawRenderedImage(RenderedImage image, AffineTransform xform)
{
drawRaster(image.getColorModel(), image.getData(), xform, null);
}
public void drawRenderableImage(RenderableImage image, AffineTransform xform)
{
drawRenderedImage(image.createRendering(new RenderContext(xform)), xform);
}
public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs)
{
return drawImage(img, xform, null, obs);
}
public void drawImage(BufferedImage image, BufferedImageOp op, int x, int y)
{
Image filtered = image;
if (op != null)
filtered = op.filter(image, null);
drawImage(filtered, new AffineTransform(1f, 0f, 0f, 1f, x, y), null, null);
}
public boolean drawImage(Image img, int x, int y, ImageObserver observer)
{
return drawImage(img, new AffineTransform(1f, 0f, 0f, 1f, x, y), null,
observer);
}
public boolean drawImage(Image img, int x, int y, Color bgcolor,
ImageObserver observer)
{
return drawImage(img, x, y, img.getWidth(observer),
img.getHeight(observer), bgcolor, observer);
}
public boolean drawImage(Image img, int x, int y, int width, int height,
Color bgcolor, ImageObserver observer)
{
double scaleX = width / (double) img.getWidth(observer);
double scaleY = height / (double) img.getHeight(observer);
if( scaleX == 0 || scaleY == 0 )
return true;
return drawImage(img, new AffineTransform(scaleX, 0f, 0f, scaleY, x, y),
bgcolor, observer);
}
public boolean drawImage(Image img, int x, int y, int width, int height,
ImageObserver observer)
{
return drawImage(img, x, y, width, height, null, observer);
}
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
int sx1, int sy1, int sx2, int sy2, Color bgcolor,
ImageObserver observer)
{
if (img == null)
return false;
int sourceWidth = sx2 - sx1;
int sourceHeight = sy2 - sy1;
int destWidth = dx2 - dx1;
int destHeight = dy2 - dy1;
if(destWidth == 0 || destHeight == 0 || sourceWidth == 0 ||
sourceHeight == 0)
return true;
double scaleX = destWidth / (double) sourceWidth;
double scaleY = destHeight / (double) sourceHeight;
// FIXME: Avoid using an AT if possible here - it's at least twice as slow.
Shape oldClip = getClip();
int cx, cy, cw, ch;
if( dx1 < dx2 )
{ cx = dx1; cw = dx2 - dx1; }
else
{ cx = dx2; cw = dx1 - dx2; }
if( dy1 < dy2 )
{ cy = dy1; ch = dy2 - dy1; }
else
{ cy = dy2; ch = dy1 - dy2; }
clipRect( cx, cy, cw, ch );
AffineTransform tx = new AffineTransform();
tx.translate( dx1 - sx1*scaleX, dy1 - sy1*scaleY );
tx.scale( scaleX, scaleY );
boolean retval = drawImage(img, tx, bgcolor, observer);
setClip( oldClip );
return retval;
}
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2,
int sx1, int sy1, int sx2, int sy2,
ImageObserver observer)
{
return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer);
}
///////////////////////// TEXT METHODS ////////////////////////////////////
public void drawString(String str, float x, float y)
{
if (str == null || str.length() == 0)
return;
(new TextLayout( str, getFont(), getFontRenderContext() )).
draw(this, x, y);
}
public void drawString(String str, int x, int y)
{
drawString (str, (float) x, (float) y);
}
public void drawString(AttributedCharacterIterator ci, int x, int y)
{
drawString (ci, (float) x, (float) y);
}
public void drawGlyphVector(GlyphVector gv, float x, float y)
{
double alpha = 1.0;
if( gv.getNumGlyphs() <= 0 )
return;
if (comp instanceof AlphaComposite)
alpha = ((AlphaComposite) comp).getAlpha();
if (gv instanceof FreetypeGlyphVector && alpha == 1.0)
{
int n = gv.getNumGlyphs ();
int[] codes = gv.getGlyphCodes (0, n, null);
float[] positions = gv.getGlyphPositions (0, n, null);
setFont (gv.getFont ());
synchronized( this.font )
{
cairoDrawGlyphVector(nativePointer, (GdkFontPeer)getFont().getPeer(),
x, y, n, codes, positions);
}
}
else
{
translate(x, y);
fill(gv.getOutline());
translate(-x, -y);
}
}
public void drawString(AttributedCharacterIterator ci, float x, float y)
{
GlyphVector gv = getFont().createGlyphVector(getFontRenderContext(), ci);
drawGlyphVector(gv, x, y);
}
/**
* Should perhaps be contexct dependent, but this is left for now as an
* overloadable default implementation.
*/
public FontRenderContext getFontRenderContext()
{
return new FontRenderContext(transform, true, true);
}
// Until such time as pango is happy to talk directly to cairo, we
// actually need to redirect some calls from the GtkFontPeer and
// GtkFontMetrics into the drawing kit and ask cairo ourselves.
public FontMetrics getFontMetrics()
{
return getFontMetrics(getFont());
}
public FontMetrics getFontMetrics(Font f)
{
// the reason we go via the toolkit here is to try to get
// a cached object. the toolkit keeps such a cache.
return Toolkit.getDefaultToolkit().getFontMetrics(f);
}
public void setFont(Font f)
{
// Sun's JDK does not throw NPEs, instead it leaves the current setting
// unchanged. So do we.
if (f == null)
return;
if (f.getPeer() instanceof GdkFontPeer)
font = f;
else
font =
((ClasspathToolkit)(Toolkit.getDefaultToolkit()))
.getFont(f.getName(), f.getAttributes());
}
public Font getFont()
{
if (font == null)
return new Font("SansSerif", Font.PLAIN, 12);
return font;
}
/////////////////////// MISC. PUBLIC METHODS /////////////////////////////////
public boolean hit(Rectangle rect, Shape s, boolean onStroke)
{
if( onStroke )
{
Shape stroked = stroke.createStrokedShape( s );
return stroked.intersects( (double)rect.x, (double)rect.y,
(double)rect.width, (double)rect.height );
}
return s.intersects( (double)rect.x, (double)rect.y,
(double)rect.width, (double)rect.height );
}
public String toString()
{
return (getClass().getName()
+ "[font=" + getFont().toString()
+ ",color=" + fg.toString()
+ "]");
}
///////////////////////// PRIVATE METHODS ///////////////////////////////////
/**
* All the drawImage() methods eventually get delegated here if the image
* is not a Cairo surface.
*
* @param bgcolor - if non-null draws the background color before
* drawing the image.
*/
private boolean drawRaster(ColorModel cm, Raster r,
AffineTransform imageToUser, Color bgcolor)
{
if (r == null)
return false;
SampleModel sm = r.getSampleModel();
DataBuffer db = r.getDataBuffer();
if (db == null || sm == null)
return false;
if (cm == null)
cm = ColorModel.getRGBdefault();
double[] i2u = new double[6];
if (imageToUser != null)
imageToUser.getMatrix(i2u);
else
{
i2u[0] = 1;
i2u[1] = 0;
i2u[2] = 0;
i2u[3] = 1;
i2u[4] = 0;
i2u[5] = 0;
}
int[] pixels = findSimpleIntegerArray(cm, r);
if (pixels == null)
{
// FIXME: I don't think this code will work correctly with a non-RGB
// MultiPixelPackedSampleModel. Although this entire method should
// probably be rewritten to better utilize Cairo's different supported
// data formats.
if (sm instanceof MultiPixelPackedSampleModel)
{
pixels = r.getPixels(0, 0, r.getWidth(), r.getHeight(), pixels);
for (int i = 0; i < pixels.length; i++)
pixels[i] = cm.getRGB(pixels[i]);
}
else
{
pixels = new int[r.getWidth() * r.getHeight()];
for (int i = 0; i < pixels.length; i++)
pixels[i] = cm.getRGB(db.getElem(i));
}
}
// Change all transparent pixels in the image to the specified bgcolor,
// or (if there's no alpha) fill in an alpha channel so that it paints
// correctly.
if (cm.hasAlpha())
{
if (bgcolor != null && cm.hasAlpha())
for (int i = 0; i < pixels.length; i++)
{
if (cm.getAlpha(pixels[i]) == 0)
pixels[i] = bgcolor.getRGB();
}
}
else
for (int i = 0; i < pixels.length; i++)
pixels[i] |= 0xFF000000;
double alpha = 1.0;
if (comp instanceof AlphaComposite)
alpha = ((AlphaComposite) comp).getAlpha();
drawPixels(nativePointer, pixels, r.getWidth(), r.getHeight(),
r.getWidth(), i2u, alpha);
// Cairo seems to lose the current color which must be restored.
updateColor();
return true;
}
/**
* Shifts coordinates by 0.5.
*/
private double shifted(double coord, boolean doShift)
{
if (doShift)
return Math.floor(coord) + 0.5;
else
return coord;
}
/**
* Adds a pathIterator to the current Cairo path, also sets the cairo winding rule.
*/
private void walkPath(PathIterator p, boolean doShift)
{
double x = 0;
double y = 0;
double[] coords = new double[6];
cairoSetFillRule(nativePointer, p.getWindingRule());
for (; ! p.isDone(); p.next())
{
int seg = p.currentSegment(coords);
switch (seg)
{
case PathIterator.SEG_MOVETO:
x = shifted(coords[0], doShift);
y = shifted(coords[1], doShift);
cairoMoveTo(nativePointer, x, y);
break;
case PathIterator.SEG_LINETO:
x = shifted(coords[0], doShift);
y = shifted(coords[1], doShift);
cairoLineTo(nativePointer, x, y);
break;
case PathIterator.SEG_QUADTO:
// splitting a quadratic bezier into a cubic:
// see: http://pfaedit.sourceforge.net/bezier.html
double x1 = x + (2.0 / 3.0) * (shifted(coords[0], doShift) - x);
double y1 = y + (2.0 / 3.0) * (shifted(coords[1], doShift) - y);
double x2 = x1 + (1.0 / 3.0) * (shifted(coords[2], doShift) - x);
double y2 = y1 + (1.0 / 3.0) * (shifted(coords[3], doShift) - y);
x = shifted(coords[2], doShift);
y = shifted(coords[3], doShift);
cairoCurveTo(nativePointer, x1, y1, x2, y2, x, y);
break;
case PathIterator.SEG_CUBICTO:
x = shifted(coords[4], doShift);
y = shifted(coords[5], doShift);
cairoCurveTo(nativePointer, shifted(coords[0], doShift),
shifted(coords[1], doShift),
shifted(coords[2], doShift),
shifted(coords[3], doShift), x, y);
break;
case PathIterator.SEG_CLOSE:
cairoClosePath(nativePointer);
break;
}
}
}
/**
* Used by setRenderingHints()
*/
private Map getDefaultHints()
{
HashMap defaultHints = new HashMap();
defaultHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);
defaultHints.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_DEFAULT);
defaultHints.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
defaultHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
defaultHints.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_DEFAULT);
return defaultHints;
}
/**
* Used by drawRaster and GdkPixbufDecoder
*/
public static int[] findSimpleIntegerArray (ColorModel cm, Raster raster)
{
if (cm == null || raster == null)
return null;
if (! cm.getColorSpace().isCS_sRGB())
return null;
if (! (cm instanceof DirectColorModel))
return null;
DirectColorModel dcm = (DirectColorModel) cm;
if (dcm.getRedMask() != 0x00FF0000 || dcm.getGreenMask() != 0x0000FF00
|| dcm.getBlueMask() != 0x000000FF)
return null;
if (! (raster instanceof WritableRaster))
return null;
if (raster.getSampleModel().getDataType() != DataBuffer.TYPE_INT)
return null;
if (! (raster.getDataBuffer() instanceof DataBufferInt))
return null;
DataBufferInt db = (DataBufferInt) raster.getDataBuffer();
if (db.getNumBanks() != 1)
return null;
// Finally, we have determined that this is a single bank, [A]RGB-int
// buffer in sRGB space. It's worth checking all this, because it means
// that cairo can paint directly into the data buffer, which is very
// fast compared to all the normal copying and converting.
return db.getData();
}
/**
* Helper method to transform the clip. This is called by the various
* transformation-manipulation methods to update the clip (which is in
* userspace) accordingly.
*
* The transform usually is the inverse transform that was applied to the
* graphics object.
*
* @param t the transform to apply to the clip
*/
private void updateClip(AffineTransform t)
{
if (clip == null)
return;
if (! (clip instanceof GeneralPath))
clip = new GeneralPath(clip);
GeneralPath p = (GeneralPath) clip;
p.transform(t);
}
private static Rectangle computeIntersection(int x, int y, int w, int h,
Rectangle rect)
{
int x2 = (int) rect.x;
int y2 = (int) rect.y;
int w2 = (int) rect.width;
int h2 = (int) rect.height;
int dx = (x > x2) ? x : x2;
int dy = (y > y2) ? y : y2;
int dw = (x + w < x2 + w2) ? (x + w - dx) : (x2 + w2 - dx);
int dh = (y + h < y2 + h2) ? (y + h - dy) : (y2 + h2 - dy);
if (dw >= 0 && dh >= 0)
rect.setBounds(dx, dy, dw, dh);
else
rect.setBounds(0, 0, 0, 0);
return rect;
}
}