All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openpdf.renderer.PDFRenderer Maven / Gradle / Ivy

The newest version!
/*
 * 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 org.openpdf.renderer;

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.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ConvolveOp;
import java.awt.image.ImageObserver;
import java.awt.image.IndexColorModel;
import java.awt.image.Kernel;
import java.awt.image.WritableRaster;
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 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 final List observers;
    /** the last shape we drew (to check for overlaps) */
    private GeneralPath lastShape;
    private AffineTransform lastTransform;
    /** the info about the image, if we need to recreate it */
    private final 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(bi);

        // initialize the list of observers
        this.observers = new ArrayList();
    }

    /**
     * create a new PDFGraphics state, given a Graphics2D. This version
     * will not 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);

        // initialize the list of observers
        this.observers = new ArrayList();
    }

    /**
     * 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 (this.imageinfo.bgColor != null) {
            g.setColor(this.imageinfo.bgColor);
            g.fillRect(0, 0, this.imageinfo.width, this.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
        this.state = new GraphicsState();
        this.state.cliprgn = null;
        this.state.stroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
        this.state.strokePaint = PDFPaint.getColorPaint(Color.black);
        this.state.fillPaint = this.state.strokePaint;
        this.state.fillAlpha = AlphaComposite.getInstance(AlphaComposite.SRC);
        this.state.strokeAlpha = AlphaComposite.getInstance(AlphaComposite.SRC);
        this.state.xform = g.getTransform();

        // initialize the stack
        this.stack = new Stack();

        // initialize the current command
        this.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() {
        this.state.cliprgn = this.g.getClip();
        this.stack.push(this.state);

        this.state = (GraphicsState) this.state.clone();
    }

    /**
     * restore the state of this object to what it was when the previous
     * push() was called.
     */
    public void pop() {
    	if(this.stack.isEmpty() == false) {
            this.state = this.stack.pop();
    	}

        setTransform(this.state.xform);
        setClip(this.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, boolean autoAdjustStroke) {
        // TODO: consider autoAdjustStroke here instead of during parsing
        //      PDF specification p. 130 / > 10.6.5 
        this.g.setComposite(this.state.strokeAlpha);
        s = new GeneralPath(autoAdjustStrokeWidth(this.g, this.state.stroke).createStrokedShape(s));
        return this.state.strokePaint.fill(this, 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 = stroke.getLineWidth()<1f?1.0f:stroke.getLineWidth();
            }
            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) {
        this.g.setComposite(this.state.fillAlpha);
        this.g.setPaint(this.state.fillPaint.getPaint());
        this.g.setStroke(autoAdjustStrokeWidth(this.g, bs));
        this.g.draw(p);
    }

    /**
     * fill an outline using the current fill paint
     * @param s the path to fill
     */
    public Rectangle2D fill(GeneralPath s) {
        this.g.setComposite(this.state.fillAlpha);
        if (s == null) {
        	GraphicsState gs =  stack.peek();
          if (gs.cliprgn != null) {
          	s = new GeneralPath(gs.cliprgn);
          }
        }
        return this.state.fillPaint.fill(this, this.g, s);
    }

    /**
     * draw an image.
     * @param image the image to draw
     */
    public Rectangle2D drawImage(PDFImage image) {

        BufferedImage bi;
        try {
            bi = image.getImage();
        }catch (PDFImageParseException e) {
            // maybe it was an unsupported format, or something.
            // Nothing to draw, anyway!
            return new Rectangle2D.Double();
        }
    	
    	// transform must use bitmap size
        AffineTransform at = new AffineTransform(1f / bi.getWidth(), 0,
                0, -1f / bi.getHeight(),
                0, 1);

        if (image.isImageMask()) {
        	bi = getMaskedImage(bi);
        }

        Rectangle r = g.getTransform().createTransformedShape(new Rectangle(0,0,1,1)).getBounds();
        boolean isBlured = false;
        
        if (Configuration.getInstance().isUseBlurResizingForImages() && 
        		bi.getType() != BufferedImage.TYPE_CUSTOM && 
        		bi.getWidth() >= 1.75*r.getWidth() && bi.getHeight() >= 1.75*r.getHeight()){
        	try {
            	return smartDrawImage(image, bi, r, at);
        	}catch (Exception e) {
				// do nothing, just go on with the "default" processing 
			}
        }
        
        this.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
                
        //Image quality is better when using texturepaint instead of drawimage
        //but it is also slower :(
		this.g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
				RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        // banded rendering may lead to lower memory consumption for e.g. scanned PDFs with large images
        int bandSize = Configuration.getInstance().getThresholdForBandedImageRendering();
        if (bandSize > 0 && bi.getHeight() > bandSize) {
           // draw in bands
           int tempMax = bi.getHeight();
           for (int offset=0; offset= 1.75*r.getWidth() && bi.getHeight() >= 1.75*r.getHeight()){

        	BufferedImageOp op;
        	// indexed colored images need to be converted for the convolveOp
        	boolean colorConversion = (bi.getColorModel() instanceof IndexColorModel);
        	final float maxFactor = 3.5f;
        	final boolean RESIZE = true;
        	if (bi.getWidth() > maxFactor*r.getWidth() && bi.getHeight() > maxFactor*r.getHeight()){
        		//First resize, otherwise we risk that we get out of heapspace
        		int newHeight = (int)Math.round(maxFactor*r.getHeight());
        		int newWidth = (int)Math.round(maxFactor*r.getWidth());
        		if (!RESIZE) {
        			newHeight = bi.getHeight();
        			newWidth = bi.getWidth();
        		}
        		BufferedImage resized = new BufferedImage(newWidth, 
        				newHeight, colorConversion?BufferedImage.TYPE_INT_ARGB:bi.getType());
        		Graphics2D bg = (Graphics2D) resized.getGraphics();
        		bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
        				RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        		bg.drawImage(bi, 0, 0, newWidth, newHeight, null);
        		bi = resized;
                at = new AffineTransform(1f / bi.getWidth(), 0,
                        0, -1f / bi.getHeight(),
                        0, 1);
                
                final float weight = 1.0f/16.0f;
            	final float[] blurKernel = {
            			weight, weight, weight, weight,
            			weight, weight, weight, weight,
            			weight, weight, weight, weight,
            			weight, weight, weight, weight,
            	};
            	op = new ConvolveOp(new Kernel(4, 4, blurKernel), ConvolveOp.EDGE_NO_OP, null);            	
        	}
        	else {
        		final float weight = 1.0f/18.0f;
        		final float[] blurKernel = {
        				1*weight, 2*weight, 1*weight,
        				2*weight, 6*weight, 2*weight,
        				1*weight, 2*weight, 1*weight
        		};
        		if (colorConversion) {
            		BufferedImage colored = new BufferedImage(bi.getWidth(), 
            				bi.getHeight(), colorConversion?BufferedImage.TYPE_INT_ARGB:bi.getType());
            		Graphics2D bg = (Graphics2D) colored.getGraphics();
            		bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
            				RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            		bg.drawImage(bi, 0, 0, bi.getWidth(), bi.getHeight(), null);
            		bi = colored;
        		}
        		op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null);
        	}
        	
        	BufferedImage blured = op.createCompatibleDestImage(bi, 
        			colorConversion?ColorModel.getRGBdefault():bi.getColorModel());
        	
           	op.filter(bi, blured);
        	bi = blured;
        	isBlured = true;
        }
        
        this.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
                
        //Image quality is better when using texturepaint instead of drawimage
        //but it is also slower :(
		this.g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
				RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        // banded rendering may lead to lower memory consumption for e.g. scanned PDFs with large images
        int bandSize = Configuration.getInstance().getThresholdForBandedImageRendering();
        if (bandSize > 0 && bi.getHeight() > bandSize) {
           // draw in bands
           int tempMax = bi.getHeight();
           for (int offset=0; offset
  • Watchable.RUNNING when there are commands to be processed *
  • Watchable.NEEDS_DATA when there are no commands to be * processed, but the page is not yet complete *
  • Watchable.COMPLETED when the page is done and all * the commands have been processed *
  • Watchable.STOPPED if the image we are rendering into * has gone away * */ @Override public int iterate() throws Exception { // make sure we have a page to render if (this.page == null) { return 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 (this.imageRef != null) { bi = this.imageRef.get(); if (bi == null) { PDFDebugger.debug("Image went away. Stopping"); return STOPPED; } this.g = 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 (this.currentCommand >= this.page.getCommandCount()) { if (this.page.isFinished()) { return COMPLETED; } else { return NEEDS_DATA; } } // find the current command PDFCmd cmd = this.page.getCommand(this.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 this.globalDirtyRegion = addDirtyRegion(dirtyRegion, this.globalDirtyRegion); this.unupdatedRegion = addDirtyRegion(dirtyRegion, this.unupdatedRegion); long now = System.currentTimeMillis(); if (now > this.then || rendererFinished()) { // now tell any observers, so they can repaint notifyObservers(bi, this.unupdatedRegion); this.unupdatedRegion = null; this.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 (this.imageRef != null) { this.g = null; } // if we need to stop, it will be caught at the start of the next // iteration. return RUNNING; } /** * Called when iteration has stopped */ @Override public void cleanup() { this.page = null; this.state = null; this.stack = null; this.globalDirtyRegion = null; this.lastShape = null; this.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 (this.page == null) { return true; } return (this.page.isFinished() && this.currentCommand == this.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()) || this.observers == null || this.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 = this.imageinfo.width; height = this.imageinfo.height; } if (rendererFinished()) { flags |= ImageObserver.ALLBITS; // forget about the Graphics -- allows the image to be // garbage collected. this.g = null; } else { flags |= ImageObserver.SOMEBITS; } synchronized (this.observers) { for (Iterator i = this.observers.iterator(); i.hasNext();) { ImageObserver observer = 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; ColorModel colorModel = bi.getColorModel(); if (colorModel instanceof IndexColorModel) { int mapSize = ((IndexColorModel) colorModel).getMapSize(); int pixelSize = colorModel.getPixelSize(); if (mapSize == 2 && pixelSize == 1) { // we have a monochrome image mask with 1 bit per pixel // swap out the standard color with the current paint color int[] rgbValues = new int[2]; ((IndexColorModel) colorModel).getRGBs(rgbValues); byte[] colorComponents = null; if (rgbValues[0] == 0xff000000) { // normal case color at 0 colorComponents = new byte[]{ (byte) col.getRed(), (byte) col.getGreen(), (byte) col.getBlue(), (byte) col.getAlpha(), 0, 0, 0, 0 // the background is transparent }; } else if (rgbValues[1] == 0xff000000){ // alternate case color at 1 colorComponents = new byte[]{ 0, 0, 0, 0, // the background is transparent (byte) col.getRed(), (byte) col.getGreen(), (byte) col.getBlue(), (byte) col.getAlpha() }; } if (colorComponents != null) { // replace mapped colors int startIndex = 0; boolean hasAlpha = true; ColorModel replacementColorModel = new IndexColorModel(pixelSize, mapSize, colorComponents, startIndex, hasAlpha); WritableRaster raster = bi.getRaster(); BufferedImage adaptedImage = new BufferedImage(replacementColorModel, raster, false, null); return adaptedImage; } else { return bi; // no color replacement } } } // 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 = this.strokePaint; cState.fillPaint = this.fillPaint; cState.strokeAlpha = this.strokeAlpha; cState.fillAlpha = this.fillAlpha; // clone mutable fields cState.stroke = new BasicStroke(this.stroke.getLineWidth(), this.stroke.getEndCap(), this.stroke.getLineJoin(), this.stroke.getMiterLimit(), this.stroke.getDashArray(), this.stroke.getDashPhase()); cState.xform = (AffineTransform) this.xform.clone(); return cState; } } /************************************************************************* * @return Returns the lastTransform. ************************************************************************/ public AffineTransform getLastTransform() { return this.lastTransform; } /************************************************************************* * Remember the current transformation ************************************************************************/ public void rememberTransformation() { this.lastTransform = this.state.xform; } }




  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy