]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
Bugzilla #42067:
authorJeremias Maerki <jeremias@apache.org>
Mon, 7 May 2007 12:22:22 +0000 (12:22 +0000)
committerJeremias Maerki <jeremias@apache.org>
Mon, 7 May 2007 12:22:22 +0000 (12:22 +0000)
Add support for exact positioning of internal PDF links.
Submitted by: Paul Vinkenoog <paul.at.vinkenoog.nl>

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@535866 13f79535-47bb-0310-9956-ffa450edef68

15 files changed:
src/java/org/apache/fop/area/AreaTreeHandler.java
src/java/org/apache/fop/area/AreaTreeParser.java
src/java/org/apache/fop/area/BookmarkData.java
src/java/org/apache/fop/area/LinkResolver.java
src/java/org/apache/fop/area/PageViewport.java
src/java/org/apache/fop/area/Trait.java
src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java
src/java/org/apache/fop/layoutmgr/inline/WrapperLayoutManager.java
src/java/org/apache/fop/pdf/PDFDestination.java
src/java/org/apache/fop/pdf/PDFFactory.java
src/java/org/apache/fop/pdf/PDFGoTo.java
src/java/org/apache/fop/pdf/PDFState.java
src/java/org/apache/fop/render/pdf/PDFRenderer.java
src/java/org/apache/fop/render/xml/XMLRenderer.java
status.xml

index d818a0d83fa34ac73d6c9e075d7133df7e25828a..cc70cf1d99e9d04e00c737d1d77c0fa9dea6cc95 100644 (file)
@@ -175,7 +175,8 @@ public class AreaTreeHandler extends FOEventHandler {
             pvList = new ArrayList();
             idLocations.put(id, pvList);
             pvList.add(pv);
-
+            // 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.
index fce776e220f8325fc87c90e4bd677e09161df17d..9e293afaceb169557b310d3e3901fab49953a78b 100644 (file)
@@ -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 {
         }
 
     }
-    
+
 }
index 8ef1e608655480d1d70d5a972c4ea2eaa6476633..d87b3859212332e267686448e316c1100d9fa5c9 100644 (file)
@@ -61,7 +61,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));
@@ -83,6 +83,34 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable
         unresolvedIDRefs.put(idRef, this);
     }
 
+    /**
+     * 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
      *
@@ -93,17 +121,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 +251,3 @@ public class BookmarkData extends AbstractOffDocumentItem implements Resolvable
     }
 
 }
-
index 603aede877fb8ba4e8fb187ef01b3983ba72f394..0b0441bd2eae937ca4d76230f6cafbb9c1cce5f3 100644 (file)
@@ -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);
         }
     }
 }
index 282bce36722f8e4ef0ef7f5f1ff539d21e673832..953cb384081eb2e70ae2abd1615afc411c3e3f74 100644 (file)
@@ -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
@@ -236,6 +240,29 @@ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneabl
         return this.pageKey;
     }
 
+    /**
+     * 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
index 3cddc80062b07dd3d1b9c186277aecd72e0e238d..1ded6966390c5a4cf4d51b0219c74de59099a32b 100644 (file)
@@ -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,143 @@ 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[] values = attrValue.substring(1, len - 1).split(",", 2);
+                  if (values.length > 0) {
+                      result[0] = values[0].trim();
+                      if (values.length > 1) {
+                          result[1] = values[1].trim();
+                      }
+                  }
+              } 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.
index 3c9dd4ed994c0bcf5b13e7c55693c0d35a4c0b0f..7e261d07439b3196ba6c6aa3d06f82a987559a34 100644 (file)
@@ -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);
+                }
+            }
+        }
+    }
+}
index 9c946afbc73d270b7bec92be3debc7c7784af522..a9807573d7f16658057c690d3374368b2215434b 100644 (file)
@@ -22,6 +22,8 @@ package org.apache.fop.layoutmgr.inline;
 import org.apache.fop.area.inline.InlineArea;\r
 import org.apache.fop.fo.flow.Wrapper;\r
 import org.apache.fop.layoutmgr.LayoutContext;\r
+import org.apache.fop.layoutmgr.PositionIterator;\r
+import org.apache.fop.layoutmgr.TraitSetter;\r
 \r
 /**\r
  * This is the layout manager for the fo:wrapper formatting object.\r
@@ -41,16 +43,41 @@ public class WrapperLayoutManager extends LeafNodeLayoutManager {
 \r
     /** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager */\r
     public InlineArea get(LayoutContext context) {\r
-        //Create a zero-width, zero-height dummy area so this node can \r
-        //participate in the ID handling. Otherwise, addId() wouldn't \r
-        //be called.\r
+        // Create a zero-width, zero-height dummy area so this node can\r
+        // participate in the ID handling. Otherwise, addId() wouldn't\r
+        // be called. The area must also be added to the tree, because\r
+        // determination of the X,Y position is done in the renderer.\r
         InlineArea area = new InlineArea();\r
+        String id = fobj.getId();\r
+        if (id != null && id.length() > 0) {\r
+            TraitSetter.setProducerID(area, fobj.getId());\r
+        }\r
         return area;\r
     }\r
-    \r
+\r
+    /**\r
+     * Add the area for this layout manager.\r
+     * This adds the dummy area to the parent, *if* it has an id\r
+     * - otherwise it serves no purpose.\r
+     *\r
+     * @param posIter the position iterator\r
+     * @param context the layout context for adding the area\r
+     */\r
+    public void addAreas(PositionIterator posIter, LayoutContext context) {\r
+        String id = fobj.getId();\r
+        if (id != null && id.length() > 0) {\r
+            addId();\r
+            InlineArea area = getEffectiveArea();\r
+            parentLM.addChildArea(area);\r
+        }\r
+        while (posIter.hasNext()) {\r
+            posIter.next();\r
+        }\r
+    }\r
+\r
     /** @see org.apache.fop.layoutmgr.inline.LeafNodeLayoutManager#addId() */\r
     protected void addId() {\r
         getPSLM().addIDToPage(fobj.getId());\r
     }\r
-    \r
+\r
 }\r
index 97923e9352c87ae1cbcf4caaf7afacaa9e593ae4..b7e0989c14d76ea208db6680fd07310cf6a2a986 100644 (file)
@@ -30,7 +30,7 @@ public class PDFDestination extends PDFObject {
     /**
      * PDFReference (object reference) for this destination
      */
-     private String goToReference;
+    private String goToReference;
 
     /**
      * ID Reference for this destination
@@ -53,12 +53,26 @@ public class PDFDestination extends PDFObject {
         this.pageViewport = destinationData.getPageViewport();
     }
 
+    /**
+     * create a named destination
+     *
+     * @param idRef The ID reference for this destination - will be used as the name
+     * @param goToRef A PDF reference to a /GoTo pointing to the target area
+     * @param pv The PageViewport of the target area (merely informational)
+     */
+    public PDFDestination(String idRef, String goToRef, PageViewport pv) {
+        super();
+        this.idRef = idRef;
+        this.goToReference = goToRef;
+        this.pageViewport = pv;
+    }
+
     /**
      * @see org.apache.fop.pdf.PDFObject#toPDFString()
      */
     public String toPDFString() {
         String s = getObjectID()
-                   + "<<"
+                   + "<<\n"
                    + "/Limits [(" + idRef + ") (" + idRef + ")]\n"
                    + "/Names [(" + idRef + ") " + goToReference + "]"
                    + "\n>>\nendobj\n";
@@ -128,11 +142,11 @@ 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;
     }
 }
 
index 3005f2018596351b614484bc20baeef0621b3895..774b363b3335c2ef0c3311d79394f151d0201e54 100644 (file)
@@ -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;
@@ -144,7 +145,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
      */
@@ -814,6 +815,24 @@ public class PDFFactory {
 
     /* ============= named destinations and the name dictionary ============ */
 
+    /**
+     * Registers and returns newdest if it is unique. Otherwise, returns
+     * the equal destination already present in the document.
+     *
+     * @param newdest a new, as yet unregistered destination
+     * @return newdest if unique, else the already registered instance
+     */
+    protected PDFDestination getUniqueDestination(PDFDestination newdest) {
+        PDFDestination existing = getDocument().findDestination(newdest);
+        if (existing != null) {
+            return existing;
+        } else {
+            getDocument().registerObject(newdest);
+            getDocument().setHasDestinations(true);
+            return newdest;
+        }
+    }
+
     /**
      * Make a named destination.
      *
@@ -826,16 +845,21 @@ public class PDFFactory {
             log.warn("Unresolved destination item received: " + destinationData.getIDRef());
         }
         PDFDestination destination = new PDFDestination(destinationData);
+        return getUniqueDestination(destination);
+    }
 
-        PDFDestination oldDestination = getDocument().findDestination(destination);
-        if (destination == oldDestination) {
-            destination = oldDestination;
-        } else {
-            getDocument().registerObject(destination);
-            getDocument().setHasDestinations(true);
-        }
-
-        return destination;
+    /**
+     * Create/find a named destination object.
+     *
+     * @param idRef The ID of this destination. This will be used for the name.
+     * @param goToRef A PDF reference to the associated /GoTo
+     * @param pv The PageViewport of the destination area. Only for informational purposes.
+     *
+     * @return The new or existing named destination
+     */
+    public PDFDestination makeDestination(String idRef, String goToRef, PageViewport pv) {
+        PDFDestination destination = new PDFDestination(idRef, goToRef, pv);
+        return getUniqueDestination(destination);
     }
 
     /**
@@ -864,6 +888,31 @@ public class PDFFactory {
 
     /* ========================= 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).
+
+    /**
+     * 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...
+        }
+    }
+
     /**
      * Make an internal link.
      *
@@ -898,32 +947,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 +968,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 +1031,7 @@ public class PDFFactory {
         } else {
             gt = oldgt;
         }
-
-        goToReference = gt.referencePDF();
-        return goToReference;
+        return gt;
     }
 
     /**
@@ -996,6 +1072,42 @@ public class PDFFactory {
         return remote;
     }
 
+    /**
+     * 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
      *
@@ -1011,17 +1123,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 ===================================== */
 
     /**
index a493a9f32073c18ccad60427601c274928be68f6..16eaf73ce9901b2c0f47eed2d7724f854b2b63fe 100644 (file)
@@ -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
@@ -46,6 +48,20 @@ public class PDFGoTo extends PDFAction {
         this.pageReference = pageReference;
     }
 
+    /**
+     * 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
      *
@@ -55,6 +71,25 @@ public class PDFGoTo extends PDFAction {
         this.pageReference = pageReference;
     }
 
+    /**
+     * 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
      *
@@ -73,15 +108,6 @@ public class PDFGoTo extends PDFAction {
         destination = dest;
     }
 
-    /**
-     * 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.
      *
index e0a2eae1a9bf4daa0fe608ead40485f468b56a38..c01aab52e9d20617939ed4305194ca8e550b6a3d 100644 (file)
@@ -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
index f64fa75aabc4683961ec9e6ddd08b4f69b31f1e3..9813d08db0a94481632dfd3ce0ab3260b7b7ffea 100644 (file)
@@ -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,19 @@ 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.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,15 +170,34 @@ 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
      */
@@ -197,7 +222,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;
 
@@ -478,10 +508,35 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
         pdfDoc.getRoot().addOutputIntent(outputIntent);
     }
 
+    /**
+     * 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 +553,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
         currentPage = null;
         currentState = null;
         currentFontName = "";
+
+        idPositions.clear();
+        idGoTos.clear();
     }
 
     /**
@@ -514,17 +572,9 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
     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);
-        }
+            renderDestination((DestinationData) odi);
         // render Bookmark-Tree
-        else if (odi instanceof BookmarkData) {
+        else if (odi instanceof BookmarkData) {
             renderBookmarkTree((BookmarkData) odi);
         } else if (odi instanceof OffDocumentExtensionAttachment) {
             ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)odi).getAttachment();
@@ -534,6 +584,24 @@ 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) {
+                String pvKey = pv.getKey();
+                PDFGoTo gt = getPDFGoToForID(targetID, pvKey);
+                // create/find and register PDFDestination object:
+                pdfDoc.getFactory().makeDestination(targetID, gt.referencePDF(), pv);
+            } else {
+                log.warn("DestinationData item with IDRef \"" 
+                         + targetID + "\" has a null PageViewport.");
+            }
+        } 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 +613,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 +773,8 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
         } else {
             setupPage(page);
         }
+        currentPageRef = currentPage.referencePDF();
+
         Rectangle2D bounds = page.getViewArea();
         double h = bounds.getHeight();
         pageHeight = (int) h;
@@ -1088,6 +1156,188 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
         comment("------ done.");
     }
 
+    /**
+     * 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)
      */
@@ -1096,6 +1346,14 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
         closeText();
     }
 
+    /**
+     * @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
@@ -1103,47 +1361,74 @@ public class PDFRenderer extends AbstractPathOrientedRenderer {
      * @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 +1484,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 +1508,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 +1567,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 +1594,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);
index 857847e10a061399eb7e05392a5fb678da8f7d7d..cccdb6d716eb0e856321df9a9c7f204c642c67ed 100644 (file)
@@ -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();
@@ -443,6 +450,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)
      */
@@ -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) {
@@ -567,6 +604,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)
      */
@@ -758,6 +805,24 @@ public class XMLRenderer extends PrintRenderer {
         endElement("lineArea");
     }
 
+    /**
+     * @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)
      */
index 37f0f53f9c77a477e61efada156a3438cc18d336..33f9f514b8d83428f08b848d5ed9129893bb3d54 100644 (file)
@@ -28,6 +28,9 @@
 
   <changes>
     <release version="FOP Trunk">
+      <action context="Code" dev="JM" type="add" fixes-bug="42067" due-to="Paul Vinkenoog">
+        Add support for exact positioning of internal PDF links.
+      </action>
       <action context="Code" dev="JM" type="fix" fixes-bug="41434" due-to="Martin Kögler">
         Fix PDF Genaration for non-ASCII compatible locales.
       </action>