/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.gvt.renderer;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.CompositeGraphicsNode;
import org.apache.batik.gvt.GraphicsNodeRenderContext;
import org.apache.batik.gvt.TextPainter;
import org.apache.batik.gvt.Selector;
import org.apache.batik.gvt.Selectable;
import org.apache.batik.gvt.filter.GraphicsNodeRable;
import org.apache.batik.gvt.filter.GraphicsNodeRableFactory;
import org.apache.batik.gvt.filter.ConcreteGraphicsNodeRableFactory;
import org.apache.batik.gvt.filter.GraphicsNodeRable8Bit;
import org.apache.batik.gvt.text.ConcreteTextSelector;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.ext.awt.image.PadMode;
import org.apache.batik.ext.awt.image.rendered.CachableRed;
import org.apache.batik.ext.awt.image.rendered.PadRed;
import java.util.Iterator;
import java.util.Stack;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.Raster;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.renderable.RenderContext;
/**
* Simple implementation of the Renderer that simply does static
* rendering in an offscreen buffer image.
*
* @author <a href="mailto:vincent.hardy@eng.sun.com>Vincent Hardy</a>
* @version $Id: StaticRenderer.java,v 1.12 2001/03/26 21:27:37 deweese Exp $
*/
public class StaticRenderer implements ImageRenderer {
/**
* Error messages
*/
private static final String ILLEGAL_ARGUMENT_NULL_OFFSCREEN =
"offScreen should not be null";
private static final String ILLEGAL_ARGUMENT_ZERO_WIDTH_OR_HEIGHT =
"offScreen should have positive width/height";
/**
* Tree this Renderer paints.
*/
protected GraphicsNode rootGN;
protected GraphicsNodeRable rootGNR;
protected CachableRed rootCR;
/**
* Flag for double buffering.
*/
private boolean isDoubleBuffered = false;
/**
* The Selector instance which listens to TextSelection gestures.
*/
private Selector textSelector = null;
/**
* Offscreen image where the Renderer does its rendering
*/
protected WritableRaster currentBaseRaster;
protected WritableRaster currentRaster;
protected BufferedImage currentOffScreen;
protected WritableRaster workingBaseRaster;
protected WritableRaster workingRaster;
protected BufferedImage workingOffScreen;
protected int offScreenWidth;
protected int offScreenHeight;
/**
* Passed to the GVT tree to describe the rendering environment
*/
protected GraphicsNodeRenderContext nodeRenderContext;
/**
* @param offScreen image where the Renderer should do its rendering
* @param rc a GraphicsNodeRenderContext which this renderer should use
*/
public StaticRenderer(GraphicsNodeRenderContext rc){
setRenderContext(rc);
}
/**
* @param offScreen image where the Renderer should do its rendering
*/
public StaticRenderer(){
RenderingHints hints = new RenderingHints(null);
hints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
FontRenderContext fontRenderContext =
new FontRenderContext(new AffineTransform(), true, true);
TextPainter textPainter = new StrokingTextPainter();
GraphicsNodeRableFactory gnrFactory =
new ConcreteGraphicsNodeRableFactory();
this.nodeRenderContext =
new GraphicsNodeRenderContext(new AffineTransform(),
null,
hints,
fontRenderContext,
textPainter,
gnrFactory);
}
/**
* Disposes all resources of this renderer.
*/
public void dispose() {
rootGN = null;
rootGNR = null;
rootCR = null;
workingOffScreen = null;
workingBaseRaster = null;
workingRaster = null;
currentOffScreen = null;
currentBaseRaster = null;
currentRaster = null;
nodeRenderContext = null;
}
/**
* This associates the given GVT Tree with this renderer.
* Any previous tree association is forgotten.
* Not certain if this should be just GraphicsNode, or CanvasGraphicsNode.
*/
public void setTree(GraphicsNode rootGN){
this.rootGN = rootGN;
rootGNR = null;
rootCR = null;
workingOffScreen = null;
workingBaseRaster = null;
workingRaster = null;
currentOffScreen = null;
currentBaseRaster = null;
currentRaster = null;
}
/**
* @return the GVT tree associated with this renderer
*/
public GraphicsNode getTree(){
return rootGN;
}
/**
* @param rc a GraphicsNodeRenderContext which the Renderer should use
* for its rendering
*/
public void setRenderContext(GraphicsNodeRenderContext rc) {
this.nodeRenderContext = rc;
rootGNR = null;
rootCR = null;
workingOffScreen = null;
workingBaseRaster = null;
workingRaster = null;
currentOffScreen = null;
currentBaseRaster = null;
currentRaster = null;
}
/**
* @return the GraphicsNodeRenderContext which the Renderer uses
* for its rendering
*/
public GraphicsNodeRenderContext getRenderContext() {
return nodeRenderContext;
}
/**
* Sets the transform from the current user space (as defined by
* the top node of the GVT tree, to the associated device space.
*
* @param usr2dev the new user space to device space transform. If null,
* the identity transform will be set.
*/
public void setTransform(AffineTransform usr2dev){
if(usr2dev == null)
usr2dev = new AffineTransform();
// Update the RenderContext in the nodeRenderContext
nodeRenderContext.setTransform(usr2dev);
rootCR = null;
}
/**
* Returns a copy of the transform from the current user space (as
* defined by the top node of the GVT tree) to the device space (1
* unit = 1/72nd of an inch / 1 pixel, roughly speaking
*/
public AffineTransform getTransform(){
return nodeRenderContext.getTransform();
}
/**
* Returns true if the Renderer is currently doubleBuffering is
* rendering requests. If it is then getOffscreen will only
* return completed renderings (or null if nothing is available).
*/
public boolean isDoubleBuffered(){
return isDoubleBuffered;
}
/**
* Turns on/off double buffering in renderer. Turning off
* double buffering makes it possible to see the ongoing results
* of a render operation.
*
* @param isDoubleBuffered the new value for double buffering
*/
public void setDoubleBuffered(boolean isDoubleBuffered){
this.isDoubleBuffered = isDoubleBuffered;
if (isDoubleBuffered) {
// Now double buffering, so make sure they can't see work buffers.
currentOffScreen = null;
currentBaseRaster = null;
currentRaster = null;
} else {
// No longer double buffering so join work and current buffers.
currentOffScreen = workingOffScreen;
currentBaseRaster = workingBaseRaster;
currentRaster = workingRaster;
}
}
/**
* Update the size of the image to be returned by getOffScreen.
* Note that this change will not be reflected by calls to
* getOffscreen until either clearOffscreen has completed (when
* isDoubleBuffered is false) or reapint has completed (when
* isDoubleBuffered is true).
*
*/
public void updateOffScreen(int width, int height) {
offScreenWidth = width;
offScreenHeight = height;
}
/**
* Returns the current offscreen image.
*
* The exact symantics of this vary base on the value of
* isDoubleBuffered. If isDoubleBuffered is false this will
* return the image currently being worked on as soon as it is
* available.
*
* if isDoubleBuffered is false this will return the most recently
* completed result of repaint.
*/
public BufferedImage getOffScreen() {
if (rootGN == null)
return null;
return currentOffScreen;
}
/**
* Sets up and clears the current offscreen buffer.
*
* When not double buffering one should call this method before
* calling getOffscreen to get the offscreen being drawn into.
* This ensures the buffer is up to date and doesn't contain junk.
*
* When double buffering this call can effectively be skipped,
* since getOffscreen will only refect the new rendering after
* repaint completes.
*/
public void clearOffScreen() {
WritableRaster syncRaster;
ColorModel cm;
updateWorkingBuffers();
if ((rootCR == null) ||
(workingBaseRaster == null))
return;
cm = rootCR.getColorModel();
syncRaster = workingBaseRaster;
// Ensure only one thread works on baseRaster at a time...
synchronized (syncRaster) {
BufferedImage bi = new BufferedImage
(cm, workingBaseRaster, cm.isAlphaPremultiplied(), null);
Graphics2D g2d = bi.createGraphics();
g2d.setComposite(AlphaComposite.Clear);
g2d.fillRect(0, 0, bi.getWidth(), bi.getHeight());
g2d.dispose();
}
}
/**
* Repaints the associated GVT tree under 'area'.
*
* If double buffered is true if this method completes cleanly it
* will set the result of the repaint as the image returned by
* getOffscreen
*
* @param area region to be repainted, in the current user space
* coordinate system.
*/
public void repaint(Shape area) throws InterruptedException {
if (area == null)
return;
// long t0 = System.currentTimeMillis();
CachableRed cr;
WritableRaster syncRaster;
WritableRaster copyRaster;
// While we are synchronized pull all the relavent info out
// of member variables into local variables.
updateWorkingBuffers();
if ((rootCR == null) ||
(workingBaseRaster == null))
return;
cr = rootCR;
syncRaster = workingBaseRaster;
copyRaster = workingRaster;
Rectangle srcR = rootCR.getBounds();
Rectangle dstR = workingRaster.getBounds();
if ((dstR.x < srcR.x) ||
(dstR.y < srcR.y) ||
(dstR.x+dstR.width > srcR.x+srcR.width) ||
(dstR.y+dstR.height > srcR.y+srcR.height))
cr = new PadRed(cr, dstR, PadMode.ZERO_PAD, null);
// Ensure only one thread works on baseRaster at a time...
synchronized (syncRaster) {
cr.copyData(copyRaster);
}
if (!Thread.currentThread().isInterrupted()) {
// Swap the buffers if the rendering completed cleanly.
BufferedImage tmpBI = workingOffScreen;
workingBaseRaster = currentBaseRaster;
workingRaster = currentRaster;
workingOffScreen = currentOffScreen;
currentRaster = copyRaster;
currentBaseRaster = syncRaster;
currentOffScreen = tmpBI;
}
}
/**
* Internal method used to synchronize local state in response to
* various set methods.
*/
protected void updateWorkingBuffers() {
if (rootGNR == null) {
rootGNR = new GraphicsNodeRable8Bit(rootGN,
nodeRenderContext);
rootCR = null;
}
if (rootCR == null) {
workingBaseRaster = null;
workingRaster = null;
workingOffScreen = null;
RenderContext rc = new RenderContext
(nodeRenderContext.getTransform(),
null,
nodeRenderContext.getRenderingHints());
RenderedImage ri = rootGNR.createRendering(rc);
if (ri == null)
return;
rootCR = GraphicsUtil.wrap(ri);
rootCR = GraphicsUtil.convertTosRGB(rootCR);
}
SampleModel sm = rootCR.getSampleModel();
int w = offScreenWidth;
int h = offScreenHeight;
int tw = sm.getWidth();
int th = sm.getHeight();
w = (((w+tw-1)/tw)+1)*tw;
h = (((h+th-1)/th)+1)*th;
if ((workingBaseRaster == null) ||
(workingBaseRaster.getWidth() < w) ||
(workingBaseRaster.getHeight() < h)) {
sm = sm.createCompatibleSampleModel(w, h);
workingBaseRaster
= Raster.createWritableRaster(sm, new Point(0,0));
}
int tgx = -rootCR.getTileGridXOffset();
int tgy = -rootCR.getTileGridYOffset();
int xt, yt;
if (tgx>=0) xt = tgx/tw;
else xt = (tgx-tw+1)/tw;
if (tgy>=0) yt = tgy/th;
else yt = (tgy-th+1)/th;
int xloc = xt*tw - tgx;
int yloc = yt*th - tgy;
// System.out.println("Info: [" +
// xloc + "," + yloc + "] [" +
// tgx + "," + tgy + "] [" +
// xt + "," + yt + "] [" +
// tw + "," + th + "]");
// This raster should be aligned with cr's tile grid.
workingRaster = workingBaseRaster.createWritableChild
(0, 0, w, h, xloc, yloc, null);
workingOffScreen = new BufferedImage
(rootCR.getColorModel(),
workingRaster.createWritableChild (0, 0, offScreenWidth,
offScreenHeight, 0, 0, null),
rootCR.getColorModel().isAlphaPremultiplied(), null);
if (!isDoubleBuffered) {
currentOffScreen = workingOffScreen;
currentBaseRaster = workingBaseRaster;
currentRaster = workingRaster;
}
}
}