blob: ad1071f58869ab5747e1d6c13acb2f16efd6d12f [file] [log] [blame]
/*
* 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;
// Java
import java.io.OutputStream;
import java.util.List;
import org.xml.sax.SAXException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.extensions.ExtensionAttachment;
import org.apache.fop.fo.extensions.ExternalDocument;
import org.apache.fop.fo.extensions.destination.Destination;
import org.apache.fop.fo.pagination.AbstractPageSequence;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.fo.pagination.bookmarks.BookmarkTree;
import org.apache.fop.layoutmgr.ExternalDocumentLayoutManager;
import org.apache.fop.layoutmgr.LayoutManagerMaker;
import org.apache.fop.layoutmgr.LayoutManagerMapping;
import org.apache.fop.layoutmgr.PageSequenceLayoutManager;
import org.apache.fop.layoutmgr.TopLevelLayoutManager;
/**
* Area tree handler for formatting objects.
*
* Concepts: The area tree is to be as small as possible. With minimal classes
* and data to fully represent an area tree for formatting objects. The area
* tree needs to be simple to render and follow the spec closely. This area tree
* has the concept of page sequences. Wherever possible information is discarded
* or optimized to keep memory use low. The data is also organized to make it
* possible for renderers to minimize their output. A page can be saved if not
* fully resolved and once rendered a page contains only size and id reference
* information. The area tree pages are organized in a model that depends on the
* type of renderer.
*/
public class AreaTreeHandler extends FOEventHandler {
private static Log log = LogFactory.getLog(AreaTreeHandler.class);
// Recorder of debug statistics
private Statistics statistics = null;
// The LayoutManager maker
private LayoutManagerMaker lmMaker;
/** The AreaTreeModel in use */
protected AreaTreeModel model;
// Flag for controlling complex script features (default: true).
private boolean useComplexScriptFeatures = true;
// Keeps track of all meaningful id references
private IDTracker idTracker;
// The fo:root node of the document
private Root rootFObj;
// The formatting results to be handed back to the caller.
private FormattingResults results = new FormattingResults();
private TopLevelLayoutManager prevPageSeqLM;
private int idGen = 0;
/**
* Constructor.
*
* @param userAgent FOUserAgent object for process
* @param outputFormat the MIME type of the output format to use (ex.
* "application/pdf").
* @param stream OutputStream
* @throws FOPException if the RenderPagesModel cannot be created
*/
public AreaTreeHandler(FOUserAgent userAgent, String outputFormat,
OutputStream stream) throws FOPException {
super(userAgent);
setupModel(userAgent, outputFormat, stream);
this.lmMaker = userAgent.getFactory().getLayoutManagerMakerOverride();
if (lmMaker == null) {
lmMaker = new LayoutManagerMapping();
}
this.idTracker = new IDTracker();
this.useComplexScriptFeatures = userAgent.isComplexScriptFeaturesEnabled();
if (log.isDebugEnabled()) {
statistics = new Statistics();
}
}
/**
* Sets up the AreaTreeModel instance for use by the AreaTreeHandler.
*
* @param userAgent FOUserAgent object for process
* @param outputFormat the MIME type of the output format to use (ex.
* "application/pdf").
* @param stream OutputStream
* @throws FOPException if the RenderPagesModel cannot be created
*/
protected void setupModel(FOUserAgent userAgent, String outputFormat,
OutputStream stream) throws FOPException {
if (userAgent.isConserveMemoryPolicyEnabled()) {
this.model = new CachedRenderPagesModel(userAgent, outputFormat, fontInfo, stream);
} else {
this.model = new RenderPagesModel(userAgent, outputFormat, fontInfo, stream);
}
}
/**
* Get the area tree model for this area tree.
*
* @return AreaTreeModel the model being used for this area tree
*/
public AreaTreeModel getAreaTreeModel() {
return this.model;
}
/**
* Get the LayoutManager maker for this area tree.
*
* @return LayoutManagerMaker the LayoutManager maker being used for this
* area tree
*/
public LayoutManagerMaker getLayoutManagerMaker() {
return this.lmMaker;
}
/**
* Get the IDTracker for this area tree.
*
* @return IDTracker used to track reference ids for items in this area tree
*/
public IDTracker getIDTracker() {
return this.idTracker;
}
/**
* Get information about the rendered output, like number of pages created.
*
* @return the results structure
*/
public FormattingResults getResults() {
return this.results;
}
/**
* Check whether complex script features are enabled.
*
* @return true if using complex script features
*/
public boolean isComplexScriptFeaturesEnabled() {
return useComplexScriptFeatures;
}
/**
* Prepare AreaTreeHandler for document processing This is called from
* FOTreeBuilder.startDocument()
*
* @throws SAXException
* if there is an error
*/
@Override
public void startDocument() throws SAXException {
// Initialize statistics
if (statistics != null) {
statistics.start();
}
}
/**
* finish the previous pageSequence
*/
private void finishPrevPageSequence(Numeric initialPageNumber) {
if (prevPageSeqLM != null) {
prevPageSeqLM.doForcePageCount(initialPageNumber);
prevPageSeqLM.finishPageSequence();
prevPageSeqLM = null;
}
}
/** {@inheritDoc} */
@Override
public void startPageSequence(PageSequence pageSequence) {
startAbstractPageSequence(pageSequence);
}
private void startAbstractPageSequence(AbstractPageSequence pageSequence) {
rootFObj = pageSequence.getRoot();
//Before the first page-sequence...
if (this.prevPageSeqLM == null) {
// extension attachments from fo:root
wrapAndAddExtensionAttachments(rootFObj.getExtensionAttachments());
// extension attachments from fo:declarations
if (rootFObj.getDeclarations() != null) {
wrapAndAddExtensionAttachments(
rootFObj.getDeclarations().getExtensionAttachments());
}
}
finishPrevPageSequence(pageSequence.getInitialPageNumber());
pageSequence.initPageNumber();
}
private void wrapAndAddExtensionAttachments(List<ExtensionAttachment> list) {
for (ExtensionAttachment attachment : list) {
addOffDocumentItem(new OffDocumentExtensionAttachment(attachment));
}
}
/**
* End the PageSequence. The PageSequence formats Pages and adds them to the
* AreaTree. The area tree then handles what happens with the pages.
*
* @param pageSequence the page sequence ending
*/
@Override
public void endPageSequence(PageSequence pageSequence) {
if (statistics != null) {
statistics.end();
}
// If no main flow, nothing to layout!
if (pageSequence.getMainFlow() != null) {
PageSequenceLayoutManager pageSLM;
pageSLM = getLayoutManagerMaker().makePageSequenceLayoutManager(
this, pageSequence);
pageSLM.activateLayout();
// preserve the current PageSequenceLayoutManger for the
// force-page-count check at the beginning of the next PageSequence
prevPageSeqLM = pageSLM;
}
}
/** {@inheritDoc} */
@Override
public void startExternalDocument(ExternalDocument document) {
startAbstractPageSequence(document);
}
/** {@inheritDoc} */
@Override
public void endExternalDocument(ExternalDocument document) {
if (statistics != null) {
statistics.end();
}
ExternalDocumentLayoutManager edLM;
edLM = getLayoutManagerMaker().makeExternalDocumentLayoutManager(this, document);
edLM.activateLayout();
// preserve the current PageSequenceLayoutManger for the
// force-page-count check at the beginning of the next PageSequence
prevPageSeqLM = edLM;
}
/**
* Called by the PageSequenceLayoutManager when it is finished with a
* page-sequence.
*
* @param pageSequence the page-sequence just finished
* @param pageCount The number of pages generated for the page-sequence
*/
public void notifyPageSequenceFinished(AbstractPageSequence pageSequence,
int pageCount) {
this.results.haveFormattedPageSequence(pageSequence, pageCount);
if (log.isDebugEnabled()) {
log.debug("Last page-sequence produced " + pageCount + " pages.");
}
}
/**
* End the document.
*
* @throws SAXException if there is some error
*/
@Override
public void endDocument() throws SAXException {
finishPrevPageSequence(null);
// process fox:destination elements
if (rootFObj != null) {
List<Destination> destinationList = rootFObj.getDestinationList();
if (destinationList != null) {
while (destinationList.size() > 0) {
Destination destination = destinationList.remove(0);
DestinationData destinationData = new DestinationData(destination);
addOffDocumentItem(destinationData);
}
}
// process fo:bookmark-tree
BookmarkTree bookmarkTree = rootFObj.getBookmarkTree();
if (bookmarkTree != null) {
BookmarkData data = new BookmarkData(bookmarkTree);
addOffDocumentItem(data);
if (!data.isResolved()) {
// bookmarks did not fully resolve, add anyway. (hacky? yeah)
model.handleOffDocumentItem(data);
}
}
idTracker.signalIDProcessed(rootFObj.getId());
}
model.endDocument();
if (statistics != null) {
statistics.logResults();
}
}
/**
* Add a OffDocumentItem to the area tree model. This checks if the
* OffDocumentItem is resolvable and attempts to resolve or add the
* resolvable ids for later resolution.
*
* @param odi the OffDocumentItem to add.
*/
private void addOffDocumentItem(OffDocumentItem odi) {
if (odi instanceof Resolvable) {
Resolvable res = (Resolvable) odi;
String[] ids = res.getIDRefs();
for (String id : ids) {
List<PageViewport> pageVPList = idTracker.getPageViewportsContainingID(id);
if (pageVPList != null && !pageVPList.isEmpty()) {
res.resolveIDRef(id, pageVPList);
} else {
AreaEventProducer eventProducer = AreaEventProducer.Provider.get(
getUserAgent().getEventBroadcaster());
eventProducer.unresolvedIDReference(this, odi.getName(), id);
idTracker.addUnresolvedIDRef(id, res);
}
}
// check to see if ODI is now fully resolved, if so process it
if (res.isResolved()) {
model.handleOffDocumentItem(odi);
}
} else {
model.handleOffDocumentItem(odi);
}
}
/**
* Generates and returns a unique key for a page viewport.
*
* @return the generated key.
*/
public String generatePageViewportKey() {
this.idGen++;
return "P" + this.idGen;
}
/**
* Tie a PageViewport with an ID found on a child area of the PV. Note that
* an area with a given ID may be on more than one PV, hence an ID may have
* more than one PV associated with it.
*
* @param id the property ID of the area
* @param pv a page viewport that contains the area with this ID
* @deprecated use getIDTracker().associateIDWithPageViewport(id, pv) instead
*/
@Deprecated
public void associateIDWithPageViewport(String id, PageViewport pv) {
idTracker.associateIDWithPageViewport(id, pv);
}
/**
* This method tie an ID to the areaTreeHandler until this one is ready to
* be processed. This is used in page-number-citation-last processing so we
* know when an id can be resolved.
*
* @param id the id of the object being processed
* @deprecated use getIDTracker().signalPendingID(id) instead
*/
@Deprecated
public void signalPendingID(String id) {
idTracker.signalPendingID(id);
}
/**
* Signals that all areas for the formatting object with the given ID have
* been generated. This is used to determine when page-number-citation-last
* ref-ids can be resolved.
*
* @param id the id of the formatting object which was just finished
* @deprecated use getIDTracker().signalIDProcessed(id) instead
*/
@Deprecated
public void signalIDProcessed(String id) {
idTracker.signalIDProcessed(id);
}
/**
* Check if an ID has already been resolved
*
* @param id the id to check
* @return true if the ID has been resolved
* @deprecated use getIDTracker().alreadyResolvedID(id) instead
*/
@Deprecated
public boolean alreadyResolvedID(String id) {
return idTracker.alreadyResolvedID(id);
}
/**
* Tries to resolve all unresolved ID references on the given page.
*
* @param pv page viewport whose ID refs to resolve
* @deprecated use getIDTracker().tryIDResolution(pv) instead
*/
@Deprecated
public void tryIDResolution(PageViewport pv) {
idTracker.tryIDResolution(pv);
}
/**
* Get the set of page viewports that have an area with a given id.
*
* @param id the id to lookup
* @return the list of PageViewports
* @deprecated use getIDTracker().getPageViewportsContainingID(id) instead
*/
@Deprecated
public List<PageViewport> getPageViewportsContainingID(String id) {
return idTracker.getPageViewportsContainingID(id);
}
/**
* Add an Resolvable object with an unresolved idref
*
* @param idref the idref whose target id has not yet been located
* @param res the Resolvable object needing the idref to be resolved
* @deprecated use getIDTracker().addUnresolvedIDRef(idref, res) instead
*/
@Deprecated
public void addUnresolvedIDRef(String idref, Resolvable res) {
idTracker.addUnresolvedIDRef(idref, res);
}
private class Statistics {
// for statistics gathering
private Runtime runtime;
// heap memory allocated (for statistics)
private long initialMemory;
// time used in rendering (for statistics)
private long startTime;
/**
* Default constructor
*/
protected Statistics() {
this.runtime = Runtime.getRuntime();
}
/**
* starts the area tree handler statistics gathering
*/
protected void start() {
this.initialMemory = runtime.totalMemory() - runtime.freeMemory();
this.startTime = System.currentTimeMillis();
}
/**
* ends the area tree handler statistics gathering
*/
protected void end() {
long memoryNow = runtime.totalMemory() - runtime.freeMemory();
log.debug("Current heap size: " + (memoryNow / 1024L) + "KB");
}
/**
* logs the results of the area tree handler statistics gathering
*/
protected void logResults() {
long memoryNow = runtime.totalMemory() - runtime.freeMemory();
long memoryUsed = (memoryNow - initialMemory) / 1024L;
long timeUsed = System.currentTimeMillis() - startTime;
int pageCount = rootFObj.getTotalPagesGenerated();
log.debug("Initial heap size: " + (initialMemory / 1024L) + "KB");
log.debug("Current heap size: " + (memoryNow / 1024L) + "KB");
log.debug("Total memory used: " + memoryUsed + "KB");
log.debug("Total time used: " + timeUsed + "ms");
log.debug("Pages rendered: " + pageCount);
if (pageCount > 0) {
long perPage = (timeUsed / pageCount);
long ppm = (timeUsed != 0 ? Math.round(60000 * pageCount
/ (double) timeUsed) : -1);
log.debug("Avg render time: " + perPage + "ms/page (" + ppm + "pages/min)");
}
}
}
}