From: Jeremias Maerki Date: Thu, 11 Dec 2008 14:15:32 +0000 (+0000) Subject: My first attempt at implementing links was messed up. This second attempt actually... X-Git-Tag: fop-1_0~115^2~117 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=3287952fed8dbc91af8f5ed50830061594958a3f;p=xmlgraphics-fop.git My first attempt at implementing links was messed up. This second attempt actually works. Optimized IF serialization a bit: only real changes in the IF state are serialized to the IF (makes smaller files and results in higher performance). git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_AreaTreeNewDesign@725690 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/java/org/apache/fop/render/intermediate/IFRenderer.java b/src/java/org/apache/fop/render/intermediate/IFRenderer.java index 4b2b04395..b3748dc2e 100644 --- a/src/java/org/apache/fop/render/intermediate/IFRenderer.java +++ b/src/java/org/apache/fop/render/intermediate/IFRenderer.java @@ -142,6 +142,9 @@ public class IFRenderer extends AbstractPathOrientedRenderer { // can't use a Set because PDFGoTo.equals returns true if the target is the same, // even if the object number differs + /** Maps unique PageViewport key to page indices (for link target handling) */ + protected Map pageIndices = new java.util.HashMap(); + private BookmarkTree bookmarkTree; private List deferredDestinations = new java.util.ArrayList(); private List deferredLinks = new java.util.ArrayList(); @@ -267,21 +270,18 @@ public class IFRenderer extends AbstractPathOrientedRenderer { documentHandler.startDocumentTrailer(); //Wrap up document navigation - finishOpenGoTos(); - Iterator iter; - iter = this.actionSet.getActions(); - while (iter.hasNext()) { - getDocumentNavigationHandler().addResolvedAction((AbstractAction)iter.next()); - } - iter = this.deferredDestinations.iterator(); - while (iter.hasNext()) { - NamedDestination dest = (NamedDestination)iter.next(); - iter.remove(); - getDocumentNavigationHandler().renderNamedDestination(dest); - } + if (hasDocumentNavigation()) { + finishOpenGoTos(); + Iterator iter = this.deferredDestinations.iterator(); + while (iter.hasNext()) { + NamedDestination dest = (NamedDestination)iter.next(); + iter.remove(); + getDocumentNavigationHandler().renderNamedDestination(dest); + } - if (this.bookmarkTree != null) { - getDocumentNavigationHandler().renderBookmarkTree(this.bookmarkTree); + if (this.bookmarkTree != null) { + getDocumentNavigationHandler().renderBookmarkTree(this.bookmarkTree); + } } documentHandler.endDocumentTrailer(); @@ -289,6 +289,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { } catch (IFException e) { handleIFExceptionWithIOException(e); } + pageIndices.clear(); idPositions.clear(); actionSet.clear(); super.stopRenderer(); @@ -322,8 +323,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { PageViewport pv = dd.getPageViewport(); if (pv != null) { GoToXYAction action = getGoToActionForID(targetID, pv.getPageIndex()); - NamedDestination namedDestination = new NamedDestination(targetID, - action.createReference()); + NamedDestination namedDestination = new NamedDestination(targetID, action); this.deferredDestinations.add(namedDestination); } else { //Warning already issued by AreaTreeHandler (debug level is sufficient) @@ -367,7 +367,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { Bookmark b = new Bookmark( bookmarkItem.getBookmarkTitle(), bookmarkItem.showChildItems(), - (action != null ? action.createReference() : null)); + action); for (int i = 0; i < bookmarkItem.getCount(); i++) { b.addChildBookmark(renderBookmarkItem(bookmarkItem.getSubData(i))); } @@ -383,9 +383,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer { GoToXYAction action = (GoToXYAction)actionSet.get(targetID); //GoToXYAction action = (GoToXYAction)idGoTos.get(targetID); if (action == null) { + if (pageIndex < 0) { + //pageIndex = page + } Point position = (Point)idPositions.get(targetID); // can the GoTo already be fully filled in? - if (position != null) { + if (pageIndex >= 0 && position != null) { action = new GoToXYAction(targetID, pageIndex, position); } else { // Not complete yet, can't use getPDFGoTo: @@ -415,10 +418,16 @@ public class IFRenderer extends AbstractPathOrientedRenderer { private void noteGoToPosition(GoToXYAction action, Point position) { action.setTargetLocation(position); + try { + getDocumentNavigationHandler().addResolvedAction(action); + } catch (IFException ife) { + handleIFException(ife); + } unfinishedGoTos.remove(action); } private void noteGoToPosition(GoToXYAction action, PageViewport pv, Point position) { + action.setPageIndex(pv.getPageIndex()); noteGoToPosition(action, position); } @@ -427,7 +436,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { Point position = new Point(relativeIPP, relativeBPP); tf.transform(position, position); idPositions.put(id, position); - // is there already a PDFGoTo waiting to be completed? + // is there already a GoTo action waiting to be completed? GoToXYAction action = (GoToXYAction)actionSet.get(id); if (action != null) { noteGoToPosition(action, pv, position); @@ -439,9 +448,9 @@ public class IFRenderer extends AbstractPathOrientedRenderer { relativeIPP, relativeBPP, graphicContext.getTransform()); } - protected void saveBlockPosIfTargetable(Block block) { + private void saveBlockPosIfTargetable(Block block) { String id = getTargetableID(block); - if (id != null) { + if (hasDocumentNavigation() && 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. @@ -458,7 +467,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { private void saveInlinePosIfTargetable(InlineArea inlineArea) { String id = getTargetableID(inlineArea); - if (id != null) { + if (hasDocumentNavigation() && id != null) { int extraMarginBefore = 5000; // millipoints int ipp = currentIPPosition; int bpp = currentBPPosition + inlineArea.getOffset() - extraMarginBefore; @@ -523,12 +532,18 @@ public class IFRenderer extends AbstractPathOrientedRenderer { return xmp; } + /** {@inheritDoc} */ + public void preparePage(PageViewport page) { + super.preparePage(page); + } + /** {@inheritDoc} */ public void renderPage(PageViewport page) throws IOException, FOPException { if (log.isTraceEnabled()) { log.trace("renderPage() " + page); } try { + pageIndices.put(page.getKey(), new Integer(page.getPageIndex())); Rectangle2D viewArea = page.getViewArea(); Dimension dim = new Dimension( (int)Math.ceil(viewArea.getWidth()), @@ -865,12 +880,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer { 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) { - action = getGoToActionForID(idRef, this.currentPageViewport.getPageIndex()); + Integer pageIndex = (Integer)pageIndices.get(pvKey); + action = getGoToActionForID(idRef, (pageIndex != null ? pageIndex.intValue() : -1)); } else { //Warnings already issued by AreaTreeHandler } @@ -891,7 +906,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { // warn if link trait found but not allowed, else create link if (linkTraitFound) { - Link link = new Link(action.createReference(), ipRect); + Link link = new Link(action, ipRect); this.deferredLinks.add(link); } } @@ -987,7 +1002,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { protected void renderText(String s, int[] letterAdjust, Font font, AbstractTextArea parentArea) { - float fontSize = font.getFontSize() / 1000f; + //float fontSize = font.getFontSize() / 1000f; int l = s.length(); diff --git a/src/java/org/apache/fop/render/intermediate/IFSerializer.java b/src/java/org/apache/fop/render/intermediate/IFSerializer.java index c4fb0cb0d..c54fe6407 100644 --- a/src/java/org/apache/fop/render/intermediate/IFSerializer.java +++ b/src/java/org/apache/fop/render/intermediate/IFSerializer.java @@ -26,6 +26,7 @@ import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.util.Iterator; +import java.util.List; import java.util.Map; import org.w3c.dom.Document; @@ -39,13 +40,16 @@ import org.apache.xmlgraphics.util.XMLizable; import org.apache.fop.fonts.FontInfo; import org.apache.fop.render.RenderingContext; import org.apache.fop.render.intermediate.extensions.AbstractAction; +import org.apache.fop.render.intermediate.extensions.Bookmark; import org.apache.fop.render.intermediate.extensions.BookmarkTree; +import org.apache.fop.render.intermediate.extensions.DocumentNavigationExtensionConstants; import org.apache.fop.render.intermediate.extensions.Link; import org.apache.fop.render.intermediate.extensions.NamedDestination; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.ColorUtil; import org.apache.fop.util.DOM2SAX; +import org.apache.fop.util.XMLConstants; import org.apache.fop.util.XMLUtil; /** @@ -56,6 +60,9 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler private IFDocumentHandler mimicHandler; + /** Holds the intermediate format state */ + private IFState state; + /** * Default constructor. */ @@ -93,10 +100,19 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler return this; } + /** + * Tells this serializer to mimic the given document handler (mostly applies to the font set + * that is used during layout). + * @param targetHandler the document handler to mimic + */ public void mimicDocumentHandler(IFDocumentHandler targetHandler) { this.mimicHandler = targetHandler; } + /** + * Returns the document handler that is being mimicked by this serializer. + * @return the mimicked document handler or null if no such document handler has been set + */ public IFDocumentHandler getMimickedDocumentHandler() { return this.mimicHandler; } @@ -126,6 +142,8 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler handler.startDocument(); handler.startPrefixMapping("", NAMESPACE); handler.startPrefixMapping(XLINK_PREFIX, XLINK_NAMESPACE); + handler.startPrefixMapping(DocumentNavigationExtensionConstants.PREFIX, + DocumentNavigationExtensionConstants.NAMESPACE); handler.startElement(EL_DOCUMENT); } catch (SAXException e) { throw new IFException("SAX error in startDocument()", e); @@ -173,6 +191,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler try { handler.endElement(EL_DOCUMENT); handler.endDocument(); + finishDocumentNavigation(); } catch (SAXException e) { throw new IFException("SAX error in endDocument()", e); } @@ -201,7 +220,8 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler } /** {@inheritDoc} */ - public void startPage(int index, String name, String pageMasterName, Dimension size) throws IFException { + public void startPage(int index, String name, String pageMasterName, Dimension size) + throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "index", Integer.toString(index)); @@ -237,6 +257,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler public IFPainter startPageContent() throws IFException { try { handler.startElement(EL_PAGE_CONTENT); + this.state = IFState.create(); return this; } catch (SAXException e) { throw new IFException("SAX error in startPageContent()", e); @@ -246,6 +267,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler /** {@inheritDoc} */ public void endPageContent() throws IFException { try { + this.state = null; handler.endElement(EL_PAGE_CONTENT); } catch (SAXException e) { throw new IFException("SAX error in endPageContent()", e); @@ -256,6 +278,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler public void startPageTrailer() throws IFException { try { handler.startElement(EL_PAGE_TRAILER); + commitNavigation(); } catch (SAXException e) { throw new IFException("SAX error in startPageTrailer()", e); } @@ -510,25 +533,52 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler Color color) throws IFException { try { AttributesImpl atts = new AttributesImpl(); + boolean changed; if (family != null) { - addAttribute(atts, "family", family); + changed = !family.equals(state.getFontFamily()); + if (changed) { + state.setFontFamily(family); + addAttribute(atts, "family", family); + } } if (style != null) { - addAttribute(atts, "style", style); + changed = !style.equals(state.getFontStyle()); + if (changed) { + state.setFontStyle(style); + addAttribute(atts, "style", style); + } } if (weight != null) { - addAttribute(atts, "weight", weight.toString()); + changed = (weight.intValue() != state.getFontWeight()); + if (changed) { + state.setFontWeight(weight.intValue()); + addAttribute(atts, "weight", weight.toString()); + } } if (variant != null) { - addAttribute(atts, "variant", variant); + changed = !variant.equals(state.getFontVariant()); + if (changed) { + state.setFontVariant(variant); + addAttribute(atts, "variant", variant); + } } if (size != null) { - addAttribute(atts, "size", size.toString()); + changed = (size.intValue() != state.getFontSize()); + if (changed) { + state.setFontSize(size.intValue()); + addAttribute(atts, "size", size.toString()); + } } if (color != null) { - addAttribute(atts, "color", toString(color)); + changed = !color.equals(state.getTextColor()); + if (changed) { + state.setTextColor(color); + addAttribute(atts, "color", toString(color)); + } + } + if (atts.getLength() > 0) { + handler.element(EL_FONT, atts); } - handler.element(EL_FONT, atts); } catch (SAXException e) { throw new IFException("SAX error in setFont()", e); } @@ -565,27 +615,112 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler // ---=== IFDocumentNavigationHandler ===--- + private Map incompleteActions = new java.util.HashMap(); + private List completeActions = new java.util.LinkedList(); + + private void noteAction(AbstractAction action) { + if (action == null) { + throw new NullPointerException("action must not be null"); + } + if (!action.isComplete()) { + assert action.hasID(); + incompleteActions.put(action.getID(), action); + } + } + /** {@inheritDoc} */ public void renderNamedDestination(NamedDestination destination) throws IFException { - renderXMLizable(destination); + noteAction(destination.getAction()); + + AttributesImpl atts = new AttributesImpl(); + atts.addAttribute(null, "name", "name", XMLConstants.CDATA, destination.getName()); + try { + handler.startElement(DocumentNavigationExtensionConstants.NAMED_DESTINATION, atts); + serializeXMLizable(destination.getAction()); + handler.endElement(DocumentNavigationExtensionConstants.NAMED_DESTINATION); + } catch (SAXException e) { + throw new IFException("SAX error serializing named destination", e); + } } /** {@inheritDoc} */ public void renderBookmarkTree(BookmarkTree tree) throws IFException { - renderXMLizable(tree); + AttributesImpl atts = new AttributesImpl(); + try { + handler.startElement(DocumentNavigationExtensionConstants.BOOKMARK_TREE, atts); + Iterator iter = tree.getBookmarks().iterator(); + while (iter.hasNext()) { + Bookmark b = (Bookmark)iter.next(); + serializeBookmark(b); + } + handler.endElement(DocumentNavigationExtensionConstants.BOOKMARK_TREE); + } catch (SAXException e) { + throw new IFException("SAX error serializing bookmark tree", e); + } } - /** {@inheritDoc} */ - public void addResolvedAction(AbstractAction action) throws IFException { - renderXMLizable(action); + private void serializeBookmark(Bookmark bookmark) throws SAXException, IFException { + noteAction(bookmark.getAction()); + + AttributesImpl atts = new AttributesImpl(); + atts.addAttribute(null, "title", "title", XMLUtil.CDATA, bookmark.getTitle()); + atts.addAttribute(null, "starting-state", "starting-state", + XMLUtil.CDATA, bookmark.isShown() ? "show" : "hide"); + handler.startElement(DocumentNavigationExtensionConstants.BOOKMARK, atts); + serializeXMLizable(bookmark.getAction()); + Iterator iter = bookmark.getChildBookmarks().iterator(); + while (iter.hasNext()) { + Bookmark b = (Bookmark)iter.next(); + serializeBookmark(b); + } + handler.endElement(DocumentNavigationExtensionConstants.BOOKMARK); + } /** {@inheritDoc} */ public void renderLink(Link link) throws IFException { - renderXMLizable(link); + noteAction(link.getAction()); + + AttributesImpl atts = new AttributesImpl(); + atts.addAttribute(null, "rect", "rect", + XMLConstants.CDATA, IFUtil.toString(link.getTargetRect())); + try { + handler.startElement(DocumentNavigationExtensionConstants.LINK, atts); + serializeXMLizable(link.getAction()); + handler.endElement(DocumentNavigationExtensionConstants.LINK); + } catch (SAXException e) { + throw new IFException("SAX error serializing link", e); + } + } + + /** {@inheritDoc} */ + public void addResolvedAction(AbstractAction action) throws IFException { + assert action.isComplete(); + assert action.hasID(); + AbstractAction noted = (AbstractAction)incompleteActions.get(action.getID()); + if (noted != null) { + incompleteActions.remove(action.getID()); + completeActions.add(action); + } else { + //ignore as it was already complete when it was first used. + } + } + + private void commitNavigation() throws IFException { + Iterator iter = this.completeActions.iterator(); + while (iter.hasNext()) { + AbstractAction action = (AbstractAction)iter.next(); + iter.remove(); + serializeXMLizable(action); + } + assert this.completeActions.size() == 0; + } + + private void finishDocumentNavigation() { + assert this.incompleteActions.size() == 0 : "Still holding incomplete actions!"; } - private void renderXMLizable(XMLizable object) throws IFException { + private void serializeXMLizable(XMLizable object) throws IFException { try { object.toSAX(handler); } catch (SAXException e) { diff --git a/src/java/org/apache/fop/render/intermediate/extensions/AbstractAction.java b/src/java/org/apache/fop/render/intermediate/extensions/AbstractAction.java index f5bde16e1..f396fd09e 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/AbstractAction.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/AbstractAction.java @@ -61,19 +61,12 @@ public abstract class AbstractAction implements XMLizable { public abstract boolean isSame(AbstractAction other); /** - * Indicates whether this action is a reference. - * @return true if this action is a reference, false for a normal action + * Indicates whether the action is complete, i.e has all the required information to be + * rendered in the target format. + * @return true if the action is complete */ - public boolean isReference() { - return false; - } - - /** - * Creates a reference to this action. - * @return the reference - */ - public AbstractAction createReference() { - return new ReferencedAction(getID()); + public boolean isComplete() { + return true; } /** diff --git a/src/java/org/apache/fop/render/intermediate/extensions/ActionSet.java b/src/java/org/apache/fop/render/intermediate/extensions/ActionSet.java index df663baef..b980f1051 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/ActionSet.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/ActionSet.java @@ -22,21 +22,21 @@ package org.apache.fop.render.intermediate.extensions; import java.util.Iterator; import java.util.Map; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; - -import org.apache.xmlgraphics.util.XMLizable; - /** * This class manages actions and action references. Some action (like {@link GoToXYAction}s) * cannot be fully resolved at the time they are needed, so they are deferred. This class * helps manages the references and resolution. */ -public class ActionSet implements XMLizable { +public class ActionSet { private int lastGeneratedID = 0; private Map actionRegistry = new java.util.HashMap(); + /** + * Generates a new synthetic ID for an action. + * @param action the action + * @return the generated ID + */ public synchronized String generateNewID(AbstractAction action) { this.lastGeneratedID++; String prefix = action.getIDPrefix(); @@ -46,10 +46,21 @@ public class ActionSet implements XMLizable { return prefix + this.lastGeneratedID; } + /** + * Returns the action with the given ID. + * @param id the ID + * @return the action or null if no action with this ID is stored + */ public AbstractAction get(String id) { return (AbstractAction)this.actionRegistry.get(id); } + /** + * Puts an action into the set and returns the normalized instance (another one if the given + * one is equal to another. + * @param action the action + * @return the action instance that should be used in place of the given one + */ public AbstractAction put(AbstractAction action) { if (!action.hasID()) { action.setID(generateNewID(action)); @@ -61,11 +72,14 @@ public class ActionSet implements XMLizable { return effAction; } + /** + * Clears the set. + */ public void clear() { this.actionRegistry.clear(); } - public AbstractAction normalize(AbstractAction action) { + private AbstractAction normalize(AbstractAction action) { Iterator iter = this.actionRegistry.values().iterator(); while (iter.hasNext()) { AbstractAction a = (AbstractAction)iter.next(); @@ -76,12 +90,4 @@ public class ActionSet implements XMLizable { return action; } - public Iterator getActions() { - return this.actionRegistry.values().iterator(); - } - - public void toSAX(ContentHandler handler) throws SAXException { - // TODO Auto-generated method stub - - } } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/Bookmark.java b/src/java/org/apache/fop/render/intermediate/extensions/Bookmark.java index b4c4bf192..5921ae676 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/Bookmark.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/Bookmark.java @@ -20,22 +20,12 @@ package org.apache.fop.render.intermediate.extensions; import java.util.Collections; -import java.util.Iterator; import java.util.List; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.AttributesImpl; - -import org.apache.xmlgraphics.util.XMLizable; - -import org.apache.fop.util.XMLConstants; -import org.apache.fop.util.XMLUtil; - /** * This class is a bookmark element for use in the intermediate format. */ -public class Bookmark implements XMLizable, DocumentNavigationExtensionConstants { +public class Bookmark { private String title; private boolean show; @@ -109,30 +99,4 @@ public class Bookmark implements XMLizable, DocumentNavigationExtensionConstants } } - /** {@inheritDoc} */ - public void toSAX(ContentHandler handler) throws SAXException { - AttributesImpl atts = new AttributesImpl(); - atts.addAttribute(null, "title", "title", XMLUtil.CDATA, getTitle()); - atts.addAttribute(null, "starting-state", "starting-state", - XMLUtil.CDATA, isShown() ? "show" : "hide"); - if (getAction().isReference()) { - atts.addAttribute(null, ACTION_REF, ACTION_REF, - XMLConstants.CDATA, getAction().getID()); - } - handler.startElement(BOOKMARK.getNamespaceURI(), - BOOKMARK.getLocalName(), BOOKMARK.getQName(), atts); - if (!getAction().isReference()) { - getAction().toSAX(handler); - } - if (this.childBookmarks != null) { - Iterator iter = this.childBookmarks.iterator(); - while (iter.hasNext()) { - Bookmark b = (Bookmark)iter.next(); - b.toSAX(handler); - } - } - handler.endElement(BOOKMARK.getNamespaceURI(), - BOOKMARK.getLocalName(), BOOKMARK.getQName()); - } - } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/BookmarkTree.java b/src/java/org/apache/fop/render/intermediate/extensions/BookmarkTree.java index 77e9726b3..e9fc6bf95 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/BookmarkTree.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/BookmarkTree.java @@ -20,19 +20,12 @@ package org.apache.fop.render.intermediate.extensions; import java.util.Collections; -import java.util.Iterator; import java.util.List; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.AttributesImpl; - -import org.apache.xmlgraphics.util.XMLizable; - /** * This class is the root of the bookmark tree for use in the intermediate format. */ -public class BookmarkTree implements XMLizable, DocumentNavigationExtensionConstants { +public class BookmarkTree { private List bookmarks = new java.util.ArrayList(); @@ -59,18 +52,4 @@ public class BookmarkTree implements XMLizable, DocumentNavigationExtensionConst return Collections.unmodifiableList(this.bookmarks); } - /** {@inheritDoc} */ - public void toSAX(ContentHandler handler) throws SAXException { - AttributesImpl atts = new AttributesImpl(); - handler.startElement(BOOKMARK_TREE.getNamespaceURI(), - BOOKMARK_TREE.getLocalName(), BOOKMARK_TREE.getQName(), atts); - Iterator iter = this.bookmarks.iterator(); - while (iter.hasNext()) { - Bookmark b = (Bookmark)iter.next(); - b.toSAX(handler); - } - handler.endElement(BOOKMARK_TREE.getNamespaceURI(), - BOOKMARK_TREE.getLocalName(), BOOKMARK_TREE.getQName()); - } - } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/DocumentNavigationExtensionConstants.java b/src/java/org/apache/fop/render/intermediate/extensions/DocumentNavigationExtensionConstants.java index 6c61f219e..b5e767bb2 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/DocumentNavigationExtensionConstants.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/DocumentNavigationExtensionConstants.java @@ -28,25 +28,26 @@ import org.apache.fop.render.intermediate.IFConstants; */ public interface DocumentNavigationExtensionConstants { - /** Namespace URI for the bookmark extension */ + /** Namespace URI for the document navigation namespace */ String NAMESPACE = IFConstants.NAMESPACE + "/document-navigation"; + /** Default Namespace prefix for the document navigation namespace */ + String PREFIX = "nav"; + /** the bookmark-tree element */ - QName BOOKMARK_TREE = new QName(NAMESPACE, "bookmark-tree"); + QName BOOKMARK_TREE = new QName(NAMESPACE, PREFIX, "bookmark-tree"); /** the bookmark element */ - QName BOOKMARK = new QName(NAMESPACE, "bookmark"); + QName BOOKMARK = new QName(NAMESPACE, PREFIX, "bookmark"); /** the named-destination element */ - QName NAMED_DESTINATION = new QName(NAMESPACE, "named-destination"); + QName NAMED_DESTINATION = new QName(NAMESPACE, PREFIX, "named-destination"); /** the link element */ - QName LINK = new QName(NAMESPACE, "link"); + QName LINK = new QName(NAMESPACE, PREFIX, "link"); /** the goto-xy element */ - QName GOTO_XY = new QName(NAMESPACE, "goto-xy"); + QName GOTO_XY = new QName(NAMESPACE, PREFIX, "goto-xy"); /** the goto-uri element */ - QName GOTO_URI = new QName(NAMESPACE, "goto-uri"); + QName GOTO_URI = new QName(NAMESPACE, PREFIX, "goto-uri"); - /** Attribute name for the action reference */ - String ACTION_REF = "action-ref"; } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/GoToXYAction.java b/src/java/org/apache/fop/render/intermediate/extensions/GoToXYAction.java index 4148b76f6..7efb82a12 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/GoToXYAction.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/GoToXYAction.java @@ -32,20 +32,43 @@ import org.apache.fop.util.XMLUtil; */ public class GoToXYAction extends AbstractAction implements DocumentNavigationExtensionConstants { - private int pageIndex; + private int pageIndex = -1; private Point targetLocation; + /** + * Creates a new instance with yet unknown location. + * @param id the identifier for this action + */ + public GoToXYAction(String id) { + this(id, -1, null); + } + /** * Creates a new instance. - * @param pageIndex the page index (0-based) of the target page - * @param targetLocation the absolute location on the page (coordinates in millipoints) + * @param id the identifier for this action + * @param pageIndex the index (0-based) of the target page, -1 if the page index is + * still unknown + * @param targetLocation the absolute location on the page (coordinates in millipoints), + * or null, if the position isn't known, yet */ public GoToXYAction(String id, int pageIndex, Point targetLocation) { setID(id); - this.pageIndex = pageIndex; + if (pageIndex < 0 && targetLocation != null) { + throw new IllegalArgumentException( + "Page index may not be null if target location is known!"); + } + setPageIndex(pageIndex); setTargetLocation(targetLocation); } + /** + * Sets the index of the target page. + * @param pageIndex the index (0-based) of the target page + */ + public void setPageIndex(int pageIndex) { + this.pageIndex = pageIndex; + } + /** * Returns the page index of the target page. * @return the page index (0-based) @@ -70,6 +93,11 @@ public class GoToXYAction extends AbstractAction implements DocumentNavigationEx this.targetLocation = location; } + /** {@inheritDoc} */ + public boolean isComplete() { + return (getPageIndex() >= 0) && (getTargetLocation() != null); + } + /** {@inheritDoc} */ public boolean isSame(AbstractAction other) { if (other == null) { @@ -82,7 +110,7 @@ public class GoToXYAction extends AbstractAction implements DocumentNavigationEx if (getPageIndex() != otherAction.getPageIndex()) { return false; } - if (getTargetLocation() == null && otherAction.getTargetLocation() != null) { + if (getTargetLocation() == null || otherAction.getTargetLocation() == null) { return false; } if (!getTargetLocation().equals(otherAction.getTargetLocation())) { @@ -97,17 +125,27 @@ public class GoToXYAction extends AbstractAction implements DocumentNavigationEx setTargetLocation(new Point(0, 0)); } AttributesImpl atts = new AttributesImpl(); - if (hasID()) { + if (isComplete()) { atts.addAttribute(null, "id", "id", XMLUtil.CDATA, getID()); + atts.addAttribute(null, "page-index", "page-index", + XMLUtil.CDATA, Integer.toString(pageIndex)); + atts.addAttribute(null, "x", "x", XMLUtil.CDATA, Integer.toString(targetLocation.x)); + atts.addAttribute(null, "y", "y", XMLUtil.CDATA, Integer.toString(targetLocation.y)); + } else { + atts.addAttribute(null, "idref", "idref", XMLUtil.CDATA, getID()); } - atts.addAttribute(null, "page-index", "page-index", - XMLUtil.CDATA, Integer.toString(pageIndex)); - atts.addAttribute(null, "x", "x", XMLUtil.CDATA, Integer.toString(targetLocation.x)); - atts.addAttribute(null, "y", "y", XMLUtil.CDATA, Integer.toString(targetLocation.y)); handler.startElement(GOTO_XY.getNamespaceURI(), GOTO_XY.getLocalName(), GOTO_XY.getQName(), atts); handler.endElement(GOTO_XY.getNamespaceURI(), GOTO_XY.getLocalName(), GOTO_XY.getQName()); } + /** {@inheritDoc} */ + public String toString() { + return "GoToXY: ID=" + getID() + + ", page=" + getPageIndex() + + ", loc=" + getTargetLocation() + ", " + + (isComplete() ? "complete" : "INCOMPLETE"); + } + } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/Link.java b/src/java/org/apache/fop/render/intermediate/extensions/Link.java index b54697ac4..8e1558d55 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/Link.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/Link.java @@ -21,22 +21,10 @@ package org.apache.fop.render.intermediate.extensions; import java.awt.Rectangle; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.AttributesImpl; - -import org.apache.xmlgraphics.util.XMLizable; - -import org.apache.fop.render.intermediate.IFUtil; -import org.apache.fop.util.XMLConstants; - /** * This class is a link element for use in the intermediate format. */ -public class Link implements XMLizable, DocumentNavigationExtensionConstants { - - /** Attribute name for the target rectangle */ - public static final String RECT = "rect"; +public class Link { private AbstractAction action; private Rectangle targetRect; @@ -75,25 +63,4 @@ public class Link implements XMLizable, DocumentNavigationExtensionConstants { this.action = action; } - /** {@inheritDoc} */ - public void toSAX(ContentHandler handler) throws SAXException { - if (getAction() == null) { - throw new IllegalStateException("Action has not been set"); - } - AttributesImpl atts = new AttributesImpl(); - atts.addAttribute(null, RECT, RECT, - XMLConstants.CDATA, IFUtil.toString(getTargetRect())); - if (getAction().isReference()) { - atts.addAttribute(null, ACTION_REF, ACTION_REF, - XMLConstants.CDATA, getAction().getID()); - } - handler.startElement(LINK.getNamespaceURI(), - LINK.getLocalName(), LINK.getQName(), atts); - if (!getAction().isReference()) { - getAction().toSAX(handler); - } - handler.endElement(LINK.getNamespaceURI(), - LINK.getLocalName(), LINK.getQName()); - } - } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/NamedDestination.java b/src/java/org/apache/fop/render/intermediate/extensions/NamedDestination.java index 21676832b..836d8abe0 100644 --- a/src/java/org/apache/fop/render/intermediate/extensions/NamedDestination.java +++ b/src/java/org/apache/fop/render/intermediate/extensions/NamedDestination.java @@ -19,21 +19,11 @@ package org.apache.fop.render.intermediate.extensions; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.AttributesImpl; - -import org.apache.xmlgraphics.util.XMLizable; - -import org.apache.fop.util.XMLConstants; /** * This class is a named destination element for use in the intermediate format. */ -public class NamedDestination implements XMLizable, DocumentNavigationExtensionConstants { - - /** Attribute name for the destination name */ - public static final String NAME = "name"; +public class NamedDestination { private String name; private AbstractAction action; @@ -72,24 +62,4 @@ public class NamedDestination implements XMLizable, DocumentNavigationExtensionC this.action = action; } - /** {@inheritDoc} */ - public void toSAX(ContentHandler handler) throws SAXException { - if (getAction() == null) { - throw new IllegalStateException("Action has not been set"); - } - AttributesImpl atts = new AttributesImpl(); - atts.addAttribute(null, NAME, NAME, XMLConstants.CDATA, getName()); - if (getAction().isReference()) { - atts.addAttribute(null, ACTION_REF, ACTION_REF, - XMLConstants.CDATA, getAction().getID()); - } - handler.startElement(NAMED_DESTINATION.getNamespaceURI(), - NAMED_DESTINATION.getLocalName(), NAMED_DESTINATION.getQName(), atts); - if (!getAction().isReference()) { - getAction().toSAX(handler); - } - handler.endElement(NAMED_DESTINATION.getNamespaceURI(), - NAMED_DESTINATION.getLocalName(), NAMED_DESTINATION.getQName()); - } - } diff --git a/src/java/org/apache/fop/render/intermediate/extensions/ReferencedAction.java b/src/java/org/apache/fop/render/intermediate/extensions/ReferencedAction.java deleted file mode 100644 index 20ec11362..000000000 --- a/src/java/org/apache/fop/render/intermediate/extensions/ReferencedAction.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* $Id$ */ - -package org.apache.fop.render.intermediate.extensions; - -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; - -/** - * Action class which references another action. - */ -public class ReferencedAction extends AbstractAction - implements DocumentNavigationExtensionConstants { - - /** - * Creates a new instance. - * @param id the ID - */ - public ReferencedAction(String id) { - if (id == null || id.length() == 0) { - throw new NullPointerException("ID must not be set"); - } - setID(id); - } - - /** {@inheritDoc} */ - public boolean isReference() { - return true; - } - - /** {@inheritDoc} */ - public void toSAX(ContentHandler handler) throws SAXException { - //nop, handled by referencer - } - - /** {@inheritDoc} */ - public boolean isSame(AbstractAction other) { - throw new UnsupportedOperationException( - "isSame() may not be called on " + getClass().getName()); - } - -} diff --git a/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java b/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java index 6535be758..32ab2fa7a 100644 --- a/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java +++ b/src/java/org/apache/fop/render/pdf/PDFDocumentHandler.java @@ -142,7 +142,6 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler { /** {@inheritDoc} */ public void endDocument() throws IFException { try { - this.documentNavigationHandler.commit(); pdfDoc.getResources().addFonts(pdfDoc, fontInfo); pdfDoc.outputTrailer(this.outputStream); @@ -206,6 +205,7 @@ public class PDFDocumentHandler extends AbstractBinaryWritingIFDocumentHandler { /** {@inheritDoc} */ public void endPage() throws IFException { try { + this.documentNavigationHandler.commit(); this.pdfDoc.registerObject(generator.getStream()); currentPage.setContents(generator.getStream()); PDFAnnotList annots = currentPage.getAnnotations(); diff --git a/src/java/org/apache/fop/render/pdf/PDFDocumentNavigationHandler.java b/src/java/org/apache/fop/render/pdf/PDFDocumentNavigationHandler.java index a50fb85db..d09957a39 100644 --- a/src/java/org/apache/fop/render/pdf/PDFDocumentNavigationHandler.java +++ b/src/java/org/apache/fop/render/pdf/PDFDocumentNavigationHandler.java @@ -23,18 +23,17 @@ import java.awt.Rectangle; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Iterator; -import java.util.List; +import java.util.Map; import org.apache.fop.pdf.PDFAction; import org.apache.fop.pdf.PDFDocument; import org.apache.fop.pdf.PDFFactory; +import org.apache.fop.pdf.PDFGoTo; import org.apache.fop.pdf.PDFLink; import org.apache.fop.pdf.PDFOutline; -import org.apache.fop.pdf.PDFReference; import org.apache.fop.render.intermediate.IFDocumentNavigationHandler; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.extensions.AbstractAction; -import org.apache.fop.render.intermediate.extensions.ActionSet; import org.apache.fop.render.intermediate.extensions.Bookmark; import org.apache.fop.render.intermediate.extensions.BookmarkTree; import org.apache.fop.render.intermediate.extensions.GoToXYAction; @@ -50,8 +49,8 @@ public class PDFDocumentNavigationHandler implements IFDocumentNavigationHandler private PDFDocumentHandler documentHandler; - private ActionSet actionSet = new ActionSet(); - private List deferredLinks = new java.util.ArrayList(); + private Map incompleteActions = new java.util.HashMap(); + private Map completeActions = new java.util.HashMap(); /** * Default constructor. @@ -68,9 +67,9 @@ public class PDFDocumentNavigationHandler implements IFDocumentNavigationHandler /** {@inheritDoc} */ public void renderNamedDestination(NamedDestination destination) throws IFException { - PDFReference actionRef = getAction(destination.getAction()); + PDFAction action = getAction(destination.getAction()); getPDFDoc().getFactory().makeDestination( - destination.getName(), actionRef); + destination.getName(), action.makeReference()); } /** {@inheritDoc} */ @@ -86,9 +85,9 @@ public class PDFDocumentNavigationHandler implements IFDocumentNavigationHandler if (parent == null) { parent = getPDFDoc().getOutlineRoot(); } - PDFReference actionRef = getAction(bookmark.getAction()); + PDFAction action = getAction(bookmark.getAction()); PDFOutline pdfOutline = getPDFDoc().getFactory().makeOutline(parent, - bookmark.getTitle(), actionRef.toString(), bookmark.isShown()); + bookmark.getTitle(), action.makeReference().toString(), bookmark.isShown()); Iterator iter = bookmark.getChildBookmarks().iterator(); while (iter.hasNext()) { Bookmark b = (Bookmark)iter.next(); @@ -98,65 +97,85 @@ public class PDFDocumentNavigationHandler implements IFDocumentNavigationHandler /** {@inheritDoc} */ public void renderLink(Link link) throws IFException { - this.deferredLinks.add(link); + Rectangle targetRect = link.getTargetRect(); + int pageHeight = documentHandler.currentPageRef.getPageDimension().height; + Rectangle2D targetRect2D = new Rectangle2D.Double( + targetRect.getMinX() / 1000.0, + (pageHeight - targetRect.getMinY() - targetRect.getHeight()) / 1000.0, + targetRect.getWidth() / 1000.0, + targetRect.getHeight() / 1000.0); + PDFAction pdfAction = getAction(link.getAction()); + //makeLink() currently needs a PDFAction and not a reference + //TODO Revisit when PDFLink is converted to a PDFDictionary + PDFLink pdfLink = getPDFDoc().getFactory().makeLink( + targetRect2D, pdfAction); + documentHandler.currentPage.addAnnotation(pdfLink); } /** * Commits all pending elements to the PDF document. */ public void commit() { - Iterator iter = this.deferredLinks.iterator(); - while (iter.hasNext()) { - Link link = (Link)iter.next(); - Rectangle targetRect = link.getTargetRect(); - int pageHeight = documentHandler.currentPageRef.getPageDimension().height; - Rectangle2D targetRect2D = new Rectangle2D.Double( - targetRect.getMinX() / 1000.0, - (pageHeight - targetRect.getMinY()) / 1000.0, - targetRect.getWidth() / 1000.0, - targetRect.getHeight() / 1000.0); - PDFReference actionRef = getAction(link.getAction()); - //makeLink() currently needs a PDFAction and not a reference - //TODO Revisit when PDFLink is converted to a PDFDictionary - PDFAction pdfAction = (PDFAction)actionRef.getObject(); - PDFLink pdfLink = getPDFDoc().getFactory().makeLink( - targetRect2D, pdfAction); - documentHandler.currentPage.addAnnotation(pdfLink); - } } /** {@inheritDoc} */ public void addResolvedAction(AbstractAction action) throws IFException { - actionSet.put(action); + assert action.isComplete(); + PDFAction pdfAction = (PDFAction)this.incompleteActions.remove(action.getID()); + if (pdfAction == null) { + getAction(action); + } else if (pdfAction instanceof PDFGoTo) { + PDFGoTo pdfGoTo = (PDFGoTo)pdfAction; + updateTargetLocation(pdfGoTo, (GoToXYAction)action); + } else { + throw new UnsupportedOperationException( + "Action type not supported: " + pdfAction.getClass().getName()); + } } - private PDFReference getAction(AbstractAction action) { - if (action.isReference()) { - String id = action.getID(); - action = actionSet.get(id); - if (action == null) { - throw new IllegalStateException("Action could not be resolved: " + id); - } - } - PDFFactory factory = getPDFDoc().getFactory(); - if (action instanceof GoToXYAction) { + private PDFAction getAction(AbstractAction action) { + PDFAction pdfAction = (PDFAction)this.completeActions.get(action.getID()); + if (pdfAction != null) { + return pdfAction; + } else if (action instanceof GoToXYAction) { GoToXYAction a = (GoToXYAction)action; - PageReference pageRef = this.documentHandler.getPageReference(a.getPageIndex()); - //Convert target location from millipoints to points and adjust for different - //page origin - Point2D p2d = new Point2D.Double( - a.getTargetLocation().x / 1000.0, - (pageRef.getPageDimension().height - a.getTargetLocation().y) / 1000.0); - return factory.getPDFGoTo(pageRef.getPageRef().toString(), p2d).makeReference(); + PDFGoTo pdfGoTo = new PDFGoTo(null); + getPDFDoc().assignObjectNumber(pdfGoTo); + if (action.isComplete()) { + updateTargetLocation(pdfGoTo, a); + } else { + this.incompleteActions.put(action.getID(), pdfGoTo); + } + return pdfGoTo; } else if (action instanceof URIAction) { URIAction u = (URIAction)action; - PDFAction pdfAction = factory.getExternalAction(u.getURI(), u.isNewWindow()); + assert u.isComplete(); + PDFFactory factory = getPDFDoc().getFactory(); + pdfAction = factory.getExternalAction(u.getURI(), u.isNewWindow()); getPDFDoc().registerObject(pdfAction); - return pdfAction.makeReference(); + this.completeActions.put(action.getID(), pdfAction); + return pdfAction; } else { throw new UnsupportedOperationException("Unsupported action type: " + action + " (" + action.getClass().getName() + ")"); } } + private void updateTargetLocation(PDFGoTo pdfGoTo, GoToXYAction action) { + PageReference pageRef = this.documentHandler.getPageReference(action.getPageIndex()); + //Convert target location from millipoints to points and adjust for different + //page origin + Point2D p2d = null; + p2d = new Point2D.Double( + action.getTargetLocation().x / 1000.0, + (pageRef.getPageDimension().height - action.getTargetLocation().y) / 1000.0); + String pdfPageRef = pageRef.getPageRef().toString(); + pdfGoTo.setPageReference(pdfPageRef); + pdfGoTo.setPosition(p2d); + + //Queue this object now that it's complete + getPDFDoc().addObject(pdfGoTo); + this.completeActions.put(action.getID(), pdfGoTo); + } + } diff --git a/src/java/org/apache/fop/util/GenerationHelperContentHandler.java b/src/java/org/apache/fop/util/GenerationHelperContentHandler.java index 11a7c8f6a..64fabbc8a 100644 --- a/src/java/org/apache/fop/util/GenerationHelperContentHandler.java +++ b/src/java/org/apache/fop/util/GenerationHelperContentHandler.java @@ -24,6 +24,8 @@ import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; +import org.apache.xmlgraphics.util.QName; + /** * This class is a delegating SAX ContentHandler which has the purpose to provide a few handy * methods that make life easier when generating SAX events. @@ -79,8 +81,27 @@ public class GenerationHelperContentHandler extends DelegatingContentHandler { * @throws SAXException if a SAX exception occurs */ public void startElement(String localName) throws SAXException { - getDelegateContentHandler().startElement(getMainNamespace(), localName, localName, - EMPTY_ATTS); + startElement(localName, EMPTY_ATTS); + } + + /** + * Convenience method to generate a startElement SAX event. + * @param qName the qualified name of the element + * @param atts the attributes + * @throws SAXException if a SAX exception occurs + */ + public void startElement(QName qName, Attributes atts) throws SAXException { + getDelegateContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalName(), + qName.getQName(), atts); + } + + /** + * Convenience method to generate a startElement SAX event. + * @param qName the qualified name of the element + * @throws SAXException if a SAX exception occurs + */ + public void startElement(QName qName) throws SAXException { + startElement(qName, EMPTY_ATTS); } /** @@ -93,7 +114,17 @@ public class GenerationHelperContentHandler extends DelegatingContentHandler { } /** - * Convenience method to generate an empty element. + * Convenience method to generate a startElement SAX event. + * @param qName the qualified name of the element + * @throws SAXException if a SAX exception occurs + */ + public void endElement(QName qName) throws SAXException { + getDelegateContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalName(), + qName.getQName()); + } + + /** + * Convenience method to generate an empty element with attributes. * @param localName the local name of the element * @param atts the attributes * @throws SAXException if a SAX exception occurs @@ -103,4 +134,17 @@ public class GenerationHelperContentHandler extends DelegatingContentHandler { getDelegateContentHandler().endElement(getMainNamespace(), localName, localName); } + /** + * Convenience method to generate an empty element with attributes. + * @param qName the qualified name of the element + * @param atts the attributes + * @throws SAXException if a SAX exception occurs + */ + public void element(QName qName, Attributes atts) throws SAXException { + getDelegateContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalName(), + qName.getQName(), atts); + getDelegateContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalName(), + qName.getQName()); + } + }