/* * $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; } } }