3597 lines
132 KiB
Java
3597 lines
132 KiB
Java
/*
|
|
* Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code 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
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package sun.java2d;
|
|
|
|
import java.awt.Graphics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.RenderingHints;
|
|
import java.awt.RenderingHints.Key;
|
|
import java.awt.geom.Area;
|
|
import java.awt.geom.AffineTransform;
|
|
import java.awt.geom.NoninvertibleTransformException;
|
|
import java.awt.AlphaComposite;
|
|
import java.awt.BasicStroke;
|
|
import java.awt.image.BufferedImage;
|
|
import java.awt.image.BufferedImageOp;
|
|
import java.awt.image.RenderedImage;
|
|
import java.awt.image.renderable.RenderableImage;
|
|
import java.awt.image.renderable.RenderContext;
|
|
import java.awt.image.AffineTransformOp;
|
|
import java.awt.image.Raster;
|
|
import java.awt.image.WritableRaster;
|
|
import java.awt.Image;
|
|
import java.awt.Composite;
|
|
import java.awt.Color;
|
|
import java.awt.image.ColorModel;
|
|
import java.awt.GraphicsConfiguration;
|
|
import java.awt.Paint;
|
|
import java.awt.GradientPaint;
|
|
import java.awt.LinearGradientPaint;
|
|
import java.awt.RadialGradientPaint;
|
|
import java.awt.TexturePaint;
|
|
import java.awt.geom.Rectangle2D;
|
|
import java.awt.geom.PathIterator;
|
|
import java.awt.geom.GeneralPath;
|
|
import java.awt.Shape;
|
|
import java.awt.Stroke;
|
|
import java.awt.FontMetrics;
|
|
import java.awt.Rectangle;
|
|
import java.text.AttributedCharacterIterator;
|
|
import java.awt.Font;
|
|
import java.awt.Point;
|
|
import java.awt.image.ImageObserver;
|
|
import java.awt.Transparency;
|
|
import java.awt.font.GlyphVector;
|
|
import java.awt.font.TextLayout;
|
|
|
|
import sun.awt.image.SurfaceManager;
|
|
import sun.font.FontDesignMetrics;
|
|
import sun.font.FontUtilities;
|
|
import sun.java2d.pipe.PixelDrawPipe;
|
|
import sun.java2d.pipe.PixelFillPipe;
|
|
import sun.java2d.pipe.ShapeDrawPipe;
|
|
import sun.java2d.pipe.ValidatePipe;
|
|
import sun.java2d.pipe.ShapeSpanIterator;
|
|
import sun.java2d.pipe.Region;
|
|
import sun.java2d.pipe.TextPipe;
|
|
import sun.java2d.pipe.DrawImagePipe;
|
|
import sun.java2d.pipe.LoopPipe;
|
|
import sun.java2d.loops.FontInfo;
|
|
import sun.java2d.loops.RenderLoops;
|
|
import sun.java2d.loops.CompositeType;
|
|
import sun.java2d.loops.SurfaceType;
|
|
import sun.java2d.loops.Blit;
|
|
import sun.java2d.loops.MaskFill;
|
|
import java.awt.font.FontRenderContext;
|
|
import sun.java2d.loops.XORComposite;
|
|
import sun.awt.ConstrainableGraphics;
|
|
import sun.awt.SunHints;
|
|
import java.util.Map;
|
|
import java.util.Iterator;
|
|
import sun.misc.PerformanceLogger;
|
|
|
|
import java.lang.annotation.Native;
|
|
import sun.awt.image.MultiResolutionImage;
|
|
|
|
import static java.awt.geom.AffineTransform.TYPE_FLIP;
|
|
import static java.awt.geom.AffineTransform.TYPE_MASK_SCALE;
|
|
import static java.awt.geom.AffineTransform.TYPE_TRANSLATION;
|
|
import sun.awt.image.MultiResolutionToolkitImage;
|
|
import sun.awt.image.ToolkitImage;
|
|
|
|
/**
|
|
* This is a the master Graphics2D superclass for all of the Sun
|
|
* Graphics implementations. This class relies on subclasses to
|
|
* manage the various device information, but provides an overall
|
|
* general framework for performing all of the requests in the
|
|
* Graphics and Graphics2D APIs.
|
|
*
|
|
* @author Jim Graham
|
|
*/
|
|
public final class SunGraphics2D
|
|
extends Graphics2D
|
|
implements ConstrainableGraphics, Cloneable, DestSurfaceProvider
|
|
{
|
|
/*
|
|
* Attribute States
|
|
*/
|
|
/* Paint */
|
|
@Native
|
|
public static final int PAINT_CUSTOM = 6; /* Any other Paint object */
|
|
@Native
|
|
public static final int PAINT_TEXTURE = 5; /* Tiled Image */
|
|
@Native
|
|
public static final int PAINT_RAD_GRADIENT = 4; /* Color RadialGradient */
|
|
@Native
|
|
public static final int PAINT_LIN_GRADIENT = 3; /* Color LinearGradient */
|
|
@Native
|
|
public static final int PAINT_GRADIENT = 2; /* Color Gradient */
|
|
@Native
|
|
public static final int PAINT_ALPHACOLOR = 1; /* Non-opaque Color */
|
|
@Native
|
|
public static final int PAINT_OPAQUECOLOR = 0; /* Opaque Color */
|
|
|
|
/* Composite*/
|
|
@Native
|
|
public static final int COMP_CUSTOM = 3;/* Custom Composite */
|
|
@Native
|
|
public static final int COMP_XOR = 2;/* XOR Mode Composite */
|
|
@Native
|
|
public static final int COMP_ALPHA = 1;/* AlphaComposite */
|
|
@Native
|
|
public static final int COMP_ISCOPY = 0;/* simple stores into destination,
|
|
* i.e. Src, SrcOverNoEa, and other
|
|
* alpha modes which replace
|
|
* the destination.
|
|
*/
|
|
|
|
/* Stroke */
|
|
@Native
|
|
public static final int STROKE_CUSTOM = 3; /* custom Stroke */
|
|
@Native
|
|
public static final int STROKE_WIDE = 2; /* BasicStroke */
|
|
@Native
|
|
public static final int STROKE_THINDASHED = 1; /* BasicStroke */
|
|
@Native
|
|
public static final int STROKE_THIN = 0; /* BasicStroke */
|
|
|
|
/* Transform */
|
|
@Native
|
|
public static final int TRANSFORM_GENERIC = 4; /* any 3x2 */
|
|
@Native
|
|
public static final int TRANSFORM_TRANSLATESCALE = 3; /* scale XY */
|
|
@Native
|
|
public static final int TRANSFORM_ANY_TRANSLATE = 2; /* non-int translate */
|
|
@Native
|
|
public static final int TRANSFORM_INT_TRANSLATE = 1; /* int translate */
|
|
@Native
|
|
public static final int TRANSFORM_ISIDENT = 0; /* Identity */
|
|
|
|
/* Clipping */
|
|
@Native
|
|
public static final int CLIP_SHAPE = 2; /* arbitrary clip */
|
|
@Native
|
|
public static final int CLIP_RECTANGULAR = 1; /* rectangular clip */
|
|
@Native
|
|
public static final int CLIP_DEVICE = 0; /* no clipping set */
|
|
|
|
/* The following fields are used when the current Paint is a Color. */
|
|
public int eargb; // ARGB value with ExtraAlpha baked in
|
|
public int pixel; // pixel value for eargb
|
|
|
|
public SurfaceData surfaceData;
|
|
|
|
public PixelDrawPipe drawpipe;
|
|
public PixelFillPipe fillpipe;
|
|
public DrawImagePipe imagepipe;
|
|
public ShapeDrawPipe shapepipe;
|
|
public TextPipe textpipe;
|
|
public MaskFill alphafill;
|
|
|
|
public RenderLoops loops;
|
|
|
|
public CompositeType imageComp; /* Image Transparency checked on fly */
|
|
|
|
public int paintState;
|
|
public int compositeState;
|
|
public int strokeState;
|
|
public int transformState;
|
|
public int clipState;
|
|
|
|
public Color foregroundColor;
|
|
public Color backgroundColor;
|
|
|
|
public AffineTransform transform;
|
|
public int transX;
|
|
public int transY;
|
|
|
|
protected static final Stroke defaultStroke = new BasicStroke();
|
|
protected static final Composite defaultComposite = AlphaComposite.SrcOver;
|
|
private static final Font defaultFont =
|
|
new Font(Font.DIALOG, Font.PLAIN, 12);
|
|
|
|
public Paint paint;
|
|
public Stroke stroke;
|
|
public Composite composite;
|
|
protected Font font;
|
|
protected FontMetrics fontMetrics;
|
|
|
|
public int renderHint;
|
|
public int antialiasHint;
|
|
public int textAntialiasHint;
|
|
protected int fractionalMetricsHint;
|
|
|
|
/* A gamma adjustment to the colour used in lcd text blitting */
|
|
public int lcdTextContrast;
|
|
private static int lcdTextContrastDefaultValue = 140;
|
|
|
|
private int interpolationHint; // raw value of rendering Hint
|
|
public int strokeHint;
|
|
|
|
public int interpolationType; // algorithm choice based on
|
|
// interpolation and render Hints
|
|
|
|
public RenderingHints hints;
|
|
|
|
public Region constrainClip; // lightweight bounds in pixels
|
|
public int constrainX;
|
|
public int constrainY;
|
|
|
|
public Region clipRegion;
|
|
public Shape usrClip;
|
|
protected Region devClip; // Actual physical drawable in pixels
|
|
|
|
private final int devScale; // Actual physical scale factor
|
|
private int resolutionVariantHint;
|
|
|
|
// cached state for text rendering
|
|
private boolean validFontInfo;
|
|
private FontInfo fontInfo;
|
|
private FontInfo glyphVectorFontInfo;
|
|
private FontRenderContext glyphVectorFRC;
|
|
|
|
private final static int slowTextTransformMask =
|
|
AffineTransform.TYPE_GENERAL_TRANSFORM
|
|
| AffineTransform.TYPE_MASK_ROTATION
|
|
| AffineTransform.TYPE_FLIP;
|
|
|
|
static {
|
|
if (PerformanceLogger.loggingEnabled()) {
|
|
PerformanceLogger.setTime("SunGraphics2D static initialization");
|
|
}
|
|
}
|
|
|
|
public SunGraphics2D(SurfaceData sd, Color fg, Color bg, Font f) {
|
|
surfaceData = sd;
|
|
foregroundColor = fg;
|
|
backgroundColor = bg;
|
|
|
|
transform = new AffineTransform();
|
|
stroke = defaultStroke;
|
|
composite = defaultComposite;
|
|
paint = foregroundColor;
|
|
|
|
imageComp = CompositeType.SrcOverNoEa;
|
|
|
|
renderHint = SunHints.INTVAL_RENDER_DEFAULT;
|
|
antialiasHint = SunHints.INTVAL_ANTIALIAS_OFF;
|
|
textAntialiasHint = SunHints.INTVAL_TEXT_ANTIALIAS_DEFAULT;
|
|
fractionalMetricsHint = SunHints.INTVAL_FRACTIONALMETRICS_OFF;
|
|
lcdTextContrast = lcdTextContrastDefaultValue;
|
|
interpolationHint = -1;
|
|
strokeHint = SunHints.INTVAL_STROKE_DEFAULT;
|
|
resolutionVariantHint = SunHints.INTVAL_RESOLUTION_VARIANT_DEFAULT;
|
|
|
|
interpolationType = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
|
|
|
|
validateColor();
|
|
|
|
devScale = sd.getDefaultScale();
|
|
if (devScale != 1) {
|
|
transform.setToScale(devScale, devScale);
|
|
invalidateTransform();
|
|
}
|
|
|
|
font = f;
|
|
if (font == null) {
|
|
font = defaultFont;
|
|
}
|
|
|
|
setDevClip(sd.getBounds());
|
|
invalidatePipe();
|
|
}
|
|
|
|
protected Object clone() {
|
|
try {
|
|
SunGraphics2D g = (SunGraphics2D) super.clone();
|
|
g.transform = new AffineTransform(this.transform);
|
|
if (hints != null) {
|
|
g.hints = (RenderingHints) this.hints.clone();
|
|
}
|
|
/* FontInfos are re-used, so must be cloned too, if they
|
|
* are valid, and be nulled out if invalid.
|
|
* The implied trade-off is that there is more to be gained
|
|
* from re-using these objects than is lost by having to
|
|
* clone them when the SG2D is cloned.
|
|
*/
|
|
if (this.fontInfo != null) {
|
|
if (this.validFontInfo) {
|
|
g.fontInfo = (FontInfo)this.fontInfo.clone();
|
|
} else {
|
|
g.fontInfo = null;
|
|
}
|
|
}
|
|
if (this.glyphVectorFontInfo != null) {
|
|
g.glyphVectorFontInfo =
|
|
(FontInfo)this.glyphVectorFontInfo.clone();
|
|
g.glyphVectorFRC = this.glyphVectorFRC;
|
|
}
|
|
//g.invalidatePipe();
|
|
return g;
|
|
} catch (CloneNotSupportedException e) {
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a new SunGraphics2D based on this one.
|
|
*/
|
|
public Graphics create() {
|
|
return (Graphics) clone();
|
|
}
|
|
|
|
public void setDevClip(int x, int y, int w, int h) {
|
|
Region c = constrainClip;
|
|
if (c == null) {
|
|
devClip = Region.getInstanceXYWH(x, y, w, h);
|
|
} else {
|
|
devClip = c.getIntersectionXYWH(x, y, w, h);
|
|
}
|
|
validateCompClip();
|
|
}
|
|
|
|
public void setDevClip(Rectangle r) {
|
|
setDevClip(r.x, r.y, r.width, r.height);
|
|
}
|
|
|
|
/**
|
|
* Constrain rendering for lightweight objects.
|
|
*/
|
|
public void constrain(int x, int y, int w, int h, Region region) {
|
|
if ((x | y) != 0) {
|
|
translate(x, y);
|
|
}
|
|
if (transformState > TRANSFORM_TRANSLATESCALE) {
|
|
clipRect(0, 0, w, h);
|
|
return;
|
|
}
|
|
// changes parameters according to the current scale and translate.
|
|
final double scaleX = transform.getScaleX();
|
|
final double scaleY = transform.getScaleY();
|
|
x = constrainX = (int) transform.getTranslateX();
|
|
y = constrainY = (int) transform.getTranslateY();
|
|
w = Region.dimAdd(x, Region.clipScale(w, scaleX));
|
|
h = Region.dimAdd(y, Region.clipScale(h, scaleY));
|
|
|
|
Region c = constrainClip;
|
|
if (c == null) {
|
|
c = Region.getInstanceXYXY(x, y, w, h);
|
|
} else {
|
|
c = c.getIntersectionXYXY(x, y, w, h);
|
|
}
|
|
if (region != null) {
|
|
region = region.getScaledRegion(scaleX, scaleY);
|
|
region = region.getTranslatedRegion(x, y);
|
|
c = c.getIntersection(region);
|
|
}
|
|
|
|
if (c == constrainClip) {
|
|
// Common case to ignore
|
|
return;
|
|
}
|
|
|
|
constrainClip = c;
|
|
if (!devClip.isInsideQuickCheck(c)) {
|
|
devClip = devClip.getIntersection(c);
|
|
validateCompClip();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constrain rendering for lightweight objects.
|
|
*
|
|
* REMIND: This method will back off to the "workaround"
|
|
* of using translate and clipRect if the Graphics
|
|
* to be constrained has a complex transform. The
|
|
* drawback of the workaround is that the resulting
|
|
* clip and device origin cannot be "enforced".
|
|
*
|
|
* @exception IllegalStateException If the Graphics
|
|
* to be constrained has a complex transform.
|
|
*/
|
|
@Override
|
|
public void constrain(int x, int y, int w, int h) {
|
|
constrain(x, y, w, h, null);
|
|
}
|
|
|
|
protected static ValidatePipe invalidpipe = new ValidatePipe();
|
|
|
|
/*
|
|
* Invalidate the pipeline
|
|
*/
|
|
protected void invalidatePipe() {
|
|
drawpipe = invalidpipe;
|
|
fillpipe = invalidpipe;
|
|
shapepipe = invalidpipe;
|
|
textpipe = invalidpipe;
|
|
imagepipe = invalidpipe;
|
|
loops = null;
|
|
}
|
|
|
|
public void validatePipe() {
|
|
/* This workaround is for the situation when we update the Pipelines
|
|
* for invalid SurfaceData and run further code when the current
|
|
* pipeline doesn't support the type of new SurfaceData created during
|
|
* the current pipeline's work (in place of the invalid SurfaceData).
|
|
* Usually SurfaceData and Pipelines are repaired (through revalidateAll)
|
|
* and called again in the exception handlers */
|
|
|
|
if (!surfaceData.isValid()) {
|
|
throw new InvalidPipeException("attempt to validate Pipe with invalid SurfaceData");
|
|
}
|
|
|
|
surfaceData.validatePipe(this);
|
|
}
|
|
|
|
/*
|
|
* Intersect two Shapes by the simplest method, attempting to produce
|
|
* a simplified result.
|
|
* The boolean arguments keep1 and keep2 specify whether or not
|
|
* the first or second shapes can be modified during the operation
|
|
* or whether that shape must be "kept" unmodified.
|
|
*/
|
|
Shape intersectShapes(Shape s1, Shape s2, boolean keep1, boolean keep2) {
|
|
if (s1 instanceof Rectangle && s2 instanceof Rectangle) {
|
|
return ((Rectangle) s1).intersection((Rectangle) s2);
|
|
}
|
|
if (s1 instanceof Rectangle2D) {
|
|
return intersectRectShape((Rectangle2D) s1, s2, keep1, keep2);
|
|
} else if (s2 instanceof Rectangle2D) {
|
|
return intersectRectShape((Rectangle2D) s2, s1, keep2, keep1);
|
|
}
|
|
return intersectByArea(s1, s2, keep1, keep2);
|
|
}
|
|
|
|
/*
|
|
* Intersect a Rectangle with a Shape by the simplest method,
|
|
* attempting to produce a simplified result.
|
|
* The boolean arguments keep1 and keep2 specify whether or not
|
|
* the first or second shapes can be modified during the operation
|
|
* or whether that shape must be "kept" unmodified.
|
|
*/
|
|
Shape intersectRectShape(Rectangle2D r, Shape s,
|
|
boolean keep1, boolean keep2) {
|
|
if (s instanceof Rectangle2D) {
|
|
Rectangle2D r2 = (Rectangle2D) s;
|
|
Rectangle2D outrect;
|
|
if (!keep1) {
|
|
outrect = r;
|
|
} else if (!keep2) {
|
|
outrect = r2;
|
|
} else {
|
|
outrect = new Rectangle2D.Float();
|
|
}
|
|
double x1 = Math.max(r.getX(), r2.getX());
|
|
double x2 = Math.min(r.getX() + r.getWidth(),
|
|
r2.getX() + r2.getWidth());
|
|
double y1 = Math.max(r.getY(), r2.getY());
|
|
double y2 = Math.min(r.getY() + r.getHeight(),
|
|
r2.getY() + r2.getHeight());
|
|
|
|
if (((x2 - x1) < 0) || ((y2 - y1) < 0))
|
|
// Width or height is negative. No intersection.
|
|
outrect.setFrameFromDiagonal(0, 0, 0, 0);
|
|
else
|
|
outrect.setFrameFromDiagonal(x1, y1, x2, y2);
|
|
return outrect;
|
|
}
|
|
if (r.contains(s.getBounds2D())) {
|
|
if (keep2) {
|
|
s = cloneShape(s);
|
|
}
|
|
return s;
|
|
}
|
|
return intersectByArea(r, s, keep1, keep2);
|
|
}
|
|
|
|
protected static Shape cloneShape(Shape s) {
|
|
return new GeneralPath(s);
|
|
}
|
|
|
|
/*
|
|
* Intersect two Shapes using the Area class. Presumably other
|
|
* attempts at simpler intersection methods proved fruitless.
|
|
* The boolean arguments keep1 and keep2 specify whether or not
|
|
* the first or second shapes can be modified during the operation
|
|
* or whether that shape must be "kept" unmodified.
|
|
* @see #intersectShapes
|
|
* @see #intersectRectShape
|
|
*/
|
|
Shape intersectByArea(Shape s1, Shape s2, boolean keep1, boolean keep2) {
|
|
Area a1, a2;
|
|
|
|
// First see if we can find an overwriteable source shape
|
|
// to use as our destination area to avoid duplication.
|
|
if (!keep1 && (s1 instanceof Area)) {
|
|
a1 = (Area) s1;
|
|
} else if (!keep2 && (s2 instanceof Area)) {
|
|
a1 = (Area) s2;
|
|
s2 = s1;
|
|
} else {
|
|
a1 = new Area(s1);
|
|
}
|
|
|
|
if (s2 instanceof Area) {
|
|
a2 = (Area) s2;
|
|
} else {
|
|
a2 = new Area(s2);
|
|
}
|
|
|
|
a1.intersect(a2);
|
|
if (a1.isRectangular()) {
|
|
return a1.getBounds();
|
|
}
|
|
|
|
return a1;
|
|
}
|
|
|
|
/*
|
|
* Intersect usrClip bounds and device bounds to determine the composite
|
|
* rendering boundaries.
|
|
*/
|
|
public Region getCompClip() {
|
|
if (!surfaceData.isValid()) {
|
|
// revalidateAll() implicitly recalculcates the composite clip
|
|
revalidateAll();
|
|
}
|
|
|
|
return clipRegion;
|
|
}
|
|
|
|
public Font getFont() {
|
|
if (font == null) {
|
|
font = defaultFont;
|
|
}
|
|
return font;
|
|
}
|
|
|
|
private static final double[] IDENT_MATRIX = {1, 0, 0, 1};
|
|
private static final AffineTransform IDENT_ATX =
|
|
new AffineTransform();
|
|
|
|
private static final int MINALLOCATED = 8;
|
|
private static final int TEXTARRSIZE = 17;
|
|
private static double[][] textTxArr = new double[TEXTARRSIZE][];
|
|
private static AffineTransform[] textAtArr =
|
|
new AffineTransform[TEXTARRSIZE];
|
|
|
|
static {
|
|
for (int i=MINALLOCATED;i<TEXTARRSIZE;i++) {
|
|
textTxArr[i] = new double [] {i, 0, 0, i};
|
|
textAtArr[i] = new AffineTransform( textTxArr[i]);
|
|
}
|
|
}
|
|
|
|
// cached state for various draw[String,Char,Byte] optimizations
|
|
public FontInfo checkFontInfo(FontInfo info, Font font,
|
|
FontRenderContext frc) {
|
|
/* Do not create a FontInfo object as part of construction of an
|
|
* SG2D as its possible it may never be needed - ie if no text
|
|
* is drawn using this SG2D.
|
|
*/
|
|
if (info == null) {
|
|
info = new FontInfo();
|
|
}
|
|
|
|
float ptSize = font.getSize2D();
|
|
int txFontType;
|
|
AffineTransform devAt, textAt=null;
|
|
if (font.isTransformed()) {
|
|
textAt = font.getTransform();
|
|
textAt.scale(ptSize, ptSize);
|
|
txFontType = textAt.getType();
|
|
info.originX = (float)textAt.getTranslateX();
|
|
info.originY = (float)textAt.getTranslateY();
|
|
textAt.translate(-info.originX, -info.originY);
|
|
if (transformState >= TRANSFORM_TRANSLATESCALE) {
|
|
transform.getMatrix(info.devTx = new double[4]);
|
|
devAt = new AffineTransform(info.devTx);
|
|
textAt.preConcatenate(devAt);
|
|
} else {
|
|
info.devTx = IDENT_MATRIX;
|
|
devAt = IDENT_ATX;
|
|
}
|
|
textAt.getMatrix(info.glyphTx = new double[4]);
|
|
double shearx = textAt.getShearX();
|
|
double scaley = textAt.getScaleY();
|
|
if (shearx != 0) {
|
|
scaley = Math.sqrt(shearx * shearx + scaley * scaley);
|
|
}
|
|
info.pixelHeight = (int)(Math.abs(scaley)+0.5);
|
|
} else {
|
|
txFontType = AffineTransform.TYPE_IDENTITY;
|
|
info.originX = info.originY = 0;
|
|
if (transformState >= TRANSFORM_TRANSLATESCALE) {
|
|
transform.getMatrix(info.devTx = new double[4]);
|
|
devAt = new AffineTransform(info.devTx);
|
|
info.glyphTx = new double[4];
|
|
for (int i = 0; i < 4; i++) {
|
|
info.glyphTx[i] = info.devTx[i] * ptSize;
|
|
}
|
|
textAt = new AffineTransform(info.glyphTx);
|
|
double shearx = transform.getShearX();
|
|
double scaley = transform.getScaleY();
|
|
if (shearx != 0) {
|
|
scaley = Math.sqrt(shearx * shearx + scaley * scaley);
|
|
}
|
|
info.pixelHeight = (int)(Math.abs(scaley * ptSize)+0.5);
|
|
} else {
|
|
/* If the double represents a common integral, we
|
|
* may have pre-allocated objects.
|
|
* A "sparse" array be seems to be as fast as a switch
|
|
* even for 3 or 4 pt sizes, and is more flexible.
|
|
* This should perform comparably in single-threaded
|
|
* rendering to the old code which synchronized on the
|
|
* class and scale better on MP systems.
|
|
*/
|
|
int pszInt = (int)ptSize;
|
|
if (ptSize == pszInt &&
|
|
pszInt >= MINALLOCATED && pszInt < TEXTARRSIZE) {
|
|
info.glyphTx = textTxArr[pszInt];
|
|
textAt = textAtArr[pszInt];
|
|
info.pixelHeight = pszInt;
|
|
} else {
|
|
info.pixelHeight = (int)(ptSize+0.5);
|
|
}
|
|
if (textAt == null) {
|
|
info.glyphTx = new double[] {ptSize, 0, 0, ptSize};
|
|
textAt = new AffineTransform(info.glyphTx);
|
|
}
|
|
|
|
info.devTx = IDENT_MATRIX;
|
|
devAt = IDENT_ATX;
|
|
}
|
|
}
|
|
|
|
info.font2D = FontUtilities.getFont2D(font);
|
|
|
|
int fmhint = fractionalMetricsHint;
|
|
if (fmhint == SunHints.INTVAL_FRACTIONALMETRICS_DEFAULT) {
|
|
fmhint = SunHints.INTVAL_FRACTIONALMETRICS_OFF;
|
|
}
|
|
info.lcdSubPixPos = false; // conditionally set true in LCD mode.
|
|
|
|
/* The text anti-aliasing hints that are set by the client need
|
|
* to be interpreted for the current state and stored in the
|
|
* FontInfo.aahint which is what will actually be used and
|
|
* will be one of OFF, ON, LCD_HRGB or LCD_VRGB.
|
|
* This is what pipe selection code should typically refer to, not
|
|
* textAntialiasHint. This means we are now evaluating the meaning
|
|
* of "default" here. Any pipe that really cares about that will
|
|
* also need to consult that variable.
|
|
* Otherwise these are being used only as args to getStrike,
|
|
* and are encapsulated in that object which is part of the
|
|
* FontInfo, so we do not need to store them directly as fields
|
|
* in the FontInfo object.
|
|
* That could change if FontInfo's were more selectively
|
|
* revalidated when graphics state changed. Presently this
|
|
* method re-evaluates all fields in the fontInfo.
|
|
* The strike doesn't need to know the RGB subpixel order. Just
|
|
* if its H or V orientation, so if an LCD option is specified we
|
|
* always pass in the RGB hint to the strike.
|
|
* frc is non-null only if this is a GlyphVector. For reasons
|
|
* which are probably a historical mistake the AA hint in a GV
|
|
* is honoured when we render, overriding the Graphics setting.
|
|
*/
|
|
int aahint;
|
|
if (frc == null) {
|
|
aahint = textAntialiasHint;
|
|
} else {
|
|
aahint = ((SunHints.Value)frc.getAntiAliasingHint()).getIndex();
|
|
}
|
|
if (aahint == SunHints.INTVAL_TEXT_ANTIALIAS_DEFAULT) {
|
|
if (antialiasHint == SunHints.INTVAL_ANTIALIAS_ON) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_ON;
|
|
} else {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_OFF;
|
|
}
|
|
} else {
|
|
/* If we are in checkFontInfo because a rendering hint has been
|
|
* set then all pipes are revalidated. But we can also
|
|
* be here because setFont() has been called when the 'gasp'
|
|
* hint is set, as then the font size determines the text pipe.
|
|
* See comments in SunGraphics2d.setFont(Font).
|
|
*/
|
|
if (aahint == SunHints.INTVAL_TEXT_ANTIALIAS_GASP) {
|
|
if (info.font2D.useAAForPtSize(info.pixelHeight)) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_ON;
|
|
} else {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_OFF;
|
|
}
|
|
} else if (aahint >= SunHints.INTVAL_TEXT_ANTIALIAS_LCD_HRGB) {
|
|
/* loops for default rendering modes are installed in the SG2D
|
|
* constructor. If there are none this will be null.
|
|
* Not all compositing modes update the render loops, so
|
|
* we also test that this is a mode we know should support
|
|
* this. One minor issue is that the loops aren't necessarily
|
|
* installed for a new rendering mode until after this
|
|
* method is called during pipeline validation. So it is
|
|
* theoretically possible that it was set to null for a
|
|
* compositing mode, the composite is then set back to Src,
|
|
* but the loop is still null when this is called and AA=ON
|
|
* is installed instead of an LCD mode.
|
|
* However this is done in the right order in SurfaceData.java
|
|
* so this is not likely to be a problem - but not
|
|
* guaranteed.
|
|
*/
|
|
if (
|
|
!surfaceData.canRenderLCDText(this)
|
|
// loops.drawGlyphListLCDLoop == null ||
|
|
// compositeState > COMP_ISCOPY ||
|
|
// paintState > PAINT_ALPHACOLOR
|
|
) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_ON;
|
|
} else {
|
|
info.lcdRGBOrder = true;
|
|
/* Collapse these into just HRGB or VRGB.
|
|
* Pipe selection code needs only to test for these two.
|
|
* Since these both select the same pipe anyway its
|
|
* tempting to collapse into one value. But they are
|
|
* different strikes (glyph caches) so the distinction
|
|
* needs to be made for that purpose.
|
|
*/
|
|
if (aahint == SunHints.INTVAL_TEXT_ANTIALIAS_LCD_HBGR) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_LCD_HRGB;
|
|
info.lcdRGBOrder = false;
|
|
} else if
|
|
(aahint == SunHints.INTVAL_TEXT_ANTIALIAS_LCD_VBGR) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_LCD_VRGB;
|
|
info.lcdRGBOrder = false;
|
|
}
|
|
/* Support subpixel positioning only for the case in
|
|
* which the horizontal resolution is increased
|
|
*/
|
|
info.lcdSubPixPos =
|
|
fmhint == SunHints.INTVAL_FRACTIONALMETRICS_ON &&
|
|
aahint == SunHints.INTVAL_TEXT_ANTIALIAS_LCD_HRGB;
|
|
}
|
|
}
|
|
}
|
|
if (FontUtilities.isMacOSX14 &&
|
|
(aahint == SunHints.INTVAL_TEXT_ANTIALIAS_OFF))
|
|
{
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_ON;
|
|
}
|
|
info.aaHint = aahint;
|
|
info.fontStrike = info.font2D.getStrike(font, devAt, textAt,
|
|
aahint, fmhint);
|
|
return info;
|
|
}
|
|
|
|
public static boolean isRotated(double [] mtx) {
|
|
if ((mtx[0] == mtx[3]) &&
|
|
(mtx[1] == 0.0) &&
|
|
(mtx[2] == 0.0) &&
|
|
(mtx[0] > 0.0))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void setFont(Font font) {
|
|
/* replacing the reference equality test font != this.font with
|
|
* !font.equals(this.font) did not yield any measurable difference
|
|
* in testing, but there may be yet to be identified cases where it
|
|
* is beneficial.
|
|
*/
|
|
if (font != null && font!=this.font/*!font.equals(this.font)*/) {
|
|
/* In the GASP AA case the textpipe depends on the glyph size
|
|
* as determined by graphics and font transforms as well as the
|
|
* font size, and information in the font. But we may invalidate
|
|
* the pipe only to find that it made no difference.
|
|
* Deferring pipe invalidation to checkFontInfo won't work because
|
|
* when called we may already be rendering to the wrong pipe.
|
|
* So, if the font is transformed, or the graphics has more than
|
|
* a simple scale, we'll take that as enough of a hint to
|
|
* revalidate everything. But if they aren't we will
|
|
* use the font's point size to query the gasp table and see if
|
|
* what it says matches what's currently being used, in which
|
|
* case there's no need to invalidate the textpipe.
|
|
* This should be sufficient for all typical uses cases.
|
|
*/
|
|
if (textAntialiasHint == SunHints.INTVAL_TEXT_ANTIALIAS_GASP &&
|
|
textpipe != invalidpipe &&
|
|
(transformState > TRANSFORM_ANY_TRANSLATE ||
|
|
font.isTransformed() ||
|
|
fontInfo == null || // Precaution, if true shouldn't get here
|
|
(fontInfo.aaHint == SunHints.INTVAL_TEXT_ANTIALIAS_ON) !=
|
|
FontUtilities.getFont2D(font).
|
|
useAAForPtSize(font.getSize()))) {
|
|
textpipe = invalidpipe;
|
|
}
|
|
this.font = font;
|
|
this.fontMetrics = null;
|
|
this.validFontInfo = false;
|
|
}
|
|
}
|
|
|
|
public FontInfo getFontInfo() {
|
|
if (!validFontInfo) {
|
|
this.fontInfo = checkFontInfo(this.fontInfo, font, null);
|
|
validFontInfo = true;
|
|
}
|
|
return this.fontInfo;
|
|
}
|
|
|
|
/* Used by drawGlyphVector which specifies its own font. */
|
|
public FontInfo getGVFontInfo(Font font, FontRenderContext frc) {
|
|
if (glyphVectorFontInfo != null &&
|
|
glyphVectorFontInfo.font == font &&
|
|
glyphVectorFRC == frc) {
|
|
return glyphVectorFontInfo;
|
|
} else {
|
|
glyphVectorFRC = frc;
|
|
return glyphVectorFontInfo =
|
|
checkFontInfo(glyphVectorFontInfo, font, frc);
|
|
}
|
|
}
|
|
|
|
public FontMetrics getFontMetrics() {
|
|
if (this.fontMetrics != null) {
|
|
return this.fontMetrics;
|
|
}
|
|
/* NB the constructor and the setter disallow "font" being null */
|
|
return this.fontMetrics =
|
|
FontDesignMetrics.getMetrics(font, getFontRenderContext());
|
|
}
|
|
|
|
public FontMetrics getFontMetrics(Font font) {
|
|
if ((this.fontMetrics != null) && (font == this.font)) {
|
|
return this.fontMetrics;
|
|
}
|
|
FontMetrics fm =
|
|
FontDesignMetrics.getMetrics(font, getFontRenderContext());
|
|
|
|
if (this.font == font) {
|
|
this.fontMetrics = fm;
|
|
}
|
|
return fm;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a Path intersects the specified Rectangle in device
|
|
* space. The rendering attributes taken into account include the
|
|
* clip, transform, and stroke attributes.
|
|
* @param rect The area in device space to check for a hit.
|
|
* @param p The path to check for a hit.
|
|
* @param onStroke Flag to choose between testing the stroked or
|
|
* the filled path.
|
|
* @return True if there is a hit, false otherwise.
|
|
* @see #setStroke
|
|
* @see #fillPath
|
|
* @see #drawPath
|
|
* @see #transform
|
|
* @see #setTransform
|
|
* @see #clip
|
|
* @see #setClip
|
|
*/
|
|
public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
|
|
if (onStroke) {
|
|
s = stroke.createStrokedShape(s);
|
|
}
|
|
|
|
s = transformShape(s);
|
|
if ((constrainX|constrainY) != 0) {
|
|
rect = new Rectangle(rect);
|
|
rect.translate(constrainX, constrainY);
|
|
}
|
|
|
|
return s.intersects(rect);
|
|
}
|
|
|
|
/**
|
|
* Return the ColorModel associated with this Graphics2D.
|
|
*/
|
|
public ColorModel getDeviceColorModel() {
|
|
return surfaceData.getColorModel();
|
|
}
|
|
|
|
/**
|
|
* Return the device configuration associated with this Graphics2D.
|
|
*/
|
|
public GraphicsConfiguration getDeviceConfiguration() {
|
|
return surfaceData.getDeviceConfiguration();
|
|
}
|
|
|
|
/**
|
|
* Return the SurfaceData object assigned to manage the destination
|
|
* drawable surface of this Graphics2D.
|
|
*/
|
|
public final SurfaceData getSurfaceData() {
|
|
return surfaceData;
|
|
}
|
|
|
|
/**
|
|
* Sets the Composite in the current graphics state. Composite is used
|
|
* in all drawing methods such as drawImage, drawString, drawPath,
|
|
* and fillPath. It specifies how new pixels are to be combined with
|
|
* the existing pixels on the graphics device in the rendering process.
|
|
* @param comp The Composite object to be used for drawing.
|
|
* @see java.awt.Graphics#setXORMode
|
|
* @see java.awt.Graphics#setPaintMode
|
|
* @see AlphaComposite
|
|
*/
|
|
public void setComposite(Composite comp) {
|
|
if (composite == comp) {
|
|
return;
|
|
}
|
|
int newCompState;
|
|
CompositeType newCompType;
|
|
if (comp instanceof AlphaComposite) {
|
|
AlphaComposite alphacomp = (AlphaComposite) comp;
|
|
newCompType = CompositeType.forAlphaComposite(alphacomp);
|
|
if (newCompType == CompositeType.SrcOverNoEa) {
|
|
if (paintState == PAINT_OPAQUECOLOR ||
|
|
(paintState > PAINT_ALPHACOLOR &&
|
|
paint.getTransparency() == Transparency.OPAQUE))
|
|
{
|
|
newCompState = COMP_ISCOPY;
|
|
} else {
|
|
newCompState = COMP_ALPHA;
|
|
}
|
|
} else if (newCompType == CompositeType.SrcNoEa ||
|
|
newCompType == CompositeType.Src ||
|
|
newCompType == CompositeType.Clear)
|
|
{
|
|
newCompState = COMP_ISCOPY;
|
|
} else if (surfaceData.getTransparency() == Transparency.OPAQUE &&
|
|
newCompType == CompositeType.SrcIn)
|
|
{
|
|
newCompState = COMP_ISCOPY;
|
|
} else {
|
|
newCompState = COMP_ALPHA;
|
|
}
|
|
} else if (comp instanceof XORComposite) {
|
|
newCompState = COMP_XOR;
|
|
newCompType = CompositeType.Xor;
|
|
} else if (comp == null) {
|
|
throw new IllegalArgumentException("null Composite");
|
|
} else {
|
|
surfaceData.checkCustomComposite();
|
|
newCompState = COMP_CUSTOM;
|
|
newCompType = CompositeType.General;
|
|
}
|
|
if (compositeState != newCompState ||
|
|
imageComp != newCompType)
|
|
{
|
|
compositeState = newCompState;
|
|
imageComp = newCompType;
|
|
invalidatePipe();
|
|
validFontInfo = false;
|
|
}
|
|
composite = comp;
|
|
if (paintState <= PAINT_ALPHACOLOR) {
|
|
validateColor();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Paint in the current graphics state.
|
|
* @param paint The Paint object to be used to generate color in
|
|
* the rendering process.
|
|
* @see java.awt.Graphics#setColor
|
|
* @see GradientPaint
|
|
* @see TexturePaint
|
|
*/
|
|
public void setPaint(Paint paint) {
|
|
if (paint instanceof Color) {
|
|
setColor((Color) paint);
|
|
return;
|
|
}
|
|
if (paint == null || this.paint == paint) {
|
|
return;
|
|
}
|
|
this.paint = paint;
|
|
if (imageComp == CompositeType.SrcOverNoEa) {
|
|
// special case where compState depends on opacity of paint
|
|
if (paint.getTransparency() == Transparency.OPAQUE) {
|
|
if (compositeState != COMP_ISCOPY) {
|
|
compositeState = COMP_ISCOPY;
|
|
}
|
|
} else {
|
|
if (compositeState == COMP_ISCOPY) {
|
|
compositeState = COMP_ALPHA;
|
|
}
|
|
}
|
|
}
|
|
Class<? extends Paint> paintClass = paint.getClass();
|
|
if (paintClass == GradientPaint.class) {
|
|
paintState = PAINT_GRADIENT;
|
|
} else if (paintClass == LinearGradientPaint.class) {
|
|
paintState = PAINT_LIN_GRADIENT;
|
|
} else if (paintClass == RadialGradientPaint.class) {
|
|
paintState = PAINT_RAD_GRADIENT;
|
|
} else if (paintClass == TexturePaint.class) {
|
|
paintState = PAINT_TEXTURE;
|
|
} else {
|
|
paintState = PAINT_CUSTOM;
|
|
}
|
|
validFontInfo = false;
|
|
invalidatePipe();
|
|
}
|
|
|
|
static final int NON_UNIFORM_SCALE_MASK =
|
|
(AffineTransform.TYPE_GENERAL_TRANSFORM |
|
|
AffineTransform.TYPE_GENERAL_SCALE);
|
|
public static final double MinPenSizeAA =
|
|
sun.java2d.pipe.RenderingEngine.getInstance().getMinimumAAPenSize();
|
|
public static final double MinPenSizeAASquared =
|
|
(MinPenSizeAA * MinPenSizeAA);
|
|
// Since inaccuracies in the trig package can cause us to
|
|
// calculated a rotated pen width of just slightly greater
|
|
// than 1.0, we add a fudge factor to our comparison value
|
|
// here so that we do not misclassify single width lines as
|
|
// wide lines under certain rotations.
|
|
public static final double MinPenSizeSquared = 1.000000001;
|
|
|
|
private void validateBasicStroke(BasicStroke bs) {
|
|
boolean aa = (antialiasHint == SunHints.INTVAL_ANTIALIAS_ON);
|
|
if (transformState < TRANSFORM_TRANSLATESCALE) {
|
|
if (aa) {
|
|
if (bs.getLineWidth() <= MinPenSizeAA) {
|
|
if (bs.getDashArray() == null) {
|
|
strokeState = STROKE_THIN;
|
|
} else {
|
|
strokeState = STROKE_THINDASHED;
|
|
}
|
|
} else {
|
|
strokeState = STROKE_WIDE;
|
|
}
|
|
} else {
|
|
if (bs == defaultStroke) {
|
|
strokeState = STROKE_THIN;
|
|
} else if (bs.getLineWidth() <= 1.0f) {
|
|
if (bs.getDashArray() == null) {
|
|
strokeState = STROKE_THIN;
|
|
} else {
|
|
strokeState = STROKE_THINDASHED;
|
|
}
|
|
} else {
|
|
strokeState = STROKE_WIDE;
|
|
}
|
|
}
|
|
} else {
|
|
double widthsquared;
|
|
if ((transform.getType() & NON_UNIFORM_SCALE_MASK) == 0) {
|
|
/* sqrt omitted, compare to squared limits below. */
|
|
widthsquared = Math.abs(transform.getDeterminant());
|
|
} else {
|
|
/* First calculate the "maximum scale" of this transform. */
|
|
double A = transform.getScaleX(); // m00
|
|
double C = transform.getShearX(); // m01
|
|
double B = transform.getShearY(); // m10
|
|
double D = transform.getScaleY(); // m11
|
|
|
|
/*
|
|
* Given a 2 x 2 affine matrix [ A B ] such that
|
|
* [ C D ]
|
|
* v' = [x' y'] = [Ax + Cy, Bx + Dy], we want to
|
|
* find the maximum magnitude (norm) of the vector v'
|
|
* with the constraint (x^2 + y^2 = 1).
|
|
* The equation to maximize is
|
|
* |v'| = sqrt((Ax+Cy)^2+(Bx+Dy)^2)
|
|
* or |v'| = sqrt((AA+BB)x^2 + 2(AC+BD)xy + (CC+DD)y^2).
|
|
* Since sqrt is monotonic we can maximize |v'|^2
|
|
* instead and plug in the substitution y = sqrt(1 - x^2).
|
|
* Trigonometric equalities can then be used to get
|
|
* rid of most of the sqrt terms.
|
|
*/
|
|
double EA = A*A + B*B; // x^2 coefficient
|
|
double EB = 2*(A*C + B*D); // xy coefficient
|
|
double EC = C*C + D*D; // y^2 coefficient
|
|
|
|
/*
|
|
* There is a lot of calculus omitted here.
|
|
*
|
|
* Conceptually, in the interests of understanding the
|
|
* terms that the calculus produced we can consider
|
|
* that EA and EC end up providing the lengths along
|
|
* the major axes and the hypot term ends up being an
|
|
* adjustment for the additional length along the off-axis
|
|
* angle of rotated or sheared ellipses as well as an
|
|
* adjustment for the fact that the equation below
|
|
* averages the two major axis lengths. (Notice that
|
|
* the hypot term contains a part which resolves to the
|
|
* difference of these two axis lengths in the absence
|
|
* of rotation.)
|
|
*
|
|
* In the calculus, the ratio of the EB and (EA-EC) terms
|
|
* ends up being the tangent of 2*theta where theta is
|
|
* the angle that the long axis of the ellipse makes
|
|
* with the horizontal axis. Thus, this equation is
|
|
* calculating the length of the hypotenuse of a triangle
|
|
* along that axis.
|
|
*/
|
|
double hypot = Math.sqrt(EB*EB + (EA-EC)*(EA-EC));
|
|
|
|
/* sqrt omitted, compare to squared limits below. */
|
|
widthsquared = ((EA + EC + hypot)/2.0);
|
|
}
|
|
if (bs != defaultStroke) {
|
|
widthsquared *= bs.getLineWidth() * bs.getLineWidth();
|
|
}
|
|
if (widthsquared <=
|
|
(aa ? MinPenSizeAASquared : MinPenSizeSquared))
|
|
{
|
|
if (bs.getDashArray() == null) {
|
|
strokeState = STROKE_THIN;
|
|
} else {
|
|
strokeState = STROKE_THINDASHED;
|
|
}
|
|
} else {
|
|
strokeState = STROKE_WIDE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Sets the Stroke in the current graphics state.
|
|
* @param s The Stroke object to be used to stroke a Path in
|
|
* the rendering process.
|
|
* @see BasicStroke
|
|
*/
|
|
public void setStroke(Stroke s) {
|
|
if (s == null) {
|
|
throw new IllegalArgumentException("null Stroke");
|
|
}
|
|
int saveStrokeState = strokeState;
|
|
stroke = s;
|
|
if (s instanceof BasicStroke) {
|
|
validateBasicStroke((BasicStroke) s);
|
|
} else {
|
|
strokeState = STROKE_CUSTOM;
|
|
}
|
|
if (strokeState != saveStrokeState) {
|
|
invalidatePipe();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the preferences for the rendering algorithms.
|
|
* Hint categories include controls for rendering quality and
|
|
* overall time/quality trade-off in the rendering process.
|
|
* @param hintKey The key of hint to be set. The strings are
|
|
* defined in the RenderingHints class.
|
|
* @param hintValue The value indicating preferences for the specified
|
|
* hint category. These strings are defined in the RenderingHints
|
|
* class.
|
|
* @see RenderingHints
|
|
*/
|
|
public void setRenderingHint(Key hintKey, Object hintValue) {
|
|
// If we recognize the key, we must recognize the value
|
|
// otherwise throw an IllegalArgumentException
|
|
// and do not change the Hints object
|
|
// If we do not recognize the key, just pass it through
|
|
// to the Hints object untouched
|
|
if (!hintKey.isCompatibleValue(hintValue)) {
|
|
throw new IllegalArgumentException
|
|
(hintValue+" is not compatible with "+hintKey);
|
|
}
|
|
if (hintKey instanceof SunHints.Key) {
|
|
boolean stateChanged;
|
|
boolean textStateChanged = false;
|
|
boolean recognized = true;
|
|
SunHints.Key sunKey = (SunHints.Key) hintKey;
|
|
int newHint;
|
|
if (sunKey == SunHints.KEY_TEXT_ANTIALIAS_LCD_CONTRAST) {
|
|
newHint = ((Integer)hintValue).intValue();
|
|
} else {
|
|
newHint = ((SunHints.Value) hintValue).getIndex();
|
|
}
|
|
switch (sunKey.getIndex()) {
|
|
case SunHints.INTKEY_RENDERING:
|
|
stateChanged = (renderHint != newHint);
|
|
if (stateChanged) {
|
|
renderHint = newHint;
|
|
if (interpolationHint == -1) {
|
|
interpolationType =
|
|
(newHint == SunHints.INTVAL_RENDER_QUALITY
|
|
? AffineTransformOp.TYPE_BILINEAR
|
|
: AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
|
|
}
|
|
}
|
|
break;
|
|
case SunHints.INTKEY_ANTIALIASING:
|
|
stateChanged = (antialiasHint != newHint);
|
|
antialiasHint = newHint;
|
|
if (stateChanged) {
|
|
textStateChanged =
|
|
(textAntialiasHint ==
|
|
SunHints.INTVAL_TEXT_ANTIALIAS_DEFAULT);
|
|
if (strokeState != STROKE_CUSTOM) {
|
|
validateBasicStroke((BasicStroke) stroke);
|
|
}
|
|
}
|
|
break;
|
|
case SunHints.INTKEY_TEXT_ANTIALIASING:
|
|
stateChanged = (textAntialiasHint != newHint);
|
|
textStateChanged = stateChanged;
|
|
textAntialiasHint = newHint;
|
|
break;
|
|
case SunHints.INTKEY_FRACTIONALMETRICS:
|
|
stateChanged = (fractionalMetricsHint != newHint);
|
|
textStateChanged = stateChanged;
|
|
fractionalMetricsHint = newHint;
|
|
break;
|
|
case SunHints.INTKEY_AATEXT_LCD_CONTRAST:
|
|
stateChanged = false;
|
|
/* Already have validated it is an int 100 <= newHint <= 250 */
|
|
lcdTextContrast = newHint;
|
|
break;
|
|
case SunHints.INTKEY_INTERPOLATION:
|
|
interpolationHint = newHint;
|
|
switch (newHint) {
|
|
case SunHints.INTVAL_INTERPOLATION_BICUBIC:
|
|
newHint = AffineTransformOp.TYPE_BICUBIC;
|
|
break;
|
|
case SunHints.INTVAL_INTERPOLATION_BILINEAR:
|
|
newHint = AffineTransformOp.TYPE_BILINEAR;
|
|
break;
|
|
default:
|
|
case SunHints.INTVAL_INTERPOLATION_NEAREST_NEIGHBOR:
|
|
newHint = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
|
|
break;
|
|
}
|
|
stateChanged = (interpolationType != newHint);
|
|
interpolationType = newHint;
|
|
break;
|
|
case SunHints.INTKEY_STROKE_CONTROL:
|
|
stateChanged = (strokeHint != newHint);
|
|
strokeHint = newHint;
|
|
break;
|
|
case SunHints.INTKEY_RESOLUTION_VARIANT:
|
|
stateChanged = (resolutionVariantHint != newHint);
|
|
resolutionVariantHint = newHint;
|
|
break;
|
|
default:
|
|
recognized = false;
|
|
stateChanged = false;
|
|
break;
|
|
}
|
|
if (recognized) {
|
|
if (stateChanged) {
|
|
invalidatePipe();
|
|
if (textStateChanged) {
|
|
fontMetrics = null;
|
|
this.cachedFRC = null;
|
|
validFontInfo = false;
|
|
this.glyphVectorFontInfo = null;
|
|
}
|
|
}
|
|
if (hints != null) {
|
|
hints.put(hintKey, hintValue);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// Nothing we recognize so none of "our state" has changed
|
|
if (hints == null) {
|
|
hints = makeHints(null);
|
|
}
|
|
hints.put(hintKey, hintValue);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the preferences for the rendering algorithms.
|
|
* @param hintCategory The category of hint to be set. The strings
|
|
* are defined in the RenderingHints class.
|
|
* @return The preferences for rendering algorithms. The strings
|
|
* are defined in the RenderingHints class.
|
|
* @see RenderingHints
|
|
*/
|
|
public Object getRenderingHint(Key hintKey) {
|
|
if (hints != null) {
|
|
return hints.get(hintKey);
|
|
}
|
|
if (!(hintKey instanceof SunHints.Key)) {
|
|
return null;
|
|
}
|
|
int keyindex = ((SunHints.Key)hintKey).getIndex();
|
|
switch (keyindex) {
|
|
case SunHints.INTKEY_RENDERING:
|
|
return SunHints.Value.get(SunHints.INTKEY_RENDERING,
|
|
renderHint);
|
|
case SunHints.INTKEY_ANTIALIASING:
|
|
return SunHints.Value.get(SunHints.INTKEY_ANTIALIASING,
|
|
antialiasHint);
|
|
case SunHints.INTKEY_TEXT_ANTIALIASING:
|
|
return SunHints.Value.get(SunHints.INTKEY_TEXT_ANTIALIASING,
|
|
textAntialiasHint);
|
|
case SunHints.INTKEY_FRACTIONALMETRICS:
|
|
return SunHints.Value.get(SunHints.INTKEY_FRACTIONALMETRICS,
|
|
fractionalMetricsHint);
|
|
case SunHints.INTKEY_AATEXT_LCD_CONTRAST:
|
|
return new Integer(lcdTextContrast);
|
|
case SunHints.INTKEY_INTERPOLATION:
|
|
switch (interpolationHint) {
|
|
case SunHints.INTVAL_INTERPOLATION_NEAREST_NEIGHBOR:
|
|
return SunHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
|
|
case SunHints.INTVAL_INTERPOLATION_BILINEAR:
|
|
return SunHints.VALUE_INTERPOLATION_BILINEAR;
|
|
case SunHints.INTVAL_INTERPOLATION_BICUBIC:
|
|
return SunHints.VALUE_INTERPOLATION_BICUBIC;
|
|
}
|
|
return null;
|
|
case SunHints.INTKEY_STROKE_CONTROL:
|
|
return SunHints.Value.get(SunHints.INTKEY_STROKE_CONTROL,
|
|
strokeHint);
|
|
case SunHints.INTKEY_RESOLUTION_VARIANT:
|
|
return SunHints.Value.get(SunHints.INTKEY_RESOLUTION_VARIANT,
|
|
resolutionVariantHint);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets the preferences for the rendering algorithms.
|
|
* Hint categories include controls for rendering quality and
|
|
* overall time/quality trade-off in the rendering process.
|
|
* @param hints The rendering hints to be set
|
|
* @see RenderingHints
|
|
*/
|
|
public void setRenderingHints(Map<?,?> hints) {
|
|
this.hints = null;
|
|
renderHint = SunHints.INTVAL_RENDER_DEFAULT;
|
|
antialiasHint = SunHints.INTVAL_ANTIALIAS_OFF;
|
|
textAntialiasHint = SunHints.INTVAL_TEXT_ANTIALIAS_DEFAULT;
|
|
fractionalMetricsHint = SunHints.INTVAL_FRACTIONALMETRICS_OFF;
|
|
lcdTextContrast = lcdTextContrastDefaultValue;
|
|
interpolationHint = -1;
|
|
interpolationType = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
|
|
boolean customHintPresent = false;
|
|
Iterator<?> iter = hints.keySet().iterator();
|
|
while (iter.hasNext()) {
|
|
Object key = iter.next();
|
|
if (key == SunHints.KEY_RENDERING ||
|
|
key == SunHints.KEY_ANTIALIASING ||
|
|
key == SunHints.KEY_TEXT_ANTIALIASING ||
|
|
key == SunHints.KEY_FRACTIONALMETRICS ||
|
|
key == SunHints.KEY_TEXT_ANTIALIAS_LCD_CONTRAST ||
|
|
key == SunHints.KEY_STROKE_CONTROL ||
|
|
key == SunHints.KEY_INTERPOLATION)
|
|
{
|
|
setRenderingHint((Key) key, hints.get(key));
|
|
} else {
|
|
customHintPresent = true;
|
|
}
|
|
}
|
|
if (customHintPresent) {
|
|
this.hints = makeHints(hints);
|
|
}
|
|
invalidatePipe();
|
|
}
|
|
|
|
/**
|
|
* Adds a number of preferences for the rendering algorithms.
|
|
* Hint categories include controls for rendering quality and
|
|
* overall time/quality trade-off in the rendering process.
|
|
* @param hints The rendering hints to be set
|
|
* @see RenderingHints
|
|
*/
|
|
public void addRenderingHints(Map<?,?> hints) {
|
|
boolean customHintPresent = false;
|
|
Iterator<?> iter = hints.keySet().iterator();
|
|
while (iter.hasNext()) {
|
|
Object key = iter.next();
|
|
if (key == SunHints.KEY_RENDERING ||
|
|
key == SunHints.KEY_ANTIALIASING ||
|
|
key == SunHints.KEY_TEXT_ANTIALIASING ||
|
|
key == SunHints.KEY_FRACTIONALMETRICS ||
|
|
key == SunHints.KEY_TEXT_ANTIALIAS_LCD_CONTRAST ||
|
|
key == SunHints.KEY_STROKE_CONTROL ||
|
|
key == SunHints.KEY_INTERPOLATION)
|
|
{
|
|
setRenderingHint((Key) key, hints.get(key));
|
|
} else {
|
|
customHintPresent = true;
|
|
}
|
|
}
|
|
if (customHintPresent) {
|
|
if (this.hints == null) {
|
|
this.hints = makeHints(hints);
|
|
} else {
|
|
this.hints.putAll(hints);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the preferences for the rendering algorithms.
|
|
* Hint categories include controls for rendering quality and
|
|
* overall time/quality trade-off in the rendering process.
|
|
* @see RenderingHints
|
|
*/
|
|
public RenderingHints getRenderingHints() {
|
|
if (hints == null) {
|
|
return makeHints(null);
|
|
} else {
|
|
return (RenderingHints) hints.clone();
|
|
}
|
|
}
|
|
|
|
RenderingHints makeHints(Map hints) {
|
|
RenderingHints model = new RenderingHints(hints);
|
|
model.put(SunHints.KEY_RENDERING,
|
|
SunHints.Value.get(SunHints.INTKEY_RENDERING,
|
|
renderHint));
|
|
model.put(SunHints.KEY_ANTIALIASING,
|
|
SunHints.Value.get(SunHints.INTKEY_ANTIALIASING,
|
|
antialiasHint));
|
|
model.put(SunHints.KEY_TEXT_ANTIALIASING,
|
|
SunHints.Value.get(SunHints.INTKEY_TEXT_ANTIALIASING,
|
|
textAntialiasHint));
|
|
model.put(SunHints.KEY_FRACTIONALMETRICS,
|
|
SunHints.Value.get(SunHints.INTKEY_FRACTIONALMETRICS,
|
|
fractionalMetricsHint));
|
|
model.put(SunHints.KEY_TEXT_ANTIALIAS_LCD_CONTRAST,
|
|
Integer.valueOf(lcdTextContrast));
|
|
Object value;
|
|
switch (interpolationHint) {
|
|
case SunHints.INTVAL_INTERPOLATION_NEAREST_NEIGHBOR:
|
|
value = SunHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
|
|
break;
|
|
case SunHints.INTVAL_INTERPOLATION_BILINEAR:
|
|
value = SunHints.VALUE_INTERPOLATION_BILINEAR;
|
|
break;
|
|
case SunHints.INTVAL_INTERPOLATION_BICUBIC:
|
|
value = SunHints.VALUE_INTERPOLATION_BICUBIC;
|
|
break;
|
|
default:
|
|
value = null;
|
|
break;
|
|
}
|
|
if (value != null) {
|
|
model.put(SunHints.KEY_INTERPOLATION, value);
|
|
}
|
|
model.put(SunHints.KEY_STROKE_CONTROL,
|
|
SunHints.Value.get(SunHints.INTKEY_STROKE_CONTROL,
|
|
strokeHint));
|
|
return model;
|
|
}
|
|
|
|
/**
|
|
* Concatenates the current transform of this Graphics2D with a
|
|
* translation transformation.
|
|
* This is equivalent to calling transform(T), where T is an
|
|
* AffineTransform represented by the following matrix:
|
|
* <pre>
|
|
* [ 1 0 tx ]
|
|
* [ 0 1 ty ]
|
|
* [ 0 0 1 ]
|
|
* </pre>
|
|
*/
|
|
public void translate(double tx, double ty) {
|
|
transform.translate(tx, ty);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Concatenates the current transform of this Graphics2D with a
|
|
* rotation transformation.
|
|
* This is equivalent to calling transform(R), where R is an
|
|
* AffineTransform represented by the following matrix:
|
|
* <pre>
|
|
* [ cos(theta) -sin(theta) 0 ]
|
|
* [ sin(theta) cos(theta) 0 ]
|
|
* [ 0 0 1 ]
|
|
* </pre>
|
|
* Rotating with a positive angle theta rotates points on the positive
|
|
* x axis toward the positive y axis.
|
|
* @param theta The angle of rotation in radians.
|
|
*/
|
|
public void rotate(double theta) {
|
|
transform.rotate(theta);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Concatenates the current transform of this Graphics2D with a
|
|
* translated rotation transformation.
|
|
* This is equivalent to the following sequence of calls:
|
|
* <pre>
|
|
* translate(x, y);
|
|
* rotate(theta);
|
|
* translate(-x, -y);
|
|
* </pre>
|
|
* Rotating with a positive angle theta rotates points on the positive
|
|
* x axis toward the positive y axis.
|
|
* @param theta The angle of rotation in radians.
|
|
* @param x The x coordinate of the origin of the rotation
|
|
* @param y The x coordinate of the origin of the rotation
|
|
*/
|
|
public void rotate(double theta, double x, double y) {
|
|
transform.rotate(theta, x, y);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Concatenates the current transform of this Graphics2D with a
|
|
* scaling transformation.
|
|
* This is equivalent to calling transform(S), where S is an
|
|
* AffineTransform represented by the following matrix:
|
|
* <pre>
|
|
* [ sx 0 0 ]
|
|
* [ 0 sy 0 ]
|
|
* [ 0 0 1 ]
|
|
* </pre>
|
|
*/
|
|
public void scale(double sx, double sy) {
|
|
transform.scale(sx, sy);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Concatenates the current transform of this Graphics2D with a
|
|
* shearing transformation.
|
|
* This is equivalent to calling transform(SH), where SH is an
|
|
* AffineTransform represented by the following matrix:
|
|
* <pre>
|
|
* [ 1 shx 0 ]
|
|
* [ shy 1 0 ]
|
|
* [ 0 0 1 ]
|
|
* </pre>
|
|
* @param shx The factor by which coordinates are shifted towards the
|
|
* positive X axis direction according to their Y coordinate
|
|
* @param shy The factor by which coordinates are shifted towards the
|
|
* positive Y axis direction according to their X coordinate
|
|
*/
|
|
public void shear(double shx, double shy) {
|
|
transform.shear(shx, shy);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Composes a Transform object with the transform in this
|
|
* Graphics2D according to the rule last-specified-first-applied.
|
|
* If the currrent transform is Cx, the result of composition
|
|
* with Tx is a new transform Cx'. Cx' becomes the current
|
|
* transform for this Graphics2D.
|
|
* Transforming a point p by the updated transform Cx' is
|
|
* equivalent to first transforming p by Tx and then transforming
|
|
* the result by the original transform Cx. In other words,
|
|
* Cx'(p) = Cx(Tx(p)).
|
|
* A copy of the Tx is made, if necessary, so further
|
|
* modifications to Tx do not affect rendering.
|
|
* @param Tx The Transform object to be composed with the current
|
|
* transform.
|
|
* @see #setTransform
|
|
* @see AffineTransform
|
|
*/
|
|
public void transform(AffineTransform xform) {
|
|
this.transform.concatenate(xform);
|
|
invalidateTransform();
|
|
}
|
|
|
|
/**
|
|
* Translate
|
|
*/
|
|
public void translate(int x, int y) {
|
|
transform.translate(x, y);
|
|
if (transformState <= TRANSFORM_INT_TRANSLATE) {
|
|
transX += x;
|
|
transY += y;
|
|
transformState = (((transX | transY) == 0) ?
|
|
TRANSFORM_ISIDENT : TRANSFORM_INT_TRANSLATE);
|
|
} else {
|
|
invalidateTransform();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Transform in the current graphics state.
|
|
* @param Tx The Transform object to be used in the rendering process.
|
|
* @see #transform
|
|
* @see TransformChain
|
|
* @see AffineTransform
|
|
*/
|
|
@Override
|
|
public void setTransform(AffineTransform Tx) {
|
|
if ((constrainX | constrainY) == 0 && devScale == 1) {
|
|
transform.setTransform(Tx);
|
|
} else {
|
|
transform.setTransform(devScale, 0, 0, devScale, constrainX,
|
|
constrainY);
|
|
transform.concatenate(Tx);
|
|
}
|
|
invalidateTransform();
|
|
}
|
|
|
|
protected void invalidateTransform() {
|
|
int type = transform.getType();
|
|
int origTransformState = transformState;
|
|
if (type == AffineTransform.TYPE_IDENTITY) {
|
|
transformState = TRANSFORM_ISIDENT;
|
|
transX = transY = 0;
|
|
} else if (type == AffineTransform.TYPE_TRANSLATION) {
|
|
double dtx = transform.getTranslateX();
|
|
double dty = transform.getTranslateY();
|
|
transX = (int) Math.floor(dtx + 0.5);
|
|
transY = (int) Math.floor(dty + 0.5);
|
|
if (dtx == transX && dty == transY) {
|
|
transformState = TRANSFORM_INT_TRANSLATE;
|
|
} else {
|
|
transformState = TRANSFORM_ANY_TRANSLATE;
|
|
}
|
|
} else if ((type & (AffineTransform.TYPE_FLIP |
|
|
AffineTransform.TYPE_MASK_ROTATION |
|
|
AffineTransform.TYPE_GENERAL_TRANSFORM)) == 0)
|
|
{
|
|
transformState = TRANSFORM_TRANSLATESCALE;
|
|
transX = transY = 0;
|
|
} else {
|
|
transformState = TRANSFORM_GENERIC;
|
|
transX = transY = 0;
|
|
}
|
|
|
|
if (transformState >= TRANSFORM_TRANSLATESCALE ||
|
|
origTransformState >= TRANSFORM_TRANSLATESCALE)
|
|
{
|
|
/* Its only in this case that the previous or current transform
|
|
* was more than a translate that font info is invalidated
|
|
*/
|
|
cachedFRC = null;
|
|
this.validFontInfo = false;
|
|
this.fontMetrics = null;
|
|
this.glyphVectorFontInfo = null;
|
|
|
|
if (transformState != origTransformState) {
|
|
invalidatePipe();
|
|
}
|
|
}
|
|
if (strokeState != STROKE_CUSTOM) {
|
|
validateBasicStroke((BasicStroke) stroke);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current Transform in the Graphics2D state.
|
|
* @see #transform
|
|
* @see #setTransform
|
|
*/
|
|
@Override
|
|
public AffineTransform getTransform() {
|
|
if ((constrainX | constrainY) == 0 && devScale == 1) {
|
|
return new AffineTransform(transform);
|
|
}
|
|
final double invScale = 1.0 / devScale;
|
|
AffineTransform tx = new AffineTransform(invScale, 0, 0, invScale,
|
|
-constrainX * invScale,
|
|
-constrainY * invScale);
|
|
tx.concatenate(transform);
|
|
return tx;
|
|
}
|
|
|
|
/**
|
|
* Returns the current Transform ignoring the "constrain"
|
|
* rectangle.
|
|
*/
|
|
public AffineTransform cloneTransform() {
|
|
return new AffineTransform(transform);
|
|
}
|
|
|
|
/**
|
|
* Returns the current Paint in the Graphics2D state.
|
|
* @see #setPaint
|
|
* @see java.awt.Graphics#setColor
|
|
*/
|
|
public Paint getPaint() {
|
|
return paint;
|
|
}
|
|
|
|
/**
|
|
* Returns the current Composite in the Graphics2D state.
|
|
* @see #setComposite
|
|
*/
|
|
public Composite getComposite() {
|
|
return composite;
|
|
}
|
|
|
|
public Color getColor() {
|
|
return foregroundColor;
|
|
}
|
|
|
|
/*
|
|
* Validate the eargb and pixel fields against the current color.
|
|
*
|
|
* The eargb field must take into account the extraAlpha
|
|
* value of an AlphaComposite. It may also take into account
|
|
* the Fsrc Porter-Duff blending function if such a function is
|
|
* a constant (see handling of Clear mode below). For instance,
|
|
* by factoring in the (Fsrc == 0) state of the Clear mode we can
|
|
* use a SrcNoEa loop just as easily as a general Alpha loop
|
|
* since the math will be the same in both cases.
|
|
*
|
|
* The pixel field will always be the best pixel data choice for
|
|
* the final result of all calculations applied to the eargb field.
|
|
*
|
|
* Note that this method is only necessary under the following
|
|
* conditions:
|
|
* (paintState <= PAINT_ALPHA_COLOR &&
|
|
* compositeState <= COMP_CUSTOM)
|
|
* though nothing bad will happen if it is run in other states.
|
|
*/
|
|
final void validateColor() {
|
|
int eargb;
|
|
if (imageComp == CompositeType.Clear) {
|
|
eargb = 0;
|
|
} else {
|
|
eargb = foregroundColor.getRGB();
|
|
if (compositeState <= COMP_ALPHA &&
|
|
imageComp != CompositeType.SrcNoEa &&
|
|
imageComp != CompositeType.SrcOverNoEa)
|
|
{
|
|
AlphaComposite alphacomp = (AlphaComposite) composite;
|
|
int a = Math.round(alphacomp.getAlpha() * (eargb >>> 24));
|
|
eargb = (eargb & 0x00ffffff) | (a << 24);
|
|
}
|
|
}
|
|
this.eargb = eargb;
|
|
this.pixel = surfaceData.pixelFor(eargb);
|
|
}
|
|
|
|
public void setColor(Color color) {
|
|
if (color == null || color == paint) {
|
|
return;
|
|
}
|
|
this.paint = foregroundColor = color;
|
|
validateColor();
|
|
if ((eargb >> 24) == -1) {
|
|
if (paintState == PAINT_OPAQUECOLOR) {
|
|
return;
|
|
}
|
|
paintState = PAINT_OPAQUECOLOR;
|
|
if (imageComp == CompositeType.SrcOverNoEa) {
|
|
// special case where compState depends on opacity of paint
|
|
compositeState = COMP_ISCOPY;
|
|
}
|
|
} else {
|
|
if (paintState == PAINT_ALPHACOLOR) {
|
|
return;
|
|
}
|
|
paintState = PAINT_ALPHACOLOR;
|
|
if (imageComp == CompositeType.SrcOverNoEa) {
|
|
// special case where compState depends on opacity of paint
|
|
compositeState = COMP_ALPHA;
|
|
}
|
|
}
|
|
validFontInfo = false;
|
|
invalidatePipe();
|
|
}
|
|
|
|
/**
|
|
* Sets the background color in this context used for clearing a region.
|
|
* When Graphics2D is constructed for a component, the backgroung color is
|
|
* inherited from the component. Setting the background color in the
|
|
* Graphics2D context only affects the subsequent clearRect() calls and
|
|
* not the background color of the component. To change the background
|
|
* of the component, use appropriate methods of the component.
|
|
* @param color The background color that should be used in
|
|
* subsequent calls to clearRect().
|
|
* @see getBackground
|
|
* @see Graphics.clearRect()
|
|
*/
|
|
public void setBackground(Color color) {
|
|
backgroundColor = color;
|
|
}
|
|
|
|
/**
|
|
* Returns the background color used for clearing a region.
|
|
* @see setBackground
|
|
*/
|
|
public Color getBackground() {
|
|
return backgroundColor;
|
|
}
|
|
|
|
/**
|
|
* Returns the current Stroke in the Graphics2D state.
|
|
* @see setStroke
|
|
*/
|
|
public Stroke getStroke() {
|
|
return stroke;
|
|
}
|
|
|
|
public Rectangle getClipBounds() {
|
|
if (clipState == CLIP_DEVICE) {
|
|
return null;
|
|
}
|
|
return getClipBounds(new Rectangle());
|
|
}
|
|
|
|
public Rectangle getClipBounds(Rectangle r) {
|
|
if (clipState != CLIP_DEVICE) {
|
|
if (transformState <= TRANSFORM_INT_TRANSLATE) {
|
|
if (usrClip instanceof Rectangle) {
|
|
r.setBounds((Rectangle) usrClip);
|
|
} else {
|
|
r.setFrame(usrClip.getBounds2D());
|
|
}
|
|
r.translate(-transX, -transY);
|
|
} else {
|
|
r.setFrame(getClip().getBounds2D());
|
|
}
|
|
} else if (r == null) {
|
|
throw new NullPointerException("null rectangle parameter");
|
|
}
|
|
return r;
|
|
}
|
|
|
|
public boolean hitClip(int x, int y, int width, int height) {
|
|
if (width <= 0 || height <= 0) {
|
|
return false;
|
|
}
|
|
if (transformState > TRANSFORM_INT_TRANSLATE) {
|
|
// Note: Technically the most accurate test would be to
|
|
// raster scan the parallelogram of the transformed rectangle
|
|
// and do a span for span hit test against the clip, but for
|
|
// speed we approximate the test with a bounding box of the
|
|
// transformed rectangle. The cost of rasterizing the
|
|
// transformed rectangle is probably high enough that it is
|
|
// not worth doing so to save the caller from having to call
|
|
// a rendering method where we will end up discovering the
|
|
// same answer in about the same amount of time anyway.
|
|
// This logic breaks down if this hit test is being performed
|
|
// on the bounds of a group of shapes in which case it might
|
|
// be beneficial to be a little more accurate to avoid lots
|
|
// of subsequent rendering calls. In either case, this relaxed
|
|
// test should not be significantly less accurate than the
|
|
// optimal test for most transforms and so the conservative
|
|
// answer should not cause too much extra work.
|
|
|
|
double d[] = {
|
|
x, y,
|
|
x+width, y,
|
|
x, y+height,
|
|
x+width, y+height
|
|
};
|
|
transform.transform(d, 0, d, 0, 4);
|
|
x = (int) Math.floor(Math.min(Math.min(d[0], d[2]),
|
|
Math.min(d[4], d[6])));
|
|
y = (int) Math.floor(Math.min(Math.min(d[1], d[3]),
|
|
Math.min(d[5], d[7])));
|
|
width = (int) Math.ceil(Math.max(Math.max(d[0], d[2]),
|
|
Math.max(d[4], d[6])));
|
|
height = (int) Math.ceil(Math.max(Math.max(d[1], d[3]),
|
|
Math.max(d[5], d[7])));
|
|
} else {
|
|
x += transX;
|
|
y += transY;
|
|
width += x;
|
|
height += y;
|
|
}
|
|
|
|
try {
|
|
if (!getCompClip().intersectsQuickCheckXYXY(x, y, width, height)) {
|
|
return false;
|
|
}
|
|
} catch (InvalidPipeException e) {
|
|
return false;
|
|
}
|
|
// REMIND: We could go one step further here and examine the
|
|
// non-rectangular clip shape more closely if there is one.
|
|
// Since the clip has already been rasterized, the performance
|
|
// penalty of doing the scan is probably still within the bounds
|
|
// of a good tradeoff between speed and quality of the answer.
|
|
return true;
|
|
}
|
|
|
|
protected void validateCompClip() {
|
|
int origClipState = clipState;
|
|
if (usrClip == null) {
|
|
clipState = CLIP_DEVICE;
|
|
clipRegion = devClip;
|
|
} else if (usrClip instanceof Rectangle2D) {
|
|
clipState = CLIP_RECTANGULAR;
|
|
if (usrClip instanceof Rectangle) {
|
|
clipRegion = devClip.getIntersection((Rectangle)usrClip);
|
|
} else {
|
|
clipRegion = devClip.getIntersection(usrClip.getBounds());
|
|
}
|
|
} else {
|
|
PathIterator cpi = usrClip.getPathIterator(null);
|
|
int box[] = new int[4];
|
|
ShapeSpanIterator sr = LoopPipe.getFillSSI(this);
|
|
try {
|
|
sr.setOutputArea(devClip);
|
|
sr.appendPath(cpi);
|
|
sr.getPathBox(box);
|
|
Region r = Region.getInstance(box);
|
|
r.appendSpans(sr);
|
|
clipRegion = r;
|
|
clipState =
|
|
r.isRectangular() ? CLIP_RECTANGULAR : CLIP_SHAPE;
|
|
} finally {
|
|
sr.dispose();
|
|
}
|
|
}
|
|
if (origClipState != clipState &&
|
|
(clipState == CLIP_SHAPE || origClipState == CLIP_SHAPE))
|
|
{
|
|
validFontInfo = false;
|
|
invalidatePipe();
|
|
}
|
|
}
|
|
|
|
static final int NON_RECTILINEAR_TRANSFORM_MASK =
|
|
(AffineTransform.TYPE_GENERAL_TRANSFORM |
|
|
AffineTransform.TYPE_GENERAL_ROTATION);
|
|
|
|
protected Shape transformShape(Shape s) {
|
|
if (s == null) {
|
|
return null;
|
|
}
|
|
if (transformState > TRANSFORM_INT_TRANSLATE) {
|
|
return transformShape(transform, s);
|
|
} else {
|
|
return transformShape(transX, transY, s);
|
|
}
|
|
}
|
|
|
|
public Shape untransformShape(Shape s) {
|
|
if (s == null) {
|
|
return null;
|
|
}
|
|
if (transformState > TRANSFORM_INT_TRANSLATE) {
|
|
try {
|
|
return transformShape(transform.createInverse(), s);
|
|
} catch (NoninvertibleTransformException e) {
|
|
return null;
|
|
}
|
|
} else {
|
|
return transformShape(-transX, -transY, s);
|
|
}
|
|
}
|
|
|
|
protected static Shape transformShape(int tx, int ty, Shape s) {
|
|
if (s == null) {
|
|
return null;
|
|
}
|
|
|
|
if (s instanceof Rectangle) {
|
|
Rectangle r = s.getBounds();
|
|
r.translate(tx, ty);
|
|
return r;
|
|
}
|
|
if (s instanceof Rectangle2D) {
|
|
Rectangle2D rect = (Rectangle2D) s;
|
|
return new Rectangle2D.Double(rect.getX() + tx,
|
|
rect.getY() + ty,
|
|
rect.getWidth(),
|
|
rect.getHeight());
|
|
}
|
|
|
|
if (tx == 0 && ty == 0) {
|
|
return cloneShape(s);
|
|
}
|
|
|
|
AffineTransform mat = AffineTransform.getTranslateInstance(tx, ty);
|
|
return mat.createTransformedShape(s);
|
|
}
|
|
|
|
protected static Shape transformShape(AffineTransform tx, Shape clip) {
|
|
if (clip == null) {
|
|
return null;
|
|
}
|
|
|
|
if (clip instanceof Rectangle2D &&
|
|
(tx.getType() & NON_RECTILINEAR_TRANSFORM_MASK) == 0)
|
|
{
|
|
Rectangle2D rect = (Rectangle2D) clip;
|
|
double matrix[] = new double[4];
|
|
matrix[0] = rect.getX();
|
|
matrix[1] = rect.getY();
|
|
matrix[2] = matrix[0] + rect.getWidth();
|
|
matrix[3] = matrix[1] + rect.getHeight();
|
|
tx.transform(matrix, 0, matrix, 0, 2);
|
|
fixRectangleOrientation(matrix, rect);
|
|
return new Rectangle2D.Double(matrix[0], matrix[1],
|
|
matrix[2] - matrix[0],
|
|
matrix[3] - matrix[1]);
|
|
}
|
|
|
|
if (tx.isIdentity()) {
|
|
return cloneShape(clip);
|
|
}
|
|
|
|
return tx.createTransformedShape(clip);
|
|
}
|
|
|
|
/**
|
|
* Sets orientation of the rectangle according to the clip.
|
|
*/
|
|
private static void fixRectangleOrientation(double[] m, Rectangle2D clip) {
|
|
if (clip.getWidth() > 0 != (m[2] - m[0] > 0)) {
|
|
double t = m[0];
|
|
m[0] = m[2];
|
|
m[2] = t;
|
|
}
|
|
if (clip.getHeight() > 0 != (m[3] - m[1] > 0)) {
|
|
double t = m[1];
|
|
m[1] = m[3];
|
|
m[3] = t;
|
|
}
|
|
}
|
|
|
|
public void clipRect(int x, int y, int w, int h) {
|
|
clip(new Rectangle(x, y, w, h));
|
|
}
|
|
|
|
public void setClip(int x, int y, int w, int h) {
|
|
setClip(new Rectangle(x, y, w, h));
|
|
}
|
|
|
|
public Shape getClip() {
|
|
return untransformShape(usrClip);
|
|
}
|
|
|
|
public void setClip(Shape sh) {
|
|
usrClip = transformShape(sh);
|
|
validateCompClip();
|
|
}
|
|
|
|
/**
|
|
* Intersects the current clip with the specified Path and sets the
|
|
* current clip to the resulting intersection. The clip is transformed
|
|
* with the current transform in the Graphics2D state before being
|
|
* intersected with the current clip. This method is used to make the
|
|
* current clip smaller. To make the clip larger, use any setClip method.
|
|
* @param p The Path to be intersected with the current clip.
|
|
*/
|
|
public void clip(Shape s) {
|
|
s = transformShape(s);
|
|
if (usrClip != null) {
|
|
s = intersectShapes(usrClip, s, true, true);
|
|
}
|
|
usrClip = s;
|
|
validateCompClip();
|
|
}
|
|
|
|
public void setPaintMode() {
|
|
setComposite(AlphaComposite.SrcOver);
|
|
}
|
|
|
|
public void setXORMode(Color c) {
|
|
if (c == null) {
|
|
throw new IllegalArgumentException("null XORColor");
|
|
}
|
|
setComposite(new XORComposite(c, surfaceData));
|
|
}
|
|
|
|
Blit lastCAblit;
|
|
Composite lastCAcomp;
|
|
|
|
public void copyArea(int x, int y, int w, int h, int dx, int dy) {
|
|
try {
|
|
doCopyArea(x, y, w, h, dx, dy);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
doCopyArea(x, y, w, h, dx, dy);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
private void doCopyArea(int x, int y, int w, int h, int dx, int dy) {
|
|
if (w <= 0 || h <= 0) {
|
|
return;
|
|
}
|
|
SurfaceData theData = surfaceData;
|
|
if (theData.copyArea(this, x, y, w, h, dx, dy)) {
|
|
return;
|
|
}
|
|
if (transformState > TRANSFORM_TRANSLATESCALE) {
|
|
throw new InternalError("transformed copyArea not implemented yet");
|
|
}
|
|
// REMIND: This method does not deal with missing data from the
|
|
// source object (i.e. it does not send exposure events...)
|
|
|
|
Region clip = getCompClip();
|
|
|
|
Composite comp = composite;
|
|
if (lastCAcomp != comp) {
|
|
SurfaceType dsttype = theData.getSurfaceType();
|
|
CompositeType comptype = imageComp;
|
|
if (CompositeType.SrcOverNoEa.equals(comptype) &&
|
|
theData.getTransparency() == Transparency.OPAQUE)
|
|
{
|
|
comptype = CompositeType.SrcNoEa;
|
|
}
|
|
lastCAblit = Blit.locate(dsttype, comptype, dsttype);
|
|
lastCAcomp = comp;
|
|
}
|
|
|
|
double[] coords = {x, y, x + w, y + h, x + dx, y + dy};
|
|
transform.transform(coords, 0, coords, 0, 3);
|
|
|
|
x = (int)Math.ceil(coords[0] - 0.5);
|
|
y = (int)Math.ceil(coords[1] - 0.5);
|
|
w = ((int)Math.ceil(coords[2] - 0.5)) - x;
|
|
h = ((int)Math.ceil(coords[3] - 0.5)) - y;
|
|
dx = ((int)Math.ceil(coords[4] - 0.5)) - x;
|
|
dy = ((int)Math.ceil(coords[5] - 0.5)) - y;
|
|
|
|
// In case of negative scale transform, reflect the rect coords.
|
|
if (w < 0) {
|
|
w *= -1;
|
|
x -= w;
|
|
}
|
|
if (h < 0) {
|
|
h *= -1;
|
|
y -= h;
|
|
}
|
|
|
|
Blit ob = lastCAblit;
|
|
try {
|
|
if (dy == 0 && dx > 0 && dx < w) {
|
|
while (w > 0) {
|
|
int partW = Math.min(w, dx);
|
|
w -= partW;
|
|
int sx = Math.addExact(x, w);
|
|
ob.Blit(theData, theData, comp, clip,
|
|
sx, y, sx+dx, y+dy, partW, h);
|
|
}
|
|
return;
|
|
}
|
|
if (dy > 0 && dy < h && dx > -w && dx < w) {
|
|
while (h > 0) {
|
|
int partH = Math.min(h, dy);
|
|
h -= partH;
|
|
int sy = Math.addExact(y, h);
|
|
ob.Blit(theData, theData, comp, clip,
|
|
x, sy, Math.addExact(x, dx), sy+dy, w, partH);
|
|
}
|
|
return;
|
|
}
|
|
ob.Blit(theData, theData, comp, clip, x, y,
|
|
Math.addExact(x, dx), Math.addExact(y, dy), w, h);
|
|
} catch (ArithmeticException ex) {
|
|
// We are hitting integer overflow in Math.addExact()
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
public void XcopyArea(int x, int y, int w, int h, int dx, int dy) {
|
|
Rectangle rect = new Rectangle(x, y, w, h);
|
|
rect = transformBounds(rect, transform);
|
|
Point2D point = new Point2D.Float(dx, dy);
|
|
Point2D root = new Point2D.Float(0, 0);
|
|
point = transform.transform(point, point);
|
|
root = transform.transform(root, root);
|
|
int fdx = (int)(point.getX()-root.getX());
|
|
int fdy = (int)(point.getY()-root.getY());
|
|
|
|
Rectangle r = getCompBounds().intersection(rect.getBounds());
|
|
|
|
if (r.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Begin Rasterizer for Clip Shape
|
|
boolean skipClip = true;
|
|
byte[] clipAlpha = null;
|
|
|
|
if (clipState == CLIP_SHAPE) {
|
|
|
|
int box[] = new int[4];
|
|
|
|
clipRegion.getBounds(box);
|
|
Rectangle devR = new Rectangle(box[0], box[1],
|
|
box[2] - box[0],
|
|
box[3] - box[1]);
|
|
if (!devR.isEmpty()) {
|
|
OutputManager mgr = getOutputManager();
|
|
RegionIterator ri = clipRegion.getIterator();
|
|
while (ri.nextYRange(box)) {
|
|
int spany = box[1];
|
|
int spanh = box[3] - spany;
|
|
while (ri.nextXBand(box)) {
|
|
int spanx = box[0];
|
|
int spanw = box[2] - spanx;
|
|
mgr.copyArea(this, null,
|
|
spanw, 0,
|
|
spanx, spany,
|
|
spanw, spanh,
|
|
fdx, fdy,
|
|
null);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// End Rasterizer for Clip Shape
|
|
|
|
getOutputManager().copyArea(this, null,
|
|
r.width, 0,
|
|
r.x, r.y, r.width,
|
|
r.height, fdx, fdy,
|
|
null);
|
|
}
|
|
*/
|
|
|
|
public void drawLine(int x1, int y1, int x2, int y2) {
|
|
try {
|
|
drawpipe.drawLine(this, x1, y1, x2, y2);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawLine(this, x1, y1, x2, y2);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawRoundRect(int x, int y, int w, int h, int arcW, int arcH) {
|
|
try {
|
|
drawpipe.drawRoundRect(this, x, y, w, h, arcW, arcH);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawRoundRect(this, x, y, w, h, arcW, arcH);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void fillRoundRect(int x, int y, int w, int h, int arcW, int arcH) {
|
|
try {
|
|
fillpipe.fillRoundRect(this, x, y, w, h, arcW, arcH);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
fillpipe.fillRoundRect(this, x, y, w, h, arcW, arcH);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawOval(int x, int y, int w, int h) {
|
|
try {
|
|
drawpipe.drawOval(this, x, y, w, h);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawOval(this, x, y, w, h);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void fillOval(int x, int y, int w, int h) {
|
|
try {
|
|
fillpipe.fillOval(this, x, y, w, h);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
fillpipe.fillOval(this, x, y, w, h);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawArc(int x, int y, int w, int h,
|
|
int startAngl, int arcAngl) {
|
|
try {
|
|
drawpipe.drawArc(this, x, y, w, h, startAngl, arcAngl);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawArc(this, x, y, w, h, startAngl, arcAngl);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void fillArc(int x, int y, int w, int h,
|
|
int startAngl, int arcAngl) {
|
|
try {
|
|
fillpipe.fillArc(this, x, y, w, h, startAngl, arcAngl);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
fillpipe.fillArc(this, x, y, w, h, startAngl, arcAngl);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawPolyline(int xPoints[], int yPoints[], int nPoints) {
|
|
try {
|
|
drawpipe.drawPolyline(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawPolyline(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawPolygon(int xPoints[], int yPoints[], int nPoints) {
|
|
try {
|
|
drawpipe.drawPolygon(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawPolygon(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void fillPolygon(int xPoints[], int yPoints[], int nPoints) {
|
|
try {
|
|
fillpipe.fillPolygon(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
fillpipe.fillPolygon(this, xPoints, yPoints, nPoints);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawRect (int x, int y, int w, int h) {
|
|
try {
|
|
drawpipe.drawRect(this, x, y, w, h);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
drawpipe.drawRect(this, x, y, w, h);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void fillRect (int x, int y, int w, int h) {
|
|
try {
|
|
fillpipe.fillRect(this, x, y, w, h);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
fillpipe.fillRect(this, x, y, w, h);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
private void revalidateAll() {
|
|
try {
|
|
// REMIND: This locking needs to be done around the
|
|
// caller of this method so that the pipe stays valid
|
|
// long enough to call the new primitive.
|
|
// REMIND: No locking yet in screen SurfaceData objects!
|
|
// surfaceData.lock();
|
|
surfaceData = surfaceData.getReplacement();
|
|
if (surfaceData == null) {
|
|
surfaceData = NullSurfaceData.theInstance;
|
|
}
|
|
|
|
invalidatePipe();
|
|
|
|
// this will recalculate the composite clip
|
|
setDevClip(surfaceData.getBounds());
|
|
|
|
if (paintState <= PAINT_ALPHACOLOR) {
|
|
validateColor();
|
|
}
|
|
if (composite instanceof XORComposite) {
|
|
Color c = ((XORComposite) composite).getXorColor();
|
|
setComposite(new XORComposite(c, surfaceData));
|
|
}
|
|
validatePipe();
|
|
} finally {
|
|
// REMIND: No locking yet in screen SurfaceData objects!
|
|
// surfaceData.unlock();
|
|
}
|
|
}
|
|
|
|
public void clearRect(int x, int y, int w, int h) {
|
|
// REMIND: has some "interesting" consequences if threads are
|
|
// not synchronized
|
|
Composite c = composite;
|
|
Paint p = paint;
|
|
setComposite(AlphaComposite.Src);
|
|
setColor(getBackground());
|
|
fillRect(x, y, w, h);
|
|
setPaint(p);
|
|
setComposite(c);
|
|
}
|
|
|
|
/**
|
|
* Strokes the outline of a Path using the settings of the current
|
|
* graphics state. The rendering attributes applied include the
|
|
* clip, transform, paint or color, composite and stroke attributes.
|
|
* @param p The path to be drawn.
|
|
* @see #setStroke
|
|
* @see #setPaint
|
|
* @see java.awt.Graphics#setColor
|
|
* @see #transform
|
|
* @see #setTransform
|
|
* @see #clip
|
|
* @see #setClip
|
|
* @see #setComposite
|
|
*/
|
|
public void draw(Shape s) {
|
|
try {
|
|
shapepipe.draw(this, s);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
shapepipe.draw(this, s);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Fills the interior of a Path using the settings of the current
|
|
* graphics state. The rendering attributes applied include the
|
|
* clip, transform, paint or color, and composite.
|
|
* @see #setPaint
|
|
* @see java.awt.Graphics#setColor
|
|
* @see #transform
|
|
* @see #setTransform
|
|
* @see #setComposite
|
|
* @see #clip
|
|
* @see #setClip
|
|
*/
|
|
public void fill(Shape s) {
|
|
try {
|
|
shapepipe.fill(this, s);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
shapepipe.fill(this, s);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given AffineTransform is an integer
|
|
* translation.
|
|
*/
|
|
private static boolean isIntegerTranslation(AffineTransform xform) {
|
|
if (xform.isIdentity()) {
|
|
return true;
|
|
}
|
|
if (xform.getType() == AffineTransform.TYPE_TRANSLATION) {
|
|
double tx = xform.getTranslateX();
|
|
double ty = xform.getTranslateY();
|
|
return (tx == (int)tx && ty == (int)ty);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the tile corresponding to the supplied position
|
|
* given the tile grid offset and size along the same axis.
|
|
*/
|
|
private static int getTileIndex(int p, int tileGridOffset, int tileSize) {
|
|
p -= tileGridOffset;
|
|
if (p < 0) {
|
|
p += 1 - tileSize; // force round to -infinity (ceiling)
|
|
}
|
|
return p/tileSize;
|
|
}
|
|
|
|
/**
|
|
* Returns a rectangle in image coordinates that may be required
|
|
* in order to draw the given image into the given clipping region
|
|
* through a pair of AffineTransforms. In addition, horizontal and
|
|
* vertical padding factors for antialising and interpolation may
|
|
* be used.
|
|
*/
|
|
private static Rectangle getImageRegion(RenderedImage img,
|
|
Region compClip,
|
|
AffineTransform transform,
|
|
AffineTransform xform,
|
|
int padX, int padY) {
|
|
Rectangle imageRect =
|
|
new Rectangle(img.getMinX(), img.getMinY(),
|
|
img.getWidth(), img.getHeight());
|
|
|
|
Rectangle result = null;
|
|
try {
|
|
double p[] = new double[8];
|
|
p[0] = p[2] = compClip.getLoX();
|
|
p[4] = p[6] = compClip.getHiX();
|
|
p[1] = p[5] = compClip.getLoY();
|
|
p[3] = p[7] = compClip.getHiY();
|
|
|
|
// Inverse transform the output bounding rect
|
|
transform.inverseTransform(p, 0, p, 0, 4);
|
|
xform.inverseTransform(p, 0, p, 0, 4);
|
|
|
|
// Determine a bounding box for the inverse transformed region
|
|
double x0,x1,y0,y1;
|
|
x0 = x1 = p[0];
|
|
y0 = y1 = p[1];
|
|
|
|
for (int i = 2; i < 8; ) {
|
|
double pt = p[i++];
|
|
if (pt < x0) {
|
|
x0 = pt;
|
|
} else if (pt > x1) {
|
|
x1 = pt;
|
|
}
|
|
pt = p[i++];
|
|
if (pt < y0) {
|
|
y0 = pt;
|
|
} else if (pt > y1) {
|
|
y1 = pt;
|
|
}
|
|
}
|
|
|
|
// This is padding for anti-aliasing and such. It may
|
|
// be more than is needed.
|
|
int x = (int)x0 - padX;
|
|
int w = (int)(x1 - x0 + 2*padX);
|
|
int y = (int)y0 - padY;
|
|
int h = (int)(y1 - y0 + 2*padY);
|
|
|
|
Rectangle clipRect = new Rectangle(x,y,w,h);
|
|
result = clipRect.intersection(imageRect);
|
|
} catch (NoninvertibleTransformException nte) {
|
|
// Worst case bounds are the bounds of the image.
|
|
result = imageRect;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Draws an image, applying a transform from image space into user space
|
|
* before drawing.
|
|
* The transformation from user space into device space is done with
|
|
* the current transform in the Graphics2D.
|
|
* The given transformation is applied to the image before the
|
|
* transform attribute in the Graphics2D state is applied.
|
|
* The rendering attributes applied include the clip, transform,
|
|
* and composite attributes. Note that the result is
|
|
* undefined, if the given transform is noninvertible.
|
|
* @param img The image to be drawn. Does nothing if img is null.
|
|
* @param xform The transformation from image space into user space.
|
|
* @see #transform
|
|
* @see #setTransform
|
|
* @see #setComposite
|
|
* @see #clip
|
|
* @see #setClip
|
|
*/
|
|
public void drawRenderedImage(RenderedImage img,
|
|
AffineTransform xform) {
|
|
|
|
if (img == null) {
|
|
return;
|
|
}
|
|
|
|
// BufferedImage case: use a simple drawImage call
|
|
if (img instanceof BufferedImage) {
|
|
BufferedImage bufImg = (BufferedImage)img;
|
|
drawImage(bufImg,xform,null);
|
|
return;
|
|
}
|
|
|
|
// transformState tracks the state of transform and
|
|
// transX, transY contain the integer casts of the
|
|
// translation factors
|
|
boolean isIntegerTranslate =
|
|
(transformState <= TRANSFORM_INT_TRANSLATE) &&
|
|
isIntegerTranslation(xform);
|
|
|
|
// Include padding for interpolation/antialiasing if necessary
|
|
int pad = isIntegerTranslate ? 0 : 3;
|
|
|
|
Region clip;
|
|
try {
|
|
clip = getCompClip();
|
|
} catch (InvalidPipeException e) {
|
|
return;
|
|
}
|
|
|
|
// Determine the region of the image that may contribute to
|
|
// the clipped drawing area
|
|
Rectangle region = getImageRegion(img,
|
|
clip,
|
|
transform,
|
|
xform,
|
|
pad, pad);
|
|
if (region.width <= 0 || region.height <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Attempt to optimize integer translation of tiled images.
|
|
// Although theoretically we are O.K. if the concatenation of
|
|
// the user transform and the device transform is an integer
|
|
// translation, we'll play it safe and only optimize the case
|
|
// where both are integer translations.
|
|
if (isIntegerTranslate) {
|
|
// Use optimized code
|
|
// Note that drawTranslatedRenderedImage calls copyImage
|
|
// which takes the user space to device space transform into
|
|
// account, but we need to provide the image space to user space
|
|
// translations.
|
|
|
|
drawTranslatedRenderedImage(img, region,
|
|
(int) xform.getTranslateX(),
|
|
(int) xform.getTranslateY());
|
|
return;
|
|
}
|
|
|
|
// General case: cobble the necessary region into a single Raster
|
|
Raster raster = img.getData(region);
|
|
|
|
// Make a new Raster with the same contents as raster
|
|
// but starting at (0, 0). This raster is thus in the same
|
|
// coordinate system as the SampleModel of the original raster.
|
|
WritableRaster wRaster =
|
|
Raster.createWritableRaster(raster.getSampleModel(),
|
|
raster.getDataBuffer(),
|
|
null);
|
|
|
|
// If the original raster was in a different coordinate
|
|
// system than its SampleModel, we need to perform an
|
|
// additional translation in order to get the (minX, minY)
|
|
// pixel of raster to be pixel (0, 0) of wRaster. We also
|
|
// have to have the correct width and height.
|
|
int minX = raster.getMinX();
|
|
int minY = raster.getMinY();
|
|
int width = raster.getWidth();
|
|
int height = raster.getHeight();
|
|
int px = minX - raster.getSampleModelTranslateX();
|
|
int py = minY - raster.getSampleModelTranslateY();
|
|
if (px != 0 || py != 0 || width != wRaster.getWidth() ||
|
|
height != wRaster.getHeight()) {
|
|
wRaster =
|
|
wRaster.createWritableChild(px,
|
|
py,
|
|
width,
|
|
height,
|
|
0, 0,
|
|
null);
|
|
}
|
|
|
|
// Now we have a BufferedImage starting at (0, 0)
|
|
// with the same contents that started at (minX, minY)
|
|
// in raster. So we must draw the BufferedImage with a
|
|
// translation of (minX, minY).
|
|
AffineTransform transXform = (AffineTransform)xform.clone();
|
|
transXform.translate(minX, minY);
|
|
|
|
ColorModel cm = img.getColorModel();
|
|
BufferedImage bufImg = new BufferedImage(cm,
|
|
wRaster,
|
|
cm.isAlphaPremultiplied(),
|
|
null);
|
|
drawImage(bufImg, transXform, null);
|
|
}
|
|
|
|
/**
|
|
* Intersects <code>destRect</code> with <code>clip</code> and
|
|
* overwrites <code>destRect</code> with the result.
|
|
* Returns false if the intersection was empty, true otherwise.
|
|
*/
|
|
private boolean clipTo(Rectangle destRect, Rectangle clip) {
|
|
int x1 = Math.max(destRect.x, clip.x);
|
|
int x2 = Math.min(destRect.x + destRect.width, clip.x + clip.width);
|
|
int y1 = Math.max(destRect.y, clip.y);
|
|
int y2 = Math.min(destRect.y + destRect.height, clip.y + clip.height);
|
|
if (((x2 - x1) < 0) || ((y2 - y1) < 0)) {
|
|
destRect.width = -1; // Set both just to be safe
|
|
destRect.height = -1;
|
|
return false;
|
|
} else {
|
|
destRect.x = x1;
|
|
destRect.y = y1;
|
|
destRect.width = x2 - x1;
|
|
destRect.height = y2 - y1;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw a portion of a RenderedImage tile-by-tile with a given
|
|
* integer image to user space translation. The user to
|
|
* device transform must also be an integer translation.
|
|
*/
|
|
private void drawTranslatedRenderedImage(RenderedImage img,
|
|
Rectangle region,
|
|
int i2uTransX,
|
|
int i2uTransY) {
|
|
// Cache tile grid info
|
|
int tileGridXOffset = img.getTileGridXOffset();
|
|
int tileGridYOffset = img.getTileGridYOffset();
|
|
int tileWidth = img.getTileWidth();
|
|
int tileHeight = img.getTileHeight();
|
|
|
|
// Determine the tile index extrema in each direction
|
|
int minTileX =
|
|
getTileIndex(region.x, tileGridXOffset, tileWidth);
|
|
int minTileY =
|
|
getTileIndex(region.y, tileGridYOffset, tileHeight);
|
|
int maxTileX =
|
|
getTileIndex(region.x + region.width - 1,
|
|
tileGridXOffset, tileWidth);
|
|
int maxTileY =
|
|
getTileIndex(region.y + region.height - 1,
|
|
tileGridYOffset, tileHeight);
|
|
|
|
// Create a single ColorModel to use for all BufferedImages
|
|
ColorModel colorModel = img.getColorModel();
|
|
|
|
// Reuse the same Rectangle for each iteration
|
|
Rectangle tileRect = new Rectangle();
|
|
|
|
for (int ty = minTileY; ty <= maxTileY; ty++) {
|
|
for (int tx = minTileX; tx <= maxTileX; tx++) {
|
|
// Get the current tile.
|
|
Raster raster = img.getTile(tx, ty);
|
|
|
|
// Fill in tileRect with the tile bounds
|
|
tileRect.x = tx*tileWidth + tileGridXOffset;
|
|
tileRect.y = ty*tileHeight + tileGridYOffset;
|
|
tileRect.width = tileWidth;
|
|
tileRect.height = tileHeight;
|
|
|
|
// Clip the tile against the image bounds and
|
|
// backwards mapped clip region
|
|
// The result can't be empty
|
|
clipTo(tileRect, region);
|
|
|
|
// Create a WritableRaster containing the tile
|
|
WritableRaster wRaster = null;
|
|
if (raster instanceof WritableRaster) {
|
|
wRaster = (WritableRaster)raster;
|
|
} else {
|
|
// Create a WritableRaster in the same coordinate system
|
|
// as the original raster.
|
|
wRaster =
|
|
Raster.createWritableRaster(raster.getSampleModel(),
|
|
raster.getDataBuffer(),
|
|
null);
|
|
}
|
|
|
|
// Translate wRaster to start at (0, 0) and to contain
|
|
// only the relevent portion of the tile
|
|
wRaster = wRaster.createWritableChild(tileRect.x, tileRect.y,
|
|
tileRect.width,
|
|
tileRect.height,
|
|
0, 0,
|
|
null);
|
|
|
|
// Wrap wRaster in a BufferedImage
|
|
BufferedImage bufImg =
|
|
new BufferedImage(colorModel,
|
|
wRaster,
|
|
colorModel.isAlphaPremultiplied(),
|
|
null);
|
|
// Now we have a BufferedImage starting at (0, 0) that
|
|
// represents data from a Raster starting at
|
|
// (tileRect.x, tileRect.y). Additionally, it needs
|
|
// to be translated by (i2uTransX, i2uTransY). We call
|
|
// copyImage to draw just the region of interest
|
|
// without needing to create a child image.
|
|
copyImage(bufImg, tileRect.x + i2uTransX,
|
|
tileRect.y + i2uTransY, 0, 0, tileRect.width,
|
|
tileRect.height, null, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void drawRenderableImage(RenderableImage img,
|
|
AffineTransform xform) {
|
|
|
|
if (img == null) {
|
|
return;
|
|
}
|
|
|
|
AffineTransform pipeTransform = transform;
|
|
AffineTransform concatTransform = new AffineTransform(xform);
|
|
concatTransform.concatenate(pipeTransform);
|
|
AffineTransform reverseTransform;
|
|
|
|
RenderContext rc = new RenderContext(concatTransform);
|
|
|
|
try {
|
|
reverseTransform = pipeTransform.createInverse();
|
|
} catch (NoninvertibleTransformException nte) {
|
|
rc = new RenderContext(pipeTransform);
|
|
reverseTransform = new AffineTransform();
|
|
}
|
|
|
|
RenderedImage rendering = img.createRendering(rc);
|
|
drawRenderedImage(rendering,reverseTransform);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Transform the bounding box of the BufferedImage
|
|
*/
|
|
protected Rectangle transformBounds(Rectangle rect,
|
|
AffineTransform tx) {
|
|
if (tx.isIdentity()) {
|
|
return rect;
|
|
}
|
|
|
|
Shape s = transformShape(tx, rect);
|
|
return s.getBounds();
|
|
}
|
|
|
|
// text rendering methods
|
|
public void drawString(String str, int x, int y) {
|
|
if (str == null) {
|
|
throw new NullPointerException("String is null");
|
|
}
|
|
|
|
if (font.hasLayoutAttributes()) {
|
|
if (str.length() == 0) {
|
|
return;
|
|
}
|
|
new TextLayout(str, font, getFontRenderContext()).draw(this, x, y);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
textpipe.drawString(this, str, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
textpipe.drawString(this, str, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawString(String str, float x, float y) {
|
|
if (str == null) {
|
|
throw new NullPointerException("String is null");
|
|
}
|
|
|
|
if (font.hasLayoutAttributes()) {
|
|
if (str.length() == 0) {
|
|
return;
|
|
}
|
|
new TextLayout(str, font, getFontRenderContext()).draw(this, x, y);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
textpipe.drawString(this, str, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
textpipe.drawString(this, str, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawString(AttributedCharacterIterator iterator,
|
|
int x, int y) {
|
|
if (iterator == null) {
|
|
throw new NullPointerException("AttributedCharacterIterator is null");
|
|
}
|
|
if (iterator.getBeginIndex() == iterator.getEndIndex()) {
|
|
return; /* nothing to draw */
|
|
}
|
|
TextLayout tl = new TextLayout(iterator, getFontRenderContext());
|
|
tl.draw(this, (float) x, (float) y);
|
|
}
|
|
|
|
public void drawString(AttributedCharacterIterator iterator,
|
|
float x, float y) {
|
|
if (iterator == null) {
|
|
throw new NullPointerException("AttributedCharacterIterator is null");
|
|
}
|
|
if (iterator.getBeginIndex() == iterator.getEndIndex()) {
|
|
return; /* nothing to draw */
|
|
}
|
|
TextLayout tl = new TextLayout(iterator, getFontRenderContext());
|
|
tl.draw(this, x, y);
|
|
}
|
|
|
|
public void drawGlyphVector(GlyphVector gv, float x, float y)
|
|
{
|
|
if (gv == null) {
|
|
throw new NullPointerException("GlyphVector is null");
|
|
}
|
|
|
|
try {
|
|
textpipe.drawGlyphVector(this, gv, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
textpipe.drawGlyphVector(this, gv, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawChars(char data[], int offset, int length, int x, int y) {
|
|
|
|
if (data == null) {
|
|
throw new NullPointerException("char data is null");
|
|
}
|
|
if (offset < 0 || length < 0 || offset + length < length ||
|
|
offset + length > data.length) {
|
|
throw new ArrayIndexOutOfBoundsException("bad offset/length");
|
|
}
|
|
if (font.hasLayoutAttributes()) {
|
|
if (data.length == 0) {
|
|
return;
|
|
}
|
|
new TextLayout(new String(data, offset, length),
|
|
font, getFontRenderContext()).draw(this, x, y);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
textpipe.drawChars(this, data, offset, length, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
textpipe.drawChars(this, data, offset, length, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawBytes(byte data[], int offset, int length, int x, int y) {
|
|
if (data == null) {
|
|
throw new NullPointerException("byte data is null");
|
|
}
|
|
if (offset < 0 || length < 0 || offset + length < length ||
|
|
offset + length > data.length) {
|
|
throw new ArrayIndexOutOfBoundsException("bad offset/length");
|
|
}
|
|
/* Byte data is interpreted as 8-bit ASCII. Re-use drawChars loops */
|
|
char chData[] = new char[length];
|
|
for (int i = length; i-- > 0; ) {
|
|
chData[i] = (char)(data[i+offset] & 0xff);
|
|
}
|
|
if (font.hasLayoutAttributes()) {
|
|
if (data.length == 0) {
|
|
return;
|
|
}
|
|
new TextLayout(new String(chData),
|
|
font, getFontRenderContext()).draw(this, x, y);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
textpipe.drawChars(this, chData, 0, length, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
textpipe.drawChars(this, chData, 0, length, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
// end of text rendering methods
|
|
|
|
private boolean isHiDPIImage(final Image img) {
|
|
return (SurfaceManager.getImageScale(img) != 1) ||
|
|
(resolutionVariantHint != SunHints.INTVAL_RESOLUTION_VARIANT_OFF
|
|
&& img instanceof MultiResolutionImage);
|
|
}
|
|
|
|
private boolean drawHiDPIImage(Image img, int dx1, int dy1, int dx2,
|
|
int dy2, int sx1, int sy1, int sx2, int sy2,
|
|
Color bgcolor, ImageObserver observer) {
|
|
|
|
if (SurfaceManager.getImageScale(img) != 1) { // Volatile Image
|
|
final int scale = SurfaceManager.getImageScale(img);
|
|
sx1 = Region.clipScale(sx1, scale);
|
|
sx2 = Region.clipScale(sx2, scale);
|
|
sy1 = Region.clipScale(sy1, scale);
|
|
sy2 = Region.clipScale(sy2, scale);
|
|
} else if (img instanceof MultiResolutionImage) {
|
|
// get scaled destination image size
|
|
|
|
int width = img.getWidth(observer);
|
|
int height = img.getHeight(observer);
|
|
|
|
Image resolutionVariant = getResolutionVariant(
|
|
(MultiResolutionImage) img, width, height,
|
|
dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2);
|
|
|
|
if (resolutionVariant != img && resolutionVariant != null) {
|
|
// recalculate source region for the resolution variant
|
|
|
|
ImageObserver rvObserver = MultiResolutionToolkitImage.
|
|
getResolutionVariantObserver(img, observer,
|
|
width, height, -1, -1);
|
|
|
|
int rvWidth = resolutionVariant.getWidth(rvObserver);
|
|
int rvHeight = resolutionVariant.getHeight(rvObserver);
|
|
|
|
if (0 < width && 0 < height && 0 < rvWidth && 0 < rvHeight) {
|
|
|
|
float widthScale = ((float) rvWidth) / width;
|
|
float heightScale = ((float) rvHeight) / height;
|
|
|
|
sx1 = Region.clipScale(sx1, widthScale);
|
|
sy1 = Region.clipScale(sy1, heightScale);
|
|
sx2 = Region.clipScale(sx2, widthScale);
|
|
sy2 = Region.clipScale(sy2, heightScale);
|
|
|
|
observer = rvObserver;
|
|
img = resolutionVariant;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1, sy1,
|
|
sx2, sy2, bgcolor, observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2, sx1,
|
|
sy1, sx2, sy2, bgcolor, observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
private Image getResolutionVariant(MultiResolutionImage img,
|
|
int srcWidth, int srcHeight, int dx1, int dy1, int dx2, int dy2,
|
|
int sx1, int sy1, int sx2, int sy2) {
|
|
|
|
if (srcWidth <= 0 || srcHeight <= 0) {
|
|
return null;
|
|
}
|
|
|
|
int sw = sx2 - sx1;
|
|
int sh = sy2 - sy1;
|
|
|
|
if (sw == 0 || sh == 0) {
|
|
return null;
|
|
}
|
|
|
|
int type = transform.getType();
|
|
int dw = dx2 - dx1;
|
|
int dh = dy2 - dy1;
|
|
double destRegionWidth;
|
|
double destRegionHeight;
|
|
|
|
if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP)) == 0) {
|
|
destRegionWidth = dw;
|
|
destRegionHeight = dh;
|
|
} else if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP | TYPE_MASK_SCALE)) == 0) {
|
|
destRegionWidth = dw * transform.getScaleX();
|
|
destRegionHeight = dh * transform.getScaleY();
|
|
} else {
|
|
destRegionWidth = dw * Math.hypot(
|
|
transform.getScaleX(), transform.getShearY());
|
|
destRegionHeight = dh * Math.hypot(
|
|
transform.getShearX(), transform.getScaleY());
|
|
}
|
|
|
|
int destImageWidth = (int) Math.abs(srcWidth * destRegionWidth / sw);
|
|
int destImageHeight = (int) Math.abs(srcHeight * destRegionHeight / sh);
|
|
|
|
Image resolutionVariant
|
|
= img.getResolutionVariant(destImageWidth, destImageHeight);
|
|
|
|
if (resolutionVariant instanceof ToolkitImage
|
|
&& ((ToolkitImage) resolutionVariant).hasError()) {
|
|
return null;
|
|
}
|
|
|
|
return resolutionVariant;
|
|
}
|
|
|
|
/**
|
|
* Draws an image scaled to x,y,w,h in nonblocking mode with a
|
|
* callback object.
|
|
*/
|
|
public boolean drawImage(Image img, int x, int y, int width, int height,
|
|
ImageObserver observer) {
|
|
return drawImage(img, x, y, width, height, null, observer);
|
|
}
|
|
|
|
/**
|
|
* Not part of the advertised API but a useful utility method
|
|
* to call internally. This is for the case where we are
|
|
* drawing to/from given coordinates using a given width/height,
|
|
* but we guarantee that the surfaceData's width/height of the src and dest
|
|
* areas are equal (no scale needed). Note that this method intentionally
|
|
* ignore scale factor of the source image, and copy it as is.
|
|
*/
|
|
public boolean copyImage(Image img, int dx, int dy, int sx, int sy,
|
|
int width, int height, Color bgcolor,
|
|
ImageObserver observer) {
|
|
try {
|
|
return imagepipe.copyImage(this, img, dx, dy, sx, sy,
|
|
width, height, bgcolor, observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.copyImage(this, img, dx, dy, sx, sy,
|
|
width, height, bgcolor, observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws an image scaled to x,y,w,h in nonblocking mode with a
|
|
* solid background color and a callback object.
|
|
*/
|
|
public boolean drawImage(Image img, int x, int y, int width, int height,
|
|
Color bg, ImageObserver observer) {
|
|
|
|
if (img == null) {
|
|
return true;
|
|
}
|
|
|
|
if ((width == 0) || (height == 0)) {
|
|
return true;
|
|
}
|
|
|
|
final int imgW = img.getWidth(null);
|
|
final int imgH = img.getHeight(null);
|
|
if (isHiDPIImage(img)) {
|
|
return drawHiDPIImage(img, x, y, x + width, y + height, 0, 0, imgW,
|
|
imgH, bg, observer);
|
|
}
|
|
|
|
if (width == imgW && height == imgH) {
|
|
return copyImage(img, x, y, 0, 0, width, height, bg, observer);
|
|
}
|
|
|
|
try {
|
|
return imagepipe.scaleImage(this, img, x, y, width, height,
|
|
bg, observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.scaleImage(this, img, x, y, width, height,
|
|
bg, observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws an image at x,y in nonblocking mode.
|
|
*/
|
|
public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
|
|
return drawImage(img, x, y, null, observer);
|
|
}
|
|
|
|
/**
|
|
* Draws an image at x,y in nonblocking mode with a solid background
|
|
* color and a callback object.
|
|
*/
|
|
public boolean drawImage(Image img, int x, int y, Color bg,
|
|
ImageObserver observer) {
|
|
|
|
if (img == null) {
|
|
return true;
|
|
}
|
|
|
|
if (isHiDPIImage(img)) {
|
|
final int imgW = img.getWidth(null);
|
|
final int imgH = img.getHeight(null);
|
|
return drawHiDPIImage(img, x, y, x + imgW, y + imgH, 0, 0, imgW,
|
|
imgH, bg, observer);
|
|
}
|
|
|
|
try {
|
|
return imagepipe.copyImage(this, img, x, y, bg, observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.copyImage(this, img, x, y, bg, observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws a subrectangle of an image scaled to a destination rectangle
|
|
* in nonblocking mode with a callback object.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Draws a subrectangle of an image scaled to a destination rectangle in
|
|
* nonblocking mode with a solid background color and a callback object.
|
|
*/
|
|
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 true;
|
|
}
|
|
|
|
if (dx1 == dx2 || dy1 == dy2 ||
|
|
sx1 == sx2 || sy1 == sy2)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (isHiDPIImage(img)) {
|
|
return drawHiDPIImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2,
|
|
bgcolor, observer);
|
|
}
|
|
|
|
if (((sx2 - sx1) == (dx2 - dx1)) &&
|
|
((sy2 - sy1) == (dy2 - dy1)))
|
|
{
|
|
// Not a scale - forward it to a copy routine
|
|
int srcX, srcY, dstX, dstY, width, height;
|
|
if (sx2 > sx1) {
|
|
width = sx2 - sx1;
|
|
srcX = sx1;
|
|
dstX = dx1;
|
|
} else {
|
|
width = sx1 - sx2;
|
|
srcX = sx2;
|
|
dstX = dx2;
|
|
}
|
|
if (sy2 > sy1) {
|
|
height = sy2-sy1;
|
|
srcY = sy1;
|
|
dstY = dy1;
|
|
} else {
|
|
height = sy1-sy2;
|
|
srcY = sy2;
|
|
dstY = dy2;
|
|
}
|
|
return copyImage(img, dstX, dstY, srcX, srcY,
|
|
width, height, bgcolor, observer);
|
|
}
|
|
|
|
try {
|
|
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2,
|
|
sx1, sy1, sx2, sy2, bgcolor,
|
|
observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.scaleImage(this, img, dx1, dy1, dx2, dy2,
|
|
sx1, sy1, sx2, sy2, bgcolor,
|
|
observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw an image, applying a transform from image space into user space
|
|
* before drawing.
|
|
* The transformation from user space into device space is done with
|
|
* the current transform in the Graphics2D.
|
|
* The given transformation is applied to the image before the
|
|
* transform attribute in the Graphics2D state is applied.
|
|
* The rendering attributes applied include the clip, transform,
|
|
* paint or color and composite attributes. Note that the result is
|
|
* undefined, if the given transform is non-invertible.
|
|
* @param img The image to be drawn.
|
|
* @param xform The transformation from image space into user space.
|
|
* @param observer The image observer to be notified on the image producing
|
|
* progress.
|
|
* @see #transform
|
|
* @see #setComposite
|
|
* @see #setClip
|
|
*/
|
|
public boolean drawImage(Image img,
|
|
AffineTransform xform,
|
|
ImageObserver observer) {
|
|
|
|
if (img == null) {
|
|
return true;
|
|
}
|
|
|
|
if (xform == null || xform.isIdentity()) {
|
|
return drawImage(img, 0, 0, null, observer);
|
|
}
|
|
|
|
if (isHiDPIImage(img)) {
|
|
final int w = img.getWidth(null);
|
|
final int h = img.getHeight(null);
|
|
final AffineTransform tx = new AffineTransform(transform);
|
|
transform(xform);
|
|
boolean result = drawHiDPIImage(img, 0, 0, w, h, 0, 0, w, h, null,
|
|
observer);
|
|
transform.setTransform(tx);
|
|
invalidateTransform();
|
|
return result;
|
|
}
|
|
|
|
try {
|
|
return imagepipe.transformImage(this, img, xform, observer);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
return imagepipe.transformImage(this, img, xform, observer);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
return false;
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
public void drawImage(BufferedImage bImg,
|
|
BufferedImageOp op,
|
|
int x,
|
|
int y) {
|
|
|
|
if (bImg == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
imagepipe.transformImage(this, bImg, op, x, y);
|
|
} catch (InvalidPipeException e) {
|
|
try {
|
|
revalidateAll();
|
|
imagepipe.transformImage(this, bImg, op, x, y);
|
|
} catch (InvalidPipeException e2) {
|
|
// Still catching the exception; we are not yet ready to
|
|
// validate the surfaceData correctly. Fail for now and
|
|
// try again next time around.
|
|
}
|
|
} finally {
|
|
surfaceData.markDirty();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the rendering context of the font
|
|
* within this Graphics2D context.
|
|
*/
|
|
public FontRenderContext getFontRenderContext() {
|
|
if (cachedFRC == null) {
|
|
int aahint = textAntialiasHint;
|
|
if (aahint == SunHints.INTVAL_TEXT_ANTIALIAS_DEFAULT &&
|
|
antialiasHint == SunHints.INTVAL_ANTIALIAS_ON) {
|
|
aahint = SunHints.INTVAL_TEXT_ANTIALIAS_ON;
|
|
}
|
|
// Translation components should be excluded from the FRC transform
|
|
AffineTransform tx = null;
|
|
if (transformState >= TRANSFORM_TRANSLATESCALE) {
|
|
if (transform.getTranslateX() == 0 &&
|
|
transform.getTranslateY() == 0) {
|
|
tx = transform;
|
|
} else {
|
|
tx = new AffineTransform(transform.getScaleX(),
|
|
transform.getShearY(),
|
|
transform.getShearX(),
|
|
transform.getScaleY(),
|
|
0, 0);
|
|
}
|
|
}
|
|
cachedFRC = new FontRenderContext(tx,
|
|
SunHints.Value.get(SunHints.INTKEY_TEXT_ANTIALIASING, aahint),
|
|
SunHints.Value.get(SunHints.INTKEY_FRACTIONALMETRICS,
|
|
fractionalMetricsHint));
|
|
}
|
|
return cachedFRC;
|
|
}
|
|
private FontRenderContext cachedFRC;
|
|
|
|
/**
|
|
* This object has no resources to dispose of per se, but the
|
|
* doc comments for the base method in java.awt.Graphics imply
|
|
* that this object will not be useable after it is disposed.
|
|
* So, we sabotage the object to prevent further use to prevent
|
|
* developers from relying on behavior that may not work on
|
|
* other, less forgiving, VMs that really need to dispose of
|
|
* resources.
|
|
*/
|
|
public void dispose() {
|
|
surfaceData = NullSurfaceData.theInstance;
|
|
invalidatePipe();
|
|
}
|
|
|
|
/**
|
|
* Graphics has a finalize method that automatically calls dispose()
|
|
* for subclasses. For SunGraphics2D we do not need to be finalized
|
|
* so that method simply causes us to be enqueued on the Finalizer
|
|
* queues for no good reason. Unfortunately, that method and
|
|
* implementation are now considered part of the public contract
|
|
* of that base class so we can not remove or gut the method.
|
|
* We override it here with an empty method and the VM is smart
|
|
* enough to know that if our override is empty then it should not
|
|
* mark us as finalizeable.
|
|
*/
|
|
public void finalize() {
|
|
// DO NOT REMOVE THIS METHOD
|
|
}
|
|
|
|
/**
|
|
* Returns destination that this Graphics renders to. This could be
|
|
* either an Image or a Component; subclasses of SurfaceData are
|
|
* responsible for returning the appropriate object.
|
|
*/
|
|
public Object getDestination() {
|
|
return surfaceData.getDestination();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* @see sun.java2d.DestSurfaceProvider#getDestSurface
|
|
*/
|
|
@Override
|
|
public Surface getDestSurface() {
|
|
return surfaceData;
|
|
}
|
|
}
|