From 719a8a50772a780263366f9286f7f132f4561e0c Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Mon, 7 May 2007 12:22:22 +0000 Subject: [PATCH] Bugzilla #42067: Add support for exact positioning of internal PDF links. Submitted by: Paul Vinkenoog git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@535866 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/fop/area/AreaTreeHandler.java | 3 +- .../org/apache/fop/area/AreaTreeParser.java | 201 +++++--- .../org/apache/fop/area/BookmarkData.java | 43 +- .../org/apache/fop/area/LinkResolver.java | 18 +- .../org/apache/fop/area/PageViewport.java | 27 ++ src/java/org/apache/fop/area/Trait.java | 144 +++++- .../inline/BasicLinkLayoutManager.java | 51 +- .../inline/WrapperLayoutManager.java | 37 +- .../org/apache/fop/pdf/PDFDestination.java | 22 +- src/java/org/apache/fop/pdf/PDFFactory.java | 201 ++++++-- src/java/org/apache/fop/pdf/PDFGoTo.java | 44 +- src/java/org/apache/fop/pdf/PDFState.java | 17 +- .../apache/fop/render/pdf/PDFRenderer.java | 441 ++++++++++++++---- .../apache/fop/render/xml/XMLRenderer.java | 79 +++- status.xml | 3 + 15 files changed, 1084 insertions(+), 247 deletions(-) diff --git a/src/java/org/apache/fop/area/AreaTreeHandler.java b/src/java/org/apache/fop/area/AreaTreeHandler.java index d818a0d83..cc70cf1d9 100644 --- a/src/java/org/apache/fop/area/AreaTreeHandler.java +++ b/src/java/org/apache/fop/area/AreaTreeHandler.java @@ -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. 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..d87b38592 100644 --- a/src/java/org/apache/fop/area/BookmarkData.java +++ b/src/java/org/apache/fop/area/BookmarkData.java @@ -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 + * 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 + * 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 } } - 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 @@ -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 diff --git a/src/java/org/apache/fop/area/Trait.java b/src/java/org/apache/fop/area/Trait.java index 3cddc8006..1ded69663 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,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. 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/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/pdf/PDFDestination.java b/src/java/org/apache/fop/pdf/PDFDestination.java index 97923e935..b7e0989c1 100644 --- a/src/java/org/apache/fop/pdf/PDFDestination.java +++ b/src/java/org/apache/fop/pdf/PDFDestination.java @@ -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; } } diff --git a/src/java/org/apache/fop/pdf/PDFFactory.java b/src/java/org/apache/fop/pdf/PDFFactory.java index 3005f2018..774b363b3 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; @@ -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 ===================================== */ /** 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 @@ -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. * 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/render/pdf/PDFRenderer.java b/src/java/org/apache/fop/render/pdf/PDFRenderer.java index f64fa75aa..9813d08db 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,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); 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(); @@ -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) */ diff --git a/status.xml b/status.xml index 37f0f53f9..33f9f514b 100644 --- a/status.xml +++ b/status.xml @@ -28,6 +28,9 @@ + + Add support for exact positioning of internal PDF links. + Fix PDF Genaration for non-ASCII compatible locales. -- 2.39.5