--- src/java/org/apache/fop/render/pdf/PDFRenderer.java (revision 695454) +++ src/java/org/apache/fop/render/pdf/PDFRenderer.java (working copy) @@ -1401,6 +1401,26 @@ } } + /** + * Render an internal link explicitly. + * For normal PDF text, this is handled by + * renderInlineParent. This is used when rendering SVG's to + * explicitly add internal links. + * @param rect Area to add link to + * @param idRef The target id to link to + */ + public void renderInternalLink(Rectangle2D rect, String idRef) { + boolean annotsAllowed = pdfDoc.getProfile().isAnnotationAllowed(); + boolean idRefOK = idRef != null && idRef.length() > 0; + if (idRefOK && annotsAllowed) { + PDFFactory factory = pdfDoc.getFactory(); + PDFAction action = getPDFGoToForID(idRef, null); + /* using null as pvKey will delay the link resolution */ + PDFLink pdfLink = factory.makeLink(rect, action); + currentPage.addAnnotation(pdfLink); + } + } + private Typeface getTypeface(String fontName) { Typeface tf = (Typeface) fontInfo.getFonts().get(fontName); if (tf instanceof LazyFont) { --- src/java/org/apache/fop/render/pdf/PDFSVGHandler.java (revision 695454) +++ src/java/org/apache/fop/render/pdf/PDFSVGHandler.java (working copy) @@ -20,7 +20,9 @@ package org.apache.fop.render.pdf; import java.awt.Color; +import java.awt.Shape; import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.OutputStream; import java.util.Map; @@ -51,6 +53,7 @@ import org.apache.fop.render.RendererContext; import org.apache.fop.render.RendererContextConstants; import org.apache.fop.svg.PDFAElementBridge; +import org.apache.fop.svg.PDFANode; import org.apache.fop.svg.PDFBridgeContext; import org.apache.fop.svg.PDFGraphics2D; import org.apache.fop.svg.SVGEventProducer; @@ -140,7 +143,7 @@ */ protected void renderSVGDocument(RendererContext context, Document doc) { - PDFRenderer renderer = (PDFRenderer)context.getRenderer(); + final PDFRenderer renderer = (PDFRenderer)context.getRenderer(); PDFInfo pdfInfo = getPDFInfo(context); if (pdfInfo.paintAsBitmap) { try { @@ -182,8 +185,16 @@ (strokeText ? null : pdfInfo.fi), userAgent.getFactory().getImageManager(), userAgent.getImageSessionContext(), - new AffineTransform()); + new AffineTransform()) { + /** {@inheritDoc} */ + protected PDFAElementBridge createPDFAElementBridge() { + //return super.createPDFAElementBridge(); + return new LinkBackAElementBridge(renderer); + } + + }; + GraphicsNode root; try { root = builder.build(ctx, doc); @@ -276,4 +287,45 @@ public boolean supportsRenderer(Renderer renderer) { return (renderer instanceof PDFRenderer); } + + private class LinkBackAElementBridge extends PDFAElementBridge { + + private PDFRenderer renderer; + + public LinkBackAElementBridge(PDFRenderer renderer) { + this.renderer = renderer; + } + + /** {@inheritDoc} */ + protected GraphicsNode instantiateGraphicsNode() { + return new LinkBackPDFANode(this.renderer); + } + + } + + private class LinkBackPDFANode extends PDFANode { + + private PDFRenderer renderer; + + public LinkBackPDFANode(PDFRenderer renderer) { + this.renderer = renderer; + } + + /** {@inheritDoc} */ + protected boolean generateFragmentLink(Rectangle2D bounds, PDFGraphics2D pdfg) { + assert getDestination().charAt(0) == '#'; + boolean annotsAllowed = renderer.pdfDoc.getProfile().isAnnotationAllowed(); + if (!annotsAllowed) { + return false; + } + + String target = getDestination().substring(1); + Shape b = pdfg.getTransform().createTransformedShape(bounds); + b = this.getOnPageTransform().createTransformedShape(b); + + renderer.renderInternalLink(b.getBounds2D(), target); + return true; + } + + } } --- src/java/org/apache/fop/svg/PDFAElementBridge.java (revision 695454) +++ src/java/org/apache/fop/svg/PDFAElementBridge.java (working copy) @@ -21,14 +21,13 @@ import java.awt.geom.AffineTransform; +import org.w3c.dom.Element; +import org.w3c.dom.svg.SVGAElement; + import org.apache.batik.bridge.AbstractGraphicsNodeBridge; import org.apache.batik.bridge.BridgeContext; - import org.apache.batik.gvt.GraphicsNode; -import org.w3c.dom.Element; -import org.w3c.dom.svg.SVGAElement; - /** * Bridge class for the <a> element. * @@ -83,7 +82,7 @@ public GraphicsNode createGraphicsNode(BridgeContext ctx, Element e) { PDFANode aNode = (PDFANode)super.createGraphicsNode(ctx, e); aNode.setDestination(((SVGAElement)e).getHref().getBaseVal()); - aNode.setTransform(transform); + aNode.setOnPageTransform(transform); return aNode; } --- src/java/org/apache/fop/svg/PDFANode.java (revision 695454) +++ src/java/org/apache/fop/svg/PDFANode.java (working copy) @@ -19,15 +19,14 @@ package org.apache.fop.svg; -import org.apache.batik.gvt.CompositeGraphicsNode; - import java.awt.Graphics2D; import java.awt.Shape; +import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; -import java.awt.geom.AffineTransform; - import java.util.StringTokenizer; +import org.apache.batik.gvt.CompositeGraphicsNode; + /** * A graphics node that represents an image described as a graphics node. * @@ -35,7 +34,7 @@ */ public class PDFANode extends CompositeGraphicsNode { private String destination; - private AffineTransform transform; + private AffineTransform onPageTransform; /** * Constructs a new empty PDFANode. @@ -52,14 +51,32 @@ } /** - * Set the current transform of this node. + * Returns the destination value for the node. + * @return the destination + */ + public String getDestination() { + return this.destination; + } + + /** + * Set the "on-page" transform of this node. This corrects the placement of the link hot-zones + * with respect of the SVG graphic on the page. * @param tf the transform */ - public void setTransform(AffineTransform tf) { - transform = tf; + public void setOnPageTransform(AffineTransform tf) { + this.onPageTransform = tf; } /** + * Returns the "on-page" transform of this node. + * See {@code #setOnPageTransform(AffineTransform)} for details. + * @return the "on-page" transform + */ + public AffineTransform getOnPageTransform() { + return this.onPageTransform; + } + + /** * Paints this node if visible. * * @param g2d the Graphics2D to use @@ -104,9 +121,10 @@ } catch (Exception e) { //TODO Move this to setDestination() and throw an IllegalArgumentException e.printStackTrace(); + return; } Rectangle2D destRect = new Rectangle2D.Float(x, y, width, height); - destRect = transform.createTransformedShape(destRect).getBounds(); + destRect = getOnPageTransform().createTransformedShape(destRect).getBounds(); // these numbers need conversion to current // svg position and scaled for the page x = (float)destRect.getX(); @@ -116,11 +134,27 @@ destination = "" + x + " " + y + " " + (x + width) + " " + (y + height); + } else if (destination.startsWith("#")) { + if (generateFragmentLink(getBounds(), pdfg)) { + return; //Returns early if the method indicated that it has generated a link + } } - pdfg.addLink(getBounds(), transform, destination, type); + pdfg.addLink(getBounds(), getOnPageTransform(), destination, type); } } } + /** + * Generates a link for a fragment reference. By default, it does nothing. Subclasses can + * generate links to objects (ex. in XSL-FO) identified by the fragment reference. + * @param bounds the boundaries of the hot zone + * @param pdfg the PDFGraphics2D object + * @return true if it has generated a link for the fragment reference + */ + protected boolean generateFragmentLink(Rectangle2D bounds, PDFGraphics2D pdfg) { + //nop + return false; + } + } --- src/java/org/apache/fop/svg/PDFBridgeContext.java (revision 695454) +++ src/java/org/apache/fop/svg/PDFBridgeContext.java (working copy) @@ -155,7 +155,7 @@ "org.apache.batik.bridge.svg12.SVGFlowRootElementBridge"); } - PDFAElementBridge pdfAElementBridge = new PDFAElementBridge(); + PDFAElementBridge pdfAElementBridge = createPDFAElementBridge(); if (linkTransform != null) { pdfAElementBridge.setCurrentTransform(linkTransform); } else { @@ -166,6 +166,14 @@ putBridge(new PDFImageElementBridge()); } + /** + * Creates the "a" element bridge for the bridge context. + * @return the element bridge + */ + protected PDFAElementBridge createPDFAElementBridge() { + return new PDFAElementBridge(); + } + // Make sure any 'sub bridge contexts' also have our bridges. //TODO There's no matching method in the super-class here public BridgeContext createBridgeContext() {