/*
* $Id: PDFRenderer.java,v 1.9 2010-05-23 22:20:08 lujke Exp $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.sun.pdfview;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
/**
* This class turns a set of PDF Commands from a PDF page into an image. It
* encapsulates the state of drawing in terms of stroke, fill, transform,
* etc., as well as pushing and popping these states.
*
* When the run method is called, this class goes through all remaining commands
* in the PDF Page and draws them to its buffered image. It then updates any
* ImageConsumers with the drawn data.
*/
public class PDFRenderer extends BaseWatchable implements Runnable {
/** the page we were generate from */
private PDFPage page;
/** where we are in the page's command list */
private int currentCommand;
/** a weak reference to the image we render into. For the image
* to remain available, some other code must retain a strong reference to it.
*/
private WeakReference imageRef;
/** the graphics object for use within an iteration. Note this must be
* set to null at the end of each iteration, or the image will not be
* collected
*/
private Graphics2D g;
/** the current graphics state */
private GraphicsState state;
/** the stack of push()ed graphics states */
private Stack<GraphicsState> stack;
/** the total region of this image that has been written to */
private Rectangle2D globalDirtyRegion;
/** the image observers that will be updated when this image changes */
private List<ImageObserver> observers;
/** the last shape we drew (to check for overlaps) */
private GeneralPath lastShape;
/** the info about the image, if we need to recreate it */
private ImageInfo imageinfo;
/** the next time the image should be notified about updates */
private long then = 0;
/** the sum of all the individual dirty regions since the last update */
private Rectangle2D unupdatedRegion;
/** how long (in milliseconds) to wait between image updates */
public static final long UPDATE_DURATION = 200;
public static final float NOPHASE = -1000;
public static final float NOWIDTH = -1000;
public static final float NOLIMIT = -1000;
public static final int NOCAP = -1000;
public static final float[] NODASH = null;
public static final int NOJOIN = -1000;
/**
* create a new PDFGraphics state
* @param page the current page
* @param imageinfo the paramters of the image to render
*/
public PDFRenderer(PDFPage page, ImageInfo imageinfo, BufferedImage bi) {
super();
this.page = page;
this.imageinfo = imageinfo;
this.imageRef = new WeakReference<BufferedImage>(bi);
// initialize the list of observers
observers = new ArrayList<ImageObserver>();
}
/**
* create a new PDFGraphics state, given a Graphics2D. This version
* will <b>not</b> create an image, and you will get a NullPointerException
* if you attempt to call getImage().
* @param page the current page
* @param g the Graphics2D object to use for drawing
* @param imgbounds the bounds of the image into which to fit the page
* @param clip the portion of the page to draw, in page space, or null
* if the whole page should be drawn
* @param bgColor the color to draw the background of the image, or
* null for no color (0 alpha value)
*/
public PDFRenderer(PDFPage page, Graphics2D g, Rectangle imgbounds,
Rectangle2D clip, Color bgColor) {
super();
this.page = page;
this.g = g;
this.imageinfo = new ImageInfo(imgbounds.width, imgbounds.height,
clip, bgColor);
g.translate(imgbounds.x, imgbounds.y);
// System.out.println("Translating by "+imgbounds.x+","+imgbounds.y);
// initialize the list of observers
observers = new ArrayList<ImageObserver>();
}
/**
* Set up the graphics transform to match the clip region
* to the image size.
*/
private void setupRendering(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
if (imageinfo.bgColor != null) {
g.setColor(imageinfo.bgColor);
g.fillRect(0, 0, imageinfo.width, imageinfo.height);
}
g.setColor(Color.BLACK);
// set the initial clip and transform on the graphics
AffineTransform at = getInitialTransform();
g.transform(at);
// set up the initial graphics state
state = new GraphicsState();
state.cliprgn = null;
state.stroke = new BasicStroke();
state.strokePaint = PDFPaint.getColorPaint(Color.black);
state.fillPaint = state.strokePaint;
state.fillAlpha = AlphaComposite.getInstance(AlphaComposite.SRC);
state.strokeAlpha = AlphaComposite.getInstance(AlphaComposite.SRC);
state.xform = g.getTransform();
// initialize the stack
stack = new Stack<GraphicsState>();
// initialize the current command
currentCommand = 0;
}
/**
* push the current graphics state onto the stack. Continue working
* with the current object; calling pop() restores the state of this
* object to its state when push() was called.
*/
public void push() {
state.cliprgn = g.getClip();
stack.push(state);
state = (GraphicsState) state.clone();
}
/**
* restore the state of this object to what it was when the previous
* push() was called.
*/
public void pop() {
state = (GraphicsState) stack.pop();
setTransform(state.xform);
setClip(state.cliprgn);
}
/**
* draw an outline using the current stroke and draw paint
* @param s the path to stroke
* @return a Rectangle2D to which the current region being
* drawn will be added. May also be null, in which case no dirty
* region will be recorded.
*/
public Rectangle2D stroke(GeneralPath s) {
g.setComposite(state.strokeAlpha);
s = new GeneralPath(autoAdjustStrokeWidth(g, state.stroke).createStrokedShape(s));
return state.strokePaint.fill(this, g, s);
}
/**
* auto adjust the stroke width, according to 6.5.4, which presumes that
* the device characteristics (an image) require a single pixel wide
* line, even if the width is set to less. We determine the scaling to
* see if we would produce a line that was too small, and if so, scale
* it up to produce a graphics line of 1 pixel, or so. This matches our
* output with Adobe Reader.
*
* @param g
* @param bs
* @return
*/
private BasicStroke autoAdjustStrokeWidth(Graphics2D g, BasicStroke bs) {
AffineTransform bt = new AffineTransform(g.getTransform());
float width = bs.getLineWidth() * (float) bt.getScaleX();
BasicStroke stroke = bs;
if (width < 1f) {
if (bt.getScaleX() > 0.01) {
width = 1.0f / (float) bt.getScaleX();
} else {
// prevent division by a really small number
width = 1.0f;
}
stroke = new BasicStroke(width,
bs.getEndCap(),
bs.getLineJoin(),
bs.getMiterLimit(),
bs.getDashArray(),
bs.getDashPhase());
}
return stroke;
}
/**
* draw an outline.
* @param p the path to draw
* @param bs the stroke with which to draw the path
*/
public void draw(GeneralPath p, BasicStroke bs) {
g.setComposite(state.fillAlpha);
g.setPaint(state.fillPaint.getPaint());
g.setStroke(autoAdjustStrokeWidth(g, bs));
g.draw(p);
}
/**
* fill an outline using the current fill paint
* @param s the path to fill
*/
public Rectangle2D fill(GeneralPath s) {
g.setComposite(state.fillAlpha);
return state.fillPaint.fill(this, g, s);
}
/**
* draw an image.
* @param image the image to draw
*/
public Rectangle2D drawImage(PDFImage image) {
AffineTransform at = new AffineTransform(1f / image.getWidth(), 0,
0, -1f / image.getHeight(),
0, 1);
BufferedImage bi = image.getImage();
if (bi == null) {
// maybe it was an unsupported format, or something.
// Nothing to draw, anyway!
return new Rectangle2D.Double();
}
if (image.isImageMask()) {
bi = getMaskedImage(bi);
}
/*
javax.swing.JFrame frame = new javax.swing.JFrame("Original Image");
frame.getContentPane().add(new javax.swing.JLabel(new javax.swing.ImageIcon(bi)));
frame.pack();
frame.show();
*/
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
if (!g.drawImage(bi, at, null)) {
System.out.println("Image not completed!");
}
// get the total transform that was executed
AffineTransform bt = new AffineTransform(g.getTransform());
bt.concatenate(at);
double minx = bi.getMinX();
double miny = bi.getMinY();
double[] points = new double[]{
minx, miny, minx + bi.getWidth(), miny + bi.getHeight()
};
bt.transform(points, 0, points, 0, 2);
return new Rectangle2D.Double(points[0], points[1],
points[2] - points[0],
points[3] - points[1]);
}
/**
* add the path to the current clip. The new clip will be the intersection
* of the old clip and given path.
*/
public void clip(GeneralPath s) {
g.clip(s);
}
/**
* set the clip to be the given shape. The current clip is not taken
* into account.
*/
private void setClip(Shape s) {
state.cliprgn = s;
g.setClip(null);
g.clip(s);
}
/**
* get the current affinetransform
*/
public AffineTransform getTransform() {
return state.xform;
}
/**
* concatenate the given transform with the current transform
*/
public void transform(AffineTransform at) {
state.xform.concatenate(at);
g.setTransform(state.xform);
}
/**
* replace the current transform with the given one.
*/
public void setTransform(AffineTransform at) {
state.xform = at;
g.setTransform(state.xform);
}
/**
* get the initial transform from page space to Java space
*/
public AffineTransform getInitialTransform() {
return page.getInitialTransform(imageinfo.width,
imageinfo.height,
imageinfo.clip);
}
/**
* Set some or all aspects of the current stroke.
* @param w the width of the stroke, or NOWIDTH to leave it unchanged
* @param cap the end cap style, or NOCAP to leave it unchanged
* @param join the join style, or NOJOIN to leave it unchanged
* @param limit the miter limit, or NOLIMIT to leave it unchanged
* @param phase the phase of the dash array, or NOPHASE to leave it
* unchanged
* @param ary the dash array, or null to leave it unchanged. phase
* and ary must both be valid, or phase must be NOPHASE while ary is null.
*/
public void setStrokeParts(float w, int cap, int join, float limit, float[] ary, float phase) {
if (w == NOWIDTH) {
w = state.stroke.getLineWidth();
}
if (cap == NOCAP) {
cap = state.stroke.getEndCap();
}
if (join == NOJOIN) {
join = state.stroke.getLineJoin();
}
if (limit == NOLIMIT) {
limit = state.stroke.getMiterLimit();
}
if (phase == NOPHASE) {
ary = state.stroke.getDashArray();
phase = state.stroke.getDashPhase();
}
if (ary != null && ary.length == 0) {
ary = null;
}
if (phase == NOPHASE) {
state.stroke = new BasicStroke(w, cap, join, limit);
} else {
state.stroke = new BasicStroke(w, cap, join, limit, ary, phase);
}
}
/**
* get the current stroke as a BasicStroke
*/
public BasicStroke getStroke() {
return state.stroke;
}
/**
* set the current stroke as a BasicStroke
*/
public void setStroke(BasicStroke bs) {
state.stroke = bs;
}
/**
* set the stroke color
*/
public void setStrokePaint(PDFPaint paint) {
state.strokePaint = paint;
}
/**
* set the fill color
*/
public void setFillPaint(PDFPaint paint) {
state.fillPaint = paint;
}
/**
* set the stroke alpha
*/
public void setStrokeAlpha(float alpha) {
state.strokeAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha);
}
/**
* set the stroke alpha
*/
public void setFillAlpha(float alpha) {
state.fillAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha);
}
/**
* Add an image observer
*/
public void addObserver(ImageObserver observer) {
if (observer == null) {
return;
}
// update the new observer to the current state
Image i = (Image) imageRef.get();
if (rendererFinished()) {
// if we're finished, just send a finished notification, don't
// add to the list of observers
// System.out.println("Late notify");
observer.imageUpdate(i, ImageObserver.ALLBITS, 0, 0,
imageinfo.width, imageinfo.height);
return;
} else {
// if we're not yet finished, add to the list of observers and
// notify of the current dirty region
synchronized (observers) {
observers.add(observer);
}
if (globalDirtyRegion != null) {
observer.imageUpdate(i, ImageObserver.SOMEBITS,
(int) globalDirtyRegion.getMinX(),
(int) globalDirtyRegion.getMinY(),
(int) globalDirtyRegion.getWidth(),
(int) globalDirtyRegion.getHeight());
}
}
}
/**
* Remove an image observer
*/
public void removeObserver(ImageObserver observer) {
synchronized (observers) {
observers.remove(observer);
}
}
/**
* Set the last shape drawn
*/
public void setLastShape(GeneralPath shape) {
this.lastShape = shape;
}
/**
* Get the last shape drawn
*/
public GeneralPath getLastShape() {
return lastShape;
}
/**
* Setup rendering. Called before iteration begins
*/
@Override
public void setup() {
Graphics2D graphics = null;
if (imageRef != null) {
BufferedImage bi = (BufferedImage) imageRef.get();
if (bi != null) {
graphics = bi.createGraphics();
}
} else {
graphics = g;
}
if (graphics != null) {
setupRendering(graphics);
}
}
/**
* Draws the next command in the PDFPage to the buffered image.
* The image will be notified about changes no less than every
* UPDATE_DURATION milliseconds.
*
* @return <ul><li>Watchable.RUNNING when there are commands to be processed
* <li>Watchable.NEEDS_DATA when there are no commands to be
* processed, but the page is not yet complete
* <li>Watchable.COMPLETED when the page is done and all
* the commands have been processed
* <li>Watchable.STOPPED if the image we are rendering into
* has gone away
* </ul>
*/
public int iterate() throws Exception {
// make sure we have a page to render
if (page == null) {
return Watchable.COMPLETED;
}
// check if this renderer is based on a weak reference to a graphics
// object. If it is, and the graphics is no longer valid, then just quit
BufferedImage bi = null;
if (imageRef != null) {
bi = (BufferedImage) imageRef.get();
if (bi == null) {
System.out.println("Image went away. Stopping");
return Watchable.STOPPED;
}
g = (Graphics2D) bi.createGraphics();
}
// check if there are any commands to parse. If there aren't,
// just return, but check if we'return really finished or not
if (currentCommand >= page.getCommandCount()) {
if (page.isFinished()) {
return Watchable.COMPLETED;
} else {
return Watchable.NEEDS_DATA;
}
}
// find the current command
PDFCmd cmd = page.getCommand(currentCommand++);
if (cmd == null) {
// uh oh. Synchronization problem!
throw new PDFParseException("Command not found!");
}
// execute the command
Rectangle2D dirtyRegion = cmd.execute(this);
// append to the global dirty region
globalDirtyRegion = addDirtyRegion(dirtyRegion, globalDirtyRegion);
unupdatedRegion = addDirtyRegion(dirtyRegion, unupdatedRegion);
long now = System.currentTimeMillis();
if (now > then || rendererFinished()) {
// now tell any observers, so they can repaint
notifyObservers(bi, unupdatedRegion);
unupdatedRegion = null;
then = now + UPDATE_DURATION;
}
// if we are based on a reference to a graphics, don't hold on to it
// since that will prevent the image from being collected.
if (imageRef != null) {
g = null;
}
// if we need to stop, it will be caught at the start of the next
// iteration.
return Watchable.RUNNING;
}
/**
* Called when iteration has stopped
*/
@Override
public void cleanup() {
page = null;
state = null;
stack = null;
globalDirtyRegion = null;
lastShape = null;
observers.clear();
// keep around the image ref and image info for use in
// late addObserver() call
}
/**
* Append a rectangle to the total dirty region of this shape
*/
private Rectangle2D addDirtyRegion(Rectangle2D region, Rectangle2D glob) {
if (region == null) {
return glob;
} else if (glob == null) {
return region;
} else {
Rectangle2D.union(glob, region, glob);
return glob;
}
}
/**
* Determine if we are finished
*/
private boolean rendererFinished() {
if (page == null) {
return true;
}
return (page.isFinished() && currentCommand == page.getCommandCount());
}
/**
* Notify the observer that a region of the image has changed
*/
private void notifyObservers(BufferedImage bi, Rectangle2D region) {
if (bi == null) {
return;
}
int startx, starty, width, height;
int flags = 0;
// don't do anything if nothing is there or no one is listening
if ((region == null && !rendererFinished()) || observers == null ||
observers.size() == 0) {
return;
}
if (region != null) {
// get the image data for the total dirty region
startx = (int) Math.floor(region.getMinX());
starty = (int) Math.floor(region.getMinY());
width = (int) Math.ceil(region.getWidth());
height = (int) Math.ceil(region.getHeight());
// sometimes width or height is negative. Grrr...
if (width < 0) {
startx += width;
width = -width;
}
if (height < 0) {
starty += height;
height = -height;
}
flags = 0;
} else {
startx = 0;
starty = 0;
width = imageinfo.width;
height = imageinfo.height;
}
if (rendererFinished()) {
flags |= ImageObserver.ALLBITS;
// forget about the Graphics -- allows the image to be
// garbage collected.
g = null;
} else {
flags |= ImageObserver.SOMEBITS;
}
synchronized (observers) {
for (Iterator i = observers.iterator(); i.hasNext();) {
ImageObserver observer = (ImageObserver) i.next();
boolean result = observer.imageUpdate(bi, flags,
startx, starty,
width, height);
// if result is false, the observer no longer wants to
// be notified of changes
if (!result) {
i.remove();
}
}
}
}
/**
* Convert an image mask into an image by painting over any pixels
* that have a value in the image with the current paint
*/
private BufferedImage getMaskedImage(BufferedImage bi) {
// get the color of the current paint
final Paint paint = state.fillPaint.getPaint();
if (!(paint instanceof Color)) {
// TODO - support other types of Paint
return bi;
}
Color col = (Color) paint;
// format as 8 bits each of ARGB
int paintColor = col.getAlpha() << 24;
paintColor |= col.getRed() << 16;
paintColor |= col.getGreen() << 8;
paintColor |= col.getBlue();
// transparent (alpha = 1)
int noColor = 0;
// get the coordinates of the source image
int startX = bi.getMinX();
int startY = bi.getMinY();
int width = bi.getWidth();
int height = bi.getHeight();
// create a destion image of the same size
BufferedImage dstImage =
new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// copy the pixels row by row
for (int i = 0; i < height; i++) {
int[] srcPixels = new int[width];
int[] dstPixels = new int[srcPixels.length];
// read a row of pixels from the source
bi.getRGB(startX, startY + i, width, 1, srcPixels, 0, height);
// figure out which ones should get painted
for (int j = 0; j < srcPixels.length; j++) {
if (srcPixels[j] == 0xff000000) {
dstPixels[j] = paintColor;
} else {
dstPixels[j] = noColor;
}
}
// write the destination image
dstImage.setRGB(startX, startY + i, width, 1, dstPixels, 0, height);
}
return dstImage;
}
class GraphicsState implements Cloneable {
/** the clip region */
Shape cliprgn;
/** the current stroke */
BasicStroke stroke;
/** the current paint for drawing strokes */
PDFPaint strokePaint;
/** the current paint for filling shapes */
PDFPaint fillPaint;
/** the current compositing alpha for stroking */
AlphaComposite strokeAlpha;
/** the current compositing alpha for filling */
AlphaComposite fillAlpha;
/** the current transform */
AffineTransform xform;
/** Clone this Graphics state.
*
* Note that cliprgn is not cloned. It must be set manually from
* the current graphics object's clip
*/
@Override
public Object clone() {
GraphicsState cState = new GraphicsState();
cState.cliprgn = null;
// copy immutable fields
cState.strokePaint = strokePaint;
cState.fillPaint = fillPaint;
cState.strokeAlpha = strokeAlpha;
cState.fillAlpha = fillAlpha;
// clone mutable fields
cState.stroke = new BasicStroke(stroke.getLineWidth(),
stroke.getEndCap(),
stroke.getLineJoin(),
stroke.getMiterLimit(),
stroke.getDashArray(),
stroke.getDashPhase());
cState.xform = (AffineTransform) xform.clone();
return cState;
}
}
}