/*
* Copyright 2006-2012 ICEsoft Technologies Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS
* IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.icepdf.core.pobjects;
import org.icepdf.core.events.PaintPageEvent;
import org.icepdf.core.events.PaintPageListener;
import org.icepdf.core.io.SeekableInput;
import org.icepdf.core.io.SequenceInputStream;
import org.icepdf.core.pobjects.annotations.Annotation;
import org.icepdf.core.pobjects.annotations.AnnotationFactory;
import org.icepdf.core.pobjects.annotations.AnnotationState;
import org.icepdf.core.pobjects.graphics.Shapes;
import org.icepdf.core.pobjects.graphics.text.GlyphText;
import org.icepdf.core.pobjects.graphics.text.LineText;
import org.icepdf.core.pobjects.graphics.text.PageText;
import org.icepdf.core.pobjects.graphics.text.WordText;
import org.icepdf.core.util.*;
import org.icepdf.core.views.common.TextSelectionPageHandler;
import org.icepdf.core.views.swing.PageViewComponentImpl;
import java.awt.*;
import java.awt.geom.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>This class represents the leaves of a <code>PageTree</code> object known
* as <code>Page</code> class. The page dictionary specifies attributes
* of the single page of the document. Many of the page's attributes are
* inherited from the page tree dictionary if not specified in the page
* dictionary.</p>
* <p/>
* <p>The page object also provides a method which will extract a page's content,
* such as text and images. The <code>paint</code> method is the core of
* the ICEpdf renderer, allowing page content to be painted to any Java graphics
* context. </p>
* <p/>
* <p>Page objects in a PDF document have different boundaries defined which
* govern various aspects of the pre-press process, such as cropping, bleed,
* and trimming. Facilities for including printer's marks, such a registration
* targets, gray ramps color bars, and cut marks which assist in the production
* process. When getting a page's size, the default boundary used is the crop
* box which is what most viewer applications should use. However, if your application
* requires the use of different page boundaries, they can be specified when
* using the getSize or paint methods. If in doubt, always use the crop box
* constant.</p>
*
* @see org.icepdf.core.pobjects.PageTree
* @since 1.0
*/
public class Page extends Dictionary implements MemoryManageable {
private static final Logger logger =
Logger.getLogger(Page.class.toString());
public static final Name ANNOTS_KEY = new Name("Annots");
public static final Name CONTENTS_KEY = new Name("Contents");
/**
* Defines the boundaries of the physical medium on which the page is
* intended to be displayed or printed.
*/
public static final int BOUNDARY_MEDIABOX = 1;
/**
* Defines the visible region of the default user space. When the page
* is displayed or printed, its contents are to be clipped to this
* rectangle and then imposed on the output medium in some implementation
* defined manner.
*/
public static final int BOUNDARY_CROPBOX = 2;
/**
* Defines the region to which the contents of the page should be clipped
* when output in a production environment (Mainly commercial printing).
*/
public static final int BOUNDARY_BLEEDBOX = 3;
/**
* Defines the intended dimensions of the finished page after trimming.
*/
public static final int BOUNDARY_TRIMBOX = 4;
/**
* Defines the extent of the page's meaningful content as intended by the
* page's creator.
*/
public static final int BOUNDARY_ARTBOX = 5;
// Flag for call to init method, very simple cache
private boolean isInited = false;
// resources for page's parent pages, default fonts, etc.
private Resources resources;
// Vector of annotations
private ArrayList<Annotation> annotations;
// Contents
private Vector<Stream> contents;
// Container for all shapes stored on page
private Shapes shapes = null;
// the collection of objects listening for page paint events
private Vector<PaintPageListener> paintPageListeners = new Vector<PaintPageListener>(8);
// Defines the boundaries of the physical medium on which the page is
// intended to be displayed on.
private PRectangle mediaBox;
// Defining the visible region of default user space.
private PRectangle cropBox;
// Defines the region to which the contents of the page should be clipped
// when output in a production environment.
private PRectangle bleedBox;
// Defines the intended dimension of the finished page after trimming.
private PRectangle trimBox;
// Defines the extent of the pages meaningful content as intended by the
// pages creator.
private PRectangle artBox;
// page has default rotation value
private float pageRotation = 0;
/**
* Create a new Page object. A page object represents a PDF object that
* has the name page associated with it. It also conceptually represents
* a page entity and all of it child elements that are associated with it.
*
* @param l pointer to default library containing all document objects
* @param h hashtable containing all of the dictionary entries
*/
public Page(Library l, Hashtable h) {
super(l, h);
}
/**
* Dispose the Page.
*
* @param cache if true, cached files are removed; otherwise, objects are freed
* but object caches are left intact.
*/
protected synchronized void dispose(boolean cache) {
// Do not null out Library library reference here, without taking
// into account that MemoryManager.releaseAllByLibrary(Library)
// requires Page to still have Library library in getLibrary()
// dispose only if the pages has been initiated
if (isInited) {
// un-init a page to free up memory
isInited = false;
// null data collections for page content
if (annotations != null) {
annotations.clear();
annotations.trimToSize();
}
// work through contents and null any stream that have images in them
if (contents != null) {
//System.out.println(" Content size " + contents.size());
for (Stream stream : contents) {
stream.dispose(cache);
}
contents.clear();
contents.trimToSize();
}
// work through contents and null any stream that have images in them
if (shapes != null) {
shapes.dispose();
shapes = null;
}
// work through resources and null any images in the image hash
if (resources != null) {
resources.dispose(cache, this);
resources = null;
}
// clean up references in library to avoid slow bleed
if (cache) {
// remove the page
library.removeObject(this.getPObjectReference());
// annotations
Object tmp = entries.get(ANNOTS_KEY.getName());
if (tmp != null && tmp instanceof Vector) {
Vector annots = (Vector) tmp;
for (Object ref : annots) {
if (ref instanceof Reference) {
library.removeObject((Reference) ref);
}
}
}
}
}
// clear vector of listeners
if (paintPageListeners != null) {
paintPageListeners.clear();
paintPageListeners.trimToSize();
}
}
public boolean isInitiated() {
return isInited;
}
private void initPageContents() throws InterruptedException {
Object pageContent = library.getObject(entries, CONTENTS_KEY.getName());
// if a stream process it as needed
if (pageContent instanceof Stream) {
contents = new Vector<Stream>(1);
Stream tmpStream = (Stream) pageContent;
tmpStream.setPObjectReference(
library.getObjectReference(entries, CONTENTS_KEY.getName()));
contents.addElement(tmpStream);
}
// if a vector, process it as needed
else if (pageContent instanceof Vector) {
Vector conts = (Vector) pageContent;
int sz = conts.size();
contents = new Vector<Stream>(Math.max(sz, 1));
// pull all of the page content references from the library
for (int i = 0; i < sz; i++) {
if (Thread.interrupted()) {
throw new InterruptedException("Page Content initialization thread interrupted");
}
Stream tmpStream = (Stream) library.getObject((Reference) conts.elementAt(i));
if (tmpStream != null) {
tmpStream.setPObjectReference((Reference) conts.elementAt(i));
contents.addElement(tmpStream);
}
}
}
}
private void initPageResources() throws InterruptedException {
Resources res = library.getResources(entries, "Resources");
if (res == null) {
PageTree pt = getParent();
while (pt != null) {
if (Thread.interrupted()) {
throw new InterruptedException("Page Resource initialization thread interrupted");
}
Resources parentResources = pt.getResources();
if (parentResources != null) {
res = parentResources;
break;
}
pt = pt.getParent();
}
}
resources = res;
if (resources != null) {
resources.addReference(this);
}
}
private void initPageAnnotations() throws InterruptedException {
// find annotations in main library for our pages dictionary
Object annots = library.getObject(entries, ANNOTS_KEY.getName());
if (annots != null && annots instanceof Vector) {
Vector v = (Vector) annots;
annotations = new ArrayList<Annotation>(v.size() + 1);
// add annotations
Object annotObj;
org.icepdf.core.pobjects.annotations.Annotation a = null;
for (int i = 0; i < v.size(); i++) {
if (Thread.interrupted()) {
throw new InterruptedException(
"Page Annotation initialization thread interrupted");
}
annotObj = v.elementAt(i);
Reference ref = null;
// we might have a reference
if (annotObj instanceof Reference) {
ref = (Reference) v.elementAt(i);
annotObj = library.getObject(ref);
}
// but most likely its an annotations base class
if (annotObj instanceof Annotation) {
a = (Annotation) annotObj;
}
// or build annotations from dictionary.
else if (annotObj instanceof Hashtable) { // Hashtable lacks "Type"->"Annot" entry
a = Annotation.buildAnnotation(library, (Hashtable) annotObj);
}
// set the object reference, so we can save the state correct
// and update any references accordingly.
if (ref != null) {
a.setPObjectReference(ref);
}
// add any found annotations to the vector.
annotations.add(a);
}
}
}
/**
* Initialize the Page object. This method triggers the parsing of a page's
* child elements. Once a page has been initialized, it can be painted.
*/
public synchronized void init() {
try {
// make sure we are not revisiting this method
if (isInited) {
return;
}
//try { throw new RuntimeException("Page.init() ****"); } catch(Exception e) { e.printStackTrace(); }
// do a little clean up to keep the mem footprint small
boolean lowMemory = MemoryManager.getInstance().isLowMemory();
if (lowMemory && logger.isLoggable(Level.FINER)) {
logger.finer("Low memory conditions encountered, clearing page cache");
}
// get pages resources
initPageResources();
// annotations
initPageAnnotations();
// Get the value of the page's content entry
initPageContents();
/**
* Finally iterate through the contents vector and concat all of the
* the resourse streams together so that the content parser can
* go to town and build all of the page's shapes.
*/
if (contents != null) {
Vector<InputStream> inputStreamsVec = new Vector<InputStream>(contents.size());
for (Stream stream : contents) {
//byte[] streamBytes = stream.getBytes();
//ByteArrayInputStream input = new ByteArrayInputStream(streamBytes);
InputStream input = stream.getInputStreamForDecodedStreamBytes();
inputStreamsVec.add(input);
/*
InputStream input = stream.getInputStreamForDecodedStreamBytes();
InputStream[] inArray = new InputStream[] { input };////
String content = Utils.getContentAndReplaceInputStream( inArray, false );
input = inArray[0];
System.out.println("Page.init() Stream: " + stream);
System.out.println("Page.init() Content: " + content);
*/
}
SequenceInputStream sis = new SequenceInputStream(inputStreamsVec.iterator());
// push the library and resources to the content parse
// and return the the shapes vector for the screen elements
// for the page/resources in question.
try {
ContentParser cp = new ContentParser(library, resources);
shapes = cp.parse(sis);
} catch (Exception e) {
shapes = new Shapes();
logger.log(Level.FINE, "Error initializing Page.", e);
} finally {
try {
sis.close();
} catch (IOException e) {
logger.log(Level.FINE, "Error closing page stream.", e);
}
}
}
// empty page, nothing to do.
else {
shapes = new Shapes();
}
// set the initiated flag
isInited = true;
} catch (InterruptedException e) {
// keeps shapes vector so we can paint what we have but make init state as false
// so we can try to re parse it later.
isInited = false;
logger.log(Level.SEVERE, "Page initializing thread interrupted.", e);
}
}
/**
* Gets a Thumbnail object associated with this page. If no Thumbnail
* entry exists then null is returned.
*
* @return thumbnail object of this page, null if no thumbnail value is
* encoded.
*/
public Thumbnail getThumbnail() {
Object thumb = library.getObject(entries, "Thumb");
if (thumb != null && thumb instanceof Stream) {
return new Thumbnail(library, entries);
} else {
return null;
}
}
public void paint(Graphics g, int renderHintType, final int boundary,
float userRotation, float userZoom) {
paint(g, renderHintType, boundary, userRotation, userZoom, null);
}
/**
* Paints the contents of this page to the graphics context using
* the specified rotation, zoom, rendering hints and page boundary.
*
* @param g graphics context to which the page content will be painted.
* @param renderHintType Constant specified by the GraphicsRenderingHints class.
* There are two possible entries, SCREEN and PRINT, each with configurable
* rendering hints settings.
* @param boundary Constant specifying the page boundary to use when
* painting the page content.
* @param userRotation Rotation factor, in degrees, to be applied to the rendered page
* @param userZoom Zoom factor to be applied to the rendered page
* @param pagePainter class which will receive paint events.
*/
public void paint(Graphics g, int renderHintType, final int boundary,
float userRotation, float userZoom, PageViewComponentImpl.PagePainter pagePainter) {
paint(g, renderHintType, boundary, userRotation, userZoom, pagePainter, true, true);
}
/**
* Paints the contents of this page to the graphics context using
* the specified rotation, zoom, rendering hints and page boundary.
*
* @param g graphics context to which the page content will be painted.
* @param renderHintType Constant specified by the GraphicsRenderingHints class.
* There are two possible entries, SCREEN and PRINT, each with configurable
* rendering hints settings.
* @param boundary Constant specifying the page boundary to use when
* painting the page content.
* @param userRotation Rotation factor, in degrees, to be applied to the rendered page
* @param userZoom Zoom factor to be applied to the rendered page
* @param pagePainter class which will receive paint events.
* @param paintAnnotations true enables the painting of page annotations. False
* paints no annotaitons for a given page.
* @param paintSearchHighlight true enables the painting of search highlight
* state of text object. The search controller can
* be used to easily search and add highlighted state
* for search terms.
*/
public void paint(Graphics g, int renderHintType, final int boundary,
float userRotation, float userZoom,
PageViewComponentImpl.PagePainter pagePainter,
boolean paintAnnotations, boolean paintSearchHighlight) {
if (!isInited && pagePainter == null) {
init();
} else if (!isInited) {
// make sure we don't do a page init on the awt thread in the viewer
// ri, let the
return;
}
Graphics2D g2 = (Graphics2D) g;
GraphicsRenderingHints grh = GraphicsRenderingHints.getDefault();
g2.setRenderingHints(grh.getRenderingHints(renderHintType));
AffineTransform at = getPageTransform(boundary, userRotation, userZoom);
g2.transform(at);
PRectangle pageBoundary = getPageBoundary(boundary);
float x = 0 - pageBoundary.x;
float y = 0 - (pageBoundary.y - pageBoundary.height);
// Draw the (typically white) background
Color backgroundColor = grh.getPageBackgroundColor(renderHintType);
if (backgroundColor != null) {
g2.setColor(backgroundColor);
g2.fillRect((int) (0 - x),
(int) (0 - y),
(int) pageBoundary.width,
(int) pageBoundary.height);
}
// We have to impose a page clip because some documents don't separate
// pages into separate Page objects, but instead reuse the Page object,
// but with a different clip
// And we can't stomp over the clip, because the PageView might be
// trying to only draw a portion of the page for performance, or
// other reasons
Rectangle2D rect = new Rectangle2D.Double(-x, -y, pageBoundary.width, pageBoundary.height);
Shape oldClip = g2.getClip();
if (oldClip == null) {
g2.setClip(rect);
} else {
Area area = new Area(oldClip);
area.intersect(new Area(rect));
g2.setClip(area);
}
// draw page content
if (shapes != null) {
AffineTransform pageTransform = g2.getTransform();
Shape pageClip = g2.getClip();
shapes.setPageParent(this);
shapes.paint(g2, pagePainter);
shapes.setPageParent(null);
g2.setTransform(pageTransform);
g2.setClip(pageClip);
}
// paint annotations if available and desired.
if (annotations != null && paintAnnotations) {
float totalRotation = getTotalRotation(userRotation);
int num = annotations.size();
for (int i = 0; i < num; i++) {
Annotation annot = annotations.get(i);
annot.render(g2, renderHintType, totalRotation, userZoom, false);
}
}
// paint search highlight values
if (paintSearchHighlight) {
PageText pageText = getViewText();
if (pageText != null) {
g2.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER,
TextSelectionPageHandler.selectionAlpha));
// paint the sprites
GeneralPath textPath;
// iterate over the data structure.
for (LineText lineText : pageText.getPageLines()) {
for (WordText wordText : lineText.getWords()) {
// paint whole word
if (wordText.isHighlighted()) {
textPath = new GeneralPath(wordText.getBounds());
g2.setColor(TextSelectionPageHandler.highlightColor);
g2.fill(textPath);
} else {
for (GlyphText glyph : wordText.getGlyphs()) {
if (glyph.isHighlighted()) {
textPath = new GeneralPath(glyph.getBounds());
g2.setColor(TextSelectionPageHandler.highlightColor);
g2.fill(textPath);
}
}
}
}
}
}
}
// one last repaint, just to be sure
notifyPaintPageListeners();
}
/**
* The Java Graphics coordinate system has the origin at the top-left
* of the screen, with Y values increasing as one moves down the screen.
* The PDF coordinate system has the origin at the bottom-left of the
* document, with Y values increasing as one moved up the document.
* As well, PDFs can be displayed both rotated and zoomed.
* This method gives an AffineTransform which can be passed to
* java.awt.Graphics2D.transform(AffineTransform) so that one can then
* use that Graphics2D in the user-perspectived PDF coordinate space.
*
* @param boundary Constant specifying the page boundary to use when
* painting the page content.
* @param userRotation Rotation factor, in degrees, to be applied to the rendered page
* @param userZoom Zoom factor to be applied to the rendered page
* @return AffineTransform for translating from the rotated and zoomed PDF
* coordinate system to the Java Graphics coordinate system
*/
public AffineTransform getPageTransform(final int boundary,
float userRotation,
float userZoom) {
AffineTransform at = new AffineTransform();
Rectangle2D.Double boundingBox = getBoundingBox(boundary, userRotation, userZoom);
at.translate(0, boundingBox.getHeight());
// setup canvas for PDF document orientation
at.scale(1, -1);
at.scale(userZoom, userZoom);
float totalRotation = getTotalRotation(userRotation);
PRectangle pageBoundary = getPageBoundary(boundary);
if (totalRotation == 0) {
} else if (totalRotation == 90) {
at.translate(pageBoundary.height, 0);
} else if (totalRotation == 180) {
at.translate(pageBoundary.width, pageBoundary.height);
} else if (totalRotation == 270) {
at.translate(0, pageBoundary.width);
} else {
if (totalRotation > 0 && totalRotation < 90) {
double xShift = pageBoundary.height * Math.cos(Math.toRadians(90 - totalRotation));
at.translate(xShift, 0);
} else if (totalRotation > 90 && totalRotation < 180) {
double rad = Math.toRadians(180 - totalRotation);
double cosRad = Math.cos(rad);
double sinRad = Math.sin(rad);
double xShift = pageBoundary.height * sinRad + pageBoundary.width * cosRad;
double yShift = pageBoundary.height * cosRad;
at.translate(xShift, yShift);
} else if (totalRotation > 180 && totalRotation < 270) {
double rad = Math.toRadians(totalRotation - 180);
double cosRad = Math.cos(rad);
double sinRad = Math.sin(rad);
double xShift = pageBoundary.width * cosRad;
double yShift = pageBoundary.width * sinRad + pageBoundary.height * cosRad;
at.translate(xShift, yShift);
} else if (totalRotation > 270 && totalRotation < 360) {
double yShift = pageBoundary.width * Math.cos(Math.toRadians(totalRotation - 270));
at.translate(0, yShift);
}
}
// apply rotation on canvas, convert to Radians
at.rotate(totalRotation * Math.PI / 180.0);
// translate crop lower left corner back to where media box corner was
float x = 0 - pageBoundary.x;
float y = 0 - (pageBoundary.y - pageBoundary.height);
at.translate(x, y);
return at;
}
/**
* This method returns a Shape that represents the outline of this Page,
* after being rotated and zoomed. It is used for clipping, and drawing
* borders around the page rendering onscreen.
*
* @param boundary Constant specifying the page boundary to use
* @param userRotation Rotation factor, in degrees, to be applied
* @param userZoom Zoom factor to be applied
* @return Shape outline of the rotated and zoomed portion of this Page
* corresponding to the specified boundary
*/
public Shape getPageShape(int boundary, float userRotation, float userZoom) {
AffineTransform at = getPageTransform(boundary, userRotation, userZoom);
PRectangle pageBoundary = getPageBoundary(boundary);
float x = 0 - pageBoundary.x;
float y = 0 - (pageBoundary.y - pageBoundary.height);
Rectangle2D rect = new Rectangle2D.Double(-x, -y, pageBoundary.width, pageBoundary.height);
GeneralPath path = new GeneralPath(rect);
return path.createTransformedShape(at);
}
/**
* Creates a new annotation instance for his page. The Annotation is
* added to the appropriate dictionaries and is regisitered width the
* state manager class.
*
* @param rect location of new rectangle
* @param annotationState annotation state to use for default values. Null
* is allowed if default state is prefered.
* @return new annotation reference for this page.
*/
public Annotation createAnnotation(Rectangle rect,
AnnotationState annotationState) {
// create a new instance of the object adding it to the library
Annotation newAnnotation =
AnnotationFactory.buildAnnotation(library,
AnnotationFactory.LINK_ANNOTATION,
rect,
annotationState);
// return to caller for further manipulations.
return addAnnotation(newAnnotation);
}
/**
* Adds an annotation that was previously added to the document. It is
* assumed that the annotation has a valid object reference. This
* is commonly used with the undo/redo state manager in the RI. Use
* the method @link{#createAnnotation} for creating new annotations.
*
* @param newAnnotation
* @return reference to annotaiton that was added.
*/
public Annotation addAnnotation(Annotation newAnnotation) {
// make sure the page annotations have been initialized.
if (!isInited) {
try {
initPageAnnotations();
} catch (InterruptedException e) {
logger.warning("Annotation Initialization interupted");
}
}
StateManager stateManager = library.getStateManager();
Object annots = library.getObject(entries, ANNOTS_KEY.getName());
boolean isAnnotAReference = library.isReference(entries, ANNOTS_KEY.getName());
// does the page not already have an annotations or if the annots
// dictionary is indirect. If so we have to add the page to the state
// manager
if (!isAnnotAReference && annots != null) {
// get annots array from page
if (annots instanceof Vector) {
// update annots dictionary with new annotations reference,
Vector v = (Vector) annots;
v.add(newAnnotation.getPObjectReference());
// add the page as state change
stateManager.addChange(
new PObject(this, this.getPObjectReference()));
}
} else if (isAnnotAReference && annots != null) {
// get annots array from page
if (annots instanceof Vector) {
// update annots dictionary with new annotations reference,
Vector v = (Vector) annots;
v.add(newAnnotation.getPObjectReference());
// add the annotations reference dictionary as state has changed
stateManager.addChange(
new PObject(annots, library.getObjectReference(
entries, ANNOTS_KEY.getName())));
}
}
// we need to add the a new annots reference
else {
Vector annotsVector = new Vector(4);
annotsVector.add(newAnnotation.getPObjectReference());
// create a new Dictionary of annotaions using an external reference
PObject annotsPObject = new PObject(annotsVector,
stateManager.getNewReferencNumber());
// add the new dictionary to the page
entries.put(ANNOTS_KEY, annotsPObject.getReference());
// add it to the library so we can resolve the reference
library.addObject(annotsVector, annotsPObject.getReference());
// add the page and the new dictionary to the state change
stateManager.addChange(
new PObject(this, this.getPObjectReference()));
stateManager.addChange(annotsPObject);
annotations = new ArrayList<Annotation>();
}
// update parent page reference.
newAnnotation.getEntries().put(Annotation.PARENT_PAGE_KEY,
this.getPObjectReference());
// add the annotations to the parsed annotations list
annotations.add(newAnnotation);
// add the new annotations to the library
library.addObject(newAnnotation, newAnnotation.getPObjectReference());
// finally add the new annotations to the state manager
stateManager.addChange(new PObject(newAnnotation, newAnnotation.getPObjectReference()));
// return to caller for further manipulations.
return newAnnotation;
}
/**
* Deletes the specified annotation instance from his page. If the
* annotation was origional then either the page or the annot ref object
* is also added to the state maanger. If the annotation was new then
* we just have to update the page and or annot reference as the objects
* will allready be in the state manager.
*/
public void deleteAnnotation(Annotation annot) {
// make sure the page annotations have been initialized.
if (!isInited) {
try {
initPageAnnotations();
} catch (InterruptedException e) {
logger.warning("Annotation Initialization interupted");
}
}
StateManager stateManager = library.getStateManager();
Object annots = getObject(ANNOTS_KEY);
boolean isAnnotAReference =
library.isReference(entries, ANNOTS_KEY.getName());
// mark the item as deleted so the state manager can clean up the reference.
annot.setDeleted(true);
// check to see if this is an existing annotations, if the annotations
// is existing then we have to mark either the page or annot ref as chagned.
if (!annot.isNew() && !isAnnotAReference) {
// add the page as state change
stateManager.addChange(
new PObject(this, this.getPObjectReference()));
}
// if not new and annot is a ref, we have to add annot ref as changed.
else if (!annot.isNew() && isAnnotAReference) {
stateManager.addChange(
new PObject(annots, library.getObjectReference(
entries, ANNOTS_KEY.getName())));
}
// removed the annotations from the annots vector
if (annots instanceof Vector) {
// update annots dictionary with new annotations reference,
Vector v = (Vector) annots;
v.remove(annot.getPObjectReference());
}
// remove the annotations form the annotation cache in the page object
if (annotations != null) {
annotations.remove(annot);
}
// finally remove it from the library, probably not necessary....
// library.removeObject(annot.getPObjectReference());
}
/**
* Updates the annotation associated with this page. If the annotation
* is not in this page then the annotation is no added.
*
* @param annotation annotation object that should be updated for this page.
* @return true if the update was successful, false otherwise.
*/
public boolean updateAnnotation(Annotation annotation) {
// bail on null annotations
if (annotation == null) {
return false;
}
// make sure the page annotations have been initialized.
if (!isInited) {
try {
initPageAnnotations();
} catch (InterruptedException e) {
logger.warning("Annotation Initialization interupted");
}
}
StateManager stateManager = library.getStateManager();
// if we are doing an update we have at least on annot
Vector<Reference> annots = (Vector)
library.getObject(entries, ANNOTS_KEY.getName());
// make sure annotations is in part of page.
boolean found = false;
for (Reference ref : annots) {
if (ref.equals(annotation.getPObjectReference())) {
found = true;
break;
}
}
if (!found) {
return false;
}
// check the state manager for an instance of this object
if (stateManager.contains(annotation.getPObjectReference())) {
// if found we just have to re add the object, foot work around
// page and annotations creation has already been done.
stateManager.addChange(
new PObject(annotation, annotation.getPObjectReference()));
return true;
}
// we have to do the checks for page and annot dictionary entry.
else {
// update parent page reference.
annotation.getEntries().put(Annotation.PARENT_PAGE_KEY,
this.getPObjectReference());
// add the annotations to the parsed annotations list
annotations.add(annotation);
// add the new annotations to the library
library.addObject(annotation, annotation.getPObjectReference());
// finally add the new annotations to the state manager
stateManager.addChange(new PObject(annotation, annotation.getPObjectReference()));
return true;
}
}
/**
* Gets a reference to the page's parent page tree. A reference can be resolved
* by the Library class.
*
* @return reference to parent page tree.
* @see org.icepdf.core.util.Library
*/
protected Reference getParentReference() {
return (Reference) entries.get("Parent");
}
/**
* Gets the page's parent page tree.
*
* @return parent page tree.
*/
public PageTree getParent() {
// retrieve a pointer to the pageTreeParent
return (PageTree) library.getObject(entries, "Parent");
}
/**
* Get the width and height that the page can occupy, given the userRotation,
* page's own pageRotation and cropBox boundary. The page's default zoom of
* 1.0f is used.
*
* @param userRotation Rotation factor specified by the user under which the
* page will be rotated.
* @return Dimension of width and height of the page represented in point
* units.
* @see #getSize(float, float)
*/
public PDimension getSize(float userRotation) {
return getSize(BOUNDARY_CROPBOX, userRotation, 1.0f);
}
/**
* Get the width and height that the page can occupy, given the userRotation,
* userZoom, page's own pageRotation and cropBox boundary.
*
* @param userRotation rotation factor specified by the user under which the
* page will be rotated.
* @param userZoom zoom factor specifed by the user under which the page will
* be rotated.
* @return Dimension of width and height of the page represented in point units.
*/
public PDimension getSize(float userRotation, float userZoom) {
return getSize(BOUNDARY_CROPBOX, userRotation, userZoom);
}
/**
* Get the width and height that the page can occupy, given the userRotation,
* userZoom, page's own pageRotation and cropBox boundary.
*
* @param boundary boundary constant to specify which boundary to respect when
* calculating the page's size.
* @param userRotation rotation factor specified by the user under which the
* page will be rotated.
* @param userZoom zoom factor specified by the user under which the page will
* be rotated.
* @return Dimension of width and height of the page represented in point units.
*/
public PDimension getSize(final int boundary, float userRotation, float userZoom) {
float totalRotation = getTotalRotation(userRotation);
PRectangle pageBoundary = getPageBoundary(boundary);
float width = pageBoundary.width * userZoom;
float height = pageBoundary.height * userZoom;
// No rotation, or flipped upside down
if (totalRotation == 0 || totalRotation == 180) {
// Do nothing
}
// Rotated sideways
else if (totalRotation == 90 || totalRotation == 270) {
float temp = width;
// flip with and height.
width = height;
height = temp;
}
// Arbitrary rotation
else {
AffineTransform at = new AffineTransform();
double radians = Math.toRadians(totalRotation);
at.rotate(radians);
Rectangle2D.Double boundingBox = new Rectangle2D.Double(0.0, 0.0, 0.0, 0.0);
Point2D.Double src = new Point2D.Double();
Point2D.Double dst = new Point2D.Double();
src.setLocation(0.0, height); // Top left
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(width, height); // Top right
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(0.0, 0.0); // Bottom left
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(width, 0.0); // Bottom right
at.transform(src, dst);
boundingBox.add(dst);
width = (float) boundingBox.getWidth();
height = (float) boundingBox.getHeight();
}
return new PDimension(width, height);
}
/**
* Get the bounding box that the page can occupy, given the userRotation and
* page's own pageRotation. The boundary of BOUNDARY_CROPBOX, and the default
* zoom of 1.0f are assumed.
*
* @param userRotation Rotation factor specified by the user under which the
* page will be rotated.
* @return Dimension of width and height of the page represented in point
* units.
* @see #getSize(float, float)
*/
public Rectangle2D.Double getBoundingBox(float userRotation) {
return getBoundingBox(BOUNDARY_CROPBOX, userRotation, 1.0f);
}
/**
* Get the bounding box that the page can occupy, given the userRotation,
* userZoom, page's own pageRotation.
*
* @param userRotation rotation factor specified by the user under which the
* page will be rotated.
* @param userZoom zoom factor specified by the user under which the page will
* be rotated.
* @return Rectangle encompassing the page represented in point units.
*/
public Rectangle2D.Double getBoundingBox(float userRotation, float userZoom) {
return getBoundingBox(BOUNDARY_CROPBOX, userRotation, userZoom);
}
/**
* Get the bounding box that the page can occupy, given the userRotation,
* userZoom, page's own pageRotation and cropBox boundary.
*
* @param boundary boundary constant to specify which boundary to respect when
* calculating the page's size.
* @param userRotation rotation factor specified by the user under which the
* page will be rotated.
* @param userZoom zoom factor specified by the user under which the page will
* be rotated.
* @return Rectangle encompassing the page represented in point units.
*/
public Rectangle2D.Double getBoundingBox(final int boundary, float userRotation, float userZoom) {
float totalRotation = getTotalRotation(userRotation);
PRectangle pageBoundary = getPageBoundary(boundary);
float width = pageBoundary.width * userZoom;
float height = pageBoundary.height * userZoom;
AffineTransform at = new AffineTransform();
double radians = Math.toRadians(totalRotation);
at.rotate(radians);
Rectangle2D.Double boundingBox = new Rectangle2D.Double(0.0, 0.0, 0.0, 0.0);
Point2D.Double src = new Point2D.Double();
Point2D.Double dst = new Point2D.Double();
src.setLocation(0.0, height); // Top left
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(width, height); // Top right
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(0.0, 0.0); // Bottom left
at.transform(src, dst);
boundingBox.add(dst);
src.setLocation(width, 0.0); // Bottom right
at.transform(src, dst);
boundingBox.add(dst);
return boundingBox;
}
/**
* Utility method for appling the page boundary rules.
*
* @param specifiedBox page boundary constant
* @return bounds of page after the chain of rules have been applied.
*/
public PRectangle getPageBoundary(final int specifiedBox) {
PRectangle userSpecifiedBox = null;
// required property
if (specifiedBox == BOUNDARY_MEDIABOX) {
userSpecifiedBox = (PRectangle) getMediaBox();
}
// required property
else if (specifiedBox == BOUNDARY_CROPBOX) {
userSpecifiedBox = (PRectangle) getCropBox();
}
// optional, default value is crop box
else if (specifiedBox == BOUNDARY_BLEEDBOX) {
if (bleedBox != null)
userSpecifiedBox = (PRectangle) getBleedBox();
}
// optional, default value is crop box
else if (specifiedBox == BOUNDARY_TRIMBOX) {
if (trimBox != null)
userSpecifiedBox = (PRectangle) getTrimBox();
}
// optional, default value is crop box
else if (specifiedBox == BOUNDARY_ARTBOX) {
if (artBox != null)
userSpecifiedBox = (PRectangle) getArtBox();
}
// encase of bad usage, default to crop box
else {
userSpecifiedBox = (PRectangle) getBleedBox();
}
// just in case, make sure we return a non null boundary
if (userSpecifiedBox == null) {
userSpecifiedBox = (PRectangle) getCropBox();
}
return userSpecifiedBox;
}
/**
* Returns a summary of the page dictionary entries.
*
* @return dictionary entries.
*/
public String toString() {
return "PAGE= " + entries.toString();
}
/**
* Gets the total rotation factor of the page after applying a user rotation
* factor. This method will normalize rotation factors to be in the range
* of 0 to 360 degrees.
*
* @param userRotation rotation factor to be applied to page
* @return Total Rotation, representing pageRoation + user rotation
* factor applied to the whole document.
*/
public float getTotalRotation(float userRotation) {
float totalRotation = getPageRotation() + userRotation;
// correct to keep in rotation in 360 range.
totalRotation %= 360;
if (totalRotation < 0)
totalRotation += 360;
// If they calculated the degrees from radians or whatever,
// then we need to make our even rotation comparisons work
if (totalRotation >= -0.001f && totalRotation <= 0.001f)
return 0.0f;
else if (totalRotation >= 89.99f && totalRotation <= 90.001f)
return 90.0f;
else if (totalRotation >= 179.99f && totalRotation <= 180.001f)
return 180.0f;
else if (totalRotation >= 269.99f && totalRotation <= 270.001f)
return 270.0f;
return totalRotation;
}
private float getPageRotation() {
// Get the pages default orientation if available, if not defined
// then it is zero.
Object tmpRotation = library.getObject(entries, "Rotate");
if (tmpRotation != null) {
pageRotation = ((Number) tmpRotation).floatValue();
// System.out.println("Page Rotation " + pageRotation);
}
// check parent to see if value has been set
else {
PageTree pageTree = getParent();
while (pageTree != null) {
if (pageTree.isRotationFactor) {
pageRotation = pageTree.rotationFactor;
break;
}
pageTree = pageTree.getParent();
}
}
// PDF specifies rotation as clockwise, but Java2D does it
// counter-clockwise, so normalise it to Java2D
pageRotation = 360 - pageRotation;
pageRotation %= 360;
// System.out.println("New Page Rotation " + pageRotation);
return pageRotation;
}
/**
* Gets all annotation information associated with this page. Each entry
* in the vector represents one annotation. The size of the vector represents
* the total number of annotations associated with the page.
*
* @return annotation associated with page; null, if there are no annotations.
*/
public ArrayList<Annotation> getAnnotations() {
if (!isInited) {
init();
}
return annotations;
}
/**
* Returns the decoded content stream for this page instance. A page instance
* can have more then one content stream associated with it.
*
* @return An array of decoded content stream. Each index in the array
* represents one content stream. Null return and null String array
* values are possible.
*/
public String[] getDecodedContentSteam() {
// Some PDF's won't have any content for a given page.
try {
initPageContents();
if (contents == null) {
return null;
}
String[] decodedContentStream = new String[contents.size()];
int i = 0;
for (Stream stream : contents) {
InputStream input = stream.getInputStreamForDecodedStreamBytes();
String content;
if (input instanceof SeekableInput) {
content = Utils.getContentFromSeekableInput((SeekableInput) input, false);
} else {
InputStream[] inArray = new InputStream[]{input};
content = Utils.getContentAndReplaceInputStream(inArray, false);
}
decodedContentStream[i] = content;
input.close();
i++;
}
return decodedContentStream;
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Error initializing page Contents.", e);
} catch (IOException e) {
logger.log(Level.SEVERE, "Error closing content stream");
}
return null;
}
/**
* Gets the media box boundary defined by this page. The media box is a
* required page entry and can be inherited from its parent page tree.
*
* @return media box boundary in user space units.
*/
public Rectangle2D.Float getMediaBox() {
// add all of the pages media box dimensions to a vector and process
Vector boxDimensions = (Vector) (library.getObject(entries, "MediaBox"));
if (boxDimensions != null) {
mediaBox = new PRectangle(boxDimensions);
// System.out.println("Page - MediaBox " + mediaBox);
}
// If mediaBox is null check with the parent pages, as media box is inheritable
if (mediaBox == null) {
PageTree pageTree = getParent();
while (pageTree != null && mediaBox == null) {
mediaBox = pageTree.getMediaBox();
pageTree = pageTree.getParent();
}
}
return mediaBox;
}
/**
* Gets the crop box boundary defined by this page. The media box is a
* required page entry and can be inherited from its parent page tree.
*
* @return crop box boundary in user space units.
*/
public Rectangle2D.Float getCropBox() {
// add all of the pages crop box dimensions to a vector and process
Vector boxDimensions = (Vector) (library.getObject(entries, "CropBox"));
if (boxDimensions != null) {
cropBox = new PRectangle(boxDimensions);
// System.out.println("Page - CropBox " + cropBox);
}
// If mediaBox is null check with the parent pages, as media box is inheritable
if (cropBox == null) {
PageTree pageTree = getParent();
while (pageTree != null && cropBox == null) {
if (pageTree.getCropBox() == null) {
break;
}
cropBox = pageTree.getCropBox();
pageTree = pageTree.getParent();
}
}
// Default value of the cropBox is the MediaBox if not set implicitly
PRectangle mediaBox = (PRectangle) getMediaBox();
if (cropBox == null && mediaBox != null) {
cropBox = (PRectangle) mediaBox.clone();
} else if (mediaBox != null) {
// PDF 1.5 spec states that the media box should be intersected with the
// crop box to get the new box. But we only want to do this if the
// cropBox is not the same as the mediaBox
cropBox = mediaBox.createCartesianIntersection(cropBox);
}
return cropBox;
}
/**
* Gets the art box boundary defined by this page. The art box is a
* required page entry and can be inherited from its parent page tree.
*
* @return art box boundary in user space units.
*/
public Rectangle2D.Float getArtBox() {
// get the art box vector value
Vector boxDimensions = (Vector) (library.getObject(entries, "ArtBox"));
if (boxDimensions != null) {
artBox = new PRectangle(boxDimensions);
// System.out.println("Page - ArtBox " + artBox);
}
// Default value of the artBox is the bleed if not set implicitly
if (artBox == null) {
artBox = (PRectangle) getCropBox();
}
return artBox;
}
/**
* Gets the bleed box boundary defined by this page. The bleed box is a
* required page entry and can be inherited from its parent page tree.
*
* @return bleed box boundary in user space units.
*/
public Rectangle2D.Float getBleedBox() {
// get the art box vector value
Vector boxDimensions = (Vector) (library.getObject(entries, "BleedBox"));
if (boxDimensions != null) {
bleedBox = new PRectangle(boxDimensions);
// System.out.println("Page - BleedBox " + bleedBox);
}
// Default value of the bleedBox is the bleed if not set implicitly
if (bleedBox == null) {
bleedBox = (PRectangle) getCropBox();
}
return bleedBox;
}
/**
* Gets the trim box boundary defined by this page. The trim box is a
* required page entry and can be inherited from its parent page tree.
*
* @return trim box boundary in user space units.
*/
public Rectangle2D.Float getTrimBox() {
// get the art box vector value
Vector boxDimensions = (Vector) (library.getObject(entries, "TrimBox"));
if (boxDimensions != null) {
trimBox = new PRectangle(boxDimensions);
// System.out.println("Page - TrimBox " + trimBox);
}
// Default value of the trimBox is the bleed if not set implicitly
if (trimBox == null) {
trimBox = (PRectangle) getCropBox();
}
return trimBox;
}
/**
* Gest the PageText data structure for this page. PageText is made up
* of lines, words and glyphs which can be used for searches, text extraction
* and text highlighting. The coordinates system has been normalized
* to page space.
*
* @return list of text sprites for the given page.
*/
public synchronized PageText getViewText() {
if (!isInited) {
init();
}
return shapes.getPageText();
}
/**
* Gest the PageText data structure for this page using an accelerated
* parsing technique that ignores some text elements. This method should
* be used for straight text extraction.
*
* @return vector of Strings of all text objects inside the specified page.
*/
public synchronized PageText getText() {
// we only do this once per page
if (isInited) {
if (shapes != null && shapes.getPageText() != null) {
return shapes.getPageText();
}
}
Shapes textBlockShapes = null;
try {
/**
* Finally iterate through the contents vector and concat all of the
* the resouse streams together so that the contant parser can
* go to town and build all of the pages shapes.
*/
if (contents == null) {
// Get the value of the page's content entry
initPageContents();
}
if (resources == null) {
// get pages resources
initPageResources();
}
if (contents != null) {
Vector<InputStream> inputStreamsVec =
new Vector<InputStream>(contents.size());
for (int st = 0, max = contents.size(); st < max; st++) {
Stream stream = contents.elementAt(st);
InputStream input = stream.getInputStreamForDecodedStreamBytes();
inputStreamsVec.add(input);
}
SequenceInputStream sis = new SequenceInputStream(inputStreamsVec.iterator());
// push the library and resources to the content parse
// and return the the shapes vector for the screen elements
// for the page/resources in question.
try {
ContentParser cp = new ContentParser(library, resources);
// custom parsing for text extraction, should be faster
textBlockShapes = cp.parseTextBlocks(sis);
} catch (Exception e) {
logger.log(Level.FINE, "Error getting page text.", e);
} finally {
try {
sis.close();
} catch (IOException e) {
logger.log(Level.FINE, "Error closing page stream.", e);
}
}
}
} catch (InterruptedException e) {
// keeps shapes vector so we can paint what we have but make init state as false
// so we can try to reparse it later.
isInited = false;
logger.log(Level.SEVERE, "Page text extraction thread interrupted.", e);
}
if (textBlockShapes != null && textBlockShapes.getPageText() != null) {
return textBlockShapes.getPageText();
} else {
return null;
}
}
/**
* Gets a vector of Images where each index represents an image inside
* this page.
*
* @return vector of Images inside the current page
*/
public synchronized Vector getImages() {
if (!isInited) {
init();
}
return shapes.getImages();
}
public Resources getResources() {
return resources;
}
/**
* Reduces the amount of memory used by this object.
*/
public void reduceMemory() {
dispose(true);
}
public void addPaintPageListener(PaintPageListener listener) {
// add a listener if it is not already registered
synchronized (paintPageListeners) {
if (!paintPageListeners.contains(listener)) {
paintPageListeners.addElement(listener);
}
}
}
public void removePaintPageListener(PaintPageListener listener) {
// remove a listener if it is already registered
synchronized (paintPageListeners) {
if (paintPageListeners.contains(listener)) {
paintPageListeners.removeElement(listener);
}
}
}
public void notifyPaintPageListeners() {
// create the event object
PaintPageEvent evt = new PaintPageEvent(this);
// make a copy of the listener object vector so that it cannot
// be changed while we are firing events
// NOTE: this is good practise, but most likely a little to heavy
// for this event type
// Vector v;
// synchronized (this) {
// v = (Vector) paintPageListeners.clone();
// }
//
// // fire the event to all listeners
// PaintPageListener client;
// for (int i = v.size() - 1; i >= 0; i--) {
// client = (PaintPageListener) v.elementAt(i);
// client.paintPage(evt);
// }
// fire the event to all listeners
PaintPageListener client;
for (int i = paintPageListeners.size() - 1; i >= 0; i--) {
client = paintPageListeners.elementAt(i);
client.paintPage(evt);
}
}
/**
* Diese Methode gehört nicht zu dieser Bibliothek.
*
* Sie wurde hinzugefügt, um Zugriff auf die Elemente
* im PDF Dokument zu erhalten.
* @return
*/
public Shapes getShapes()
{
if(!isInited)
init();
return shapes;
}
}