001: /*
002: * $Id: PDFRenderer.java,v 1.3 2007/12/20 18:17:41 rbair Exp $
003: *
004: * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005: * Santa Clara, California 95054, U.S.A. All rights reserved.
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this library; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020: */
021:
022: package com.sun.pdfview;
023:
024: import java.awt.AlphaComposite;
025: import java.awt.BasicStroke;
026: import java.awt.Color;
027: import java.awt.Graphics2D;
028: import java.awt.Image;
029: import java.awt.Rectangle;
030: import java.awt.RenderingHints;
031: import java.awt.Shape;
032: import java.awt.geom.AffineTransform;
033: import java.awt.geom.GeneralPath;
034: import java.awt.geom.Rectangle2D;
035: import java.awt.image.BufferedImage;
036: import java.awt.image.ImageObserver;
037: import java.lang.ref.WeakReference;
038: import java.util.ArrayList;
039: import java.util.Iterator;
040: import java.util.List;
041: import java.util.Stack;
042:
043: /**
044: * This class turns a set of PDF Commands from a PDF page into an image. It
045: * encapsulates the state of drawing in terms of stroke, fill, transform,
046: * etc., as well as pushing and popping these states.
047: *
048: * When the run method is called, this class goes through all remaining commands
049: * in the PDF Page and draws them to its buffered image. It then updates any
050: * ImageConsumers with the drawn data.
051: */
052: public class PDFRenderer extends BaseWatchable implements Runnable {
053: /** the page we were generate from */
054: private PDFPage page;
055:
056: /** where we are in the page's command list */
057: private int currentCommand;
058:
059: /** a weak reference to the image we render into. For the image
060: * to remain available, some other code must retain a strong reference to it.
061: */
062: private WeakReference imageRef;
063:
064: /** the graphics object for use within an iteration. Note this must be
065: * set to null at the end of each iteration, or the image will not be
066: * collected
067: */
068: private Graphics2D g;
069:
070: /** the current graphics state */
071: private GraphicsState state;
072:
073: /** the stack of push()ed graphics states */
074: private Stack stack;
075:
076: /** the total region of this image that has been written to */
077: private Rectangle2D globalDirtyRegion;
078:
079: /** the image observers that will be updated when this image changes */
080: private List observers;
081:
082: /** the last shape we drew (to check for overlaps) */
083: private GeneralPath lastShape;
084:
085: /** the info about the image, if we need to recreate it */
086: private ImageInfo imageinfo;
087:
088: /** the next time the image should be notified about updates */
089: private long then = 0;
090:
091: /** the sum of all the individual dirty regions since the last update */
092: private Rectangle2D unupdatedRegion;
093:
094: /** how long (in milliseconds) to wait between image updates */
095: public static final long UPDATE_DURATION = 200;
096:
097: public static final float NOPHASE = -1000;
098: public static final float NOWIDTH = -1000;
099: public static final float NOLIMIT = -1000;
100: public static final int NOCAP = -1000;
101: public static final float[] NODASH = null;
102: public static final int NOJOIN = -1000;
103:
104: /**
105: * create a new PDFGraphics state
106: * @param page the current page
107: * @param imageinfo the paramters of the image to render
108: */
109: public PDFRenderer(PDFPage page, ImageInfo imageinfo,
110: BufferedImage bi) {
111: super ();
112:
113: this .page = page;
114: this .imageinfo = imageinfo;
115: this .imageRef = new WeakReference(bi);
116:
117: // initialize the list of observers
118: observers = new ArrayList();
119: }
120:
121: /**
122: * create a new PDFGraphics state, given a Graphics2D. This version
123: * will <b>not</b> create an image, and you will get a NullPointerException
124: * if you attempt to call getImage().
125: * @param page the current page
126: * @param g the Graphics2D object to use for drawing
127: * @param imgbounds the bounds of the image into which to fit the page
128: * @param clip the portion of the page to draw, in page space, or null
129: * if the whole page should be drawn
130: * @param bgColor the color to draw the background of the image, or
131: * null for no color (0 alpha value)
132: */
133: public PDFRenderer(PDFPage page, Graphics2D g, Rectangle imgbounds,
134: Rectangle2D clip, Color bgColor) {
135: super ();
136:
137: this .page = page;
138: this .g = g;
139: this .imageinfo = new ImageInfo(imgbounds.width,
140: imgbounds.height, clip);
141: g.translate(imgbounds.x, imgbounds.y);
142: // System.out.println("Translating by "+imgbounds.x+","+imgbounds.y);
143:
144: // initialize the list of observers
145: observers = new ArrayList();
146: }
147:
148: /**
149: * Set up the graphics transform to match the clip region
150: * to the image size.
151: */
152: private void setupRendering(Graphics2D g) {
153: g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
154: RenderingHints.VALUE_ANTIALIAS_ON);
155: g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
156: RenderingHints.VALUE_INTERPOLATION_BICUBIC);
157: g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
158: RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
159:
160: if (imageinfo.bgColor != null) {
161: g.setColor(imageinfo.bgColor);
162: g.fillRect(0, 0, imageinfo.width, imageinfo.height);
163: }
164:
165: g.setColor(Color.BLACK);
166:
167: // set the initial clip and transform on the graphics
168: AffineTransform at = getInitialTransform();
169: g.transform(at);
170:
171: // set up the initial graphics state
172: state = new GraphicsState();
173: state.cliprgn = null;
174: state.stroke = new BasicStroke();
175: state.strokePaint = PDFPaint.getColorPaint(Color.black);
176: state.fillPaint = state.strokePaint;
177: state.fillAlpha = AlphaComposite
178: .getInstance(AlphaComposite.SRC);
179: state.strokeAlpha = AlphaComposite
180: .getInstance(AlphaComposite.SRC);
181: state.xform = g.getTransform();
182:
183: // initialize the stack
184: stack = new Stack();
185:
186: // initialize the current command
187: currentCommand = 0;
188: }
189:
190: /**
191: * push the current graphics state onto the stack. Continue working
192: * with the current object; calling pop() restores the state of this
193: * object to its state when push() was called.
194: */
195: public void push() {
196: state.cliprgn = g.getClip();
197: stack.push(state);
198:
199: state = (GraphicsState) state.clone();
200: }
201:
202: /**
203: * restore the state of this object to what it was when the previous
204: * push() was called.
205: */
206: public void pop() {
207: state = (GraphicsState) stack.pop();
208:
209: setTransform(state.xform);
210: setClip(state.cliprgn);
211: }
212:
213: /**
214: * draw an outline using the current stroke and draw paint
215: * @param s the path to stroke
216: * @return a Rectangle2D to which the current region being
217: * drawn will be added. May also be null, in which case no dirty
218: * region will be recorded.
219: */
220: public Rectangle2D stroke(GeneralPath s) {
221: g.setComposite(state.strokeAlpha);
222: s = new GeneralPath(state.stroke.createStrokedShape(s));
223: return state.strokePaint.fill(this , g, s);
224: }
225:
226: /**
227: * draw an outline.
228: * @param p the path to draw
229: * @param bs the stroke with which to draw the path
230: */
231: public void draw(GeneralPath p, BasicStroke bs) {
232: g.setComposite(state.fillAlpha);
233: g.setPaint(state.fillPaint.getPaint());
234: g.setStroke(bs);
235: g.draw(p);
236: }
237:
238: /**
239: * fill an outline using the current fill paint
240: * @param s the path to fill
241: */
242: public Rectangle2D fill(GeneralPath s) {
243: g.setComposite(state.fillAlpha);
244: return state.fillPaint.fill(this , g, s);
245: }
246:
247: /**
248: * draw an image.
249: * @param image the image to draw
250: */
251: public Rectangle2D drawImage(PDFImage image) {
252: AffineTransform at = new AffineTransform(1f / image.getWidth(),
253: 0, 0, -1f / image.getHeight(), 0, 1);
254:
255: BufferedImage bi = image.getImage();
256: if (image.isImageMask()) {
257: bi = getMaskedImage(bi);
258: }
259:
260: /*
261: javax.swing.JFrame frame = new javax.swing.JFrame("Original Image");
262: frame.getContentPane().add(new javax.swing.JLabel(new javax.swing.ImageIcon(bi)));
263: frame.pack();
264: frame.show();
265: */
266:
267: g.setComposite(AlphaComposite
268: .getInstance(AlphaComposite.SRC_OVER));
269: if (!g.drawImage(bi, at, null)) {
270: System.out.println("Image not completed!");
271: }
272:
273: // get the total transform that was executed
274: AffineTransform bt = new AffineTransform(g.getTransform());
275: bt.concatenate(at);
276:
277: double minx = bi.getMinX();
278: double miny = bi.getMinY();
279:
280: double[] points = new double[] { minx, miny,
281: minx + bi.getWidth(), miny + bi.getHeight() };
282: bt.transform(points, 0, points, 0, 2);
283:
284: return new Rectangle2D.Double(points[0], points[1], points[2]
285: - points[0], points[3] - points[1]);
286:
287: }
288:
289: /**
290: * add the path to the current clip. The new clip will be the intersection
291: * of the old clip and given path.
292: */
293: public void clip(GeneralPath s) {
294: g.clip(s);
295: }
296:
297: /**
298: * set the clip to be the given shape. The current clip is not taken
299: * into account.
300: */
301: private void setClip(Shape s) {
302: state.cliprgn = s;
303: g.setClip(null);
304: g.clip(s);
305: }
306:
307: /**
308: * get the current affinetransform
309: */
310: public AffineTransform getTransform() {
311: return state.xform;
312: }
313:
314: /**
315: * concatenate the given transform with the current transform
316: */
317: public void transform(AffineTransform at) {
318: state.xform.concatenate(at);
319: g.setTransform(state.xform);
320: }
321:
322: /**
323: * replace the current transform with the given one.
324: */
325: public void setTransform(AffineTransform at) {
326: state.xform = at;
327: g.setTransform(state.xform);
328: }
329:
330: /**
331: * get the initial transform from page space to Java space
332: */
333: public AffineTransform getInitialTransform() {
334: return page.getInitialTransform(imageinfo.width,
335: imageinfo.height, imageinfo.clip);
336: }
337:
338: /**
339: * Set some or all aspects of the current stroke.
340: * @param w the width of the stroke, or NOWIDTH to leave it unchanged
341: * @param cap the end cap style, or NOCAP to leave it unchanged
342: * @param join the join style, or NOJOIN to leave it unchanged
343: * @param limit the miter limit, or NOLIMIT to leave it unchanged
344: * @param phase the phase of the dash array, or NOPHASE to leave it
345: * unchanged
346: * @param ary the dash array, or null to leave it unchanged. phase
347: * and ary must both be valid, or phase must be NOPHASE while ary is null.
348: */
349: public void setStrokeParts(float w, int cap, int join, float limit,
350: float[] ary, float phase) {
351: if (w == NOWIDTH) {
352: w = state.stroke.getLineWidth();
353: }
354: if (cap == NOCAP) {
355: cap = state.stroke.getEndCap();
356: }
357: if (join == NOJOIN) {
358: join = state.stroke.getLineJoin();
359: }
360: if (limit == NOLIMIT) {
361: limit = state.stroke.getMiterLimit();
362: }
363: if (phase == NOPHASE) {
364: ary = state.stroke.getDashArray();
365: phase = state.stroke.getDashPhase();
366: }
367: if (ary != null && ary.length == 0) {
368: ary = null;
369: }
370: if (phase == NOPHASE) {
371: state.stroke = new BasicStroke(w, cap, join, limit);
372: } else {
373: state.stroke = new BasicStroke(w, cap, join, limit, ary,
374: phase);
375: }
376: }
377:
378: /**
379: * get the current stroke as a BasicStroke
380: */
381: public BasicStroke getStroke() {
382: return state.stroke;
383: }
384:
385: /**
386: * set the current stroke as a BasicStroke
387: */
388: public void setStroke(BasicStroke bs) {
389: state.stroke = bs;
390: }
391:
392: /**
393: * set the stroke color
394: */
395: public void setStrokePaint(PDFPaint paint) {
396: state.strokePaint = paint;
397: }
398:
399: /**
400: * set the fill color
401: */
402: public void setFillPaint(PDFPaint paint) {
403: state.fillPaint = paint;
404: }
405:
406: /**
407: * set the stroke alpha
408: */
409: public void setStrokeAlpha(float alpha) {
410: state.strokeAlpha = AlphaComposite.getInstance(
411: AlphaComposite.SRC_OVER, alpha);
412: }
413:
414: /**
415: * set the stroke alpha
416: */
417: public void setFillAlpha(float alpha) {
418: state.fillAlpha = AlphaComposite.getInstance(
419: AlphaComposite.SRC_OVER, alpha);
420: }
421:
422: /**
423: * Add an image observer
424: */
425: public void addObserver(ImageObserver observer) {
426: if (observer == null) {
427: return;
428: }
429:
430: // update the new observer to the current state
431: Image i = (Image) imageRef.get();
432: if (rendererFinished()) {
433: // if we're finished, just send a finished notification, don't
434: // add to the list of observers
435: // System.out.println("Late notify");
436: observer.imageUpdate(i, ImageObserver.ALLBITS, 0, 0,
437: imageinfo.width, imageinfo.height);
438: return;
439: } else {
440: // if we're not yet finished, add to the list of observers and
441: // notify of the current dirty region
442: synchronized (observers) {
443: observers.add(observer);
444: }
445:
446: if (globalDirtyRegion != null) {
447: observer.imageUpdate(i, ImageObserver.SOMEBITS,
448: (int) globalDirtyRegion.getMinX(),
449: (int) globalDirtyRegion.getMinY(),
450: (int) globalDirtyRegion.getWidth(),
451: (int) globalDirtyRegion.getHeight());
452: }
453: }
454: }
455:
456: /**
457: * Remove an image observer
458: */
459: public void removeObserver(ImageObserver observer) {
460: synchronized (observers) {
461: observers.remove(observer);
462: }
463: }
464:
465: /**
466: * Set the last shape drawn
467: */
468: public void setLastShape(GeneralPath shape) {
469: this .lastShape = shape;
470: }
471:
472: /**
473: * Get the last shape drawn
474: */
475: public GeneralPath getLastShape() {
476: return lastShape;
477: }
478:
479: /**
480: * Setup rendering. Called before iteration begins
481: */
482: @Override
483: public void setup() {
484: Graphics2D graphics = null;
485:
486: if (imageRef != null) {
487: BufferedImage bi = (BufferedImage) imageRef.get();
488: if (bi != null) {
489: graphics = bi.createGraphics();
490: }
491: } else {
492: graphics = g;
493: }
494:
495: if (graphics != null) {
496: setupRendering(graphics);
497: }
498: }
499:
500: /**
501: * Draws the next command in the PDFPage to the buffered image.
502: * The image will be notified about changes no less than every
503: * UPDATE_DURATION milliseconds.
504: *
505: * @return <ul><li>Watchable.RUNNING when there are commands to be processed
506: * <li>Watchable.NEEDS_DATA when there are no commands to be
507: * processed, but the page is not yet complete
508: * <li>Watchable.COMPLETED when the page is done and all
509: * the commands have been processed
510: * <li>Watchable.STOPPED if the image we are rendering into
511: * has gone away
512: * </ul>
513: */
514: public int iterate() throws Exception {
515: // make sure we have a page to render
516: if (page == null) {
517: return Watchable.COMPLETED;
518: }
519:
520: // check if this renderer is based on a weak reference to a graphics
521: // object. If it is, and the graphics is no longer valid, then just quit
522: BufferedImage bi = null;
523: if (imageRef != null) {
524: bi = (BufferedImage) imageRef.get();
525: if (bi == null) {
526: System.out.println("Image went away. Stopping");
527: return Watchable.STOPPED;
528: }
529:
530: g = (Graphics2D) bi.createGraphics();
531: }
532:
533: // check if there are any commands to parse. If there aren't,
534: // just return, but check if we'return really finished or not
535: if (currentCommand >= page.getCommandCount()) {
536: if (page.isFinished()) {
537: return Watchable.COMPLETED;
538: } else {
539: return Watchable.NEEDS_DATA;
540: }
541: }
542:
543: // find the current command
544: PDFCmd cmd = page.getCommand(currentCommand++);
545: if (cmd == null) {
546: // uh oh. Synchronization problem!
547: throw new PDFParseException("Command not found!");
548: }
549:
550: // execute the command
551: Rectangle2D dirtyRegion = cmd.execute(this );
552:
553: // append to the global dirty region
554: globalDirtyRegion = addDirtyRegion(dirtyRegion,
555: globalDirtyRegion);
556: unupdatedRegion = addDirtyRegion(dirtyRegion, unupdatedRegion);
557:
558: long now = System.currentTimeMillis();
559: if (now > then || rendererFinished()) {
560: // now tell any observers, so they can repaint
561: notifyObservers(bi, unupdatedRegion);
562: unupdatedRegion = null;
563: then = now + UPDATE_DURATION;
564: }
565:
566: // if we are based on a reference to a graphics, don't hold on to it
567: // since that will prevent the image from being collected.
568: if (imageRef != null) {
569: g = null;
570: }
571:
572: // if we need to stop, it will be caught at the start of the next
573: // iteration.
574: return Watchable.RUNNING;
575: }
576:
577: /**
578: * Called when iteration has stopped
579: */
580: @Override
581: public void cleanup() {
582: page = null;
583: state = null;
584: stack = null;
585: globalDirtyRegion = null;
586: lastShape = null;
587:
588: observers.clear();
589:
590: // keep around the image ref and image info for use in
591: // late addObserver() call
592: }
593:
594: /**
595: * Append a rectangle to the total dirty region of this shape
596: */
597: private Rectangle2D addDirtyRegion(Rectangle2D region,
598: Rectangle2D glob) {
599: if (region == null) {
600: return glob;
601: } else if (glob == null) {
602: return region;
603: } else {
604: Rectangle2D.union(glob, region, glob);
605: return glob;
606: }
607: }
608:
609: /**
610: * Determine if we are finished
611: */
612: private boolean rendererFinished() {
613: if (page == null) {
614: return true;
615: }
616:
617: return (page.isFinished() && currentCommand == page
618: .getCommandCount());
619: }
620:
621: /**
622: * Notify the observer that a region of the image has changed
623: */
624: private void notifyObservers(BufferedImage bi, Rectangle2D region) {
625: if (bi == null) {
626: return;
627: }
628:
629: int startx, starty, width, height;
630: int flags = 0;
631:
632: // don't do anything if nothing is there or no one is listening
633: if ((region == null && !rendererFinished())
634: || observers == null || observers.size() == 0) {
635: return;
636: }
637:
638: if (region != null) {
639: // get the image data for the total dirty region
640: startx = (int) Math.floor(region.getMinX());
641: starty = (int) Math.floor(region.getMinY());
642: width = (int) Math.ceil(region.getWidth());
643: height = (int) Math.ceil(region.getHeight());
644:
645: // sometimes width or height is negative. Grrr...
646: if (width < 0) {
647: startx += width;
648: width = -width;
649: }
650: if (height < 0) {
651: starty += height;
652: height = -height;
653: }
654:
655: flags = 0;
656: } else {
657: startx = 0;
658: starty = 0;
659: width = imageinfo.width;
660: height = imageinfo.height;
661: }
662: if (rendererFinished()) {
663: flags |= ImageObserver.ALLBITS;
664: // forget about the Graphics -- allows the image to be
665: // garbage collected.
666: g = null;
667: } else {
668: flags |= ImageObserver.SOMEBITS;
669: }
670:
671: synchronized (observers) {
672: for (Iterator i = observers.iterator(); i.hasNext();) {
673: ImageObserver observer = (ImageObserver) i.next();
674:
675: boolean result = observer.imageUpdate(bi, flags,
676: startx, starty, width, height);
677:
678: // if result is false, the observer no longer wants to
679: // be notified of changes
680: if (!result) {
681: i.remove();
682: }
683: }
684: }
685: }
686:
687: /**
688: * Convert an image mask into an image by painting over any pixels
689: * that have a value in the image with the current paint
690: */
691: private BufferedImage getMaskedImage(BufferedImage bi) {
692: // get the color of the current paint
693: Color col = (Color) state.fillPaint.getPaint();
694:
695: // format as 8 bits each of ARGB
696: int paintColor = col.getAlpha() << 24;
697: paintColor |= col.getRed() << 16;
698: paintColor |= col.getGreen() << 8;
699: paintColor |= col.getBlue();
700:
701: // transparent (alpha = 1)
702: int noColor = 0;
703:
704: // get the coordinates of the source image
705: int startX = bi.getMinX();
706: int startY = bi.getMinY();
707: int width = bi.getWidth();
708: int height = bi.getHeight();
709:
710: // create a destion image of the same size
711: BufferedImage dstImage = new BufferedImage(width, height,
712: BufferedImage.TYPE_INT_ARGB);
713:
714: // copy the pixels row by row
715: for (int i = 0; i < height; i++) {
716: int[] srcPixels = new int[width];
717: int[] dstPixels = new int[srcPixels.length];
718:
719: // read a row of pixels from the source
720: bi.getRGB(startX, startY + i, width, 1, srcPixels, 0,
721: height);
722:
723: // figure out which ones should get painted
724: for (int j = 0; j < srcPixels.length; j++) {
725: if (srcPixels[j] == 0xff000000) {
726: dstPixels[j] = paintColor;
727: } else {
728: dstPixels[j] = noColor;
729: }
730: }
731:
732: // write the destination image
733: dstImage.setRGB(startX, startY + i, width, 1, dstPixels, 0,
734: height);
735: }
736:
737: return dstImage;
738: }
739:
740: class GraphicsState implements Cloneable {
741: /** the clip region */
742: Shape cliprgn;
743:
744: /** the current stroke */
745: BasicStroke stroke;
746:
747: /** the current paint for drawing strokes */
748: PDFPaint strokePaint;
749:
750: /** the current paint for filling shapes */
751: PDFPaint fillPaint;
752:
753: /** the current compositing alpha for stroking */
754: AlphaComposite strokeAlpha;
755:
756: /** the current compositing alpha for filling */
757: AlphaComposite fillAlpha;
758:
759: /** the current transform */
760: AffineTransform xform;
761:
762: /** Clone this Graphics state.
763: *
764: * Note that cliprgn is not cloned. It must be set manually from
765: * the current graphics object's clip
766: */
767: @Override
768: public Object clone() {
769: GraphicsState cState = new GraphicsState();
770: cState.cliprgn = null;
771:
772: // copy immutable fields
773: cState.strokePaint = strokePaint;
774: cState.fillPaint = fillPaint;
775: cState.strokeAlpha = strokeAlpha;
776: cState.fillAlpha = fillAlpha;
777:
778: // clone mutable fields
779: cState.stroke = new BasicStroke(stroke.getLineWidth(),
780: stroke.getEndCap(), stroke.getLineJoin(), stroke
781: .getMiterLimit(), stroke.getDashArray(),
782: stroke.getDashPhase());
783: cState.xform = (AffineTransform) xform.clone();
784:
785: return cState;
786: }
787: }
788: }
|