001: /*
002: * $Id: PDFPage.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.BasicStroke;
025: import java.awt.Color;
026: import java.awt.Dimension;
027: import java.awt.Image;
028: import java.awt.geom.AffineTransform;
029: import java.awt.geom.GeneralPath;
030: import java.awt.geom.Rectangle2D;
031: import java.awt.image.BufferedImage;
032: import java.awt.image.ImageObserver;
033: import java.lang.ref.WeakReference;
034: import java.util.ArrayList;
035: import java.util.Collections;
036: import java.util.HashMap;
037: import java.util.Iterator;
038: import java.util.List;
039: import java.util.Map;
040:
041: /**
042: * A PDFPage encapsulates the parsed commands required to render a
043: * single page from a PDFFile. The PDFPage is not itself drawable;
044: * instead, create a PDFImage to display something on the screen.
045: * <p>
046: * This file also contains all of the PDFCmd commands that might
047: * be a part of the command stream in a PDFPage. They probably
048: * should be inner classes of PDFPage instead of separate non-public
049: * classes.
050: *
051: * @author Mike Wessler
052: */
053: public class PDFPage {
054: /** the array of commands. The length of this array will always
055: * be greater than or equal to the actual number of commands. */
056: private List commands;
057:
058: /** whether this page has been finished. If true, there will be no
059: * more commands added to the cmds list. */
060: private boolean finished = false;
061:
062: /** the page number used to find this page */
063: private int pageNumber;
064:
065: /** the bounding box of the page, in page coordinates */
066: private Rectangle2D bbox;
067:
068: /** the rotation of this page, in degrees */
069: private int rotation;
070:
071: /** a map from image info (width, height, clip) to a soft reference to the
072: rendered image */
073: private Cache cache;
074:
075: /** a map from image info to weak references to parsers that are active */
076: private Map renderers;
077:
078: /**
079: * create a PDFPage with dimensions in bbox and rotation.
080: */
081: public PDFPage(Rectangle2D bbox, int rotation) {
082: this (-1, bbox, rotation, null);
083: }
084:
085: /**
086: * create a PDFPage with dimensions in bbox and rotation.
087: */
088: public PDFPage(int pageNumber, Rectangle2D bbox, int rotation,
089: Cache cache) {
090: this .pageNumber = pageNumber;
091: this .cache = cache;
092:
093: if (bbox == null) {
094: bbox = new Rectangle2D.Float(0, 0, 1, 1);
095: }
096:
097: if (rotation < 0) {
098: rotation += 360;
099: }
100:
101: this .rotation = rotation;
102:
103: if (rotation == 90 || rotation == 270) {
104: bbox = new Rectangle2D.Double(bbox.getX(), bbox.getY(),
105: bbox.getHeight(), bbox.getWidth());
106: }
107:
108: this .bbox = bbox;
109:
110: // initialize the cache of images and parsers
111: renderers = Collections.synchronizedMap(new HashMap());
112:
113: // initialize the list of commands
114: commands = Collections.synchronizedList(new ArrayList(250));
115: }
116:
117: /**
118: * Get the width and height of this image in the correct aspect ratio.
119: * The image returned will have at least one of the width and
120: * height values identical to those requested. The other
121: * dimension may be smaller, so as to keep the aspect ratio
122: * the same as in the original page.
123: *
124: * @param width the maximum width of the image
125: * @param height the maximum height of the image
126: * @param clip the region in <b>page space</b> of the page to
127: * display. It may be null, in which the page's defined crop box
128: * will be used.
129: */
130: public Dimension getUnstretchedSize(int width, int height,
131: Rectangle2D clip) {
132: if (clip == null) {
133: clip = bbox;
134: } else {
135: if (getRotation() == 90 || getRotation() == 270) {
136: clip = new Rectangle2D.Double(clip.getX(), clip.getY(),
137: clip.getHeight(), clip.getWidth());
138: }
139: }
140:
141: double ratio = clip.getHeight() / clip.getWidth();
142: double askratio = (double) height / (double) width;
143: if (askratio > ratio) {
144: // asked for something too high
145: height = (int) (width * ratio + 0.5);
146: } else {
147: // asked for something too wide
148: width = (int) (height / ratio + 0.5);
149: }
150:
151: return new Dimension(width, height);
152: }
153:
154: /**
155: * Get an image producer which can be used to draw the image
156: * represented by this PDFPage. The ImageProducer is guaranteed to
157: * stay in sync with the PDFPage as commands are added to it.
158: *
159: * The image will contain the section of the page specified by the clip,
160: * scaled to fit in the area given by width and height.
161: *
162: * @param width the width of the image to be produced
163: * @param height the height of the image to be produced
164: * @param clip the region in <b>page space</b> of the entire page to
165: * display
166: * @param observer an image observer who will be notified when the
167: * image changes, or null
168: * @return an Image that contains the PDF data
169: */
170: public Image getImage(int width, int height, Rectangle2D clip,
171: ImageObserver observer) {
172: return getImage(width, height, clip, observer, true, false);
173: }
174:
175: /**
176: * Get an image producer which can be used to draw the image
177: * represented by this PDFPage. The ImageProducer is guaranteed to
178: * stay in sync with the PDFPage as commands are added to it.
179: *
180: * The image will contain the section of the page specified by the clip,
181: * scaled to fit in the area given by width and height.
182: *
183: * @param width the width of the image to be produced
184: * @param height the height of the image to be produced
185: * @param clip the region in <b>page space</b> of the entire page to
186: * display
187: * @param observer an image observer who will be notified when the
188: * image changes, or null
189: * @param drawbg if true, put a white background on the image. If not,
190: * draw no color (alpha 0) for the background.
191: * @param wait if true, do not return until this image is fully rendered.
192: * @return an Image that contains the PDF data
193: */
194: public Image getImage(int width, int height, Rectangle2D clip,
195: ImageObserver observer, boolean drawbg, boolean wait) {
196: // see if we already have this image
197: BufferedImage image = null;
198: PDFRenderer renderer = null;
199: ImageInfo info = new ImageInfo(width, height, clip, null);
200:
201: if (cache != null) {
202: image = cache.getImage(this , info);
203: renderer = cache.getImageRenderer(this , info);
204: }
205:
206: // not in the cache, so create it
207: if (image == null) {
208: if (drawbg) {
209: info.bgColor = Color.WHITE;
210: }
211:
212: image = new RefImage(info.width, info.height,
213: BufferedImage.TYPE_INT_ARGB);
214: renderer = new PDFRenderer(this , info, image);
215:
216: if (cache != null) {
217: cache.addImage(this , info, image, renderer);
218: }
219:
220: renderers.put(info, new WeakReference(renderer));
221: }
222:
223: // the renderer may be null if we are getting this image from the
224: // cache and rendering has completed.
225: if (renderer != null) {
226: if (observer != null) {
227: renderer.addObserver(observer);
228: }
229:
230: if (!renderer.isFinished()) {
231: renderer.go(wait);
232: }
233: }
234:
235: // return the image
236: return image;
237: }
238:
239: /**
240: * get the page number used to lookup this page
241: * @return the page number
242: */
243: public int getPageNumber() {
244: return pageNumber;
245: }
246:
247: /**
248: * get the aspect ratio of the correctly oriented page.
249: * @return the width/height aspect ratio of the page
250: */
251: public float getAspectRatio() {
252: return getWidth() / getHeight();
253: }
254:
255: /**
256: * get the bounding box of the page, before any rotation.
257: */
258: public Rectangle2D getBBox() {
259: return bbox;
260: }
261:
262: /**
263: * get the width of this page, after rotation
264: */
265: public float getWidth() {
266: return (float) bbox.getWidth();
267: }
268:
269: /**
270: * get the height of this page, after rotation
271: */
272: public float getHeight() {
273: return (float) bbox.getHeight();
274: }
275:
276: /**
277: * get the rotation of this image
278: */
279: public int getRotation() {
280: return rotation;
281: }
282:
283: /**
284: * Get the initial transform to map from a specified clip rectangle in
285: * pdf coordinates to an image of the specfied width and
286: * height in device coordinates
287: *
288: * @param width the width of the image
289: * @param height the height of the image
290: * @param clip the desired clip rectangle (in PDF space) or null to use
291: * the page's bounding box
292: */
293: public AffineTransform getInitialTransform(int width, int height,
294: Rectangle2D clip) {
295: AffineTransform at = new AffineTransform();
296: switch (getRotation()) {
297: case 0:
298: at = new AffineTransform(1, 0, 0, -1, 0, height);
299: break;
300: case 90:
301: at = new AffineTransform(0, 1, 1, 0, 0, 0);
302: break;
303: case 180:
304: at = new AffineTransform(-1, 0, 0, 1, width, 0);
305: break;
306: case 270:
307: at = new AffineTransform(0, -1, -1, 0, width, height);
308: break;
309: }
310:
311: if (clip == null) {
312: clip = getBBox();
313: } else if (getRotation() == 90 || getRotation() == 270) {
314: int tmp = width;
315: width = height;
316: height = tmp;
317: }
318:
319: // now scale the image to be the size of the clip
320: double scaleX = width / clip.getWidth();
321: double scaleY = height / clip.getHeight();
322: at.scale(scaleX, scaleY);
323:
324: // create a transform that moves the top left corner of the clip region
325: // (minX, minY) to (0,0) in the image
326: at.translate(-clip.getMinX(), -clip.getMinY());
327:
328: return at;
329: }
330:
331: /**
332: * get the current number of commands for this page
333: */
334: public int getCommandCount() {
335: return commands.size();
336: }
337:
338: /**
339: * get the command at a given index
340: */
341: public PDFCmd getCommand(int index) {
342: return (PDFCmd) commands.get(index);
343: }
344:
345: /**
346: * get all the commands in the current page
347: */
348: public List getCommands() {
349: return commands;
350: }
351:
352: /**
353: * get all the commands in the current page starting at the given index
354: */
355: public List getCommands(int startIndex) {
356: return getCommands(startIndex, getCommandCount());
357: }
358:
359: /*
360: * get the commands in the page within the given start and end indices
361: */
362: public List getCommands(int startIndex, int endIndex) {
363: return commands.subList(startIndex, endIndex);
364: }
365:
366: /**
367: * Add a single command to the page list.
368: */
369: public void addCommand(PDFCmd cmd) {
370: synchronized (commands) {
371: commands.add(cmd);
372: }
373:
374: // notify any outstanding images
375: updateImages();
376: }
377:
378: /**
379: * add a collection of commands to the page list. This is probably
380: * invoked as the result of an XObject 'do' command, or through a
381: * type 3 font.
382: */
383: public void addCommands(PDFPage page) {
384: addCommands(page, null);
385: }
386:
387: /**
388: * add a collection of commands to the page list. This is probably
389: * invoked as the result of an XObject 'do' command, or through a
390: * type 3 font.
391: * @param page the source of other commands. It MUST be finished.
392: * @param extra a transform to perform before adding the commands.
393: * If null, no extra transform will be added.
394: */
395: public void addCommands(PDFPage page, AffineTransform extra) {
396: synchronized (commands) {
397: addPush();
398: if (extra != null) {
399: addXform(extra);
400: }
401: //addXform(page.getTransform());
402: commands.addAll(page.getCommands());
403: addPop();
404: }
405:
406: // notify any outstanding images
407: updateImages();
408: }
409:
410: /**
411: * Clear all commands off the current page
412: */
413: public void clearCommands() {
414: synchronized (commands) {
415: commands.clear();
416: }
417:
418: // notify any outstanding images
419: updateImages();
420: }
421:
422: /**
423: * get whether parsing for this PDFPage has been completed and all
424: * commands are in place.
425: */
426: public boolean isFinished() {
427: return finished;
428: }
429:
430: /**
431: * wait for finish
432: */
433: public synchronized void waitForFinish()
434: throws InterruptedException {
435: if (!finished) {
436: wait();
437: }
438: }
439:
440: /**
441: * Stop the rendering of a particular image on this page
442: */
443: public void stop(int width, int height, Rectangle2D clip) {
444: ImageInfo info = new ImageInfo(width, height, clip);
445:
446: synchronized (renderers) {
447: // find our renderer
448: WeakReference rendererRef = (WeakReference) renderers
449: .get(info);
450: if (rendererRef != null) {
451: PDFRenderer renderer = (PDFRenderer) rendererRef.get();
452: if (renderer != null) {
453: // stop it
454: renderer.stop();
455: }
456: }
457: }
458: }
459:
460: /**
461: * The entire page is done. This must only be invoked once. All
462: * observers will be notified.
463: */
464: public synchronized void finish() {
465: // System.out.println("Page finished!");
466: finished = true;
467: notifyAll();
468:
469: // notify any outstanding images
470: updateImages();
471: }
472:
473: /** push the graphics state */
474: public void addPush() {
475: addCommand(new PDFPushCmd());
476: }
477:
478: /** pop the graphics state */
479: public void addPop() {
480: addCommand(new PDFPopCmd());
481: }
482:
483: /** concatenate a transform to the graphics state */
484: public void addXform(AffineTransform at) {
485: // PDFXformCmd xc= lastXformCmd();
486: // xc.at.concatenate(at);
487: addCommand(new PDFXformCmd(new AffineTransform(at)));
488: }
489:
490: /**
491: * set the stroke width
492: * @param w the width of the stroke
493: */
494: public void addStrokeWidth(float w) {
495: PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd();
496: if (w == 0) {
497: w = 0.1f;
498: }
499: sc.setWidth(w);
500: addCommand(sc);
501: }
502:
503: /**
504: * set the end cap style
505: * @param capstyle the cap style: 0 = BUTT, 1 = ROUND, 2 = SQUARE
506: */
507: public void addEndCap(int capstyle) {
508: PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd();
509:
510: int cap = BasicStroke.CAP_BUTT;
511: switch (capstyle) {
512: case 0:
513: cap = BasicStroke.CAP_BUTT;
514: break;
515: case 1:
516: cap = BasicStroke.CAP_ROUND;
517: break;
518: case 2:
519: cap = BasicStroke.CAP_SQUARE;
520: break;
521: }
522: sc.setEndCap(cap);
523:
524: addCommand(sc);
525: }
526:
527: /**
528: * set the line join style
529: * @param joinstyle the join style: 0 = MITER, 1 = ROUND, 2 = BEVEL
530: */
531: public void addLineJoin(int joinstyle) {
532: PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd();
533:
534: int join = BasicStroke.JOIN_MITER;
535: switch (joinstyle) {
536: case 0:
537: join = BasicStroke.JOIN_MITER;
538: break;
539: case 1:
540: join = BasicStroke.JOIN_ROUND;
541: break;
542: case 2:
543: join = BasicStroke.JOIN_BEVEL;
544: break;
545: }
546: sc.setLineJoin(join);
547:
548: addCommand(sc);
549: }
550:
551: /**
552: * set the miter limit
553: */
554: public void addMiterLimit(float limit) {
555: PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd();
556:
557: sc.setMiterLimit(limit);
558:
559: addCommand(sc);
560: }
561:
562: /**
563: * set the dash style
564: * @param dashary the array of on-off lengths
565: * @param phase offset of the array at the start of the line drawing
566: */
567: public void addDash(float[] dashary, float phase) {
568: PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd();
569:
570: sc.setDash(dashary, phase);
571:
572: addCommand(sc);
573: }
574:
575: /**
576: * set the current path
577: * @param path the path
578: * @param style the style: PDFShapeCmd.STROKE, PDFShapeCmd.FILL,
579: * PDFShapeCmd.BOTH, PDFShapeCmd.CLIP, or some combination.
580: */
581: public void addPath(GeneralPath path, int style) {
582: addCommand(new PDFShapeCmd(path, style));
583: }
584:
585: /**
586: * set the fill paint
587: */
588: public void addFillPaint(PDFPaint p) {
589: addCommand(new PDFFillPaintCmd(p));
590: }
591:
592: /** set the stroke paint */
593: public void addStrokePaint(PDFPaint p) {
594: addCommand(new PDFStrokePaintCmd(p));
595: }
596:
597: /**
598: * set the fill alpha
599: */
600: public void addFillAlpha(float a) {
601: addCommand(new PDFFillAlphaCmd(a));
602: }
603:
604: /** set the stroke alpha */
605: public void addStrokeAlpha(float a) {
606: addCommand(new PDFStrokeAlphaCmd(a));
607: }
608:
609: /**
610: * draw an image
611: * @param image the image to draw
612: */
613: public void addImage(PDFImage image) {
614: addCommand(new PDFImageCmd(image));
615: }
616:
617: /**
618: * Notify all images we know about that a command has been added
619: */
620: public void updateImages() {
621: for (Iterator i = renderers.values().iterator(); i.hasNext();) {
622: WeakReference ref = (WeakReference) i.next();
623: PDFRenderer renderer = (PDFRenderer) ref.get();
624:
625: if (renderer != null) {
626: if (renderer.getStatus() == Watchable.NEEDS_DATA) {
627: // there are watchers. Set the state to paused and
628: // let the watcher decide when to start.
629: renderer.setStatus(Watchable.PAUSED);
630: }
631: }
632: }
633: }
634: }
635:
636: /**
637: * draw an image
638: */
639: class PDFImageCmd extends PDFCmd {
640: PDFImage image;
641:
642: public PDFImageCmd(PDFImage image) {
643: this .image = image;
644: }
645:
646: public Rectangle2D execute(PDFRenderer state) {
647: return state.drawImage(image);
648: }
649: }
650:
651: /**
652: * set the fill paint
653: */
654: class PDFFillPaintCmd extends PDFCmd {
655: PDFPaint p;
656:
657: public PDFFillPaintCmd(PDFPaint p) {
658: this .p = p;
659: }
660:
661: public Rectangle2D execute(PDFRenderer state) {
662: state.setFillPaint(p);
663: return null;
664: }
665: }
666:
667: /**
668: * set the stroke paint
669: */
670: class PDFStrokePaintCmd extends PDFCmd {
671: PDFPaint p;
672:
673: public PDFStrokePaintCmd(PDFPaint p) {
674: this .p = p;
675: }
676:
677: public Rectangle2D execute(PDFRenderer state) {
678: state.setStrokePaint(p);
679: return null;
680: }
681: }
682:
683: /**
684: * set the fill paint
685: */
686: class PDFFillAlphaCmd extends PDFCmd {
687: float a;
688:
689: public PDFFillAlphaCmd(float a) {
690: this .a = a;
691: }
692:
693: public Rectangle2D execute(PDFRenderer state) {
694: state.setFillAlpha(a);
695: return null;
696: }
697: }
698:
699: /**
700: * set the stroke paint
701: */
702: class PDFStrokeAlphaCmd extends PDFCmd {
703: float a;
704:
705: public PDFStrokeAlphaCmd(float a) {
706: this .a = a;
707: }
708:
709: public Rectangle2D execute(PDFRenderer state) {
710: state.setStrokeAlpha(a);
711: return null;
712: }
713: }
714:
715: /**
716: * push the graphics state
717: */
718: class PDFPushCmd extends PDFCmd {
719: public Rectangle2D execute(PDFRenderer state) {
720: state.push();
721: return null;
722: }
723: }
724:
725: /**
726: * pop the graphics state
727: */
728: class PDFPopCmd extends PDFCmd {
729: public Rectangle2D execute(PDFRenderer state) {
730: state.pop();
731: return null;
732: }
733: }
734:
735: /**
736: * concatenate a transform to the graphics state
737: */
738: class PDFXformCmd extends PDFCmd {
739: AffineTransform at;
740:
741: public PDFXformCmd(AffineTransform at) {
742: if (at == null) {
743: throw new RuntimeException("Null transform in PDFXformCmd");
744: }
745: this .at = at;
746: }
747:
748: public Rectangle2D execute(PDFRenderer state) {
749: state.transform(at);
750: return null;
751: }
752:
753: public String toString(PDFRenderer state) {
754: return "PDFXformCmd: " + at;
755: }
756:
757: @Override
758: public String getDetails() {
759: StringBuffer buf = new StringBuffer();
760: buf.append("PDFXformCommand: \n");
761: buf.append(at.toString());
762:
763: return buf.toString();
764: }
765: }
766:
767: /**
768: * change the stroke style
769: */
770: class PDFChangeStrokeCmd extends PDFCmd {
771: float w, limit, phase;
772: int cap, join;
773: float[] ary;
774:
775: public PDFChangeStrokeCmd() {
776: this .w = PDFRenderer.NOWIDTH;
777: this .cap = PDFRenderer.NOCAP;
778: this .join = PDFRenderer.NOJOIN;
779: this .limit = PDFRenderer.NOLIMIT;
780: this .ary = PDFRenderer.NODASH;
781: this .phase = PDFRenderer.NOPHASE;
782: }
783:
784: public void setWidth(float w) {
785: this .w = w;
786: }
787:
788: public void setEndCap(int cap) {
789: this .cap = cap;
790: }
791:
792: public void setLineJoin(int join) {
793: this .join = join;
794: }
795:
796: public void setMiterLimit(float limit) {
797: this .limit = limit;
798: }
799:
800: public void setDash(float[] ary, float phase) {
801: if (ary != null) {
802: // make sure no pairs start with 0, since having no opaque
803: // region doesn't make any sense.
804: for (int i = 0; i < ary.length - 1; i += 2) {
805: if (ary[i] == 0) {
806: /* Give a very small value, since 0 messes java up */
807: ary[i] = 0.00001f;
808: break;
809: }
810: }
811: }
812: this .ary = ary;
813: this .phase = phase;
814: }
815:
816: public Rectangle2D execute(PDFRenderer state) {
817: state.setStrokeParts(w, cap, join, limit, ary, phase);
818: return null;
819: }
820:
821: public String toString(PDFRenderer state) {
822: return "STROKE: w=" + w + " cap=" + cap + " join=" + join
823: + " limit=" + limit + " ary=" + ary + " phase=" + phase;
824: }
825: }
|