diff options
Diffstat (limited to 'src/java/org')
44 files changed, 2226 insertions, 1066 deletions
diff --git a/src/java/org/apache/fop/apps/FOURIResolver.java b/src/java/org/apache/fop/apps/FOURIResolver.java index 5b66ffa57..e3ffd22cc 100644 --- a/src/java/org/apache/fop/apps/FOURIResolver.java +++ b/src/java/org/apache/fop/apps/FOURIResolver.java @@ -189,10 +189,11 @@ public class FOURIResolver try { ByteArrayOutputStream baout = new ByteArrayOutputStream(combined.length() * 2); Base64EncodeStream base64 = new Base64EncodeStream(baout); - base64.write(combined.getBytes()); + //TODO Not sure what charset/encoding can be used with basic authentication + base64.write(combined.getBytes("UTF-8")); base64.close(); connection.setRequestProperty("Authorization", - "Basic " + new String(baout.toByteArray())); + "Basic " + new String(baout.toByteArray(), "UTF-8")); } catch (IOException e) { //won't happen. We're operating in-memory. throw new RuntimeException("Error during base64 encodation of username/password"); diff --git a/src/java/org/apache/fop/area/AreaTreeHandler.java b/src/java/org/apache/fop/area/AreaTreeHandler.java index e7f6effe0..0104d0688 100644 --- a/src/java/org/apache/fop/area/AreaTreeHandler.java +++ b/src/java/org/apache/fop/area/AreaTreeHandler.java @@ -53,36 +53,23 @@ import org.apache.fop.fo.extensions.destination.Destination; /** * 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 + * + * 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); - - // show statistics after document complete? - private boolean outputStatistics; - - // for statistics gathering - private Runtime runtime; - - // heap memory allocated (for statistics) - private long initialMemory; + /** debug statistics */ + private Statistics statistics = null; - // time used in rendering (for statistics) - private long startTime; + private static Log log = LogFactory.getLog(AreaTreeHandler.class); // the LayoutManager maker private LayoutManagerMaker lmMaker; @@ -93,66 +80,68 @@ public class AreaTreeHandler extends FOEventHandler { // The fo:root node of the document private Root rootFObj; - // HashMap of ID's whose area is located on one or more consecutive - // PageViewports. Each ID has an arraylist of PageViewports that + // HashMap of ID's whose area is located on one or more consecutive + // PageViewports. Each ID has an arraylist of PageViewports that // form the defined area of this ID private Map idLocations = new HashMap(); // idref's whose target PageViewports have yet to be identified // Each idref has a HashSet of Resolvable objects containing that idref private Map unresolvedIDRefs = new HashMap(); - + private Set unfinishedIDs = new HashSet(); + private Set alreadyResolvedIDs = new HashSet(); - // The formatting results to be handed back to the caller. + // The formatting results to be handed back to the caller. private FormattingResults results = new FormattingResults(); private PageSequenceLayoutManager 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 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 { + public AreaTreeHandler(FOUserAgent userAgent, String outputFormat, + OutputStream stream) throws FOPException { super(userAgent); setupModel(userAgent, outputFormat, stream); - + lmMaker = userAgent.getFactory().getLayoutManagerMakerOverride(); if (lmMaker == null) { lmMaker = new LayoutManagerMapping(); } - outputStatistics = log.isDebugEnabled(); - - if (outputStatistics) { - runtime = Runtime.getRuntime(); + 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 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, + protected void setupModel(FOUserAgent userAgent, String outputFormat, OutputStream stream) throws FOPException { - model = new RenderPagesModel(userAgent, outputFormat, fontInfo, - stream); + 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() { @@ -161,17 +150,19 @@ public class AreaTreeHandler extends FOEventHandler { /** * Get the LayoutManager maker for this area tree. - * - * @return LayoutManagerMaker the LayoutManager maker being used for this area tree + * + * @return LayoutManagerMaker the LayoutManager maker being used for this + * area tree */ public LayoutManagerMaker getLayoutManagerMaker() { return lmMaker; } /** - * 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. + * 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 */ @@ -184,23 +175,25 @@ public class AreaTreeHandler extends FOEventHandler { pvList = new ArrayList(); idLocations.put(id, pvList); pvList.add(pv); - - /* - * See if this ID is in the unresolved idref list, if so - * resolve Resolvable objects tied to it. + // signal the PageViewport that it is the first PV to contain this id: + pv.setFirstWithID(id); + /* + * See if this ID is in the unresolved idref list, if so resolve + * Resolvable objects tied to it. */ if (!unfinishedIDs.contains(id)) { - tryIDResolution(id, pv, pvList); + tryIDResolution(id, pv, pvList); } } else { pvList.add(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. + * 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 */ public void signalPendingID(String id) { @@ -209,23 +202,25 @@ public class AreaTreeHandler extends FOEventHandler { } unfinishedIDs.add(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. + * 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 */ public void signalIDProcessed(String id) { if (log.isDebugEnabled()) { log.debug("signalIDProcessed(" + id + ")"); } - + alreadyResolvedIDs.add(id); if (!unfinishedIDs.contains(id)) { return; } unfinishedIDs.remove(id); - + List pvList = (List) idLocations.get(id); Set todo = (Set) unresolvedIDRefs.get(id); if (todo != null) { @@ -236,9 +231,10 @@ public class AreaTreeHandler extends FOEventHandler { unresolvedIDRefs.remove(id); } } - + /** * Check if an ID has already been resolved + * * @param id the id to check * @return true if the ID has been resolved */ @@ -248,6 +244,7 @@ public class AreaTreeHandler extends FOEventHandler { /** * Tries to resolve all unresolved ID references on the given page. + * * @param id ID to resolve * @param pv page viewport whose ID refs to resolve * @param List of PageViewports @@ -270,6 +267,7 @@ public class AreaTreeHandler extends FOEventHandler { /** * Tries to resolve all unresolved ID references on the given page. + * * @param pv page viewport whose ID refs to resolve */ public void tryIDResolution(PageViewport pv) { @@ -283,9 +281,10 @@ public class AreaTreeHandler extends FOEventHandler { } } } - + /** * Get the list of page viewports that have an area with a given id. + * * @param id the id to lookup * @return the list of PageViewports */ @@ -294,8 +293,8 @@ public class AreaTreeHandler extends FOEventHandler { } /** - * Get information about the rendered output, like - * number of pages created. + * Get information about the rendered output, like number of pages created. + * * @return the results structure */ public FormattingResults getResults() { @@ -304,6 +303,7 @@ public class AreaTreeHandler extends FOEventHandler { /** * 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 */ @@ -318,16 +318,16 @@ public class AreaTreeHandler extends FOEventHandler { } /** - * Prepare AreaTreeHandler for document processing - * This is called from FOTreeBuilder.startDocument() - * - * @throws SAXException if there is an error + * Prepare AreaTreeHandler for document processing This is called from + * FOTreeBuilder.startDocument() + * + * @throws SAXException + * if there is an error */ public void startDocument() throws SAXException { - //Initialize statistics - if (outputStatistics) { - initialMemory = runtime.totalMemory() - runtime.freeMemory(); - startTime = System.currentTimeMillis(); + // Initialize statistics + if (statistics != null) { + statistics.start(); } } @@ -344,40 +344,39 @@ public class AreaTreeHandler extends FOEventHandler { /** * @see org.apache.fop.fo.FOEventHandler - * @param pageSequence is the pageSequence being started - * */ + * @param pageSequence + * is the pageSequence being started + */ public void startPageSequence(PageSequence pageSequence) { rootFObj = pageSequence.getRoot(); finishPrevPageSequence(pageSequence.getInitialPageNumber()); pageSequence.initPageNumber(); - //extension attachments from fo:root + // extension attachments from fo:root wrapAndAddExtensionAttachments(rootFObj.getExtensionAttachments()); - //extension attachments from fo:declarations + // extension attachments from fo:declarations if (rootFObj.getDeclarations() != null) { wrapAndAddExtensionAttachments(rootFObj.getDeclarations().getExtensionAttachments()); } } - + private void wrapAndAddExtensionAttachments(List list) { Iterator i = list.iterator(); while (i.hasNext()) { - ExtensionAttachment attachment = (ExtensionAttachment)i.next(); + ExtensionAttachment attachment = (ExtensionAttachment) i.next(); 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. - * + * 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 */ public void endPageSequence(PageSequence pageSequence) { - if (outputStatistics) { - long memoryNow = runtime.totalMemory() - runtime.freeMemory(); - log.debug("Current heap size: " + (memoryNow / 1024L) + "Kb"); + if (statistics != null) { + statistics.end(); } // If no main flow, nothing to layout! @@ -388,38 +387,38 @@ public class AreaTreeHandler extends FOEventHandler { pageSLM.activateLayout(); // preserve the current PageSequenceLayoutManger for the // force-page-count check at the beginning of the next PageSequence - prevPageSeqLM = pageSLM; + prevPageSeqLM = pageSLM; } } /** - * Called by the PageSequenceLayoutManager when it is finished with a page-sequence. + * 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(PageSequence pageSequence, - int pageCount) { - this.results.haveFormattedPageSequence(pageSequence, - pageCount); + int pageCount) { + this.results.haveFormattedPageSequence(pageSequence, pageCount); if (log.isDebugEnabled()) { - log.debug("Last page-sequence produced " - + pageCount + " pages."); + log.debug("Last page-sequence produced " + pageCount + " pages."); } } /** * End the document. - * + * * @throws SAXException if there is some error */ public void endDocument() throws SAXException { finishPrevPageSequence(null); // process fox:destination elements - ArrayList destinationList = rootFObj.getDestinationList(); + List destinationList = rootFObj.getDestinationList(); if (destinationList != null) { - while(destinationList.size() > 0) { - Destination destination = (Destination)destinationList.remove(0); + while (destinationList.size() > 0) { + Destination destination = (Destination) destinationList.remove(0); DestinationData destinationData = new DestinationData(destination); addOffDocumentItem(destinationData); } @@ -430,35 +429,23 @@ public class AreaTreeHandler extends FOEventHandler { BookmarkData data = new BookmarkData(bookmarkTree); addOffDocumentItem(data); if (!data.isResolved()) { - //bookmarks did not fully resolve, add anyway. (hacky? yeah) + // bookmarks did not fully resolve, add anyway. (hacky? yeah) model.handleOffDocumentItem(data); } } model.endDocument(); - if (outputStatistics) { - 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)"); - } + 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. + * 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) { @@ -469,8 +456,8 @@ public class AreaTreeHandler extends FOEventHandler { if (idLocations.containsKey(ids[count])) { res.resolveIDRef(ids[count], (List) idLocations.get(ids[count])); } else { - log.warn(odi.getName() + ": Unresolved id reference \"" - + ids[count] + "\" found."); + log.warn(odi.getName() + ": Unresolved id reference \"" + + ids[count] + "\" found."); addUnresolvedIDRef(ids[count], res); } } @@ -482,15 +469,60 @@ public class AreaTreeHandler extends FOEventHandler { 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; } - -} + /** + * Gather statistics when log is debug + */ + private final 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; + + private Statistics() { + runtime = Runtime.getRuntime(); + } + + public void start() { + initialMemory = runtime.totalMemory() - runtime.freeMemory(); + startTime = System.currentTimeMillis(); + } + + public void end() { + long memoryNow = runtime.totalMemory() - runtime.freeMemory(); + log.debug("Current heap size: " + (memoryNow / 1024L) + "KB"); + } + + public 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)"); + } + } + } +} diff --git a/src/java/org/apache/fop/area/AreaTreeParser.java b/src/java/org/apache/fop/area/AreaTreeParser.java index fce776e22..9e293afac 100644 --- a/src/java/org/apache/fop/area/AreaTreeParser.java +++ b/src/java/org/apache/fop/area/AreaTreeParser.java @@ -23,6 +23,7 @@ import java.awt.Color; import java.awt.geom.Rectangle2D; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Stack; import java.util.StringTokenizer; @@ -38,7 +39,9 @@ import javax.xml.transform.sax.TransformerHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.area.Trait.InternalLink; import org.apache.fop.area.Trait.Background; +import org.apache.fop.area.inline.InlineArea; import org.apache.fop.area.inline.AbstractTextArea; import org.apache.fop.area.inline.Character; import org.apache.fop.area.inline.ForeignObject; @@ -98,10 +101,10 @@ public class AreaTreeParser { transformer.setErrorListener(new DefaultErrorListener(log)); SAXResult res = new SAXResult(getContentHandler(treeModel, userAgent)); - + transformer.transform(src, res); } - + /** * Creates a new ContentHandler instance that you can send the area tree XML to. The parsed * pages are added to the AreaTreeModel instance you pass in as a parameter. @@ -110,33 +113,37 @@ public class AreaTreeParser { * @return the ContentHandler instance to receive the SAX stream from the area tree XML */ public ContentHandler getContentHandler(AreaTreeModel treeModel, FOUserAgent userAgent) { - ElementMappingRegistry elementMappingRegistry + ElementMappingRegistry elementMappingRegistry = userAgent.getFactory().getElementMappingRegistry(); return new Handler(treeModel, userAgent, elementMappingRegistry); } - + private static class Handler extends DefaultHandler { - + private Map makers = new java.util.HashMap(); - + private AreaTreeModel treeModel; private FOUserAgent userAgent; private ElementMappingRegistry elementMappingRegistry; - + private Attributes lastAttributes; private StringBuffer content = new StringBuffer(); private PageViewport currentPageViewport; + private Map pageViewportsByKey = new java.util.HashMap(); + // set of "ID firsts" that have already been assigned to a PV: + private Set idFirstsAssigned = new java.util.HashSet(); + private Stack areaStack = new Stack(); private boolean firstFlow; private boolean pendingStartPageSequence; - + private Stack delegateStack = new Stack(); private ContentHandler delegate; private DOMImplementation domImplementation; - - - public Handler(AreaTreeModel treeModel, FOUserAgent userAgent, + + + public Handler(AreaTreeModel treeModel, FOUserAgent userAgent, ElementMappingRegistry elementMappingRegistry) { this.treeModel = treeModel; this.userAgent = userAgent; @@ -159,6 +166,7 @@ public class AreaTreeParser { makers.put("beforeFloat", new BeforeFloatMaker()); makers.put("block", new BlockMaker()); makers.put("lineArea", new LineAreaMaker()); + makers.put("inline", new InlineMaker()); makers.put("inlineparent", new InlineParentMaker()); makers.put("inlineblockparent", new InlineBlockParentMaker()); makers.put("text", new TextMaker()); @@ -169,6 +177,8 @@ public class AreaTreeParser { makers.put("viewport", new ViewportMaker()); makers.put("image", new ImageMaker()); makers.put("foreignObject", new ForeignObjectMaker()); + makers.put("bookmarkTree", new BookmarkTreeMaker()); + makers.put("bookmark", new BookmarkMaker()); } private static Rectangle2D parseRect(String rect) { @@ -179,7 +189,7 @@ public class AreaTreeParser { Double.parseDouble(tokenizer.nextToken()), Double.parseDouble(tokenizer.nextToken())); } - + private Area findAreaType(Class clazz) { if (areaStack.size() > 0) { int pos = areaStack.size() - 1; @@ -193,7 +203,7 @@ public class AreaTreeParser { } return null; } - + private RegionViewport getCurrentRegionViewport() { return (RegionViewport)findAreaType(RegionViewport.class); } @@ -201,21 +211,21 @@ public class AreaTreeParser { private BodyRegion getCurrentBodyRegion() { return (BodyRegion)findAreaType(BodyRegion.class); } - + private BlockParent getCurrentBlockParent() { return (BlockParent)findAreaType(BlockParent.class); } - + private AbstractTextArea getCurrentText() { return (AbstractTextArea)findAreaType(AbstractTextArea.class); } - + private Viewport getCurrentViewport() { return (Viewport)findAreaType(Viewport.class); } - + /** @see org.xml.sax.helpers.DefaultHandler */ - public void startElement(String uri, String localName, String qName, Attributes attributes) + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (delegate != null) { delegateStack.push(qName); @@ -234,7 +244,7 @@ public class AreaTreeParser { handler.setResult(new DOMResult(doc)); Area parent = (Area)areaStack.peek(); ((ForeignObject)parent).setDocument(doc); - + //activate delegate for nested foreign document domImplementation = null; //Not needed anymore now this.delegate = handler; @@ -268,16 +278,16 @@ public class AreaTreeParser { } if (!handled) { if (uri == null || uri.length() == 0) { - throw new SAXException("Unhandled element " + localName + throw new SAXException("Unhandled element " + localName + " in namespace: " + uri); } else { - log.warn("Unhandled element " + localName + log.warn("Unhandled element " + localName + " in namespace: " + uri); } } } } - + /** @see org.xml.sax.helpers.DefaultHandler */ public void endElement(String uri, String localName, String qName) throws SAXException { if (delegate != null) { @@ -303,14 +313,14 @@ public class AreaTreeParser { content.setLength(0); //Reset text buffer (see characters()) } } - + // ============== Maker classes for the area tree objects ============= - + private static interface Maker { void startElement(Attributes attributes) throws SAXException; void endElement(); } - + private abstract class AbstractMaker implements Maker { public void startElement(Attributes attributes) throws SAXException { @@ -321,11 +331,15 @@ public class AreaTreeParser { //nop } } - + private class AreaTreeMaker extends AbstractMaker { - //no overrides + + public void startElement(Attributes attributes) { + // In case the Handler is reused: + idFirstsAssigned.clear(); + } } - + private class PageSequenceMaker extends AbstractMaker { public void startElement(Attributes attributes) { @@ -333,7 +347,7 @@ public class AreaTreeParser { //treeModel.startPageSequence(null); Done after title or on the first viewport } } - + private class TitleMaker extends AbstractMaker { public void startElement(Attributes attributes) { @@ -347,10 +361,10 @@ public class AreaTreeParser { treeModel.startPageSequence(line); pendingStartPageSequence = false; } - - + + } - + private class PageViewportMaker extends AbstractMaker { public void startElement(Attributes attributes) { @@ -367,15 +381,16 @@ public class AreaTreeParser { String pageNumberString = attributes.getValue("formatted-nr"); String pageMaster = attributes.getValue("simple-page-master-name"); boolean blank = getAttributeAsBoolean(attributes, "blank", false); - currentPageViewport = new PageViewport(viewArea, + currentPageViewport = new PageViewport(viewArea, pageNumber, pageNumberString, pageMaster, blank); transferForeignObjects(attributes, currentPageViewport); currentPageViewport.setKey(key); + pageViewportsByKey.put(key, currentPageViewport); } } - + private class PageMaker extends AbstractMaker { public void startElement(Attributes attributes) { @@ -411,7 +426,7 @@ public class AreaTreeParser { assertObjectOfClass(areaStack.pop(), RegionViewport.class); } } - + private class RegionBeforeMaker extends AbstractMaker { public void startElement(Attributes attributes) { @@ -428,7 +443,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { pushNewRegionReference(attributes, Constants.FO_REGION_AFTER); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), RegionReference.class); } @@ -439,7 +454,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { pushNewRegionReference(attributes, Constants.FO_REGION_START); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), RegionReference.class); } @@ -450,7 +465,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { pushNewRegionReference(attributes, Constants.FO_REGION_END); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), RegionReference.class); } @@ -557,7 +572,7 @@ public class AreaTreeParser { private class BlockMaker extends AbstractMaker { public void startElement(Attributes attributes) { - boolean isViewport = getAttributeAsBoolean(attributes, + boolean isViewport = getAttributeAsBoolean(attributes, "is-viewport-area", false); Block block; if (isViewport) { @@ -618,10 +633,31 @@ public class AreaTreeParser { parent.addChildArea(line); areaStack.push(line); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), LineArea.class); - } + } + } + + // Maker for "generic" inline areas + private class InlineMaker extends AbstractMaker { + + public void startElement(Attributes attributes) { + InlineArea inl = new InlineArea(); + transferForeignObjects(attributes, inl); + inl.setOffset(getAttributeAsInteger(attributes, "offset", 0)); + setAreaAttributes(attributes, inl); + setTraits(attributes, inl, SUBSET_COMMON); + setTraits(attributes, inl, SUBSET_BOX); + setTraits(attributes, inl, SUBSET_COLOR); + Area parent = (Area)areaStack.peek(); + parent.addChildArea(inl); + areaStack.push(inl); + } + + public void endElement() { + assertObjectOfClass(areaStack.pop(), InlineArea.class); + } } private class InlineParentMaker extends AbstractMaker { @@ -639,10 +675,10 @@ public class AreaTreeParser { parent.addChildArea(ip); areaStack.push(ip); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), InlineParent.class); - } + } } private class InlineBlockParentMaker extends AbstractMaker { @@ -662,7 +698,7 @@ public class AreaTreeParser { public void endElement() { assertObjectOfClass(areaStack.pop(), InlineBlockParent.class); - } + } } private class TextMaker extends AbstractMaker { @@ -679,7 +715,7 @@ public class AreaTreeParser { setTraits(attributes, text, SUBSET_FONT); text.setBaselineOffset(getAttributeAsInteger(attributes, "baseline", 0)); text.setOffset(getAttributeAsInteger(attributes, "offset", 0)); - text.setTextLetterSpaceAdjust(getAttributeAsInteger(attributes, + text.setTextLetterSpaceAdjust(getAttributeAsInteger(attributes, "tlsadjust", 0)); text.setTextWordSpaceAdjust(getAttributeAsInteger(attributes, "twsadjust", 0)); @@ -841,19 +877,58 @@ public class AreaTreeParser { getCurrentViewport().setContent(foreign); areaStack.push(foreign); } - + public void endElement() { assertObjectOfClass(areaStack.pop(), ForeignObject.class); - } + } + } + + private class BookmarkTreeMaker extends AbstractMaker { + + public void startElement(Attributes attributes) { + BookmarkData bm = new BookmarkData(); + areaStack.push(bm); + } + + public void endElement() { + Object tos = areaStack.pop(); + assertObjectOfClass(tos, BookmarkData.class); + treeModel.handleOffDocumentItem((BookmarkData) tos); + // as long as the bookmark tree comes after the last PageViewport in the + // area tree XML, we don't have to worry about resolved/unresolved. The + // only resolution needed is the mapping of the pvKey to the PV instance. + } + } + + private class BookmarkMaker extends AbstractMaker { + + public void startElement(Attributes attributes) { + String title = attributes.getValue("title"); + boolean showChildren = getAttributeAsBoolean(attributes, "show-children", false); + String[] linkdata + = InternalLink.parseXMLAttribute(attributes.getValue("internal-link")); + PageViewport pv = (PageViewport) pageViewportsByKey.get(linkdata[0]); + BookmarkData bm = new BookmarkData(title, showChildren, pv, linkdata[1]); + Object tos = areaStack.peek(); + if (tos instanceof BookmarkData) { + BookmarkData parent = (BookmarkData) tos; + parent.addSubData(bm); + } + areaStack.push(bm); + } + + public void endElement() { + assertObjectOfClass(areaStack.pop(), BookmarkData.class); + } } // ==================================================================== - + private void pushNewRegionReference(Attributes attributes, int side) { String regionName = attributes.getValue("name"); RegionViewport rv = getCurrentRegionViewport(); - RegionReference reg = new RegionReference(side, + RegionReference reg = new RegionReference(side, regionName, rv); transferForeignObjects(attributes, reg); reg.setCTM(getAttributeAsCTM(attributes, "ctm")); @@ -903,7 +978,7 @@ public class AreaTreeParser { Trait.BACKGROUND, Trait.COLOR}; private static final Object[] SUBSET_FONT = new Object[] { Trait.FONT, Trait.FONT_SIZE, Trait.BLINK, - Trait.OVERLINE, Trait.OVERLINE_COLOR, + Trait.OVERLINE, Trait.OVERLINE_COLOR, Trait.LINETHROUGH, Trait.LINETHROUGH_COLOR, Trait.UNDERLINE, Trait.UNDERLINE_COLOR}; private static final Object[] SUBSET_BOX = new Object[] { @@ -926,12 +1001,20 @@ public class AreaTreeParser { area.addTrait(trait, Boolean.valueOf(value)); } else if (cl == String.class) { area.addTrait(trait, value); + if (trait == Trait.PROD_ID + && !idFirstsAssigned.contains(value) + && currentPageViewport != null) { + currentPageViewport.setFirstWithID(value); + idFirstsAssigned.add(value); + } } else if (cl == Color.class) { try { area.addTrait(trait, ColorUtil.parseColorString(this.userAgent, value)); } catch (PropertyException e) { throw new IllegalArgumentException(e.getMessage()); } + } else if (cl == InternalLink.class) { + area.addTrait(trait, new InternalLink(value)); } else if (cl == Background.class) { Background bkg = new Background(); try { @@ -940,11 +1023,11 @@ public class AreaTreeParser { bkg.setColor(col); } catch (PropertyException e) { throw new IllegalArgumentException(e.getMessage()); - } + } String url = attributes.getValue("bkg-img"); if (url != null) { bkg.setURL(url); - + ImageFactory fact = userAgent.getFactory().getImageFactory(); FopImage img = fact.getImage(url, userAgent); if (img == null) { @@ -952,7 +1035,7 @@ public class AreaTreeParser { } else { // load dimensions if (!img.load(FopImage.DIMENSIONS)) { - log.error("Cannot read background image dimensions: " + log.error("Cannot read background image dimensions: " + url); } } @@ -962,9 +1045,9 @@ public class AreaTreeParser { if (repeat != null) { bkg.setRepeat(repeat); } - bkg.setHoriz(getAttributeAsInteger(attributes, + bkg.setHoriz(getAttributeAsInteger(attributes, "bkg-horz-offset", 0)); - bkg.setVertical(getAttributeAsInteger(attributes, + bkg.setVertical(getAttributeAsInteger(attributes, "bkg-vert-offset", 0)); } area.addTrait(trait, bkg); @@ -978,15 +1061,15 @@ public class AreaTreeParser { String fontStyle = attributes.getValue("font-style"); int fontWeight = getAttributeAsInteger( attributes, "font-weight", Font.NORMAL); - area.addTrait(trait, + area.addTrait(trait, FontInfo.createFontKey(fontName, fontStyle, fontWeight)); } } } } } - - private boolean getAttributeAsBoolean(Attributes attributes, String name, + + private boolean getAttributeAsBoolean(Attributes attributes, String name, boolean defaultValue) { String s = attributes.getValue(name); if (s == null) { @@ -1058,5 +1141,5 @@ public class AreaTreeParser { } } - + } diff --git a/src/java/org/apache/fop/area/BookmarkData.java b/src/java/org/apache/fop/area/BookmarkData.java index 8ef1e6086..2ad8273a9 100644 --- a/src/java/org/apache/fop/area/BookmarkData.java +++ b/src/java/org/apache/fop/area/BookmarkData.java @@ -19,7 +19,6 @@ package org.apache.fop.area; -import java.util.ArrayList; import java.util.List; import java.util.HashMap; @@ -32,7 +31,7 @@ import org.apache.fop.fo.pagination.bookmarks.Bookmark; * child bookmark-items under it. */ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable { - private ArrayList subData = new ArrayList(); + private List subData = new java.util.ArrayList(); // bookmark-title for this fo:bookmark private String bookmarkTitle = null; @@ -61,7 +60,7 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable whenToProcess = END_OF_DOC; // top level defined in Rec to show all child bookmarks bShow = true; - + for (int count = 0; count < bookmarkTree.getBookmarks().size(); count++) { Bookmark bkmk = (Bookmark)(bookmarkTree.getBookmarks()).get(count); addSubData(createBookmarkData(bkmk)); @@ -84,6 +83,34 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable } /** + * Create a new bookmark data root object. + * This constructor is called by the AreaTreeParser when the + * <bookmarkTree> element is read from the XML file + */ + public BookmarkData() { + idRef = null; + whenToProcess = END_OF_DOC; + bShow = true; + } + + /** + * Create a new bookmark data object. + * This constructor is called by the AreaTreeParser when a + * <bookmark> element is read from the XML file. + * + * @param title the bookmark's title + * @param showChildren whether to initially display the bookmark's children + * @param pv the target PageViewport + * @param idRef the target ID + */ + public BookmarkData(String title, boolean showChildren, PageViewport pv, String idRef) { + bookmarkTitle = title; + bShow = showChildren; + pageRef = pv; + this.idRef = idRef; + } + + /** * Get the idref for this bookmark-item * * @return the idref for the bookmark-item @@ -93,17 +120,19 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable } /** - * Add the child bookmark data object. + * Add a child bookmark data object. * This adds a child bookmark in the bookmark hierarchy. * * @param sub the child bookmark data */ public void addSubData(BookmarkData sub) { subData.add(sub); - unresolvedIDRefs.put(sub.getIDRef(), sub); - String[] ids = sub.getIDRefs(); - for (int count = 0; count < ids.length; count++) { - unresolvedIDRefs.put(ids[count], sub); + if (sub.pageRef == null || sub.pageRef.equals("")) { + unresolvedIDRefs.put(sub.getIDRef(), sub); + String[] ids = sub.getIDRefs(); + for (int count = 0; count < ids.length; count++) { + unresolvedIDRefs.put(ids[count], sub); + } } } @@ -221,4 +250,3 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable } } - diff --git a/src/java/org/apache/fop/area/DestinationData.java b/src/java/org/apache/fop/area/DestinationData.java index 7e59210e0..6b873ef84 100644 --- a/src/java/org/apache/fop/area/DestinationData.java +++ b/src/java/org/apache/fop/area/DestinationData.java @@ -28,9 +28,6 @@ import org.apache.fop.area.PageViewport; */ public class DestinationData extends AbstractOffDocumentItem implements Resolvable { - // PDFReference (object reference) for this destination - private String goToReference; - // ID Reference for this bookmark private String idRef; @@ -79,24 +76,6 @@ public class DestinationData extends AbstractOffDocumentItem implements Resolvab } /** - * Set the GoToReference for this destination - * - * @param goToReference the GoToReference to associate with this destination - */ - public void setGoToReference(String goToReference) { - this.goToReference = goToReference; - } - - /** - * Get the GoToReference for this destination - * - * @return the GoToReference associated with this destination - */ - public String getGoToReference() { - return goToReference; - } - - /** * Check if this resolvable object has been resolved. * For now, just return true. * To do: Find a way to determine whether the destination has been resolved. diff --git a/src/java/org/apache/fop/area/LinkResolver.java b/src/java/org/apache/fop/area/LinkResolver.java index 603aede87..0b0441bd2 100644 --- a/src/java/org/apache/fop/area/LinkResolver.java +++ b/src/java/org/apache/fop/area/LinkResolver.java @@ -65,15 +65,25 @@ public class LinkResolver implements Resolvable, Serializable { } /** - * Resolve by adding an internal link. + * Resolve by adding an internal link to the first PageViewport in the list. * * @see org.apache.fop.area.Resolvable#resolveIDRef(String, List) */ public void resolveIDRef(String id, List pages) { - if (idRef.equals(id) && pages != null) { + resolveIDRef(id, (PageViewport)pages.get(0)); + } + + /** + * Resolve by adding an InternalLink trait to the area + * + * @param id the target id (should be equal to the object's idRef) + * @param pv the PageViewport containing the first area with the given id + */ + public void resolveIDRef(String id, PageViewport pv) { + if (idRef.equals(id) && pv != null) { resolved = true; - PageViewport page = (PageViewport)pages.get(0); - area.addTrait(Trait.INTERNAL_LINK, page.getKey()); + Trait.InternalLink iLink = new Trait.InternalLink(pv.getKey(), idRef); + area.addTrait(Trait.INTERNAL_LINK, iLink); } } } diff --git a/src/java/org/apache/fop/area/PageViewport.java b/src/java/org/apache/fop/area/PageViewport.java index 282bce367..953cb3840 100644 --- a/src/java/org/apache/fop/area/PageViewport.java +++ b/src/java/org/apache/fop/area/PageViewport.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.Iterator; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,6 +68,9 @@ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneabl // list of id references and the rectangle on the page //private Map idReferences = null; + // set of IDs that appear first (or exclusively) on this page: + private Set idFirsts = new java.util.HashSet(); + // 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 @@ -237,6 +241,29 @@ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneabl } /** + * Add an "ID-first" to this page. + * This is typically called by the AreaTreeHandler when associating + * an ID with a 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 PageViewport are added * to unresolvedIDRefs, for subsequent resolution by AreaTreeHandler diff --git a/src/java/org/apache/fop/area/Trait.java b/src/java/org/apache/fop/area/Trait.java index 3cddc8006..32582dec1 100644 --- a/src/java/org/apache/fop/area/Trait.java +++ b/src/java/org/apache/fop/area/Trait.java @@ -43,9 +43,9 @@ public class Trait implements Serializable { /** * Internal link trait. - * This is resolved and provides a link to an internal area. + * Contains the PageViewport key and the PROD_ID of the target area */ - public static final Integer INTERNAL_LINK = new Integer(1); //resolved + public static final Integer INTERNAL_LINK = new Integer(1); /** * External link. A URL link to an external resource. @@ -224,7 +224,7 @@ public class Trait implements Serializable { static { // Create a hashmap mapping trait code to name for external representation //put(ID_LINK, new TraitInfo("id-link", String.class)); - put(INTERNAL_LINK, new TraitInfo("internal-link", String.class)); + put(INTERNAL_LINK, new TraitInfo("internal-link", InternalLink.class)); put(EXTERNAL_LINK, new TraitInfo("external-link", String.class)); put(FONT, new TraitInfo("font", FontTriplet.class)); put(FONT_SIZE, new TraitInfo("font-size", Integer.class)); @@ -411,7 +411,140 @@ public class Trait implements Serializable { return null; }*/ - + /** + * Class for internal link traits. + * Stores PageViewport key and producer ID + */ + public static class InternalLink implements Serializable { + + /** The unique key of the PageViewport. */ + private String pvKey; + + /** The PROD_ID of the link target */ + private String idRef; + + /** + * Create an InternalLink to the given PageViewport and target ID + * + * @param pvKey the PageViewport key + * @param idRef the target ID + */ + public InternalLink(String pvKey, String idRef) { + setPVKey(pvKey); + setIDRef(idRef); + } + + /** + * Create an InternalLink based on the given XML attribute value. + * This is typically called when data are read from an XML area tree. + * + * @param attrValue attribute value to be parsed by InternalLink.parseXMLAttribute + */ + public InternalLink(String attrValue) { + String[] values = parseXMLAttribute(attrValue); + setPVKey(values[0]); + setIDRef(values[1]); + } + + /** + * Sets the key of the targeted PageViewport. + * + * @param pvKey the PageViewport key + */ + public void setPVKey(String pvKey) { + this.pvKey = pvKey; + } + + /** + * Returns the key of the targeted PageViewport. + * + * @return the PageViewport key + */ + public String getPVKey() { + return pvKey; + } + + /** + * Sets the target ID. + * + * @param idRef the target ID + */ + public void setIDRef(String idRef) { + this.idRef = idRef; + } + + /** + * Returns the target ID. + * + * @return the target ID + */ + public String getIDRef() { + return idRef; + } + + /** + * Returns the attribute value for this object as + * used in the area tree XML. + * + * @return a string of the type "(thisPVKey,thisIDRef)" + */ + public String xmlAttribute() { + return makeXMLAttribute(pvKey, idRef); + } + + /** + * Returns the XML attribute value for the given PV key and ID ref. + * This value is used in the area tree XML. + * + * @param pvKey the PageViewport key of the link target + * @param idRef the ID of the link target + * @return a string of the type "(thisPVKey,thisIDRef)" + */ + public static String makeXMLAttribute(String pvKey, String idRef) { + return "(" + (pvKey == null ? "" : pvKey) + "," + + (idRef == null ? "" : idRef) + ")"; + } + + /** + * Parses XML attribute value from the area tree into + * PageViewport key + IDRef strings. If the attribute value is + * formatted like "(s1,s2)", then s1 and s2 are considered to be + * the PV key and the IDRef, respectively. + * Otherwise, the entire string is the PV key and the IDRef is null. + * + * @param attrValue the atribute value (PV key and possibly IDRef) + * @return a 2-String array containing the PV key and the IDRef. + * Both may be null. + */ + public static String[] parseXMLAttribute(String attrValue) { + String[] result = {null, null}; + if (attrValue != null) { + int len = attrValue.length(); + if (len >= 2 && attrValue.charAt(0) == '(' && attrValue.charAt(len - 1) == ')') { + String value = attrValue.substring(1, len - 1); // remove brackets + int delimIndex = value.indexOf(','); + result[0] = value.substring(0, delimIndex).trim(); // PV key + result[1] = value.substring(delimIndex + 1, value.length()).trim(); // IDRef + } else { + // PV key only, e.g. from old area tree XML: + result[0] = attrValue; + } + } + return result; + } + + /** + * Return the human-friendly string for debugging. + * @see java.lang.Object#toString() + */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("pvKey=").append(pvKey); + sb.append(",idRef=").append(idRef); + return sb.toString(); + } + } + /** * Background trait structure. * Used for storing back trait information which are related. diff --git a/src/java/org/apache/fop/fo/flow/Table.java b/src/java/org/apache/fop/fo/flow/Table.java index ed69e3100..2fa46a75e 100644 --- a/src/java/org/apache/fop/fo/flow/Table.java +++ b/src/java/org/apache/fop/fo/flow/Table.java @@ -136,13 +136,6 @@ public class Table extends TableFObj { + " behavior on fo:table. Falling back to \"auto\""); // Anyway, the bpd of a table is not used by the layout code } - if (borderCollapse != EN_SEPARATE) { - //TODO Remove once the collapsing border is at least marginally working. - borderCollapse = EN_SEPARATE; - log.debug("A table has been forced to use the separate border model" - + " (border-collapse=\"separate\") as the collapsing border model" - + " is not implemented, yet."); - } if (tableLayout == EN_AUTO) { attributeWarning("table-layout=\"auto\" is currently not supported by FOP"); } diff --git a/src/java/org/apache/fop/fo/pagination/Root.java b/src/java/org/apache/fop/fo/pagination/Root.java index 23f9cd391..929c721ec 100644 --- a/src/java/org/apache/fop/fo/pagination/Root.java +++ b/src/java/org/apache/fop/fo/pagination/Root.java @@ -21,7 +21,6 @@ package org.apache.fop.fo.pagination; // java import java.util.List; -import java.util.ArrayList; import org.xml.sax.Locator; @@ -45,7 +44,7 @@ public class Root extends FObj { private LayoutMasterSet layoutMasterSet; private Declarations declarations; private BookmarkTree bookmarkTree = null; - private ArrayList destinationList; + private List destinationList; private List pageSequences; // temporary until above list populated @@ -67,7 +66,7 @@ public class Root extends FObj { */ public Root(FONode parent) { super(parent); - pageSequences = new ArrayList(); + pageSequences = new java.util.ArrayList(); if (parent != null) { //throw new FOPException("root must be root element"); } @@ -260,7 +259,7 @@ public class Root extends FObj { */ public void addDestination(Destination destination) { if (destinationList == null) { - destinationList = new ArrayList(); + destinationList = new java.util.ArrayList(); } destinationList.add(destination); } @@ -269,7 +268,7 @@ public class Root extends FObj { * Public accessor for the list of Destination objects for this FO * @return the Destination object */ - public ArrayList getDestinationList() { + public List getDestinationList() { return destinationList; } diff --git a/src/java/org/apache/fop/layoutmgr/AreaAdditionUtil.java b/src/java/org/apache/fop/layoutmgr/AreaAdditionUtil.java index 8892eae16..aed12a687 100644 --- a/src/java/org/apache/fop/layoutmgr/AreaAdditionUtil.java +++ b/src/java/org/apache/fop/layoutmgr/AreaAdditionUtil.java @@ -101,6 +101,8 @@ public class AreaAdditionUtil { StackingIter childPosIter = new StackingIter(positionList.listIterator()); while ((childLM = childPosIter.getNextChildLM()) != null) { + // TODO vh: the test above might be problematic in some cases. See comment in + // the TableCellLM.getNextKnuthElements method // Add the block areas to Area lc.setFlags(LayoutContext.FIRST_AREA, childLM == firstLM); lc.setFlags(LayoutContext.LAST_AREA, childLM == lastLM); diff --git a/src/java/org/apache/fop/layoutmgr/ElementListUtils.java b/src/java/org/apache/fop/layoutmgr/ElementListUtils.java index 1166482a9..08450db98 100644 --- a/src/java/org/apache/fop/layoutmgr/ElementListUtils.java +++ b/src/java/org/apache/fop/layoutmgr/ElementListUtils.java @@ -143,10 +143,15 @@ public class ElementListUtils { i.add(new KnuthPenalty(0, KnuthPenalty.INFINITE, false, null, false)); } - } else if (el instanceof BreakElement) { - BreakElement breakEl = (BreakElement)el; - if (breakEl.getPenaltyValue() < KnuthPenalty.INFINITE) { - breakEl.setPenaltyValue(KnuthPenalty.INFINITE); + } else if (el.isUnresolvedElement()) { + if (el instanceof BreakElement) { + BreakElement breakEl = (BreakElement)el; + if (breakEl.getPenaltyValue() < KnuthPenalty.INFINITE) { + breakEl.setPenaltyValue(KnuthPenalty.INFINITE); + } + } else if (el instanceof UnresolvedListElementWithLength) { + UnresolvedListElementWithLength uel = (UnresolvedListElementWithLength)el; + len += uel.getLength().opt; } } else { KnuthElement kel = (KnuthElement)el; diff --git a/src/java/org/apache/fop/layoutmgr/TraitSetter.java b/src/java/org/apache/fop/layoutmgr/TraitSetter.java index 274204122..b50167b16 100644 --- a/src/java/org/apache/fop/layoutmgr/TraitSetter.java +++ b/src/java/org/apache/fop/layoutmgr/TraitSetter.java @@ -208,8 +208,6 @@ public class TraitSetter { if (bps != null) { area.addTrait(Trait.BORDER_END, bps); } - - addPadding(area, bordProps, context); } private static void addPadding(Area area, CommonBorderPaddingBackground bordProps, diff --git a/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java index 3c9dd4ed9..7e261d074 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java @@ -22,17 +22,17 @@ package org.apache.fop.layoutmgr.inline; import org.apache.fop.datatypes.URISpecification; import org.apache.fop.fo.flow.BasicLink; import org.apache.fop.layoutmgr.LayoutManager; +import org.apache.fop.layoutmgr.PageSequenceLayoutManager; import org.apache.fop.area.inline.InlineArea; import org.apache.fop.area.Trait; import org.apache.fop.area.LinkResolver; -import org.apache.fop.area.PageViewport; /** * LayoutManager for the fo:basic-link formatting object */ public class BasicLinkLayoutManager extends InlineLayoutManager { private BasicLink fobj; - + /** * Create an fo:basic-link layout manager. * @@ -49,22 +49,33 @@ public class BasicLinkLayoutManager extends InlineLayoutManager { setupBasicLinkArea(parentLM, area); return area; } - - private void setupBasicLinkArea(LayoutManager parentLM, - InlineArea area) { - if (fobj.getExternalDestination() != null) { - area.addTrait(Trait.EXTERNAL_LINK, - URISpecification.getURL(fobj.getExternalDestination())); - } else { - String idref = fobj.getInternalDestination(); - PageViewport page = getPSLM().getFirstPVWithID(idref); - if (page != null) { - area.addTrait(Trait.INTERNAL_LINK, page.getKey()); - } else { - LinkResolver res = new LinkResolver(idref, area); - getPSLM().addUnresolvedArea(idref, res); - } - } - } -} + /* + * Detect internal or external link and add it as an area trait + * + * @param parentLM the parent LayoutManager + * @param area the basic-link's area + */ + private void setupBasicLinkArea(LayoutManager parentLM, InlineArea area) { + // internal destinations take precedence: + String idref = fobj.getInternalDestination(); + if (idref != null && idref.length() > 0) { + PageSequenceLayoutManager pslm = getPSLM(); + // the INTERNAL_LINK trait is added by the LinkResolver + // if and when the link is resolved: + LinkResolver res = new LinkResolver(idref, area); + res.resolveIDRef(idref, pslm.getFirstPVWithID(idref)); + if (!res.isResolved()) { + pslm.addUnresolvedArea(idref, res); + } + } else { + String extdest = fobj.getExternalDestination(); + if (extdest != null) { + String url = URISpecification.getURL(extdest); + if (url.length() > 0) { + area.addTrait(Trait.EXTERNAL_LINK, url); + } + } + } + } +} diff --git a/src/java/org/apache/fop/layoutmgr/inline/FootnoteLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/FootnoteLayoutManager.java index 91b7270eb..849ae08d8 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/FootnoteLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/FootnoteLayoutManager.java @@ -26,19 +26,20 @@ import java.util.ListIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.fo.flow.Footnote; -import org.apache.fop.layoutmgr.AbstractLayoutManager; import org.apache.fop.layoutmgr.FootnoteBodyLayoutManager; import org.apache.fop.layoutmgr.InlineKnuthSequence; import org.apache.fop.layoutmgr.KnuthElement; import org.apache.fop.layoutmgr.KnuthSequence; import org.apache.fop.layoutmgr.LayoutContext; -import org.apache.fop.layoutmgr.Position; +import org.apache.fop.layoutmgr.LayoutManager; +import org.apache.fop.layoutmgr.ListElement; +import org.apache.fop.layoutmgr.NonLeafPosition; +import org.apache.fop.layoutmgr.PositionIterator; /** * Layout manager for fo:footnote. */ -public class FootnoteLayoutManager extends AbstractLayoutManager - implements InlineLevelLayoutManager { +public class FootnoteLayoutManager extends InlineStackingLayoutManager { /** * logging instance @@ -72,13 +73,10 @@ public class FootnoteLayoutManager extends AbstractLayoutManager /** @see org.apache.fop.layoutmgr.LayoutManager */ public LinkedList getNextKnuthElements(LayoutContext context, int alignment) { - // this is the only method that must be implemented: - // all other methods will never be called, as the returned elements - // contain Positions created by the citationLM, so its methods will - // be called instead - - // set the citationLM parent to be this LM's parent - citationLM.setParent(getParent()); + // for the moment, this LM is set as the citationLM's parent + // later on, when this LM will have nothing more to do, the citationLM's parent + // will be set to the fotnoteLM's parent + citationLM.setParent(this); citationLM.initialize(); bodyLM.setParent(this); bodyLM.initialize(); @@ -104,22 +102,82 @@ public class FootnoteLayoutManager extends AbstractLayoutManager addAnchor(returnedList); + // "wrap" the Position stored in each list inside returnedList + ListIterator listIterator = returnedList.listIterator(); + ListIterator elementIterator = null; + KnuthSequence list = null; + ListElement element = null; + while (listIterator.hasNext()) { + list = (KnuthSequence) listIterator.next(); + elementIterator = list.listIterator(); + while (elementIterator.hasNext()) { + element = (KnuthElement) elementIterator.next(); + element.setPosition(notifyPos(new NonLeafPosition(this, element.getPosition()))); + } + } + + return returnedList; + } + + /** + * @see org.apache.fop.layoutmgr.LayoutManager#getChangedKnuthElements(java.util.List, int) + */ + public LinkedList getChangedKnuthElements(List oldList, + int alignment) { + LinkedList returnedList = super.getChangedKnuthElements(oldList, alignment); + addAnchor(returnedList); return returnedList; } + + /** + * @see org.apache.fop.layoutmgr.LayoutManager#addAreas(PositionIterator posIter, LayoutContext context); + */ + public void addAreas(PositionIterator posIter, LayoutContext context) { + // "Unwrap" the NonLeafPositions stored in posIter and put + // them in a new list, that will be given to the citationLM + LinkedList positionList = new LinkedList(); + NonLeafPosition pos = null; + while (posIter.hasNext()) { + pos = (NonLeafPosition) posIter.next(); + if (pos != null && pos.getPosition() != null) { + positionList.add(pos.getPosition()); + } + } + + // FootnoteLM does not create any area, + // so the citationLM child will add directly to the FootnoteLM parent area + citationLM.setParent(getParent()); + + // make the citationLM add its areas + LayoutContext childContext = new LayoutContext(context); + StackingIter childPosIter = new StackingIter(positionList.listIterator()); + LayoutManager childLM; + while ((childLM = childPosIter.getNextChildLM()) != null) { + childLM.addAreas(childPosIter, childContext); + childContext.setLeadingSpace(childContext.getTrailingSpace()); + childContext.setFlags(LayoutContext.RESOLVE_LEADING_SPACE, true); + } + } + + /** + * Find the last box in the sequence, and add a reference to the FootnoteBodyLM + * @param citationList the list of elements representing the footnote citation + */ private void addAnchor(LinkedList citationList) { - // find the last box in the sequence, and add a reference - // to the FootnoteBodyLM KnuthInlineBox lastBox = null; + // the list of elements is searched backwards, until we find a box ListIterator citationIterator = citationList.listIterator(citationList.size()); while (citationIterator.hasPrevious() && lastBox == null) { Object obj = citationIterator.previous(); if (obj instanceof KnuthElement) { + // obj is an element KnuthElement element = (KnuthElement)obj; if (element instanceof KnuthInlineBox) { lastBox = (KnuthInlineBox) element; } } else { + // obj is a sequence of elements KnuthSequence seq = (KnuthSequence)obj; ListIterator nestedIterator = seq.listIterator(seq.size()); while (nestedIterator.hasPrevious() && lastBox == null) { @@ -137,45 +195,4 @@ public class FootnoteLayoutManager extends AbstractLayoutManager //throw new IllegalStateException("No anchor box was found for a footnote."); } } - - /** @see org.apache.fop.layoutmgr.inline.InlineLevelLayoutManager */ - public List addALetterSpaceTo(List oldList) { - log.warn("null implementation of addALetterSpaceTo() called!"); - return oldList; - } - - /** - * Remove the word space represented by the given elements - * - * @param oldList the elements representing the word space - */ - public void removeWordSpace(List oldList) { - // do nothing - log.warn(this.getClass().getName() + " should not receive a call to removeWordSpace(list)"); - } - - /** @see org.apache.fop.layoutmgr.inline.InlineLevelLayoutManager */ - public void getWordChars(StringBuffer sbChars, Position pos) { - log.warn("null implementation of getWordChars() called!"); - } - - /** @see org.apache.fop.layoutmgr.inline.InlineLevelLayoutManager */ - public void hyphenate(Position pos, HyphContext hc) { - log.warn("null implementation of hyphenate called!"); - } - - /** @see org.apache.fop.layoutmgr.inline.InlineLevelLayoutManager */ - public boolean applyChanges(List oldList) { - log.warn("null implementation of applyChanges() called!"); - return false; - } - - /** - * @see org.apache.fop.layoutmgr.LayoutManager#getChangedKnuthElements(java.util.List, int) - */ - public LinkedList getChangedKnuthElements(List oldList, - int alignment) { - log.warn("null implementation of getChangeKnuthElement() called!"); - return null; - } } diff --git a/src/java/org/apache/fop/layoutmgr/inline/WrapperLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/WrapperLayoutManager.java index 9c946afbc..a9807573d 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/WrapperLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/WrapperLayoutManager.java @@ -22,6 +22,8 @@ package org.apache.fop.layoutmgr.inline; import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.fo.flow.Wrapper;
import org.apache.fop.layoutmgr.LayoutContext;
+import org.apache.fop.layoutmgr.PositionIterator;
+import org.apache.fop.layoutmgr.TraitSetter;
/**
* This is the layout manager for the fo:wrapper formatting object.
@@ -41,16 +43,41 @@ public class WrapperLayoutManager extends LeafNodeLayoutManager { /** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager */
public InlineArea get(LayoutContext context) {
- //Create a zero-width, zero-height dummy area so this node can
- //participate in the ID handling. Otherwise, addId() wouldn't
- //be called.
+ // Create a zero-width, zero-height dummy area so this node can
+ // participate in the ID handling. Otherwise, addId() wouldn't
+ // be called. The area must also be added to the tree, because
+ // determination of the X,Y position is done in the renderer.
InlineArea area = new InlineArea();
+ String id = fobj.getId();
+ if (id != null && id.length() > 0) {
+ TraitSetter.setProducerID(area, fobj.getId());
+ }
return area;
}
-
+
+ /**
+ * Add the area for this layout manager.
+ * This adds the dummy area to the parent, *if* it has an id
+ * - otherwise it serves no purpose.
+ *
+ * @param posIter the position iterator
+ * @param context the layout context for adding the area
+ */
+ public void addAreas(PositionIterator posIter, LayoutContext context) {
+ String id = fobj.getId();
+ if (id != null && id.length() > 0) {
+ addId();
+ InlineArea area = getEffectiveArea();
+ parentLM.addChildArea(area);
+ }
+ while (posIter.hasNext()) {
+ posIter.next();
+ }
+ }
+
/** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager#addId() */
protected void addId() {
getPSLM().addIDToPage(fobj.getId());
}
-
+
}
diff --git a/src/java/org/apache/fop/layoutmgr/table/TableCellLayoutManager.java b/src/java/org/apache/fop/layoutmgr/table/TableCellLayoutManager.java index 2f17a6a25..1ea836d38 100644 --- a/src/java/org/apache/fop/layoutmgr/table/TableCellLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/table/TableCellLayoutManager.java @@ -34,6 +34,7 @@ import org.apache.fop.layoutmgr.BlockStackingLayoutManager; import org.apache.fop.layoutmgr.BreakElement; import org.apache.fop.layoutmgr.KnuthElement; import org.apache.fop.layoutmgr.KnuthGlue; +import org.apache.fop.layoutmgr.KnuthPenalty; import org.apache.fop.layoutmgr.LayoutContext; import org.apache.fop.layoutmgr.ListElement; import org.apache.fop.layoutmgr.PositionIterator; @@ -175,78 +176,55 @@ public class TableCellLayoutManager extends BlockStackingLayoutManager childLC.setFlags(LayoutContext.KEEP_WITH_PREVIOUS_PENDING, false); } - if (returnedList.size() == 1 - && ((ListElement)returnedList.getFirst()).isForcedBreak()) { - // a descendant of this block has break-before - if (returnList.size() == 0) { - // the first child (or its first child ...) has - // break-before; - // all this block, including space before, will be put in - // the - // following page - } - contentList.addAll(returnedList); - - // "wrap" the Position inside each element - // moving the elements from contentList to returnList - returnedList = new LinkedList(); - wrapPositionElements(contentList, returnList); - - //Space resolution - SpaceResolver.resolveElementList(returnList); - - return returnList; - } else { - if (prevLM != null) { - // there is a block handled by prevLM - // before the one handled by curLM - if (mustKeepTogether() - || context.isKeepWithNextPending() - || childLC.isKeepWithPreviousPending()) { - //Clear keep pending flag - context.setFlags(LayoutContext.KEEP_WITH_NEXT_PENDING, false); - childLC.setFlags(LayoutContext.KEEP_WITH_PREVIOUS_PENDING, false); - // add an infinite penalty to forbid a break between - // blocks - contentList.add(new BreakElement( - new Position(this), KnuthElement.INFINITE, context)); - //contentList.add(new KnuthPenalty(0, - // KnuthElement.INFINITE, false, - // new Position(this), false)); - } else if (!((ListElement) contentList.getLast()).isGlue()) { - // add a null penalty to allow a break between blocks - contentList.add(new BreakElement( - new Position(this), 0, context)); - //contentList.add(new KnuthPenalty(0, 0, false, - // new Position(this), false)); - } else { - // the last element in contentList is a glue; - // it is a feasible breakpoint, there is no need to add - // a penalty - } - } - contentList.addAll(returnedList); - if (returnedList.size() == 0) { - //Avoid NoSuchElementException below (happens with empty blocks) - continue; - } - if (((ListElement)returnedList.getLast()).isForcedBreak()) { - // a descendant of this block has break-after - if (curLM.isFinished()) { - // there is no other content in this block; - // it's useless to add space after before a page break - setFinished(true); - } - - returnedList = new LinkedList(); - wrapPositionElements(contentList, returnList); - - //Space resolution - SpaceResolver.resolveElementList(returnList); - - return returnList; + if (prevLM != null) { + // there is a block handled by prevLM + // before the one handled by curLM + if (mustKeepTogether() + || context.isKeepWithNextPending() + || childLC.isKeepWithPreviousPending()) { + //Clear keep pending flag + context.setFlags(LayoutContext.KEEP_WITH_NEXT_PENDING, false); + childLC.setFlags(LayoutContext.KEEP_WITH_PREVIOUS_PENDING, false); + // add an infinite penalty to forbid a break between + // blocks + contentList.add(new BreakElement( + new Position(this), KnuthElement.INFINITE, context)); + //contentList.add(new KnuthPenalty(0, + // KnuthElement.INFINITE, false, + // new Position(this), false)); + } else if (!(((ListElement) contentList.getLast()).isGlue() + || (((ListElement)contentList.getLast()).isPenalty() + && ((KnuthPenalty)contentList.getLast()).getP() < KnuthElement.INFINITE) + || (contentList.getLast() instanceof BreakElement + && ((BreakElement)contentList.getLast()).getPenaltyValue() < KnuthElement.INFINITE))) { + // TODO vh: this is hacky + // The getNextKnuthElements method of TableCellLM must not be called + // twice, otherwise some settings like indents or borders will be + // counted several times and lead to a wrong output. Anyway the + // getNextKnuthElements methods should be called only once eventually + // (i.e., when multi-threading the code), even when there are forced + // breaks. + // If we add a break possibility after a forced break the + // AreaAdditionUtil.addAreas method will act on a sequence starting + // with a SpaceResolver.SpaceHandlingBreakPosition element, having no + // LM associated to it. Thus it will stop early instead of adding + // areas for following Positions. The above test aims at preventing + // such a situation from occuring. add a null penalty to allow a break + // between blocks + contentList.add(new BreakElement( + new Position(this), 0, context)); + //contentList.add(new KnuthPenalty(0, 0, false, + // new Position(this), false)); + } else { + // the last element in contentList is a feasible breakpoint, there is + // no need to add a penalty } } + contentList.addAll(returnedList); + if (returnedList.size() == 0) { + //Avoid NoSuchElementException below (happens with empty blocks) + continue; + } if (childLC.isKeepWithNextPending()) { //Clear and propagate childLC.setFlags(LayoutContext.KEEP_WITH_NEXT_PENDING, false); @@ -388,38 +366,36 @@ public class TableCellLayoutManager extends BlockStackingLayoutManager int dx = xoffset; for (int x = 0; x < gridUnits.length; x++) { GridUnit gu = gridUnits[x]; - if (!gu.hasBorders()) { - continue; + if (gu.hasBorders()) { + //Blocks for painting grid unit borders + Block block = new Block(); + block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE); + block.setPositioning(Block.ABSOLUTE); + + int bpd = spannedGridRowHeights[y - startRow]; + bpd -= gu.getBorders().getBorderBeforeWidth(false) / 2; + bpd -= gu.getBorders().getBorderAfterWidth(false) / 2; + block.setBPD(bpd); + if (log.isTraceEnabled()) { + log.trace("pgu: " + primaryGridUnit + "; gu: " + gu + "; yoffset: " + + (dy - gu.getBorders().getBorderBeforeWidth(false) / 2) + + "; bpd: " + bpd); + } + int ipd = gu.getColumn().getColumnWidth().getValue( + (PercentBaseContext) getParent()); + int borderStartWidth = gu.getBorders().getBorderStartWidth(false) / 2; + ipd -= borderStartWidth; + ipd -= gu.getBorders().getBorderEndWidth(false) / 2; + block.setIPD(ipd); + block.setXOffset(dx + borderStartWidth); + block.setYOffset(dy - gu.getBorders().getBorderBeforeWidth(false) / 2); + outer[0] = gu.getFlag(GridUnit.FIRST_IN_TABLE); + outer[1] = gu.getFlag(GridUnit.LAST_IN_TABLE); + outer[2] = gu.getFlag(GridUnit.IN_FIRST_COLUMN); + outer[3] = gu.getFlag(GridUnit.IN_LAST_COLUMN); + TraitSetter.addCollapsingBorders(block, gu.getBorders(), outer, this); + parentLM.addChildArea(block); } - - //Blocks for painting grid unit borders - Block block = new Block(); - block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE); - block.setPositioning(Block.ABSOLUTE); - - int bpd = spannedGridRowHeights[y - startRow]; - bpd -= gu.getBorders().getBorderBeforeWidth(false) / 2; - bpd -= gu.getBorders().getBorderAfterWidth(false) / 2; - block.setBPD(bpd); - if (log.isTraceEnabled()) { - log.trace("pgu: " + primaryGridUnit + "; gu: " + gu + "; yoffset: " - + (dy - gu.getBorders().getBorderBeforeWidth(false) / 2) - + "; bpd: " + bpd); - } - int ipd = gu.getColumn().getColumnWidth().getValue( - (PercentBaseContext) getParent()); - int borderStartWidth = gu.getBorders().getBorderStartWidth(false) / 2; - ipd -= borderStartWidth; - ipd -= gu.getBorders().getBorderEndWidth(false) / 2; - block.setIPD(ipd); - block.setXOffset(dx + borderStartWidth); - block.setYOffset(dy - gu.getBorders().getBorderBeforeWidth(false) / 2); - outer[0] = gu.getFlag(GridUnit.FIRST_IN_TABLE); - outer[1] = gu.getFlag(GridUnit.LAST_IN_TABLE); - outer[2] = gu.getFlag(GridUnit.IN_FIRST_COLUMN); - outer[3] = gu.getFlag(GridUnit.IN_LAST_COLUMN); - TraitSetter.addCollapsingBorders(block, gu.getBorders(), outer, this); - parentLM.addChildArea(block); dx += gu.getColumn().getColumnWidth().getValue( (PercentBaseContext) getParent()); } @@ -427,6 +403,8 @@ public class TableCellLayoutManager extends BlockStackingLayoutManager } } } + TraitSetter.addPadding(curBlockArea, primaryGridUnit.getBorders(), + false, false, false, false, this); //Handle display-align int contentBPD = getContentHeight(primaryGridUnit); @@ -625,4 +603,3 @@ public class TableCellLayoutManager extends BlockStackingLayoutManager } } - diff --git a/src/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java b/src/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java index c3e1a41da..8abe1a3e8 100644 --- a/src/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java @@ -69,7 +69,6 @@ public class TableContentLayoutManager implements PercentBaseContext { private LinkedList footerList; private int headerNetHeight = 0; private int footerNetHeight = 0; - private boolean firstBreakBeforeServed = false; private int startXOffset; private int usedBPD; @@ -180,12 +179,24 @@ public class TableContentLayoutManager implements PercentBaseContext { LinkedList returnList = getKnuthElementsForRowIterator( bodyIter, context, alignment, TableRowIterator.BODY); if (headerAsFirst != null) { - returnList.add(0, headerAsFirst); + int insertionPoint = 0; + if (returnList.size() > 0 && ((ListElement)returnList.getFirst()).isForcedBreak()) { + insertionPoint++; + } + returnList.add(insertionPoint, headerAsFirst); } else if (headerAsSecondToLast != null) { - returnList.add(headerAsSecondToLast); + int insertionPoint = returnList.size(); + if (returnList.size() > 0 && ((ListElement)returnList.getLast()).isForcedBreak()) { + insertionPoint--; + } + returnList.add(insertionPoint, headerAsSecondToLast); } if (footerAsLast != null) { - returnList.add(footerAsLast); + int insertionPoint = returnList.size(); + if (returnList.size() > 0 && ((ListElement)returnList.getLast()).isForcedBreak()) { + insertionPoint--; + } + returnList.add(insertionPoint, footerAsLast); } return returnList; } @@ -220,16 +231,10 @@ public class TableContentLayoutManager implements PercentBaseContext { breakPoss.setBreakClass(rowFO.getBreakBefore()); } } else { - if (!firstBreakBeforeServed) { - returnList.add(new BreakElement(new Position(getTableLM()), - 0, -KnuthPenalty.INFINITE, rowFO.getBreakBefore(), context)); - iter.backToPreviousRow(); - firstBreakBeforeServed = true; - break; - } + returnList.add(new BreakElement(new Position(getTableLM()), + 0, -KnuthPenalty.INFINITE, rowFO.getBreakBefore(), context)); } } - firstBreakBeforeServed = true; //Border resolution if (!isSeparateBorderModel()) { diff --git a/src/java/org/apache/fop/layoutmgr/table/TableLayoutManager.java b/src/java/org/apache/fop/layoutmgr/table/TableLayoutManager.java index 00ecb01d2..155111ac4 100644 --- a/src/java/org/apache/fop/layoutmgr/table/TableLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/table/TableLayoutManager.java @@ -29,6 +29,7 @@ import org.apache.fop.layoutmgr.KnuthElement; import org.apache.fop.layoutmgr.KnuthGlue; import org.apache.fop.layoutmgr.LayoutContext; import org.apache.fop.layoutmgr.ListElement; +import org.apache.fop.layoutmgr.NonLeafPosition; import org.apache.fop.layoutmgr.PositionIterator; import org.apache.fop.layoutmgr.Position; import org.apache.fop.layoutmgr.RelSide; @@ -248,7 +249,34 @@ public class TableLayoutManager extends BlockStackingLayoutManager log.debug("TableContentLM signals pending keep-with-previous"); context.setFlags(LayoutContext.KEEP_WITH_PREVIOUS_PENDING); } - + + // Check if the table's content starts/ends with a forced break + // TODO this is hacky and will need to be handled better eventually + if (contentKnuthElements.size() > 0) { + ListElement element = (ListElement)contentKnuthElements.getFirst(); + if (element.isForcedBreak()) { + // The first row of the table(-body), or (the content of) one of its cells + // has a forced break-before + int breakBeforeTable = ((Table) fobj).getBreakBefore(); + if (breakBeforeTable == EN_PAGE + || breakBeforeTable == EN_COLUMN + || breakBeforeTable == EN_EVEN_PAGE + || breakBeforeTable == EN_ODD_PAGE) { + // There is already a forced break before the table; remove this one + // to prevent a double break + contentKnuthElements.removeFirst(); + } else { + element.setPosition(new NonLeafPosition(this, null)); + } + } + element = (ListElement)contentKnuthElements.getLast(); + if (element.isForcedBreak()) { + // The last row of the table(-body), or (the content of) one of its cells + // has a forced break-after + element.setPosition(new NonLeafPosition(this, null)); + } + } + //Set index values on elements coming from the content LM Iterator iter = contentKnuthElements.iterator(); while (iter.hasNext()) { @@ -256,73 +284,19 @@ public class TableLayoutManager extends BlockStackingLayoutManager notifyPos(el.getPosition()); } log.debug(contentKnuthElements); - - if (contentKnuthElements.size() == 1 - && ((ListElement)contentKnuthElements.getFirst()).isForcedBreak()) { - // a descendant of this block has break-before - if (returnList.size() == 0) { - // the first child (or its first child ...) has - // break-before; - // all this block, including space before, will be put in - // the - // following page - //FIX ME - //bSpaceBeforeServed = false; - } - contentList.addAll(contentKnuthElements); - - // "wrap" the Position inside each element - // moving the elements from contentList to returnList - contentKnuthElements = new LinkedList(); - wrapPositionElements(contentList, returnList); - - return returnList; - } else { - /* - if (prevLM != null) { - // there is a block handled by prevLM - // before the one handled by curLM - if (mustKeepTogether() - || prevLM.mustKeepWithNext() - || curLM.mustKeepWithPrevious()) { - // add an infinite penalty to forbid a break between - // blocks - contentList.add(new KnuthPenalty(0, - KnuthElement.INFINITE, false, - new Position(this), false)); - } else if (!((KnuthElement) contentList.getLast()).isGlue()) { - // add a null penalty to allow a break between blocks - contentList.add(new KnuthPenalty(0, 0, false, - new Position(this), false)); - } else { - // the last element in contentList is a glue; - // it is a feasible breakpoint, there is no need to add - // a penalty - } - }*/ - contentList.addAll(contentKnuthElements); - if (contentKnuthElements.size() > 0) { - if (((ListElement)contentKnuthElements.getLast()).isForcedBreak()) { - // a descendant of this block has break-after - if (false /*curLM.isFinished()*/) { - // there is no other content in this block; - // it's useless to add space after before a page break - setFinished(true); - } - - contentKnuthElements = new LinkedList(); - wrapPositionElements(contentList, returnList); - - return returnList; - } - } - } + contentList.addAll(contentKnuthElements); wrapPositionElements(contentList, returnList); if (getTable().isSeparateBorderModel()) { addKnuthElementsForBorderPaddingAfter(returnList, true); } addKnuthElementsForSpaceAfter(returnList, alignment); addKnuthElementsForBreakAfter(returnList, context); + if (mustKeepWithNext()) { + context.setFlags(LayoutContext.KEEP_WITH_NEXT_PENDING); + } + if (mustKeepWithPrevious()) { + context.setFlags(LayoutContext.KEEP_WITH_PREVIOUS_PENDING); + } setFinished(true); resetSpaces(); return returnList; diff --git a/src/java/org/apache/fop/layoutmgr/table/TableRowIterator.java b/src/java/org/apache/fop/layoutmgr/table/TableRowIterator.java index f8cedd7de..0f7604f20 100644 --- a/src/java/org/apache/fop/layoutmgr/table/TableRowIterator.java +++ b/src/java/org/apache/fop/layoutmgr/table/TableRowIterator.java @@ -212,13 +212,6 @@ public class TableRowIterator { } /** - * Sets the iterator to the previous row. - */ - public void backToPreviousRow() { - iteratorIndex--; - } - - /** * Returns the first effective row. * @return the requested effective row. */ diff --git a/src/java/org/apache/fop/layoutmgr/table/TableStepper.java b/src/java/org/apache/fop/layoutmgr/table/TableStepper.java index 20b80c1bd..60ccf2b0a 100644 --- a/src/java/org/apache/fop/layoutmgr/table/TableStepper.java +++ b/src/java/org/apache/fop/layoutmgr/table/TableStepper.java @@ -85,7 +85,6 @@ public class TableStepper { private boolean rowBacktrackForLastStep; private boolean skippedStep; private boolean[] keepWithNextSignals; - private boolean forcedBreak; private int lastMaxPenaltyLength; /** @@ -117,15 +116,7 @@ public class TableStepper { keepWithNextSignals = new boolean[columnCount]; Arrays.fill(end, -1); } - - private void clearBreakCondition() { - forcedBreak = false; - } - - private boolean isBreakCondition() { - return forcedBreak; - } - + /** * Returns the row currently being processed. * @@ -307,6 +298,8 @@ public class TableStepper { int boxLen = step - addedBoxLen - penaltyLen; addedBoxLen += boxLen; + boolean forcedBreak = false; + int breakClass = -1; //Put all involved grid units into a list List gridUnitParts = new java.util.ArrayList(maxColumnCount); for (int i = 0; i < columnCount; i++) { @@ -320,6 +313,10 @@ public class TableStepper { 0, pgu.getElements().size() - 1)); } else { gridUnitParts.add(new GridUnitPart(pgu, start[i], end[i])); + if (((KnuthElement)elementLists[i].get(end[i])).isForcedBreak()) { + forcedBreak = true; + breakClass = ((KnuthPenalty)elementLists[i].get(end[i])).getBreakClass(); + } } if (end[i] + 1 == elementLists[i].size()) { if (pgu.getFlag(GridUnit.KEEP_WITH_NEXT_PENDING)) { @@ -408,15 +405,14 @@ public class TableStepper { //Need to avoid breaking because borders and/or paddding from other columns would //not fit in the available space (see getNextStep()) } - if (isBreakCondition()) { + if (forcedBreak) { if (skippedStep) { log.error("This is a conflict situation. The output may be wrong." + " Please send your FO file to fop-dev@xmlgraphics.apache.org!"); } p = -KnuthPenalty.INFINITE; //Overrides any keeps (see 4.8 in XSL 1.0) - clearBreakCondition(); } - returnList.add(new BreakElement(penaltyPos, effPenaltyLen, p, -1, context)); + returnList.add(new BreakElement(penaltyPos, effPenaltyLen, p, breakClass, context)); if (log.isDebugEnabled()) { log.debug("step=" + step + " (+" + increase + ")" @@ -436,9 +432,6 @@ public class TableStepper { //we have to signal the still pending last keep-with-next using the LayoutContext. context.setFlags(LayoutContext.KEEP_WITH_NEXT_PENDING); } - if (isBreakCondition()) { - ((BreakElement)returnList.getLast()).setPenaltyValue(-KnuthPenalty.INFINITE); - } if (lastTCPos != null) { lastTCPos.setFlag(TableContentPosition.LAST_IN_ROWGROUP, true); } @@ -478,7 +471,6 @@ public class TableStepper { this.lastMaxPenaltyLength = Math.max(this.lastMaxPenaltyLength, el.getW()); if (el.getP() <= -KnuthElement.INFINITE) { log.debug("FORCED break encountered!"); - forcedBreak = true; break; } else if (el.getP() < KnuthElement.INFINITE) { //First legal break point diff --git a/src/java/org/apache/fop/pdf/PDFArray.java b/src/java/org/apache/fop/pdf/PDFArray.java index 2dcbd5224..abeed1d86 100644 --- a/src/java/org/apache/fop/pdf/PDFArray.java +++ b/src/java/org/apache/fop/pdf/PDFArray.java @@ -19,40 +19,120 @@ package org.apache.fop.pdf; +import java.util.Collection; +import java.util.List; + /** - * class representing an array object + * Class representing an array object. */ public class PDFArray extends PDFObject { /** - * Array of calues for this pdf object. + * List holding the values of this array */ - protected int[] values; + protected List values = new java.util.ArrayList(); + + /** + * Create a new, empty array object + */ + public PDFArray() { + /* generic creation of PDF object */ + super(); + } /** - * create the array object + * Create the array object * * @param values the actual array wrapped by this object */ public PDFArray(int[] values) { - /* generic creation of PDF object */ super(); - /* set fields using paramaters */ - this.values = values; + for (int i = 0, c = values.length; i < c; i++) { + this.values.add(new Integer(values[i])); + } } /** + * Create the array object + * + * @param values the actual values wrapped by this object + */ + public PDFArray(Collection values) { + /* generic creation of PDF object */ + super(); + + this.values.addAll(values); + } + + /** + * Create the array object + * + * @param values the actual array wrapped by this object + */ + public PDFArray(Object[] values) { + /* generic creation of PDF object */ + super(); + + for (int i = 0, c = values.length; i < c; i++) { + this.values.add(values[i]); + } + } + + /** + * Returns the length of the array + * @return the length of the array + */ + public int length() { + return this.values.size(); + } + + /** + * Sets an entry at a given location. + * @param index the index of the value to set + * @param obj the new value + */ + public void set(int index, Object obj) { + this.values.set(index, obj); + } + + /** + * Gets an entry at a given location. + * @param index the index of the value to set + * @return the requested value + */ + public Object get(int index) { + return this.values.get(index); + } + + /** + * Adds a new value to the array. + * @param obj the value + */ + public void add(Object obj) { + this.values.add(obj); + } + + /** * @see org.apache.fop.pdf.PDFObject#toPDFString() */ public String toPDFString() { StringBuffer p = new StringBuffer(64); - p.append(getObjectID() + "["); - for (int i = 0; i < values.length; i++) { - p.append(" "); - p.append(values[i]); + if (hasObjectNumber()) { + p.append(getObjectID()); + } + p.append("["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + p.append(" "); + } + Object obj = this.values.get(i); + formatObject(obj, p); + } + p.append("]"); + if (hasObjectNumber()) { + p.append("\nendobj\n"); } - p.append("]\nendobj\n"); return p.toString(); } diff --git a/src/java/org/apache/fop/pdf/PDFDestination.java b/src/java/org/apache/fop/pdf/PDFDestination.java index 97923e935..2b7ec0a1f 100644 --- a/src/java/org/apache/fop/pdf/PDFDestination.java +++ b/src/java/org/apache/fop/pdf/PDFDestination.java @@ -19,88 +19,77 @@ package org.apache.fop.pdf; -import org.apache.fop.area.DestinationData; -import org.apache.fop.area.PageViewport; - /** * class representing a named destination */ public class PDFDestination extends PDFObject { /** - * PDFReference (object reference) for this destination - */ - private String goToReference; - - /** * ID Reference for this destination */ private String idRef; /** - * PageViewport to which the idRef item refers + * PDFReference (object reference) for this destination */ - private PageViewport pageViewport = null; + private Object goToReference; /** - * create a named destination + * Create a named destination + * @param idRef ID Reference for this destination (the name of the destination) + * @param goToRef Object reference to the GoTo Action */ - public PDFDestination(DestinationData destinationData) { - /* generic creation of PDF object */ - super(); - this.goToReference = destinationData.getGoToReference(); - this.idRef = destinationData.getIDRef(); - this.pageViewport = destinationData.getPageViewport(); + public PDFDestination(String idRef, Object goToRef) { + this.goToReference = goToRef; + this.idRef = idRef; } /** - * @see org.apache.fop.pdf.PDFObject#toPDFString() + * Creates the key/value pair for this destination entry for the name tree. + * @return the formatted key/value pair */ - public String toPDFString() { - String s = getObjectID() - + "<<" - + "/Limits [(" + idRef + ") (" + idRef + ")]\n" - + "/Names [(" + idRef + ") " + goToReference + "]" - + "\n>>\nendobj\n"; - return s; + public String toKeyValuePair() { + StringBuffer sb = new StringBuffer(); + sb.append("(").append(getIDRef()).append(") "); + if (goToReference instanceof PDFWritable) { + sb.append(((PDFWritable)goToReference).toInlinePDFString()); + } else { + sb.append(goToReference); + } + return sb.toString(); + } + + /** @see org.apache.fop.pdf.PDFObject#toPDFString() */ + protected String toPDFString() { + return toKeyValuePair(); } - - /* - * example: - * - * 249 0 obj - * << - * /Limits [(drivervariables) (drivervariables)] - * /Names [(drivervariables) 73 0 R] - * >> - * endobj - */ /** * Sets the GoToReference in the associated DestinationData object. * * @param goToReference the reference to set in the associated DestinationData object. + * @deprecated use setGoToReference(Object) instead */ public void setGoToReference(String goToReference) { this.goToReference = goToReference; } /** - * Returns the GoToReference from the associated DestinationData object. + * Sets the GoToReference in the associated DestinationData object. * - * @return the GoToReference from the associated DestinationData object. + * @param goToReference the reference to set in the associated DestinationData object. */ - public String getGoToReference() { - return this.goToReference; + public void setGoToReference(Object goToReference) { + this.goToReference = goToReference; } /** - * Get the PageViewport object that this destination refers to + * Returns the GoToReference from the associated DestinationData object. * - * @return the PageViewport that this destination points to + * @return the GoToReference from the associated DestinationData object. */ - public PageViewport getPageViewport() { - return this.pageViewport; + public Object getGoToReference() { + return this.goToReference; } /** @@ -128,11 +117,19 @@ public class PDFDestination extends PDFObject { } PDFDestination dest = (PDFDestination)obj; - if (dest.getIDRef() == this.getIDRef()) { + if (dest.getIDRef().equals(this.getIDRef())) { return true; } - return true; + return false; + } + + /** + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return getIDRef().hashCode(); } + } diff --git a/src/java/org/apache/fop/pdf/PDFDests.java b/src/java/org/apache/fop/pdf/PDFDests.java index 96172c8e2..463ef8d7e 100644 --- a/src/java/org/apache/fop/pdf/PDFDests.java +++ b/src/java/org/apache/fop/pdf/PDFDests.java @@ -19,61 +19,29 @@ package org.apache.fop.pdf; -import org.apache.fop.area.DestinationData; +import java.util.List; /** - * class representing an /Dests object (part of a name dictionary) + * class representing an /Dests dictionary object */ -public class PDFDests extends PDFObject { - - private String limitsRef; +public class PDFDests extends PDFNameTreeNode { /** - * create a named destination + * Create a named destination */ - public PDFDests(String limitsRef) { + public PDFDests() { /* generic creation of PDF object */ super(); - this.limitsRef = limitsRef; } /** - * @see org.apache.fop.pdf.PDFObject#toPDFString() + * Create a named destination + * @param destinationList a list of destinations */ - public String toPDFString() { - String s = getObjectID() - + "<<\n" - + "/Dests " + limitsRef - + "\n>>\nendobj\n"; - return s; + public PDFDests(List destinationList) { + this(); + setNames(new PDFArray(destinationList)); } - /* - * example: - * - * 262 0 obj - * << - * /Dests 260 0 R - * >> - * endobj - */ - - /** - * Check if this equals another object. - * - * @param obj the object to compare - * @return true if this equals other object - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || !(obj instanceof PDFDests)) { - return false; - } - - return true; - } } diff --git a/src/java/org/apache/fop/pdf/PDFDictionary.java b/src/java/org/apache/fop/pdf/PDFDictionary.java new file mode 100644 index 000000000..2a9c3ffe4 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFDictionary.java @@ -0,0 +1,96 @@ +/* + * 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.pdf; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Class representing a PDF dictionary object + */ +public class PDFDictionary extends PDFObject { + + /** + * the entry map + */ + protected Map entries = new java.util.HashMap(); + + /** + * maintains the order of the entries added to the entry map. Whenever you modify + * "entries", always make sure you adjust this list accordingly. + */ + protected List order = new java.util.ArrayList(); + + /** + * Create the dictionary object + */ + public PDFDictionary() { + /* generic creation of PDF object */ + super(); + } + + /** + * Puts a new name/value pair. + * @param name the name + * @param value the value + */ + public void put(String name, Object value) { + if (!entries.containsKey(name)) { + this.order.add(name); + } + this.entries.put(name, value); + } + + /** + * Returns the value given a name. + * @param name the name of the value + * @return the value or null, if there's no value with the given name. + */ + public Object get(String name) { + return this.entries.get(name); + } + + /** + * @see org.apache.fop.pdf.PDFObject#toPDFString() + */ + public String toPDFString() { + StringBuffer p = new StringBuffer(64); + if (hasObjectNumber()) { + p.append(getObjectID()); + } + p.append("<<"); + Iterator iter = this.order.iterator(); + while (iter.hasNext()) { + String key = (String)iter.next(); + p.append("\n /"); + p.append(key); + p.append(" "); + Object obj = this.entries.get(key); + formatObject(obj, p); + } + p.append("\n>>\n"); + if (hasObjectNumber()) { + p.append("endobj\n"); + } + return p.toString(); + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFDocument.java b/src/java/org/apache/fop/pdf/PDFDocument.java index 29cb88731..748c6b3a8 100644 --- a/src/java/org/apache/fop/pdf/PDFDocument.java +++ b/src/java/org/apache/fop/pdf/PDFDocument.java @@ -5,9 +5,9 @@ * 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. @@ -19,7 +19,7 @@ package org.apache.fop.pdf; -/// Java +// Java import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,18 +28,15 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Iterator; -import java.util.ArrayList; -import java.util.Collections; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.fop.pdf.DestinationComparator; - /* image support modified from work of BoBoGi */ /* font support based on work by Takayuki Takeuchi */ @@ -67,13 +64,13 @@ import org.apache.fop.pdf.DestinationComparator; public class PDFDocument { private static final Integer LOCATION_PLACEHOLDER = new Integer(0); - + /** Integer constant to represent PDF 1.3 */ public static final int PDF_VERSION_1_3 = 3; /** Integer constant to represent PDF 1.4 */ public static final int PDF_VERSION_1_4 = 4; - + /** * the encoding to use when converting strings to PDF commandos. */ @@ -89,7 +86,7 @@ public class PDFDocument { /** * the character position of each object */ - protected List location = new ArrayList(); + protected List location = new java.util.ArrayList(); /** List of objects to write in the trailer */ private List trailerObjects = new java.util.ArrayList(); @@ -111,12 +108,12 @@ public class PDFDocument { /** Indicates what PDF version is active */ protected int pdfVersion = PDF_VERSION_1_4; - + /** * Indicates which PDF profiles are active (PDF/A, PDF/X etc.) */ protected PDFProfile pdfProfile = new PDFProfile(this); - + /** * the /Root object */ @@ -208,7 +205,7 @@ public class PDFDocument { /** * List of Destinations. */ - protected List destinations = new java.util.ArrayList(); + protected List destinations; /** * List of FileSpecs. @@ -231,19 +228,6 @@ public class PDFDocument { */ private PDFDests dests; - /** - * The PDFLimits object for the name dictionary. - * Note: This object is not a list. - */ - private PDFLimits limits; - - /** - * Whether this PDFDocument has named destinations - * (and thus needs PDFDestinations, PDFLimits, and - * PDFDests) - */ - private boolean hasDestinations = false; - private PDFFactory factory; private boolean encodingOnTheFly = true; @@ -283,7 +267,7 @@ public class PDFDocument { public int getPDFVersion() { return this.pdfVersion; } - + /** @return the String representing the active PDF version */ public String getPDFVersionString() { switch (getPDFVersion()) { @@ -300,7 +284,7 @@ public class PDFDocument { public PDFProfile getProfile() { return this.pdfProfile; } - + /** * Returns the factory for PDF objects. * @return PDFFactory the factory @@ -343,7 +327,7 @@ public class PDFDocument { /** * Set the creation date of the document. - * + * * @param date Date to be stored as creation date in the PDF. */ public void setCreationDate(Date date) { @@ -488,9 +472,6 @@ public class PDFDocument { if (obj instanceof PDFLink) { this.links.add(obj); } - if (obj instanceof PDFDestination) { - this.destinations.add(obj); - } if (obj instanceof PDFFileSpec) { this.filespecs.add(obj); } @@ -614,7 +595,12 @@ public class PDFDocument { * @return the link if found, null otherwise */ protected PDFDestination findDestination(PDFDestination compare) { - return (PDFDestination)findPDFObject(destinations, compare); + int index = getDestinationList().indexOf(compare); + if (index >= 0) { + return (PDFDestination)getDestinationList().get(index); + } else { + return null; + } } /** @@ -751,21 +737,27 @@ public class PDFDocument { } /** - * Gets the list of named destinations. - * - * @return the list of named destinations. + * Adds a destination to the document. + * @param destination the destination object */ - public ArrayList getDestinationList() { - return (ArrayList)destinations; + public void addDestination(PDFDestination destination) { + if (this.destinations == null) { + this.destinations = new java.util.ArrayList(); + } + this.destinations.add(destination); } - + /** - * Sets whether the document has named destinations. + * Gets the list of named destinations. * - * @param hasDestinations whether the document has named destinations. + * @return the list of named destinations. */ - public void setHasDestinations(boolean hasDestinations) { - this.hasDestinations = hasDestinations; + public List getDestinationList() { + if (hasDestinations()) { + return destinations; + } else { + return Collections.EMPTY_LIST; + } } /** @@ -773,17 +765,8 @@ public class PDFDocument { * * @return whether the document has named destinations. */ - public boolean getHasDestinations() { - return this.hasDestinations; - } - - /** - * Gets the PDFLimits object (part of the name dictionary). - * - * @return the PDFLimits object (part of the name dictionary). - */ - public PDFLimits getLimits() { - return limits; + public boolean hasDestinations() { + return this.destinations != null && !this.destinations.isEmpty(); } /** @@ -933,8 +916,8 @@ public class PDFDocument { this.position = 0; getProfile().verifyPDFVersion(); - - byte[] pdf = ("%PDF-" + getPDFVersionString() + "\n").getBytes(); + + byte[] pdf = encode("%PDF-" + getPDFVersionString() + "\n"); stream.write(pdf); this.position += pdf.length; @@ -955,9 +938,9 @@ public class PDFDocument { try { MessageDigest digest = MessageDigest.getInstance("MD5"); DateFormat df = new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS"); - digest.update(df.format(new Date()).getBytes()); + digest.update(encode(df.format(new Date()))); //Ignoring the filename here for simplicity even though it's recommended by the PDF spec - digest.update(String.valueOf(this.position).getBytes()); + digest.update(encode(String.valueOf(this.position))); digest.update(getInfo().toPDF()); byte[] res = digest.digest(); String s = PDFText.toHex(res); @@ -970,7 +953,7 @@ public class PDFDocument { } } } - + /** * write the trailer * @@ -978,11 +961,13 @@ public class PDFDocument { * @throws IOException if there is an exception writing to the output stream */ public void outputTrailer(OutputStream stream) throws IOException { - if (hasDestinations) { - Collections.sort((ArrayList)destinations, new DestinationComparator()); - limits = getFactory().makeLimits((ArrayList)destinations); - dests = getFactory().makeDests(limits.referencePDF()); - this.root.setNames(dests.referencePDF()); + if (hasDestinations()) { + Collections.sort(destinations, new DestinationComparator()); + dests = getFactory().makeDests(destinations); + if (this.root.getNames() == null) { + this.root.setNames(getFactory().makeNames()); + } + this.root.getNames().setDests(dests); } output(stream); for (int count = 0; count < trailerObjects.size(); count++) { @@ -1061,4 +1046,4 @@ public class PDFDocument { return pdfBytes.length; } -}
\ No newline at end of file +} diff --git a/src/java/org/apache/fop/pdf/PDFFactory.java b/src/java/org/apache/fop/pdf/PDFFactory.java index 3005f2018..8011ffc92 100644 --- a/src/java/org/apache/fop/pdf/PDFFactory.java +++ b/src/java/org/apache/fop/pdf/PDFFactory.java @@ -20,6 +20,7 @@ package org.apache.fop.pdf; // Java +import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.FileNotFoundException; import java.io.IOException; @@ -53,8 +54,6 @@ import org.apache.fop.fonts.truetype.TTFSubSetFile; import org.apache.fop.fonts.type1.PFBData; import org.apache.fop.fonts.type1.PFBParser; import org.apache.xmlgraphics.xmp.Metadata; -import org.apache.fop.area.PageViewport; -import org.apache.fop.area.DestinationData; /** * This class provides method to create and register PDF objects. @@ -144,7 +143,7 @@ public class PDFFactory { /** * Make a Metadata object. - * @param doc the DOM Document containing the XMP metadata. + * @param meta the DOM Document containing the XMP metadata. * @param readOnly true if the metadata packet should be marked read-only * @return the newly created Metadata object */ @@ -746,13 +745,11 @@ public class PDFFactory { for (currentPosition = 0; currentPosition < lastPosition; currentPosition++) { // for every consecutive color pair - PDFColor currentColor = - (PDFColor)theColors.get(currentPosition); + PDFColor currentColor = (PDFColor)theColors.get(currentPosition); PDFColor nextColor = (PDFColor)theColors.get(currentPosition + 1); // colorspace must be consistant - if (getDocument().getColorSpace() - != currentColor.getColorSpace()) { + if (getDocument().getColorSpace() != currentColor.getColorSpace()) { currentColor.setColorSpace( getDocument().getColorSpace()); } @@ -815,55 +812,116 @@ public class PDFFactory { /* ============= named destinations and the name dictionary ============ */ /** - * Make a named destination. + * Registers and returns newdest if it is unique. Otherwise, returns + * the equal destination already present in the document. * - * @param destinationData the DestinationData object that holds the info about this named destination - * @return the new PDF named destination object + * @param newdest a new, as yet unregistered destination + * @return newdest if unique, else the already registered instance */ - public PDFDestination makeDestination(DestinationData destinationData) { - PageViewport pv = destinationData.getPageViewport(); - if (pv == null) { - log.warn("Unresolved destination item received: " + destinationData.getIDRef()); - } - PDFDestination destination = new PDFDestination(destinationData); - - PDFDestination oldDestination = getDocument().findDestination(destination); - if (destination == oldDestination) { - destination = oldDestination; + protected PDFDestination getUniqueDestination(PDFDestination newdest) { + PDFDestination existing = getDocument().findDestination(newdest); + if (existing != null) { + return existing; } else { - getDocument().registerObject(destination); - getDocument().setHasDestinations(true); + getDocument().addDestination(newdest); + return newdest; } + } - return destination; + /** + * Make a named destination. + * + * @param idRef ID Reference for this destination (the name of the destination) + * @param goToRef Object reference to the GoTo Action + * @return the newly created destrination + */ + public PDFDestination makeDestination(String idRef, Object goToRef) { + PDFDestination destination = new PDFDestination(idRef, goToRef); + return getUniqueDestination(destination); + } + + /** + * Make a names dictionary (the /Names object). + * @return the new PDFNames object + */ + public PDFNames makeNames() { + PDFNames names = new PDFNames(); + getDocument().registerObject(names); + return names; } /** * Make a the head object of the name dictionary (the /Dests object). * + * @param destinationList a list of PDFDestination instances * @return the new PDFDests object */ - public PDFDests makeDests(String limitsRef) { - PDFDests dests = new PDFDests(limitsRef); + public PDFDests makeDests(List destinationList) { + PDFDests dests; + + final boolean deep = true; + //true for a "deep" structure (one node per entry), true for a "flat" structure + if (deep) { + dests = new PDFDests(); + PDFArray kids = new PDFArray(); + Iterator iter = destinationList.iterator(); + while (iter.hasNext()) { + PDFDestination dest = (PDFDestination)iter.next(); + PDFNameTreeNode node = new PDFNameTreeNode(); + getDocument().registerObject(node); + node.setLowerLimit(dest.getIDRef()); + node.setUpperLimit(dest.getIDRef()); + node.setNames(new PDFArray()); + node.getNames().add(dest); + kids.add(node); + } + dests.setLowerLimit(((PDFNameTreeNode)kids.get(0)).getLowerLimit()); + dests.setUpperLimit(((PDFNameTreeNode)kids.get(kids.length() - 1)).getUpperLimit()); + dests.setKids(kids); + } else { + dests = new PDFDests(destinationList); + } getDocument().registerObject(dests); - return dests; } /** - * Make a the limits object of the name dictionary (the /Limits object). + * Make a name tree node. * - * @return the new PDFLimits object + * @return the new name tree node */ - public PDFLimits makeLimits(ArrayList destinationList) { - PDFLimits limits = new PDFLimits(destinationList); - getDocument().registerObject(limits); + public PDFNameTreeNode makeNameTreeNode() { + PDFNameTreeNode node = new PDFNameTreeNode(); + getDocument().registerObject(node); + return node; + } + + /* ========================= links ===================================== */ + // Some of the "yoffset-only" functions in this part are obsolete and can + // possibly be removed or deprecated. Some are still called by PDFGraphics2D + // (although that could be changed, they don't need the yOffset param anyway). - return limits; + /** + * Create a PDF link to an existing PDFAction object + * + * @param rect the hotspot position in absolute coordinates + * @param pdfAction the PDFAction that this link refers to + * @return the new PDFLink object, or null if either rect or pdfAction is null + */ + public PDFLink makeLink(Rectangle2D rect, PDFAction pdfAction) { + if (rect == null || pdfAction == null) { + return null; + } else { + PDFLink link = new PDFLink(rect); + link.setAction(pdfAction); + getDocument().registerObject(link); + return link; + // does findLink make sense? I mean, how often will it happen that several + // links have the same target *and* the same hot rect? And findLink has to + // walk and compare the entire link list everytime you call it... + } } - /* ========================= links ===================================== */ - /** * Make an internal link. * @@ -898,32 +956,10 @@ public class PDFFactory { int linkType, float yoffset) { //PDFLink linkObject; - int index; - PDFLink link = new PDFLink(rect); if (linkType == PDFLink.EXTERNAL) { - // check destination - if (destination.startsWith("http://")) { - PDFUri uri = new PDFUri(destination); - link.setAction(uri); - } else if (destination.endsWith(".pdf")) { // FileSpec - PDFGoToRemote remote = getGoToPDFAction(destination, null, -1); - link.setAction(remote); - } else if ((index = destination.indexOf(".pdf#page=")) > 0) { - //String file = destination.substring(0, index + 4); - int page = Integer.parseInt(destination.substring(index + 10)); - PDFGoToRemote remote = getGoToPDFAction(destination, null, page); - link.setAction(remote); - } else if ((index = destination.indexOf(".pdf#dest=")) > 0) { - //String file = destination.substring(0, index + 4); - String dest = destination.substring(index + 10); - PDFGoToRemote remote = getGoToPDFAction(destination, dest, -1); - link.setAction(remote); - } else { // URI - PDFUri uri = new PDFUri(destination); - link.setAction(uri); - } + link.setAction(getExternalAction(destination)); } else { // linkType is internal String goToReference = getGoToReference(destination, yoffset); @@ -941,11 +977,62 @@ public class PDFFactory { return link; } - public String getGoToReference(String destination, float yoffset) { + /** + * Create/find and return the appropriate external PDFAction according to the target + * + * @param target The external target. This may be a PDF file name + * (optionally with internal page number or destination) or any type of URI. + * @return the PDFAction thus created or found + */ + public PDFAction getExternalAction(String target) { + int index; + String targetLo = target.toLowerCase(); + // HTTP URL? + if (targetLo.startsWith("http://")) { + return new PDFUri(target); + // Bare PDF file name? + } else if (targetLo.endsWith(".pdf")) { + return getGoToPDFAction(target, null, -1); + // PDF file + page? + } else if ((index = targetLo.indexOf(".pdf#page=")) > 0) { + String filename = target.substring(0, index + 4); + int page = Integer.parseInt(target.substring(index + 10)); + return getGoToPDFAction(filename, null, page); + // PDF file + destination? + } else if ((index = targetLo.indexOf(".pdf#dest=")) > 0) { + String filename = target.substring(0, index + 4); + String dest = target.substring(index + 10); + return getGoToPDFAction(filename, dest, -1); + // None of the above? Default to URI: + } else { + return new PDFUri(target); + } + } + + /** + * Create or find a PDF GoTo with the given page reference string and Y offset, + * and return its PDF object reference + * + * @param pdfPageRef the PDF page reference, e.g. "23 0 R" + * @param yoffset the distance from the bottom of the page in points + * @return the GoTo's object reference + */ + public String getGoToReference(String pdfPageRef, float yoffset) { + return getPDFGoTo(pdfPageRef, new Point2D.Float(0.0f, yoffset)).referencePDF(); + } + + /** + * Finds and returns a PDFGoTo to the given page and position. + * Creates the PDFGoTo if not found. + * + * @param pdfPageRef the PDF page reference + * @param position the (X,Y) position in points + * + * @return the new or existing PDFGoTo object + */ + public PDFGoTo getPDFGoTo(String pdfPageRef, Point2D position) { getDocument().getProfile().verifyActionAllowed(); - String goToReference = null; - PDFGoTo gt = new PDFGoTo(destination); - gt.setYPosition(yoffset); + PDFGoTo gt = new PDFGoTo(pdfPageRef, position); PDFGoTo oldgt = getDocument().findGoTo(gt); if (oldgt == null) { getDocument().assignObjectNumber(gt); @@ -953,9 +1040,7 @@ public class PDFFactory { } else { gt = oldgt; } - - goToReference = gt.referencePDF(); - return goToReference; + return gt; } /** @@ -997,6 +1082,42 @@ public class PDFFactory { } /** + * Make an outline object and add it to the given parent + * + * @param parent the parent PDFOutline object (may be null) + * @param label the title for the new outline object + * @param actionRef the action reference string to be placed after the /A + * @param showSubItems whether to initially display child outline items + * @return the new PDF outline object + */ + public PDFOutline makeOutline(PDFOutline parent, String label, + String actionRef, boolean showSubItems) { + PDFOutline pdfOutline = new PDFOutline(label, actionRef, showSubItems); + if (parent != null) { + parent.addOutline(pdfOutline); + } + getDocument().registerObject(pdfOutline); + return pdfOutline; + } + + /** + * Make an outline object and add it to the given parent + * + * @param parent the parent PDFOutline object (may be null) + * @param label the title for the new outline object + * @param pdfAction the action that this outline item points to - must not be null! + * @param showSubItems whether to initially display child outline items + * @return the new PDFOutline object, or null if pdfAction is null + */ + public PDFOutline makeOutline(PDFOutline parent, String label, + PDFAction pdfAction, boolean showSubItems) { + return pdfAction == null + ? null + : makeOutline(parent, label, pdfAction.getAction(), showSubItems); + } + + // This one is obsolete now, at least it isn't called from anywhere inside FOP + /** * Make an outline object and add it to the given outline * * @param parent parent PDFOutline object which may be null @@ -1011,17 +1132,10 @@ public class PDFFactory { boolean showSubItems) { String goToRef = getGoToReference(destination, yoffset); - PDFOutline obj = new PDFOutline(label, goToRef, showSubItems); - - if (parent != null) { - parent.addOutline(obj); - } - getDocument().registerObject(obj); - return obj; + return makeOutline(parent, label, goToRef, showSubItems); } - /* ========================= fonts ===================================== */ /** @@ -1077,15 +1191,12 @@ public class PDFFactory { * cmap.addContents(); * this.objects.add(cmap); */ - font = - (PDFFontNonBase14)PDFFont.createFont(fontname, fonttype, - basefont, - "Identity-H"); + font = (PDFFontNonBase14)PDFFont.createFont(fontname, fonttype, + basefont, "Identity-H"); } else { - font = - (PDFFontNonBase14)PDFFont.createFont(fontname, fonttype, - basefont, encoding); + font = (PDFFontNonBase14)PDFFont.createFont(fontname, fonttype, + basefont, encoding); } getDocument().registerObject(font); @@ -1098,12 +1209,12 @@ public class PDFFactory { } else { cidMetrics = (CIDFont)metrics; } - PDFCIDSystemInfo sysInfo = - new PDFCIDSystemInfo(cidMetrics.getRegistry(), + PDFCIDSystemInfo sysInfo + = new PDFCIDSystemInfo(cidMetrics.getRegistry(), cidMetrics.getOrdering(), cidMetrics.getSupplement()); - PDFCIDFont cidFont = - new PDFCIDFont(basefont, + PDFCIDFont cidFont + = new PDFCIDFont(basefont, cidMetrics.getCIDType(), cidMetrics.getDefaultWidth(), getSubsetWidths(cidMetrics), sysInfo, diff --git a/src/java/org/apache/fop/pdf/PDFGoTo.java b/src/java/org/apache/fop/pdf/PDFGoTo.java index a493a9f32..16eaf73ce 100644 --- a/src/java/org/apache/fop/pdf/PDFGoTo.java +++ b/src/java/org/apache/fop/pdf/PDFGoTo.java @@ -19,6 +19,8 @@ package org.apache.fop.pdf; +import java.awt.geom.Point2D; + /** * class representing a /GoTo object. * This can either have a Goto to a page reference and location @@ -47,6 +49,20 @@ public class PDFGoTo extends PDFAction { } /** + * create a /GoTo object. + * + * @param pageReference the PDF reference to the target page + * @param position the target area's on-page coordinates in points + */ + public PDFGoTo(String pageReference, Point2D position) { + /* generic creation of object */ + super(); + + this.pageReference = pageReference; + setPosition(position); + } + + /** * Sets page reference after object has been created * * @param pageReference the new page reference to use @@ -56,6 +72,25 @@ public class PDFGoTo extends PDFAction { } /** + * Sets the target (X,Y) position + * + * @param position the target's on-page coordinates in points + */ + public void setPosition(Point2D position) { + this.xPosition = (float) position.getX(); + this.yPosition = (float) position.getY(); + } + + /** + * Sets the x Position to jump to + * + * @param xPosition x position + */ + public void setXPosition(float xPosition) { + this.xPosition = xPosition; + } + + /** * Sets the Y position to jump to * * @param yPosition y position @@ -74,15 +109,6 @@ public class PDFGoTo extends PDFAction { } /** - * Sets the x Position to jump to - * - * @param xPosition x position - */ - public void setXPosition(int xPosition) { - this.xPosition = (xPosition / 1000f); - } - - /** * Get the PDF reference for the GoTo action. * * @return the PDF reference for the action diff --git a/src/java/org/apache/fop/pdf/PDFLimits.java b/src/java/org/apache/fop/pdf/PDFLimits.java deleted file mode 100644 index 97c097463..000000000 --- a/src/java/org/apache/fop/pdf/PDFLimits.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.pdf; - -import java.util.ArrayList; - -import org.apache.fop.pdf.PDFDestination; - -/** - * class representing a Limits object (part of the names dictionary for named destinations) - */ -public class PDFLimits extends PDFObject { - - private ArrayList destinationList; - - /** - * create a named destination - */ - public PDFLimits(ArrayList destinationList) { - /* generic creation of PDF object */ - super(); - this.destinationList = destinationList; - } - - /** - * @see org.apache.fop.pdf.PDFObject#toPDFString() - */ - public String toPDFString() { - String[] idRefs = new String[destinationList.size()]; - String kidsString = ""; - for (int i = 0; i < destinationList.size(); i++) { - PDFDestination dest = (PDFDestination)destinationList.get(i); - idRefs[i] = dest.getIDRef(); - kidsString += dest.referencePDF(); - if (!(i == destinationList.size() - 1)) { - kidsString += " "; - } - } - String s = getObjectID() - + "<<\n" - + "/Limits [(" + idRefs[0] + ") (" + idRefs[destinationList.size() - 1] + ")]\n" - + "/Kids [" + kidsString + "]" - + "\n>>\nendobj\n"; - return s; - } - - /* - * example: - * - * 260 0 obj - * << - * /Limits [(Annotate) (thumbnails)] - * /Kids [248 0 R 253 0 R 254 0 R 259 0 R] - * >> - * endobj - */ - - /** - * Check if this equals another object. - * - * @param obj the object to compare - * @return true if this equals other object - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || !(obj instanceof PDFLimits)) { - return false; - } - - return true; - } -} - diff --git a/src/java/org/apache/fop/pdf/PDFNameTreeNode.java b/src/java/org/apache/fop/pdf/PDFNameTreeNode.java new file mode 100644 index 000000000..476cfc7a2 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFNameTreeNode.java @@ -0,0 +1,121 @@ +/* + * 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.pdf; + +/** + * Class representing a PDF name tree node. + */ +public class PDFNameTreeNode extends PDFDictionary { + + private static final String KIDS = "Kids"; + private static final String NAMES = "Names"; + private static final String LIMITS = "Limits"; + + /** + * create a named destination + */ + public PDFNameTreeNode() { + /* generic creation of PDF object */ + super(); + } + + /** + * Sets the Kids array. + * @param kids the Kids array + */ + public void setKids(PDFArray kids) { + put(KIDS, kids); + } + + /** + * Returns the Kids array. + * @return the Kids array + */ + public PDFArray getKids() { + return (PDFArray)get(KIDS); + } + + /** + * Sets the Names array. + * @param names the Names array + */ + public void setNames(PDFArray names) { + put(NAMES, names); + } + + /** + * Returns the Names array. + * @return the Names array + */ + public PDFArray getNames() { + return (PDFArray)get(NAMES); + } + + /** + * Sets the lower limit value of the Limits array. + * @param key the lower limit value + */ + public void setLowerLimit(String key) { + PDFArray limits = prepareLimitsArray(); + limits.set(0, key); + } + + /** + * Returns the lower limit value of the Limits array. + * @return the lower limit value + */ + public String getLowerLimit() { + PDFArray limits = prepareLimitsArray(); + return (String)limits.get(0); + } + + /** + * Sets the upper limit value of the Limits array. + * @param key the upper limit value + */ + public void setUpperLimit(String key) { + PDFArray limits = prepareLimitsArray(); + limits.set(1, key); + } + + /** + * Returns the upper limit value of the Limits array. + * @return the upper limit value + */ + public String getUpperLimit() { + PDFArray limits = prepareLimitsArray(); + return (String)limits.get(1); + } + + + private PDFArray prepareLimitsArray() { + PDFArray limits = (PDFArray)get(LIMITS); + if (limits == null) { + limits = new PDFArray(new Object[2]); + put(LIMITS, limits); + } + if (limits.length() != 2) { + throw new IllegalStateException("Limits array must have 2 entries"); + } + return limits; + } + +} + diff --git a/src/java/org/apache/fop/pdf/PDFNames.java b/src/java/org/apache/fop/pdf/PDFNames.java new file mode 100644 index 000000000..d2d895771 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFNames.java @@ -0,0 +1,51 @@ +/* + * 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.pdf; + +/** + * Class representing a PDF Names object + */ +public class PDFNames extends PDFDictionary { + + /** + * Create the Names object + */ + public PDFNames() { + /* generic creation of PDF object */ + super(); + } + + /** + * Returns the Dests object + * @return the Dests object, or null if it's not used + */ + public PDFDests getDests() { + return (PDFDests)get("Dests"); + } + + /** + * Set the Dests object + * @param dests the Dests object + */ + public void setDests(PDFDests dests) { + put("Dests", dests); + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFObject.java b/src/java/org/apache/fop/pdf/PDFObject.java index b1e8e1245..544a30cc4 100644 --- a/src/java/org/apache/fop/pdf/PDFObject.java +++ b/src/java/org/apache/fop/pdf/PDFObject.java @@ -37,7 +37,7 @@ import org.apache.commons.logging.LogFactory; * Object has a number and a generation (although the generation will always * be 0 in new documents). */ -public abstract class PDFObject { +public abstract class PDFObject implements PDFWritable { /** logger for all PDFObjects (and descendants) */ protected static Log log = LogFactory.getLog(PDFObject.class.getName()); @@ -138,11 +138,23 @@ public abstract class PDFObject { * @return the reference string */ public String referencePDF() { + if (!hasObjectNumber()) { + throw new IllegalArgumentException( + "Cannot reference this object. It doesn't have an object number"); + } String ref = getObjectNumber() + " " + getGeneration() + " R"; return ref; } /** + * Creates and returns a reference to this object. + * @return the object reference + */ + public PDFReference makeReference() { + return new PDFReference(this); + } + + /** * Write the PDF represention of this object * * @param stream the stream to write the PDF to @@ -178,6 +190,19 @@ public abstract class PDFObject { + "Use output(OutputStream) instead."); } + /** + * Returns a representation of this object for in-object placement, i.e. if the object + * has an object number its reference is returned. Otherwise, its PDF representation is + * returned. + * @return the String representation + */ + public String toInlinePDFString() { + if (hasObjectNumber()) { + return referencePDF(); + } else { + return toPDFString(); + } + } /** * Converts text to a byte array for writing to a PDF file. @@ -220,6 +245,21 @@ public abstract class PDFObject { }*/ } + /** + * Formats an object for serialization to PDF. + * @param obj the object + * @param sb the StringBuffer to write to + */ + protected void formatObject(Object obj, StringBuffer sb) { + if (obj instanceof PDFWritable) { + sb.append(((PDFWritable)obj).toInlinePDFString()); + } else if (obj instanceof Number) { + sb.append(obj); + } else { + sb.append("(").append(obj).append(")"); + } + } + /** Formatting pattern for PDF date */ protected static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("'D:'yyyyMMddHHmmss"); diff --git a/src/java/org/apache/fop/pdf/PDFRectangle.java b/src/java/org/apache/fop/pdf/PDFRectangle.java index def6d9a2b..e84227adf 100644 --- a/src/java/org/apache/fop/pdf/PDFRectangle.java +++ b/src/java/org/apache/fop/pdf/PDFRectangle.java @@ -79,7 +79,7 @@ public class PDFRectangle { * @return the PDF */ public byte[] toPDF() { - return toPDFString().getBytes(); + return PDFDocument.encode(toPDFString()); } /** diff --git a/src/java/org/apache/fop/pdf/PDFReference.java b/src/java/org/apache/fop/pdf/PDFReference.java new file mode 100644 index 000000000..4ca50a79f --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFReference.java @@ -0,0 +1,67 @@ +/* + * 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.pdf; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; + +/** + * Class representing a PDF object reference. The object holds a soft reference to the actual + * PDF object so the garbage collector can free the object if it's not referenced elsewhere. The + * important thing about the class is the reference information to the actual PDF object in the + * PDF file. + */ +public class PDFReference implements PDFWritable { + + private String indirectReference; + + private Reference objReference; + + /** + * Creates a new PDF reference. + * @param obj the object to be referenced + */ + public PDFReference(PDFObject obj) { + this.indirectReference = obj.referencePDF(); + this.objReference = new SoftReference(obj); + } + + /** + * Returns the PDF object + * @return the PDF object, or null if it has been released + */ + public PDFObject getObject() { + if (this.objReference != null) { + PDFObject obj = (PDFObject)this.objReference.get(); + if (obj == null) { + this.objReference = null; + } + return obj; + } else { + return null; + } + } + + /** @see org.apache.fop.pdf.PDFWritable#toInlinePDFString() */ + public String toInlinePDFString() { + return this.indirectReference; + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFRoot.java b/src/java/org/apache/fop/pdf/PDFRoot.java index 60fe6b390..4a80de4c9 100644 --- a/src/java/org/apache/fop/pdf/PDFRoot.java +++ b/src/java/org/apache/fop/pdf/PDFRoot.java @@ -20,7 +20,6 @@ package org.apache.fop.pdf; import java.util.List; -import java.util.ArrayList; /** * class representing a Root (/Catalog) object @@ -63,11 +62,8 @@ public class PDFRoot extends PDFObject { /** The array of OutputIntents */ private List outputIntents; - /** - * The referencePDF value of the /Dests object, - * if this PDF has a Name Dictionary - */ - private String namesReferencePDF = null; + /** the /Dests object, if this PDF has a Names Dictionary */ + private PDFNames names; private int pageMode = PAGEMODE_USENONE; @@ -133,12 +129,20 @@ public class PDFRoot extends PDFObject { } /** - * Set the optional Metadata object. - * @param meta the Metadata object - * @since PDF 1.4 + * Set the Names object. + * @param names the Names object + * @since PDF 1.2 + */ + public void setNames(PDFNames names) { + this.names = names; + } + + /** + * @return the Names object if set, null otherwise. + * @since PDF 1.2 */ - public void setNames(String referencePDF) { - this.namesReferencePDF = referencePDF; + public PDFNames getNames() { + return this.names; } /** @@ -175,7 +179,7 @@ public class PDFRoot extends PDFObject { public String toPDFString() { StringBuffer p = new StringBuffer(128); p.append(getObjectID()); - p.append("<< /Type /Catalog\n/Pages " + p.append("<< /Type /Catalog\n /Pages " + this.rootPages.referencePDF() + "\n"); if (outline != null) { @@ -197,17 +201,17 @@ public class PDFRoot extends PDFObject { break; } } - if (getDocumentSafely().getHasDestinations() && namesReferencePDF != null) { - p.append(" /Names " + namesReferencePDF + "\n"); + if (getDocumentSafely().hasDestinations() && getNames() != null) { + p.append(" /Names " + getNames().referencePDF() + "\n"); } if (getMetadata() != null && getDocumentSafely().getPDFVersion() >= PDFDocument.PDF_VERSION_1_4) { - p.append("/Metadata " + getMetadata().referencePDF() + "\n"); + p.append(" /Metadata " + getMetadata().referencePDF() + "\n"); } if (this.outputIntents != null && this.outputIntents.size() > 0 && getDocumentSafely().getPDFVersion() >= PDFDocument.PDF_VERSION_1_4) { - p.append("/OutputIntents ["); + p.append(" /OutputIntents ["); for (int i = 0, c = this.outputIntents.size(); i < c; i++) { PDFOutputIntent outputIntent = (PDFOutputIntent)this.outputIntents.get(i); if (i > 0) { diff --git a/src/java/org/apache/fop/pdf/PDFState.java b/src/java/org/apache/fop/pdf/PDFState.java index e0a2eae1a..c01aab52e 100644 --- a/src/java/org/apache/fop/pdf/PDFState.java +++ b/src/java/org/apache/fop/pdf/PDFState.java @@ -301,7 +301,22 @@ public class PDFState { } /** - * Get the grapics state. + * Get a copy of the base transform for the page. Used to translate + * IPP/BPP values into X,Y positions when positioning is "fixed". + * + * @return the base transform, or null if the state stack is empty + */ + public AffineTransform getBaseTransform() { + if (stateStack.size() == 0) { + return null; + } else { + Data baseData = (Data) stateStack.get(0); + return (AffineTransform) baseData.transform.clone(); + } + } + + /** + * Get the graphics state. * This gets the combination of all graphic states for * the current context. * This is the graphic state set with the gs operator not diff --git a/src/java/org/apache/fop/pdf/PDFStream.java b/src/java/org/apache/fop/pdf/PDFStream.java index ea7748576..2cc9a1b0a 100644 --- a/src/java/org/apache/fop/pdf/PDFStream.java +++ b/src/java/org/apache/fop/pdf/PDFStream.java @@ -57,7 +57,7 @@ public class PDFStream extends AbstractPDFStream { */ public void add(String s) { try { - data.getOutputStream().write(s.getBytes()); + data.getOutputStream().write(PDFDocument.encode(s)); } catch (IOException ex) { //TODO throw the exception and catch it elsewhere ex.printStackTrace(); diff --git a/src/java/org/apache/fop/pdf/PDFWritable.java b/src/java/org/apache/fop/pdf/PDFWritable.java new file mode 100644 index 000000000..23bcf9ad8 --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFWritable.java @@ -0,0 +1,36 @@ +/* + * 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.pdf; + +/** + * This interface is implemented by classes that can be serialized to a PDF file either by + * serializing the object or by writing a indirect reference to the actual object. + */ +public interface PDFWritable { + + /** + * Returns a representation of this object for in-object placement, i.e. if the object + * has an object number its reference is returned. Otherwise, its PDF representation is + * returned. + * @return the String representation + */ + String toInlinePDFString(); + +} diff --git a/src/java/org/apache/fop/render/AbstractRenderer.java b/src/java/org/apache/fop/render/AbstractRenderer.java index 835445603..50c314f15 100644 --- a/src/java/org/apache/fop/render/AbstractRenderer.java +++ b/src/java/org/apache/fop/render/AbstractRenderer.java @@ -165,7 +165,7 @@ public abstract class AbstractRenderer /** * @see org.apache.fop.render.Renderer#processOffDocumentItem(OffDocumentItem) */ - public void processOffDocumentItem(OffDocumentItem oDI) { } + public void processOffDocumentItem(OffDocumentItem odi) { } /** @see org.apache.fop.render.Renderer#getGraphics2DAdapter() */ public Graphics2DAdapter getGraphics2DAdapter() { diff --git a/src/java/org/apache/fop/render/Renderer.java b/src/java/org/apache/fop/render/Renderer.java index 0b763b996..d54a61025 100644 --- a/src/java/org/apache/fop/render/Renderer.java +++ b/src/java/org/apache/fop/render/Renderer.java @@ -113,9 +113,9 @@ public interface Renderer { * document (e.g., PDF bookmarks). Note - not all renderers will process * all off-document items. * - * @param ext The extension element to be rendered + * @param odi The off-document item to be rendered */ - void processOffDocumentItem(OffDocumentItem ext); + void processOffDocumentItem(OffDocumentItem odi); /** * @return the adapter for painting Java2D images (or null if not supported) diff --git a/src/java/org/apache/fop/render/bitmap/MultiFileRenderingUtil.java b/src/java/org/apache/fop/render/bitmap/MultiFileRenderingUtil.java new file mode 100644 index 000000000..ca757fccf --- /dev/null +++ b/src/java/org/apache/fop/render/bitmap/MultiFileRenderingUtil.java @@ -0,0 +1,75 @@ +package org.apache.fop.render.bitmap;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This utility class helps renderers who generate one file per page,
+ * like the PNG renderer.
+ */
+public class MultiFileRenderingUtil {
+
+ /** The file syntax prefix, eg. "page" will output "page1.png" etc */
+ private String filePrefix;
+
+ private String fileExtension;
+
+ /** The output directory where images are to be written */
+ private File outputDir;
+
+ /**
+ * Creates a new instance.
+ * <p>
+ * The file name must not have an extension, or must have extension "png",
+ * and its last period must not be at the start (empty file prefix).
+ * @param ext the extension to be used
+ * @param outputFile the output file or null if there's no such information
+ */
+ public MultiFileRenderingUtil(String ext, File outputFile) {
+ this.fileExtension = ext;
+ // the file provided on the command line
+ if (outputFile == null) {
+ //No filename information available. Only the first page will be rendered.
+ outputDir = null;
+ filePrefix = null;
+ } else {
+ outputDir = outputFile.getParentFile();
+
+ // extracting file name syntax
+ String s = outputFile.getName();
+ int i = s.lastIndexOf(".");
+ if (i > 0) {
+ // Make sure that the file extension was "png"
+ String extension = s.substring(i + 1).toLowerCase();
+ if (!ext.equals(extension)) {
+ throw new IllegalArgumentException("Invalid file extension ('"
+ + extension + "') specified");
+ }
+ } else if (i == -1) {
+ i = s.length();
+ } else { // i == 0
+ throw new IllegalArgumentException("Invalid file name ('"
+ + s + "') specified");
+ }
+ if (s.charAt(i - 1) == '1') {
+ i--; // getting rid of the "1"
+ }
+ filePrefix = s.substring(0, i);
+ }
+ }
+
+ public OutputStream createOutputStream(int pageNumber) throws IOException {
+ if (filePrefix == null) {
+ return null;
+ } else {
+ File f = new File(outputDir,
+ filePrefix + (pageNumber + 1) + "." + fileExtension);
+ OutputStream os = new BufferedOutputStream(new FileOutputStream(f));
+ return os;
+ }
+ }
+
+}
diff --git a/src/java/org/apache/fop/render/bitmap/PNGRenderer.java b/src/java/org/apache/fop/render/bitmap/PNGRenderer.java index 19573e4d9..67bee3a8b 100644 --- a/src/java/org/apache/fop/render/bitmap/PNGRenderer.java +++ b/src/java/org/apache/fop/render/bitmap/PNGRenderer.java @@ -20,10 +20,6 @@ package org.apache.fop.render.bitmap; import java.awt.image.RenderedImage; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -33,7 +29,6 @@ import org.apache.xmlgraphics.image.writer.ImageWriterRegistry; import org.apache.commons.io.IOUtils; -import org.apache.fop.apps.FOPException; import org.apache.fop.apps.MimeConstants; import org.apache.fop.area.PageViewport; import org.apache.fop.render.java2d.Java2DRenderer; @@ -51,14 +46,11 @@ public class PNGRenderer extends Java2DRenderer { /** The file extension expected for PNG files */ private static final String PNG_FILE_EXTENSION = "png"; - /** The file syntax prefix, eg. "page" will output "page1.png" etc */ - private String filePrefix; - - /** The output directory where images are to be written */ - private File outputDir; - /** The OutputStream for the first Image */ private OutputStream firstOutputStream; + + /** Helper class for generating multiple files */ + private MultiFileRenderingUtil multiFileUtil; /** @see org.apache.fop.render.AbstractRenderer */ public String getMimeType() { @@ -68,54 +60,11 @@ public class PNGRenderer extends Java2DRenderer { /** @see org.apache.fop.render.Renderer#startRenderer(java.io.OutputStream) */ public void startRenderer(OutputStream outputStream) throws IOException { log.info("rendering areas to PNG"); - setOutputDirectory(); + multiFileUtil = new MultiFileRenderingUtil(PNG_FILE_EXTENSION, + getUserAgent().getOutputFile()); this.firstOutputStream = outputStream; } - /** - * Sets the output directory, either from the outfile specified on the - * command line, or from the directory specified in configuration file. - * Also sets the file name syntax, eg. "page". - * The file name must not have an extension, or must have extension "png", - * and its last period must not be at the start (empty file prefix). - * - * @throws IOException if an invalid output file name was specified - * (e.g., with an extension other than '.png') - */ - private void setOutputDirectory() throws IOException { - - // the file provided on the command line - File f = getUserAgent().getOutputFile(); - if (f == null) { - //No filename information available. Only the first page will be rendered. - outputDir = null; - filePrefix = null; - } else { - outputDir = f.getParentFile(); - - // extracting file name syntax - String s = f.getName(); - int i = s.lastIndexOf("."); - if (i > 0) { - // Make sure that the file extension was "png" - String extension = s.substring(i + 1).toLowerCase(); - if (!PNG_FILE_EXTENSION.equals(extension)) { - throw new IOException("Invalid file extension ('" - + extension + "') specified"); - } - } else if (i == -1) { - i = s.length(); - } else { // i == 0 - throw new IOException("Invalid file name ('" - + s + "') specified"); - } - if (s.charAt(i - 1) == '1') { - i--; // getting rid of the "1" - } - filePrefix = s.substring(0, i); - } - } - /** @see org.apache.fop.render.Renderer#stopRenderer() */ public void stopRenderer() throws IOException { @@ -131,23 +80,14 @@ public class PNGRenderer extends Java2DRenderer { } try { // Do the rendering: get the image for this page - RenderedImage image = (RenderedImage) getPageImage((PageViewport) pageViewportList - .get(i)); + PageViewport pv = (PageViewport)pageViewportList.get(i); + RenderedImage image = (RenderedImage)getPageImage(pv); // Encode this image - log.debug("Encoding page " + (i + 1)); - ImageWriterParams params = new ImageWriterParams(); - params.setResolution(Math.round(userAgent.getTargetResolution())); - - // Encode PNG image - ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(getMimeType()); - if (writer == null) { - throw new IOException("Could not get an ImageWriter to produce " - + getMimeType() + ". The most likely explanation for this is a class" - + " loading problem."); + if (log.isDebugEnabled()) { + log.debug("Encoding page " + (i + 1)); } - log.debug("Writing image using " + writer.getClass().getName()); - writer.writeImage(image, os, params); + writeImage(os, image); } finally { //Only close self-created OutputStreams if (os != firstOutputStream) { @@ -157,29 +97,36 @@ public class PNGRenderer extends Java2DRenderer { } } + private void writeImage(OutputStream os, RenderedImage image) throws IOException { + ImageWriterParams params = new ImageWriterParams(); + params.setResolution(Math.round(userAgent.getTargetResolution())); + + // Encode PNG image + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(getMimeType()); + if (writer == null) { + throw new IOException("Could not get an ImageWriter to produce " + + getMimeType() + ". The most likely explanation for this is a class" + + " loading problem."); + } + if (log.isDebugEnabled()) { + log.debug("Writing image using " + writer.getClass().getName()); + } + writer.writeImage(image, os, params); + } + /** - * Builds the OutputStream corresponding to this page - * @param 0-based pageNumber + * Returns the OutputStream corresponding to this page + * @param pageNumber 0-based page number * @return the corresponding OutputStream + * @throws IOException In case of an I/O error */ - private OutputStream getCurrentOutputStream(int pageNumber) { + protected OutputStream getCurrentOutputStream(int pageNumber) throws IOException { if (pageNumber == 0) { return firstOutputStream; - } - - if (filePrefix == null) { - return null; } else { - File f = new File(outputDir, - filePrefix + (pageNumber + 1) + "." + PNG_FILE_EXTENSION); - try { - OutputStream os = new BufferedOutputStream(new FileOutputStream(f)); - return os; - } catch (FileNotFoundException e) { - new FOPException("Can't build the OutputStream\n" + e); - return null; - } + return multiFileUtil.createOutputStream(pageNumber); } + } } diff --git a/src/java/org/apache/fop/render/pdf/PDFRenderer.java b/src/java/org/apache/fop/render/pdf/PDFRenderer.java index f64fa75aa..7d584c036 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRenderer.java +++ b/src/java/org/apache/fop/render/pdf/PDFRenderer.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.awt.geom.Point2D; import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.color.ICC_Profile; @@ -48,6 +49,8 @@ import org.apache.commons.io.IOUtils; import org.apache.fop.apps.FOPException; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.MimeConstants; +import org.apache.fop.area.Area; +import org.apache.fop.area.Block; import org.apache.fop.area.CTM; import org.apache.fop.area.LineArea; import org.apache.fop.area.OffDocumentExtensionAttachment; @@ -60,6 +63,7 @@ import org.apache.fop.area.inline.AbstractTextArea; import org.apache.fop.area.inline.TextArea; import org.apache.fop.area.inline.Image; import org.apache.fop.area.inline.Leader; +import org.apache.fop.area.inline.InlineArea; import org.apache.fop.area.inline.InlineParent; import org.apache.fop.area.inline.WordArea; import org.apache.fop.area.inline.SpaceArea; @@ -69,17 +73,20 @@ import org.apache.fop.fonts.FontSetup; import org.apache.fop.image.FopImage; import org.apache.fop.image.ImageFactory; import org.apache.fop.image.XMLImage; +import org.apache.fop.pdf.PDFAction; import org.apache.fop.pdf.PDFAMode; import org.apache.fop.pdf.PDFAnnotList; import org.apache.fop.pdf.PDFColor; import org.apache.fop.pdf.PDFConformanceException; -import org.apache.fop.pdf.PDFDestination; import org.apache.fop.pdf.PDFDocument; import org.apache.fop.pdf.PDFEncryptionManager; import org.apache.fop.pdf.PDFEncryptionParams; +import org.apache.fop.pdf.PDFFactory; import org.apache.fop.pdf.PDFFilterList; +import org.apache.fop.pdf.PDFGoTo; import org.apache.fop.pdf.PDFICCBasedColorSpace; import org.apache.fop.pdf.PDFICCStream; +import org.apache.fop.pdf.PDFGoTo; import org.apache.fop.pdf.PDFInfo; import org.apache.fop.pdf.PDFLink; import org.apache.fop.pdf.PDFMetadata; @@ -164,16 +171,35 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { protected Map pages = null; /** - * Page references are stored using the PageViewport as the key - * when a reference is made the PageViewport is used - * for pdf this means we need the pdf page reference + * Maps unique PageViewport key to PDF page reference */ protected Map pageReferences = new java.util.HashMap(); - /** Page viewport references */ + /** + * Maps unique PageViewport key back to PageViewport itself + */ protected Map pvReferences = new java.util.HashMap(); /** + * Maps XSL-FO element IDs to their on-page XY-positions + * Must be used in conjunction with the page reference to fully specify the PDFGoTo details + */ + protected Map idPositions = new java.util.HashMap(); + + /** + * Maps XSL-FO element IDs to PDFGoTo objects targeting the corresponding areas + * These objects may not all be fully filled in yet + */ + protected Map idGoTos = new java.util.HashMap(); + + /** + * The PDFGoTos in idGoTos that are not complete yet + */ + protected List unfinishedGoTos = new java.util.ArrayList(); + // can't use a Set because PDFGoTo.equals returns true if the target is the same, + // even if the object number differs + + /** * The output stream to write the document to */ protected OutputStream ostream; @@ -197,7 +223,12 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { * the current page to add annotations to */ protected PDFPage currentPage; - + + /** + * the current page's PDF reference string (to avoid numerous function calls) + */ + protected String currentPageRef; + /** the (optional) encryption parameters */ protected PDFEncryptionParams encryptionParams; @@ -479,9 +510,34 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } /** + * Checks if there are any unfinished PDFGoTos left in the list and resolves them + * to a default position on the page. Logs a warning, as this should not happen. + */ + protected void finishOpenGoTos() { + int count = unfinishedGoTos.size(); + if (count > 0) { + // TODO : page height may not be the same for all targeted pages + Point2D.Float defaultPos = new Point2D.Float(0f, pageHeight / 1000f); // top-o-page + while (!unfinishedGoTos.isEmpty()) { + PDFGoTo gt = (PDFGoTo) unfinishedGoTos.get(0); + finishIDGoTo(gt, defaultPos); + } + boolean one = count == 1; + String pl = one ? "" : "s"; + String ww = one ? "was" : "were"; + String ia = one ? "is" : "are"; + log.warn("" + count + " link target" + pl + " could not be fully resolved and " + + ww + " now point to the top of the page or " + + ia + " dysfunctional."); // dysfunctional if pageref is null + } + } + + /** * @see org.apache.fop.render.Renderer#stopRenderer() */ public void stopRenderer() throws IOException { + finishOpenGoTos(); + pdfDoc.getResources().addFonts(pdfDoc, fontInfo); pdfDoc.outputTrailer(ostream); @@ -498,6 +554,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { currentPage = null; currentState = null; currentFontName = ""; + + idPositions.clear(); + idGoTos.clear(); } /** @@ -512,19 +571,11 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { * @see org.apache.fop.render.Renderer#processOffDocumentItem(OffDocumentItem) */ public void processOffDocumentItem(OffDocumentItem odi) { - // render Destinations if (odi instanceof DestinationData) { - PDFDestination destination = pdfDoc.getFactory().makeDestination((DestinationData) odi); - PageViewport pv = destination.getPageViewport(); - String dest = (String)pageReferences.get(pv.getKey()); - Rectangle2D bounds = pv.getViewArea(); - double h = bounds.getHeight(); - float yoffset = (float)h / 1000f; - String gtRef = pdfDoc.getFactory().getGoToReference(dest, yoffset); - destination.setGoToReference(gtRef); - } - // render Bookmark-Tree - else if (odi instanceof BookmarkData) { + // render Destinations + renderDestination((DestinationData) odi); + } else if (odi instanceof BookmarkData) { + // render Bookmark-Tree renderBookmarkTree((BookmarkData) odi); } else if (odi instanceof OffDocumentExtensionAttachment) { ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)odi).getAttachment(); @@ -534,6 +585,21 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } } + private void renderDestination(DestinationData dd) { + String targetID = dd.getIDRef(); + if (targetID != null && targetID.length() > 0) { + PageViewport pv = dd.getPageViewport(); + if (pv == null) { + log.warn("Unresolved destination item received: " + dd.getIDRef()); + } + PDFGoTo gt = getPDFGoToForID(targetID, pv.getKey()); + pdfDoc.getFactory().makeDestination( + dd.getIDRef(), gt.makeReference()); + } else { + log.warn("DestinationData item with null or empty IDRef received."); + } + } + /** * Renders a Bookmark-Tree object * @param bookmarks the BookmarkData object containing all the Bookmark-Items @@ -545,36 +611,34 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } } - private void renderBookmarkItem(BookmarkData bookmarkItem, - PDFOutline parentBookmarkItem) { + private void renderBookmarkItem(BookmarkData bookmarkItem, + PDFOutline parentBookmarkItem) { PDFOutline pdfOutline = null; - PageViewport pv = bookmarkItem.getPageViewport(); - if (pv != null) { - Rectangle2D bounds = pv.getViewArea(); - double h = bounds.getHeight(); - float yoffset = (float)h / 1000f; - String intDest = (String)pageReferences.get(pv.getKey()); - if (parentBookmarkItem == null) { - PDFOutline outlineRoot = pdfDoc.getOutlineRoot(); - pdfOutline = pdfDoc.getFactory().makeOutline(outlineRoot, - bookmarkItem.getBookmarkTitle(), - intDest, yoffset, - bookmarkItem.showChildItems()); + + String targetID = bookmarkItem.getIDRef(); + if (targetID != null && targetID.length() > 0) { + PageViewport pv = bookmarkItem.getPageViewport(); + if (pv != null) { + String pvKey = pv.getKey(); + PDFGoTo gt = getPDFGoToForID(targetID, pvKey); + // create outline object: + PDFOutline parent = parentBookmarkItem != null + ? parentBookmarkItem + : pdfDoc.getOutlineRoot(); + pdfOutline = pdfDoc.getFactory().makeOutline(parent, + bookmarkItem.getBookmarkTitle(), gt, bookmarkItem.showChildItems()); } else { - pdfOutline = pdfDoc.getFactory().makeOutline(parentBookmarkItem, - bookmarkItem.getBookmarkTitle(), - intDest, yoffset, - bookmarkItem.showChildItems()); + log.warn("Bookmark with IDRef \"" + targetID + "\" has a null PageViewport."); } } else { - log.warn("Unresolved bookmark item received: " + bookmarkItem.getIDRef()); + log.warn("Bookmark item with null or empty IDRef received."); } for (int i = 0; i < bookmarkItem.getCount(); i++) { renderBookmarkItem(bookmarkItem.getSubData(i), pdfOutline); } } - + private void renderXMPMetadata(XMPMetadata metadata) { Metadata docXMP = metadata.getMetadata(); Metadata fopXMP = PDFMetadata.createXMPFromUserAgent(pdfDoc); @@ -707,6 +771,8 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } else { setupPage(page); } + currentPageRef = currentPage.referencePDF(); + Rectangle2D bounds = page.getViewArea(); double h = bounds.getHeight(); pageHeight = (int) h; @@ -1089,6 +1155,188 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } /** + * Returns area's id if it is the first area in the document with that id + * (i.e. if the area qualifies as a link target). + * Otherwise, or if the area has no id, null is returned. + * + * NOTE : area must be on currentPageViewport, otherwise result may be wrong! + * + * @param area the area for which to return the id + */ + protected String getTargetableID(Area area) { + String id = (String) area.getTrait(Trait.PROD_ID); + if (id == null || id.length() == 0 + || !currentPageViewport.isFirstWithID(id) + || idPositions.containsKey(id)) { + return null; + } else { + return id; + } + } + + /** + * Set XY position in the PDFGoTo and add it to the PDF trailer. + * + * @param gt the PDFGoTo object + * @param position the X,Y position to set + */ + protected void finishIDGoTo(PDFGoTo gt, Point2D.Float position) { + gt.setPosition(position); + pdfDoc.addTrailerObject(gt); + unfinishedGoTos.remove(gt); + } + + /** + * Set page reference and XY position in the PDFGoTo and add it to the PDF trailer. + * + * @param gt the PDFGoTo object + * @param pdfPageRef the PDF reference string of the target page object + * @param position the X,Y position to set + */ + protected void finishIDGoTo(PDFGoTo gt, String pdfPageRef, Point2D.Float position) { + gt.setPageReference(pdfPageRef); + finishIDGoTo(gt, position); + } + + /** + * Get a PDFGoTo pointing to the given id. Create one if necessary. + * It is possible that the PDFGoTo is not fully resolved yet. In that case + * it must be completed (and added to the PDF trailer) later. + * + * @param targetID the target id of the PDFGoTo + * @param pvKey the unique key of the target PageViewport + * + * @return the PDFGoTo that was found or created + */ + protected PDFGoTo getPDFGoToForID(String targetID, String pvKey) { + // Already a PDFGoTo present for this target? If not, create. + PDFGoTo gt = (PDFGoTo) idGoTos.get(targetID); + if (gt == null) { + String pdfPageRef = (String) pageReferences.get(pvKey); + Point2D.Float position = (Point2D.Float) idPositions.get(targetID); + // can the GoTo already be fully filled in? + if (pdfPageRef != null && position != null) { + // getPDFGoTo shares PDFGoTo objects as much as possible. + // It also takes care of assignObjectNumber and addTrailerObject. + gt = pdfDoc.getFactory().getPDFGoTo(pdfPageRef, position); + } else { + // Not complete yet, can't use getPDFGoTo: + gt = new PDFGoTo(pdfPageRef); + pdfDoc.assignObjectNumber(gt); + // pdfDoc.addTrailerObject() will be called later, from finishIDGoTo() + unfinishedGoTos.add(gt); + } + idGoTos.put(targetID, gt); + } + return gt; + } + + /** + * Saves id's absolute position on page for later retrieval by PDFGoTos + * + * @param id the id of the area whose position must be saved + * @param pdfPageRef the PDF page reference string + * @param relativeIPP the *relative* IP position in millipoints + * @param relativeBPP the *relative* BP position in millipoints + * @param tf the transformation to apply once the relative positions have been + * converted to points + */ + protected void saveAbsolutePosition(String id, String pdfPageRef, + int relativeIPP, int relativeBPP, AffineTransform tf) { + Point2D.Float position = new Point2D.Float(relativeIPP / 1000f, relativeBPP / 1000f); + tf.transform(position, position); + idPositions.put(id, position); + // is there already a PDFGoTo waiting to be completed? + PDFGoTo gt = (PDFGoTo) idGoTos.get(id); + if (gt != null) { + finishIDGoTo(gt, pdfPageRef, position); + } +/* + // The code below auto-creates a named destination for every id in the document. + // This should probably be controlled by a user-configurable setting, as it may + // make the PDF file grow noticeably. + // *** NOT YET WELL-TESTED ! *** + if (true) { + PDFFactory factory = pdfDoc.getFactory(); + if (gt == null) { + gt = factory.getPDFGoTo(pdfPageRef, position); + idGoTos.put(id, gt); // so others can pick it up too + } + factory.makeDestination(id, gt.referencePDF(), currentPageViewport); + // Note: using currentPageViewport is only correct if the id is indeed on + // the current PageViewport. But even if incorrect, it won't interfere with + // what gets created in the PDF. + // For speedup, we should also create a lookup map id -> PDFDestination + } +*/ + } + + /** + * Saves id's absolute position on page for later retrieval by PDFGoTos, + * using the currently valid transformation and the currently valid PDF page reference + * + * @param id the id of the area whose position must be saved + * @param relativeIPP the *relative* IP position in millipoints + * @param relativeBPP the *relative* BP position in millipoints + */ + protected void saveAbsolutePosition(String id, int relativeIPP, int relativeBPP) { + saveAbsolutePosition(id, currentPageRef, + relativeIPP, relativeBPP, currentState.getTransform()); + } + + /** + * If the given block area is a possible link target, its id + absolute position will + * be saved. The saved position is only correct if this function is called at the very + * start of renderBlock! + * + * @param block the block area in question + */ + protected void saveBlockPosIfTargetable(Block block) { + String id = getTargetableID(block); + if (id != null) { + // FIXME: Like elsewhere in the renderer code, absolute and relative + // directions are happily mixed here. This makes sure that the + // links point to the right location, but it is not correct. + int ipp = block.getXOffset(); + int bpp = block.getYOffset() + block.getSpaceBefore(); + int positioning = block.getPositioning(); + if (!(positioning == Block.FIXED || positioning == Block.ABSOLUTE)) { + ipp += currentIPPosition; + bpp += currentBPPosition; + } + AffineTransform tf = positioning == Block.FIXED + ? currentState.getBaseTransform() + : currentState.getTransform(); + saveAbsolutePosition(id, currentPageRef, ipp, bpp, tf); + } + } + + /** + * If the given inline area is a possible link target, its id + absolute position will + * be saved. The saved position is only correct if this function is called at the very + * start of renderInlineArea! + * + * @param inlineArea the inline area in question + */ + protected void saveInlinePosIfTargetable(InlineArea inlineArea) { + String id = getTargetableID(inlineArea); + if (id != null) { + int extraMarginBefore = 5000; // millipoints + int ipp = currentIPPosition; + int bpp = currentBPPosition + inlineArea.getOffset() - extraMarginBefore; + saveAbsolutePosition(id, ipp, bpp); + } + } + + /** + * @see org.apache.fop.render.AbstractRenderer#renderBlock(Block) + */ + protected void renderBlock(Block block) { + saveBlockPosIfTargetable(block); + super.renderBlock(block); + } + + /** * @see org.apache.fop.render.AbstractRenderer#renderLineArea(LineArea) */ protected void renderLineArea(LineArea line) { @@ -1097,53 +1345,88 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } /** + * @see org.apache.fop.render.AbstractRenderer#renderInlineArea(InlineArea) + */ + protected void renderInlineArea(InlineArea inlineArea) { + saveInlinePosIfTargetable(inlineArea); + super.renderInlineArea(inlineArea); + } + + /** * Render inline parent area. * For pdf this handles the inline parent area traits such as * links, border, background. * @param ip the inline parent area */ public void renderInlineParent(InlineParent ip) { - float start = currentIPPosition / 1000f; - float top = (ip.getOffset() + currentBPPosition) / 1000f; - float width = ip.getIPD() / 1000f; - float height = ip.getBPD() / 1000f; + boolean annotsAllowed = pdfDoc.getProfile().isAnnotationAllowed(); + + // stuff we only need if a link must be created: + Rectangle2D ipRect = null; + PDFFactory factory = null; + PDFAction action = null; + if (annotsAllowed) { + // make sure the rect is determined *before* calling super! + int ipp = currentIPPosition; + int bpp = currentBPPosition + ip.getOffset(); + ipRect = new Rectangle2D.Float(ipp / 1000f, bpp / 1000f, + ip.getIPD() / 1000f, ip.getBPD() / 1000f); + AffineTransform transform = currentState.getTransform(); + ipRect = transform.createTransformedShape(ipRect).getBounds2D(); + + factory = pdfDoc.getFactory(); + } + // render contents super.renderInlineParent(ip); - if (pdfDoc.getProfile().isAnnotationAllowed()) { - // place the link over the top - Object tr = ip.getTrait(Trait.INTERNAL_LINK); - boolean internal = false; - String dest = null; - float yoffset = 0; - if (tr == null) { - dest = (String)ip.getTrait(Trait.EXTERNAL_LINK); + boolean linkTraitFound = false; + + // try INTERNAL_LINK first + Trait.InternalLink intLink = (Trait.InternalLink) ip.getTrait(Trait.INTERNAL_LINK); + if (intLink != null) { + linkTraitFound = true; + String pvKey = intLink.getPVKey(); + String idRef = intLink.getIDRef(); + boolean pvKeyOK = pvKey != null && pvKey.length() > 0; + boolean idRefOK = idRef != null && idRef.length() > 0; + if (pvKeyOK && idRefOK) { + if (annotsAllowed) { + action = getPDFGoToForID(idRef, pvKey); + } + } else if (pvKeyOK) { + log.warn("Internal link trait with PageViewport key " + pvKey + + " contains no ID reference."); + } else if (idRefOK) { + log.warn("Internal link trait with ID reference " + idRef + + " contains no PageViewport key."); } else { - String pvKey = (String)tr; - dest = (String)pageReferences.get(pvKey); - if (dest != null) { - PageViewport pv = (PageViewport)pvReferences.get(pvKey); - Rectangle2D bounds = pv.getViewArea(); - double h = bounds.getHeight(); - yoffset = (float)h / 1000f; - internal = true; + log.warn("Internal link trait received with neither PageViewport key" + + " nor ID reference."); + } + } + + // no INTERNAL_LINK, look for EXTERNAL_LINK + if (!linkTraitFound) { + String extDest = (String) ip.getTrait(Trait.EXTERNAL_LINK); + if (extDest != null && extDest.length() > 0) { + linkTraitFound = true; + if (annotsAllowed) { + action = factory.getExternalAction(extDest); } } - if (dest != null) { - // add link to pdf document - Rectangle2D rect = new Rectangle2D.Float(start, top, width, height); - // transform rect to absolute coords - AffineTransform transform = currentState.getTransform(); - rect = transform.createTransformedShape(rect).getBounds2D(); - - int type = internal ? PDFLink.INTERNAL : PDFLink.EXTERNAL; - PDFLink pdflink = pdfDoc.getFactory().makeLink( - rect, dest, type, yoffset); - currentPage.addAnnotation(pdflink); + } + + // warn if link trait found but not allowed, else create link + if (linkTraitFound) { + if (!annotsAllowed) { + log.warn("Skipping annotation for a link due to PDF profile: " + + pdfDoc.getProfile()); + } else if (action != null) { + PDFLink pdfLink = factory.makeLink(ipRect, action); + currentPage.addAnnotation(pdfLink); } - } else { - log.warn("Skipping annotation for a link due to PDF profile: " + pdfDoc.getProfile()); } } @@ -1199,12 +1482,12 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { String s = word.getWord(); escapeText(s, word.getLetterAdjustArray(), font, (AbstractTextArea)word.getParentArea(), useMultiByte, pdf); - + currentStream.add(pdf.toString()); super.renderWord(word); } - + /** * @see org.apache.fop.render.AbstractRenderer#renderSpace(SpaceArea) */ @@ -1223,18 +1506,18 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { if (space.isAdjustable()) { int tws = -((TextArea) space.getParentArea()).getTextWordSpaceAdjust() - 2 * textArea.getTextLetterSpaceAdjust(); - + if (tws != 0) { pdf.append(format(tws / (font.getFontSize() / 1000f))); pdf.append(" "); } } - + currentStream.add(pdf.toString()); super.renderSpace(space); } - + /** * Escapes text according to PDF rules. * @param s Text to escape @@ -1282,9 +1565,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } } if (letterAdjust != null && i < l - 1) { - glyphAdjust -= letterAdjust[i + 1]; + glyphAdjust -= letterAdjust[i + 1]; } - + if (startPending) { pdf.append(startText); startPending = false; @@ -1309,12 +1592,12 @@ public class PDFRenderer extends AbstractPathOrientedRenderer { } float adjust = glyphAdjust / fontSize; - + if (adjust != 0) { pdf.append(endText).append(format(adjust)).append(' '); startPending = true; } - + } if (!startPending) { pdf.append(endText); diff --git a/src/java/org/apache/fop/render/xml/XMLRenderer.java b/src/java/org/apache/fop/render/xml/XMLRenderer.java index 857847e10..cccdb6d71 100644 --- a/src/java/org/apache/fop/render/xml/XMLRenderer.java +++ b/src/java/org/apache/fop/render/xml/XMLRenderer.java @@ -60,11 +60,13 @@ import org.apache.fop.area.MainReference; import org.apache.fop.area.NormalFlow; import org.apache.fop.area.OffDocumentExtensionAttachment; import org.apache.fop.area.OffDocumentItem; +import org.apache.fop.area.BookmarkData; import org.apache.fop.area.PageViewport; import org.apache.fop.area.RegionReference; import org.apache.fop.area.RegionViewport; import org.apache.fop.area.Span; import org.apache.fop.area.Trait; +import org.apache.fop.area.Trait.InternalLink; import org.apache.fop.area.Trait.Background; import org.apache.fop.area.inline.Container; import org.apache.fop.area.inline.ForeignObject; @@ -350,6 +352,9 @@ public class XMLRenderer extends PrintRenderer { addAttribute("font-name", triplet.getName()); addAttribute("font-style", triplet.getStyle()); addAttribute("font-weight", triplet.getWeight()); + } else if (clazz.equals(InternalLink.class)) { + InternalLink iLink = (InternalLink)value; + addAttribute(name, iLink.xmlAttribute()); } else if (clazz.equals(Background.class)) { Background bkg = (Background)value; //TODO Remove the following line (makes changes in the test checks necessary) @@ -431,7 +436,9 @@ public class XMLRenderer extends PrintRenderer { /** @see org.apache.fop.render.AbstractRenderer#processOffDocumentItem(OffDocumentItem) */ public void processOffDocumentItem(OffDocumentItem oDI) { - if (oDI instanceof OffDocumentExtensionAttachment) { + if (oDI instanceof BookmarkData) { + renderBookmarkTree((BookmarkData) oDI); + } else if (oDI instanceof OffDocumentExtensionAttachment) { ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)oDI).getAttachment(); if (extensionAttachments == null) { extensionAttachments = new java.util.ArrayList(); @@ -444,6 +451,40 @@ public class XMLRenderer extends PrintRenderer { } /** + * Renders a BookmarkTree object + * @param bookmarkRoot the BookmarkData object representing the top of the tree + */ + protected void renderBookmarkTree(BookmarkData bookmarkRoot) { + if (bookmarkRoot.getWhenToProcess() == OffDocumentItem.END_OF_DOC) { + endPageSequence(); + } + /* If this kind of handling is also necessary for other renderers, then + better add endPageSequence to the Renderer interface and call it + explicitly from model.endDocument() */ + + startElement("bookmarkTree"); + for (int i = 0; i < bookmarkRoot.getCount(); i++) { + renderBookmarkItem(bookmarkRoot.getSubData(i)); + } + endElement("bookmarkTree"); + } + + private void renderBookmarkItem(BookmarkData bm) { + atts.clear(); + addAttribute("title", bm.getBookmarkTitle()); + addAttribute("show-children", String.valueOf(bm.showChildItems())); + PageViewport pv = bm.getPageViewport(); + String pvKey = pv == null ? null : pv.getKey(); + addAttribute("internal-link", + InternalLink.makeXMLAttribute(pvKey, bm.getIDRef())); + startElement("bookmark", atts); + for (int i = 0; i < bm.getCount(); i++) { + renderBookmarkItem(bm.getSubData(i)); + } + endElement("bookmark"); + } + + /** * @see org.apache.fop.render.Renderer#startRenderer(OutputStream) */ public void startRenderer(OutputStream outputStream) @@ -480,9 +521,7 @@ public class XMLRenderer extends PrintRenderer { * @see org.apache.fop.render.Renderer#stopRenderer() */ public void stopRenderer() throws IOException { - if (startedSequence) { - endElement("pageSequence"); - } + endPageSequence(); endElement("areaTree"); try { handler.endDocument(); @@ -549,9 +588,7 @@ public class XMLRenderer extends PrintRenderer { */ public void startPageSequence(LineArea seqTitle) { handleDocumentExtensionAttachments(); - if (startedSequence) { - endElement("pageSequence"); - } + endPageSequence(); // move this before handleDocumentExtensionAttachments() ? startedSequence = true; startElement("pageSequence"); if (seqTitle != null) { @@ -568,6 +605,16 @@ public class XMLRenderer extends PrintRenderer { } /** + * Tells the renderer to finish the current PageSequence + */ + public void endPageSequence() { + if (startedSequence) { + endElement("pageSequence"); + } + startedSequence = false; + } + + /** * @see org.apache.fop.render.AbstractRenderer#renderRegionViewport(RegionViewport) */ protected void renderRegionViewport(RegionViewport port) { @@ -759,6 +806,24 @@ public class XMLRenderer extends PrintRenderer { } /** + * @see org.apache.fop.render.AbstractRenderer#renderInlineArea(InlineArea) + */ + protected void renderInlineArea(InlineArea inlineArea) { + atts.clear(); + if (inlineArea.getClass() == InlineArea.class) { + // Generic inline area. This is implemented to allow the 0x0 "dummy" + // area generated by fo:wrapper to pass its id. + addAreaAttributes(inlineArea); + addTraitAttributes(inlineArea); + startElement("inline", atts); + endElement("inline"); + } else { + super.renderInlineArea(inlineArea); + // calls specific renderers for Text, Space, Viewport, etc. etc. + } + } + + /** * @see org.apache.fop.render.AbstractRenderer#renderViewport(Viewport) */ protected void renderViewport(Viewport viewport) { |