| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.area; |
| |
| import java.awt.Rectangle; |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.apps.FOPException; |
| import org.apache.fop.fo.flow.Marker; |
| import org.apache.fop.fo.pagination.SimplePageMaster; |
| import org.apache.fop.traits.WritingModeTraitsGetter; |
| |
| import static org.apache.fop.fo.Constants.EN_FIC; |
| import static org.apache.fop.fo.Constants.EN_FSWP; |
| import static org.apache.fop.fo.Constants.EN_LEWP; |
| import static org.apache.fop.fo.Constants.EN_LSWP; |
| import static org.apache.fop.fo.Constants.FO_REGION_BODY; |
| |
| /** |
| * Page viewport that specifies the viewport area and holds the page contents. |
| * This is the top level object for a page and remains valid for the life |
| * of the document and the area tree. |
| * This object may be used as a key to reference a page. |
| * This is the level that creates the page. |
| * The page (reference area) is then rendered inside the page object |
| */ |
| public class PageViewport extends AreaTreeObject implements Resolvable { |
| |
| private Page page; |
| private Rectangle viewArea; |
| private String simplePageMasterName; |
| |
| /** |
| * Unique key to identify the page. pageNumberString and pageIndex are both no option |
| * for this. |
| */ |
| private String pageKey; |
| |
| private int pageNumber = -1; |
| private String pageNumberString = null; |
| private int pageIndex = -1; //-1 = undetermined |
| private boolean blank; |
| |
| private transient PageSequence pageSequence; |
| |
| // set of IDs that appear first (or exclusively) on this page: |
| private Set<String> idFirsts = new java.util.HashSet<String>(); |
| |
| // this keeps a list of currently unresolved areas or extensions |
| // once an idref is resolved it is removed |
| // when this is empty the page can be rendered |
| private Map<String, List<Resolvable>> unresolvedIDRefs |
| = new java.util.HashMap<String, List<Resolvable>>(); |
| |
| private Map<String, List<PageViewport>> pendingResolved = null; |
| |
| // hashmap of markers for this page |
| // start and end are added by the fo that contains the markers |
| private Map<String, Marker> markerFirstStart = null; |
| private Map<String, Marker> markerLastStart = null; |
| private Map<String, Marker> markerFirstAny = null; |
| private Map<String, Marker> markerLastEnd = null; |
| private Map<String, Marker> markerLastAny = null; |
| |
| /** |
| * logging instance |
| */ |
| protected static final Log log = LogFactory.getLog(PageViewport.class); |
| |
| /** |
| * Create a page viewport. |
| * @param spm SimplePageMaster indicating the page and region dimensions |
| * @param pageNumber the page number |
| * @param pageStr String representation of the page number |
| * @param blank true if this is a blank page |
| * @param spanAll true if the first span area spans all columns |
| */ |
| public PageViewport(SimplePageMaster spm, int pageNumber, String pageStr, |
| boolean blank, boolean spanAll) { |
| this.simplePageMasterName = spm.getMasterName(); |
| setExtensionAttachments(spm.getExtensionAttachments()); |
| setForeignAttributes(spm.getForeignAttributes()); |
| this.blank = blank; |
| int pageWidth = spm.getPageWidth().getValue(); |
| int pageHeight = spm.getPageHeight().getValue(); |
| this.pageNumber = pageNumber; |
| this.pageNumberString = pageStr; |
| this.viewArea = new Rectangle(0, 0, pageWidth, pageHeight); |
| this.page = new Page(spm); |
| createSpan(spanAll); |
| } |
| |
| /** |
| * Create a page viewport. |
| * @param spm SimplePageMaster indicating the page and region dimensions |
| * @param pageNumber the page number |
| * @param pageStr String representation of the page number |
| * @param blank true if this is a blank page |
| */ |
| public PageViewport(SimplePageMaster spm, int pageNumber, String pageStr, boolean blank) { |
| this(spm, pageNumber, pageStr, blank, false); |
| } |
| |
| /** |
| * Copy constructor. |
| * @param original the original PageViewport to copy from |
| * @throws FOPException when cloning of the page is not supported |
| */ |
| public PageViewport(PageViewport original) throws FOPException { |
| if (original.extensionAttachments != null) { |
| setExtensionAttachments(original.extensionAttachments); |
| } |
| if (original.foreignAttributes != null) { |
| setForeignAttributes(original.foreignAttributes); |
| } |
| this.pageIndex = original.pageIndex; |
| this.pageNumber = original.pageNumber; |
| this.pageNumberString = original.pageNumberString; |
| try { |
| this.page = (Page) original.page.clone(); |
| } catch (CloneNotSupportedException e) { |
| throw new FOPException(e); |
| } |
| this.viewArea = new Rectangle(original.viewArea); |
| this.simplePageMasterName = original.simplePageMasterName; |
| this.blank = original.blank; |
| } |
| |
| /** |
| * Constructor used by the area tree parser. |
| * @param viewArea the view area |
| * @param pageNumber the page number |
| * @param pageStr String representation of the page number |
| * @param simplePageMasterName name of the original simple-page-master that generated this page |
| * @param blank true if this is a blank page |
| */ |
| public PageViewport(Rectangle viewArea, int pageNumber, String pageStr, |
| String simplePageMasterName, boolean blank) { |
| this.viewArea = viewArea; |
| this.pageNumber = pageNumber; |
| this.pageNumberString = pageStr; |
| this.simplePageMasterName = simplePageMasterName; |
| this.blank = blank; |
| } |
| |
| /** |
| * Sets the page sequence this page belongs to |
| * @param seq the page sequence |
| */ |
| public void setPageSequence(PageSequence seq) { |
| this.pageSequence = seq; |
| } |
| |
| /** @return the page sequence this page belongs to */ |
| public PageSequence getPageSequence() { |
| return this.pageSequence; |
| } |
| |
| /** |
| * Get the view area rectangle of this viewport. |
| * @return the rectangle for this viewport |
| */ |
| public Rectangle getViewArea() { |
| return viewArea; |
| } |
| |
| /** |
| * Get the page reference area with the contents. |
| * @return the page reference area |
| */ |
| public Page getPage() { |
| return page; |
| } |
| |
| /** |
| * Sets the page object for this PageViewport. |
| * @param page the page |
| */ |
| public void setPage(Page page) { |
| this.page = page; |
| } |
| |
| /** |
| * Get the page number of this page. |
| * @return the integer value that represents this page |
| */ |
| public int getPageNumber() { |
| return pageNumber; |
| } |
| |
| /** |
| * Get the page number of this page. |
| * @return the string that represents this page |
| */ |
| public String getPageNumberString() { |
| return pageNumberString; |
| } |
| |
| /** |
| * Sets the page index of the page in this rendering run. |
| * (This is not the same as the page number!) |
| * @param index the page index (zero-based), -1 if it is undetermined |
| */ |
| public void setPageIndex(int index) { |
| this.pageIndex = index; |
| } |
| |
| /** |
| * @return the overall page index of the page in this rendering run (zero-based, |
| * -1 if it is undetermined). |
| */ |
| public int getPageIndex() { |
| return this.pageIndex; |
| } |
| |
| /** |
| * Sets the unique key for this PageViewport that will be used to reference this page. |
| * @param key the unique key. |
| */ |
| public void setKey(String key) { |
| this.pageKey = key; |
| } |
| |
| /** |
| * Get the key for this page viewport. |
| * This is used so that a serializable key can be used to |
| * lookup the page or some other reference. |
| * |
| * @return a unique page viewport key for this area tree |
| */ |
| public String getKey() { |
| if (this.pageKey == null) { |
| throw new IllegalStateException("No page key set on the PageViewport: " + toString()); |
| } |
| return this.pageKey; |
| } |
| |
| /** |
| * Add an "ID-first" to this page. |
| * This is typically called by the {@link AreaTreeHandler} when associating |
| * an ID with a {@link PageViewport}. |
| * |
| * @param id the id to be registered as first appearing on this page |
| */ |
| public void setFirstWithID(String id) { |
| if (id != null) { |
| idFirsts.add(id); |
| } |
| } |
| |
| /** |
| * Check whether a certain id first appears on this page |
| * |
| * @param id the id to be checked |
| * @return true if this page is the first where the id appears |
| */ |
| public boolean isFirstWithID(String id) { |
| return idFirsts.contains(id); |
| } |
| |
| /** |
| * Add an idref to this page. |
| * All idrefs found for child areas of this {@link PageViewport} are added |
| * to unresolvedIDRefs, for subsequent resolution by {@link AreaTreeHandler} |
| * calls to this object's {@code resolveIDRef()}. |
| * |
| * @param idref the idref |
| * @param res the child element of this page that needs this |
| * idref resolved |
| */ |
| public void addUnresolvedIDRef(String idref, Resolvable res) { |
| if (unresolvedIDRefs == null) { |
| unresolvedIDRefs = new HashMap<String, List<Resolvable>>(); |
| } |
| List<Resolvable> pageViewports = unresolvedIDRefs.get(idref); |
| if (pageViewports == null) { |
| pageViewports = new ArrayList<Resolvable>(); |
| unresolvedIDRefs.put(idref, pageViewports); |
| } |
| pageViewports.add(res); |
| } |
| |
| /** |
| * Check if this page has been fully resolved. |
| * @return true if the page is resolved and can be rendered |
| */ |
| public boolean isResolved() { |
| return unresolvedIDRefs == null |
| || unresolvedIDRefs.size() == 0; |
| } |
| |
| /** |
| * Get the unresolved idrefs for this page. |
| * @return String array of idref's that still have not been resolved |
| */ |
| public String[] getIDRefs() { |
| return (unresolvedIDRefs == null) ? null |
| : unresolvedIDRefs.keySet().toArray( |
| new String[unresolvedIDRefs.keySet().size()]); |
| } |
| |
| /** {@inheritDoc} */ |
| public void resolveIDRef(String id, List<PageViewport> pages) { |
| if (page == null) { |
| if (pendingResolved == null) { |
| pendingResolved = new HashMap<String, List<PageViewport>>(); |
| } |
| pendingResolved.put(id, pages); |
| } else { |
| if (unresolvedIDRefs != null) { |
| List<Resolvable> todo = unresolvedIDRefs.get(id); |
| if (todo != null) { |
| for (Resolvable res : todo) { |
| res.resolveIDRef(id, pages); |
| } |
| } |
| } |
| } |
| if (unresolvedIDRefs != null && pages != null) { |
| unresolvedIDRefs.remove(id); |
| if (unresolvedIDRefs.isEmpty()) { |
| unresolvedIDRefs = null; |
| } |
| } |
| } |
| |
| /** |
| * Add the markers for this page. |
| * Only the required markers are kept. |
| * For "first-starting-within-page" it adds the markers |
| * that are starting only if the marker class name is not |
| * already added. |
| * For "first-including-carryover" it adds any starting marker |
| * if the marker class name is not already added. |
| * For "last-starting-within-page" it adds all marks that |
| * are starting, replacing earlier markers. |
| * For "last-ending-within-page" it adds all markers that |
| * are ending, replacing earlier markers. |
| * |
| * Should this logic be placed in the Page layout manager. |
| * |
| * @param marks the map of markers to add |
| * @param starting if the area being added is starting or ending |
| * @param isfirst if the area being added has is-first trait |
| * @param islast if the area being added has is-last trait |
| */ |
| public void addMarkers(Map<String, Marker> marks, boolean starting, |
| boolean isfirst, boolean islast) { |
| |
| if (marks == null) { |
| return; |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("--" + marks.keySet() + ": " |
| + (starting ? "starting" : "ending") |
| + (isfirst ? ", first" : "") |
| + (islast ? ", last" : "")); |
| } |
| |
| // at the start of the area, register is-first and any areas |
| if (starting) { |
| if (isfirst) { |
| if (markerFirstStart == null) { |
| markerFirstStart = new HashMap<String, Marker>(); |
| } |
| if (markerFirstAny == null) { |
| markerFirstAny = new HashMap<String, Marker>(); |
| } |
| // first on page: only put in new values, leave current |
| for (String key : marks.keySet()) { |
| if (!markerFirstStart.containsKey(key)) { |
| markerFirstStart.put(key, marks.get(key)); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding marker " + key + " to FirstStart"); |
| } |
| } |
| if (!markerFirstAny.containsKey(key)) { |
| markerFirstAny.put(key, marks.get(key)); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding marker " + key + " to FirstAny"); |
| } |
| } |
| } |
| if (markerLastStart == null) { |
| markerLastStart = new HashMap<String, Marker>(); |
| } |
| // last on page: replace all |
| markerLastStart.putAll(marks); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding all markers to LastStart"); |
| } |
| } else { |
| if (markerFirstAny == null) { |
| markerFirstAny = new HashMap<String, Marker>(); |
| } |
| // first on page: only put in new values, leave current |
| for (String key : marks.keySet()) { |
| if (!markerFirstAny.containsKey(key)) { |
| markerFirstAny.put(key, marks.get(key)); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding marker " + key + " to FirstAny"); |
| } |
| } |
| } |
| } |
| } else { |
| // at the end of the area, register is-last and any areas |
| if (islast) { |
| if (markerLastEnd == null) { |
| markerLastEnd = new HashMap<String, Marker>(); |
| } |
| // last on page: replace all |
| markerLastEnd.putAll(marks); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding all markers to LastEnd"); |
| } |
| } |
| if (markerLastAny == null) { |
| markerLastAny = new HashMap<String, Marker>(); |
| } |
| // last on page: replace all |
| markerLastAny.putAll(marks); |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " |
| + "Adding all markers to LastAny"); |
| } |
| } |
| } |
| |
| /** |
| * Get a marker from this page. |
| * This will retrieve a marker with the class name |
| * and position. |
| * |
| * @param name The class name of the marker to retrieve |
| * @param pos the position to retrieve |
| * @return Object the marker found or null |
| */ |
| public Marker getMarker(String name, int pos) { |
| Marker mark = null; |
| String posName = null; |
| switch (pos) { |
| case EN_FSWP: |
| if (markerFirstStart != null) { |
| mark = markerFirstStart.get(name); |
| posName = "FSWP"; |
| } |
| if (mark == null && markerFirstAny != null) { |
| mark = markerFirstAny.get(name); |
| posName = "FirstAny after " + posName; |
| } |
| break; |
| case EN_FIC: |
| if (markerFirstAny != null) { |
| mark = markerFirstAny.get(name); |
| posName = "FIC"; |
| } |
| break; |
| case EN_LSWP: |
| if (markerLastStart != null) { |
| mark = markerLastStart.get(name); |
| posName = "LSWP"; |
| } |
| if (mark == null && markerLastAny != null) { |
| mark = markerLastAny.get(name); |
| posName = "LastAny after " + posName; |
| } |
| break; |
| case EN_LEWP: |
| if (markerLastEnd != null) { |
| mark = markerLastEnd.get(name); |
| posName = "LEWP"; |
| } |
| if (mark == null && markerLastAny != null) { |
| mark = markerLastAny.get(name); |
| posName = "LastAny after " + posName; |
| } |
| break; |
| default: |
| assert false; |
| } |
| if (log.isTraceEnabled()) { |
| log.trace("page " + pageNumberString + ": " + "Retrieving marker " + name |
| + " at position " + posName); |
| } |
| return mark; |
| } |
| |
| /** Dumps the current marker data to the logger. */ |
| public void dumpMarkers() { |
| if (log.isTraceEnabled()) { |
| log.trace("FirstAny: " + this.markerFirstAny); |
| log.trace("FirstStart: " + this.markerFirstStart); |
| log.trace("LastAny: " + this.markerLastAny); |
| log.trace("LastEnd: " + this.markerLastEnd); |
| log.trace("LastStart: " + this.markerLastStart); |
| } |
| } |
| |
| /** |
| * Save the page contents to an object stream. |
| * The map of unresolved references are set on the page so that |
| * the resolvers can be properly serialized and reloaded. |
| * @param out the object output stream to write the contents |
| * @throws IOException in case of an I/O error while serializing the page |
| */ |
| public void savePage(ObjectOutputStream out) throws IOException { |
| // set the unresolved references so they are serialized |
| page.setUnresolvedReferences(unresolvedIDRefs); |
| out.writeObject(page); |
| page = null; |
| } |
| |
| /** |
| * Load the page contents from an object stream. |
| * This loads the page contents from the stream and |
| * if there are any unresolved references that were resolved |
| * while saved they will be resolved on the page contents. |
| * @param in the object input stream to read the page from |
| * @throws ClassNotFoundException if a class was not found while loading the page |
| * @throws IOException if an I/O error occurred while loading the page |
| */ |
| public void loadPage(ObjectInputStream in) throws IOException, ClassNotFoundException { |
| page = (Page) in.readObject(); |
| unresolvedIDRefs = page.getUnresolvedReferences(); |
| if (unresolvedIDRefs != null && pendingResolved != null) { |
| for (String id : pendingResolved.keySet()) { |
| resolveIDRef(id, pendingResolved.get(id)); |
| } |
| pendingResolved = null; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public Object clone() throws CloneNotSupportedException { |
| PageViewport pvp = (PageViewport) super.clone(); |
| pvp.page = (Page) page.clone(); |
| pvp.viewArea = (Rectangle) viewArea.clone(); |
| return pvp; |
| } |
| |
| /** |
| * Clear the page contents to save memory. |
| * This object is kept for the life of the area tree since |
| * it holds id and marker information and is used as a key. |
| */ |
| public void clear() { |
| page = null; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String toString() { |
| StringBuffer sb = new StringBuffer(64); |
| sb.append("PageViewport: page="); |
| sb.append(getPageNumberString()); |
| return sb.toString(); |
| } |
| |
| /** @return the name of the simple-page-master that created this page */ |
| public String getSimplePageMasterName() { |
| return this.simplePageMasterName; |
| } |
| |
| /** @return True if this is a blank page. */ |
| public boolean isBlank() { |
| return this.blank; |
| } |
| |
| /** |
| * Convenience method to get BodyRegion of this PageViewport |
| * @return BodyRegion object |
| */ |
| public BodyRegion getBodyRegion() { |
| return (BodyRegion) getPage().getRegionViewport(FO_REGION_BODY).getRegionReference(); |
| } |
| |
| /** |
| * Convenience method to create a new Span for this |
| * this PageViewport. |
| * |
| * @param spanAll whether this is a single-column span |
| * @return Span object created |
| */ |
| public Span createSpan(boolean spanAll) { |
| return getBodyRegion().getMainReference().createSpan(spanAll); |
| } |
| |
| /** |
| * Convenience method to get the span-reference-area currently |
| * being processed |
| * |
| * @return span currently being processed. |
| */ |
| public Span getCurrentSpan() { |
| return getBodyRegion().getMainReference().getCurrentSpan(); |
| } |
| |
| /** |
| * Convenience method to get the normal-flow-reference-area |
| * currently being processed |
| * |
| * @return span currently being processed. |
| */ |
| public NormalFlow getCurrentFlow() { |
| return getCurrentSpan().getCurrentFlow(); |
| } |
| |
| /** |
| * Convenience method to increment the Span to the |
| * next NormalFlow to be processed, and to return that flow. |
| * |
| * @return the next NormalFlow in the Span. |
| */ |
| public NormalFlow moveToNextFlow() { |
| return getCurrentSpan().moveToNextFlow(); |
| } |
| |
| /** |
| * Convenience method to return a given region-reference-area, |
| * keyed by the Constants class identifier for the corresponding |
| * formatting object (ie. Constants.FO_REGION_BODY, FO_REGION_START, |
| * etc.) |
| * |
| * @param id the Constants class identifier for the region. |
| * @return the corresponding region-reference-area for this page. |
| */ |
| public RegionReference getRegionReference(int id) { |
| return getPage().getRegionViewport(id).getRegionReference(); |
| } |
| |
| /** |
| * Sets the writing mode traits for the page associated with this viewport. |
| * @param wmtg a WM traits getter |
| */ |
| public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { |
| if ( page != null ) { |
| page.setWritingModeTraits(wmtg); |
| } |
| } |
| |
| } |