/* * Copyright 2006-2017 ICEsoft Technologies Canada Corp. * * 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.pobjects.graphics.WatermarkCallback; import org.icepdf.core.util.Library; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.List; /** * <p>This class represents a document's page tree which defines the ordering * of pages in the document. The tree structure allows a user to quickly * open a document containing thousands of pages. The tree contains nodes of * two types, page tree nodes and page nodes, where page tree nodes are * intermediate nodes and pages are leaves. A simple example of this tree structure * is a single page tree node that references all of the document's page * objects directly.</p> * <br> * <p>The page tree is accessible via the document catalog and can be traversed * to display a desired page or extracts its content.<p> * * @see org.icepdf.core.pobjects.Page * @see org.icepdf.core.pobjects.Catalog * @since 2.0 */ public class PageTree extends Dictionary { public static final Name TYPE = new Name("Pages"); public static final Name PARENT_KEY = new Name("Parent"); public static final Name COUNT_KEY = new Name("Count"); public static final Name MEDIABOX_KEY = new Name("MediaBox"); public static final Name CROPBOX_KEY = new Name("CropBox"); public static final Name KIDS_KEY = new Name("Kids"); public static final Name ROTATE_KEY = new Name("Rotate"); public static final Name RESOURCES_KEY = new Name("Resources"); // Number of leaf nodes private int kidsCount = 0; // vector of references to leafs private List kidsReferences; // vector of the pages associated with tree private HashMap<Integer, WeakReference<Object>> kidsPageAndPages; // pointer to parent page tree private PageTree parent; // initiated flag private boolean inited; // inheritable page boundary data. private PRectangle mediaBox; private PRectangle cropBox; // inheritable Resources private Resources resources; // loaded resource flag, we can't use null check as some trees don't have // resources. private boolean loadedResources; private WatermarkCallback watermarkCallback; /** * Inheritable rotation factor by child pages. */ protected float rotationFactor = 0; /** * Indicates that the PageTree has a rotation factor which should be respected. */ protected boolean isRotationFactor = false; /** * Creates a new instance of a PageTree. * * @param l document library. * @param h PageTree dictionary entries. */ public PageTree(Library l, HashMap h) { super(l, h); } /** * Reset the pages initialized flag and as a result subsequent calls to * this PageTree may trigger a call to init(). */ public void resetInitializedState() { inited = false; } /** * Initiate the PageTree. */ public synchronized void init() { if (inited) { return; } Object parentTree = library.getObject(entries, PARENT_KEY); if (parentTree instanceof PageTree) { parent = (PageTree) parentTree; } kidsCount = library.getNumber(entries, COUNT_KEY).intValue(); kidsReferences = (List) library.getObject(entries, KIDS_KEY); kidsPageAndPages = new HashMap<Integer, WeakReference<Object>>(kidsReferences.size()); // Rotation is only respected if child pages do not have their own // rotation value. Object tmpRotation = library.getObject(entries, ROTATE_KEY); if (tmpRotation != null) { rotationFactor = ((Number) tmpRotation).floatValue(); // mark that we have an inheritable value isRotationFactor = true; } inited = true; } /** * Gets the media box boundary defined by this page tree. 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 PRectangle getMediaBox() { if (!inited) { init(); } if (mediaBox != null) { return mediaBox; } // add all of the pages media box dimensions to a vector and process List boxDimensions = (List) (library.getObject(entries, MEDIABOX_KEY)); if (boxDimensions != null) { mediaBox = new PRectangle(boxDimensions); } // 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(); if (mediaBox == null) { pageTree = pageTree.getParent(); } } } return mediaBox; } /** * Gets the crop box boundary defined by this page tree. 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 PRectangle getCropBox() { if (!inited) { init(); } if (cropBox != null) { return cropBox; } // add all of the pages crop box dimensions to a vector and process List boxDimensions = (List) (library.getObject(entries, CROPBOX_KEY)); if (boxDimensions != null) { cropBox = new PRectangle(boxDimensions); } // Default value of the cropBox is the MediaBox if not set implicitly PRectangle mediaBox = 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 Resources defined by this PageTree. The Resources entry can * be inherited by the child Page objects. * <br> * The caller is responsible for disposing of the returned Resources object. * * @return Resources associates with the PageTree */ public synchronized Resources getResources() { // make sure we synchronize this to avoid a false resource grab. if (!loadedResources) { loadedResources = true; resources = library.getResources(entries, RESOURCES_KEY); } return resources; } /** * Gets the page tree node that is the immediate parent of this one. * * @return parent page tree; null, if this is the root page tree. */ public PageTree getParent() { return parent; } /** * Gets the page number of the page specifed by a reference. * * @param r reference to a page in the page tree. * @return page number of the specified reference. If no page is found, -1 * is returned. */ public int getPageNumber(Reference r) { Page pg = (Page) library.getObject(r); if (pg == null) return -1; // pg.init(); int globalIndex = 0; Reference currChildRef = r; Reference currParentRef = pg.getParentReference(); PageTree currParent = pg.getParent(); while (currParentRef != null && currParent != null) { currParent.init(); int refIndex = currParent.indexOfKidReference(currChildRef); if (refIndex < 0) return -1; int localIndex = 0; for (int i = 0; i < refIndex; i++) { Object pageOrPages = currParent.getPageOrPagesPotentiallyNotInitedFromReferenceAt(i); if (pageOrPages instanceof Page) { localIndex++; } else if (pageOrPages instanceof PageTree) { PageTree peerPageTree = (PageTree) pageOrPages; peerPageTree.init(); localIndex += peerPageTree.getNumberOfPages(); } } globalIndex += localIndex; currChildRef = currParentRef; currParentRef = (Reference) currParent.entries.get(PARENT_KEY); currParent = currParent.parent; } return globalIndex; } /** * Utility method for getting kid index. * * @param r * @return */ private int indexOfKidReference(Reference r) { for (int i = 0; i < kidsReferences.size(); i++) { Reference ref = (Reference) kidsReferences.get(i); if (ref.equals(r)) return i; } return -1; } /** * Utility method for initializing a page in the page tree. * * @param index index in the kids vector to initialize * @return */ private Object getPageOrPagesPotentiallyNotInitedFromReferenceAt(int index) { WeakReference<Object> pageOrPages = kidsPageAndPages.get(index); if (pageOrPages == null || pageOrPages.get() == null) { Reference ref = (Reference) kidsReferences.get(index); Object tmp = library.getObject(ref); pageOrPages = new WeakReference<Object>(tmp); kidsPageAndPages.put(index, pageOrPages); return tmp; } return pageOrPages.get(); } /** * Utility method for initializing a page with its page number * * @param globalIndex * @return */ private Page getPagePotentiallyNotInitedByRecursiveIndex(int globalIndex) { int globalIndexSoFar = 0; int numLocalKids = kidsReferences.size(); Object pageOrPages; for (int i = 0; i < numLocalKids; i++) { pageOrPages = getPageOrPagesPotentiallyNotInitedFromReferenceAt(i); if (pageOrPages instanceof Page) { if (globalIndex == globalIndexSoFar) return (Page) pageOrPages; globalIndexSoFar++; } else if (pageOrPages instanceof PageTree) { PageTree childPageTree = (PageTree) pageOrPages; childPageTree.init(); int numChildPages = childPageTree.getNumberOfPages(); if (globalIndex >= globalIndexSoFar && globalIndex < (globalIndexSoFar + numChildPages)) { return childPageTree.getPagePotentiallyNotInitedByRecursiveIndex( globalIndex - globalIndexSoFar); } globalIndexSoFar += numChildPages; } // corner case where pages didn't have "pages" key. else if (pageOrPages instanceof HashMap) { HashMap dictionary = (HashMap) pageOrPages; if (dictionary.containsKey(new Name("Kids"))) { PageTree childPageTree = new PageTree(library, dictionary); childPageTree.init(); int numChildPages = childPageTree.getNumberOfPages(); if (globalIndex >= globalIndexSoFar && globalIndex < (globalIndexSoFar + numChildPages)) { return childPageTree.getPagePotentiallyNotInitedByRecursiveIndex( globalIndex - globalIndexSoFar); } globalIndexSoFar += numChildPages; } } } return null; } /** * Sets a page watermark implementation to be painted on top of the page * content. Watermark can be specified for each page or once by calling * document.setWatermark(). * * @param watermarkCallback watermark implementation. */ protected void setWatermarkCallback(WatermarkCallback watermarkCallback) { this.watermarkCallback = watermarkCallback; } /** * In a PDF file there is a root Pages object, which contains * children Page objects, as well as children PageTree objects, * all arranged in a tree. * getNumberOfPages() exists in every PageTree object, giving * the number of Page objects under it, recursively. * So, each PageTree object would have a different number of pages, * and only the root PageTree objects would have a number * representative of the whole Document. * * @return Total number of Page objects under this PageTree */ public int getNumberOfPages() { return kidsCount; } /** * Gets a Page from the PDF file, locks it for the user, * initializes the Page, and returns it. * <br> * ICEpdf uses a caching and memory management mechanism * to reduce the CPU, I/O, and time to access a Page, * which requires a locking and releasing protocol. * Calls to the <code>getPage</code> must be matched with * corresponding calls to <code>releasePage</code>. * Calls cannot be nested, meaning that <code>releasePage</code> * must be called before a subsequent invocation of * <code>getPage</code> for the same <code>pageIndex</code>. * * @param pageNumber Zero-based index of the Page to return. * @return The requested Page. */ public Page getPage(int pageNumber) { if (pageNumber < 0) return null; Page page = getPagePotentiallyNotInitedByRecursiveIndex(pageNumber); // pass in the watermark, even null to wipe a previous watermark if (page != null) { page.setWatermarkCallback(watermarkCallback); page.setPageIndex(pageNumber); } return getPagePotentiallyNotInitedByRecursiveIndex(pageNumber); } /** * Get the page reference for the specified page number. * * @param pageNumber zero-based indox of page to find reference of. * @return found page reference or null if number could not be resolved. */ public Reference getPageReference(int pageNumber) { if (pageNumber < 0) return null; Page p = getPagePotentiallyNotInitedByRecursiveIndex(pageNumber); if (p != null) { return p.getPObjectReference(); } return null; } /** * Returns a summary of the PageTree dictionary values. * * @return dictionary values. */ public String toString() { return "PAGES= " + entries.toString(); } }