/* * 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; import java.awt.Color; import java.awt.Dimension; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import org.apache.xmlgraphics.util.QName; import org.apache.xmlgraphics.util.XMLizable; import org.apache.fop.fonts.FontInfo; import org.apache.fop.render.PrintRendererConfigurator; 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; /** * IFPainter implementation that serializes the intermediate format to XML. */ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler implements IFConstants, IFPainter, IFDocumentNavigationHandler { private IFDocumentHandler mimicHandler; private int pageSequenceCounter; // used for accessibility private DocumentBuilder parser = null; // used for accessibility private Document doc = null; // used for accessibility /** Holds the intermediate format state */ private IFState state; private static class NamespaceContextImpl implements NamespaceContext { public String uri; public String prefix; public NamespaceContextImpl() { } public NamespaceContextImpl(String prefix, String uri) { this.uri = uri; this.prefix = prefix; } public String getNamespaceURI(String prefix) { return uri; } public void setNamespaceURI(String uri) { this.uri = uri; } public String getPrefix(String uri) { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public Iterator getPrefixes(String uri) { return null; } } /** * Default constructor. */ public IFSerializer() { } /** {@inheritDoc} */ protected String getMainNamespace() { return NAMESPACE; } /** {@inheritDoc} */ public boolean supportsPagesOutOfOrder() { return false; //Theoretically supported but disabled to improve performance when //rendering the IF to the final format later on } /** {@inheritDoc} */ public String getMimeType() { return MIME_TYPE; } /** {@inheritDoc} */ public IFDocumentHandlerConfigurator getConfigurator() { if (this.mimicHandler != null) { return getMimickedDocumentHandler().getConfigurator(); } else { return new PrintRendererConfigurator(getUserAgent()); } } /** {@inheritDoc} */ public IFDocumentNavigationHandler getDocumentNavigationHandler() { 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; } /** {@inheritDoc} */ public FontInfo getFontInfo() { if (this.mimicHandler != null) { return this.mimicHandler.getFontInfo(); } else { return null; } } /** {@inheritDoc} */ public void setFontInfo(FontInfo fontInfo) { if (this.mimicHandler != null) { this.mimicHandler.setFontInfo(fontInfo); } } /** {@inheritDoc} */ public void setDefaultFontInfo(FontInfo fontInfo) { if (this.mimicHandler != null) { this.mimicHandler.setDefaultFontInfo(fontInfo); } } /** {@inheritDoc} */ public void startDocument() throws IFException { super.startDocument(); try { handler.startDocument(); handler.startPrefixMapping("", NAMESPACE); handler.startPrefixMapping(XLINK_PREFIX, XLINK_NAMESPACE); handler.startPrefixMapping(DocumentNavigationExtensionConstants.PREFIX, DocumentNavigationExtensionConstants.NAMESPACE); handler.startElement(EL_DOCUMENT); if (this.getUserAgent().accessibilityEnabled()) { pageSequenceCounter = 0; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); parser = factory.newDocumentBuilder(); } } catch (SAXException e) { throw new IFException("SAX error in startDocument()", e); } catch (ParserConfigurationException pce) { throw new IFException("Error creating new DocumentBuilder", pce); } } /** {@inheritDoc} */ public void startDocumentHeader() throws IFException { try { handler.startElement(EL_HEADER); } catch (SAXException e) { throw new IFException("SAX error in startDocumentHeader()", e); } } /** {@inheritDoc} */ public void endDocumentHeader() throws IFException { try { handler.endElement(EL_HEADER); } catch (SAXException e) { throw new IFException("SAX error in startDocumentHeader()", e); } } /** {@inheritDoc} */ public void startDocumentTrailer() throws IFException { try { handler.startElement(EL_TRAILER); } catch (SAXException e) { throw new IFException("SAX error in startDocumentTrailer()", e); } } /** {@inheritDoc} */ public void endDocumentTrailer() throws IFException { try { handler.endElement(EL_TRAILER); } catch (SAXException e) { throw new IFException("SAX error in endDocumentTrailer()", e); } } /** {@inheritDoc} */ public void endDocument() throws IFException { try { handler.endElement(EL_DOCUMENT); handler.endDocument(); finishDocumentNavigation(); } catch (SAXException e) { throw new IFException("SAX error in endDocument()", e); } } /** {@inheritDoc} */ public void startPageSequence(String id) throws IFException { try { AttributesImpl atts = new AttributesImpl(); if (id != null) { atts.addAttribute(XML_NAMESPACE, "id", "xml:id", XMLUtil.CDATA, id); } handler.startElement(EL_PAGE_SEQUENCE, atts); if (this.getUserAgent().accessibilityEnabled()) { if (doc == null) { doc = parser.parse( new ByteArrayInputStream(this.getUserAgent().getReducedFOTree())); } handler.startElement(EL_STRUCTURE_TREE); // add structure tree String xpathExpr = "/fo:root/fo:page-sequence[" + Integer.toString(++pageSequenceCounter) + "]/*"; XPath xpath = XPathFactory.newInstance().newXPath(); NamespaceContext namespaceContext = new NamespaceContextImpl("fo", "http://www.w3.org/1999/XSL/Format"); xpath.setNamespaceContext(namespaceContext); NodeList nodes = (NodeList)xpath.evaluate(xpathExpr, doc, XPathConstants.NODESET); for (int i = 0, n = nodes.getLength(); i < n; i++) { Node node = nodes.item(i); new DOM2SAX(handler).writeFragment(node); } handler.endElement(EL_STRUCTURE_TREE); } } catch (SAXException e) { throw new IFException("SAX error in startPageSequence()", e); } catch (XPathExpressionException e) { throw new IFException("Error while evaluating XPath expression", e); } catch (IOException ioe) { throw new IFException("I/O error while parsing structure tree", ioe); } } /** {@inheritDoc} */ public void endPageSequence() throws IFException { try { handler.endElement(EL_PAGE_SEQUENCE); } catch (SAXException e) { throw new IFException("SAX error in endPageSequence()", e); } } /** {@inheritDoc} */ public void startPage(int index, String name, String pageMasterName, Dimension size) throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "index", Integer.toString(index)); addAttribute(atts, "name", name); addAttribute(atts, "page-master-name", pageMasterName); addAttribute(atts, "width", Integer.toString(size.width)); addAttribute(atts, "height", Integer.toString(size.height)); addForeignAttributes(atts); handler.startElement(EL_PAGE, atts); } catch (SAXException e) { throw new IFException("SAX error in startPage()", e); } } /** {@inheritDoc} */ public void startPageHeader() throws IFException { try { handler.startElement(EL_PAGE_HEADER); } catch (SAXException e) { throw new IFException("SAX error in startPageHeader()", e); } } /** {@inheritDoc} */ public void endPageHeader() throws IFException { try { handler.endElement(EL_PAGE_HEADER); } catch (SAXException e) { throw new IFException("SAX error in endPageHeader()", e); } } /** {@inheritDoc} */ 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); } } /** {@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); } } /** {@inheritDoc} */ public void startPageTrailer() throws IFException { try { handler.startElement(EL_PAGE_TRAILER); commitNavigation(); } catch (SAXException e) { throw new IFException("SAX error in startPageTrailer()", e); } } /** {@inheritDoc} */ public void endPageTrailer() throws IFException { try { handler.endElement(EL_PAGE_TRAILER); } catch (SAXException e) { throw new IFException("SAX error in endPageTrailer()", e); } } /** {@inheritDoc} */ public void endPage() throws IFException { try { handler.endElement(EL_PAGE); } catch (SAXException e) { throw new IFException("SAX error in endPage()", e); } } //---=== IFPainter ===--- /** {@inheritDoc} */ public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) throws IFException { startViewport(IFUtil.toString(transform), size, clipRect); } /** {@inheritDoc} */ public void startViewport(AffineTransform[] transforms, Dimension size, Rectangle clipRect) throws IFException { startViewport(IFUtil.toString(transforms), size, clipRect); } private void startViewport(String transform, Dimension size, Rectangle clipRect) throws IFException { try { AttributesImpl atts = new AttributesImpl(); if (transform != null && transform.length() > 0) { addAttribute(atts, "transform", transform); } addAttribute(atts, "width", Integer.toString(size.width)); addAttribute(atts, "height", Integer.toString(size.height)); if (clipRect != null) { addAttribute(atts, "clip-rect", IFUtil.toString(clipRect)); } handler.startElement(EL_VIEWPORT, atts); } catch (SAXException e) { throw new IFException("SAX error in startViewport()", e); } } /** {@inheritDoc} */ public void endViewport() throws IFException { try { handler.endElement(EL_VIEWPORT); } catch (SAXException e) { throw new IFException("SAX error in endViewport()", e); } } /** {@inheritDoc} */ public void startGroup(AffineTransform[] transforms) throws IFException { startGroup(IFUtil.toString(transforms)); } /** {@inheritDoc} */ public void startGroup(AffineTransform transform) throws IFException { startGroup(IFUtil.toString(transform)); } private void startGroup(String transform) throws IFException { try { AttributesImpl atts = new AttributesImpl(); if (transform != null && transform.length() > 0) { addAttribute(atts, "transform", transform); } handler.startElement(EL_GROUP, atts); } catch (SAXException e) { throw new IFException("SAX error in startGroup()", e); } } /** {@inheritDoc} */ public void endGroup() throws IFException { try { handler.endElement(EL_GROUP); } catch (SAXException e) { throw new IFException("SAX error in endGroup()", e); } } /** {@inheritDoc} */ public void drawImage(String uri, Rectangle rect, String ptr) throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, XLINK_HREF, uri); addAttribute(atts, "x", Integer.toString(rect.x)); addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); addForeignAttributes(atts); if (ptr != null) { addAttribute(atts, "ptr", ptr); // used for accessibility } handler.element(EL_IMAGE, atts); } catch (SAXException e) { throw new IFException("SAX error in startGroup()", e); } } private void addForeignAttributes(AttributesImpl atts) { Map foreignAttributes = getContext().getForeignAttributes(); if (!foreignAttributes.isEmpty()) { Iterator iter = foreignAttributes.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); addAttribute(atts, (QName)entry.getKey(), entry.getValue().toString()); } } } /** {@inheritDoc} */ public void drawImage(Document doc, Rectangle rect, String ptr) throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "x", Integer.toString(rect.x)); addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); addForeignAttributes(atts); if (ptr != null) { addAttribute(atts, "ptr", ptr); // used for accessibility } handler.startElement(EL_IMAGE, atts); new DOM2SAX(handler).writeDocument(doc, true); handler.endElement(EL_IMAGE); } catch (SAXException e) { throw new IFException("SAX error in startGroup()", e); } } private static String toString(Paint paint) { if (paint instanceof Color) { return ColorUtil.colorToString((Color)paint); } else { throw new UnsupportedOperationException("Paint not supported: " + paint); } } /** {@inheritDoc} */ public void clipRect(Rectangle rect) throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "x", Integer.toString(rect.x)); addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); handler.element(EL_CLIP_RECT, atts); } catch (SAXException e) { throw new IFException("SAX error in clipRect()", e); } } /** {@inheritDoc} */ public void fillRect(Rectangle rect, Paint fill) throws IFException { if (fill == null) { return; } try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "x", Integer.toString(rect.x)); addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); addAttribute(atts, "fill", toString(fill)); handler.element(EL_RECT, atts); } catch (SAXException e) { throw new IFException("SAX error in fillRect()", e); } } /** {@inheritDoc} */ public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, BorderProps start, BorderProps end) throws IFException { if (before == null && after == null && start == null && end == null) { return; } try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "x", Integer.toString(rect.x)); addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); if (before != null) { addAttribute(atts, "before", before.toString()); } if (after != null) { addAttribute(atts, "after", after.toString()); } if (start != null) { addAttribute(atts, "start", start.toString()); } if (end != null) { addAttribute(atts, "end", end.toString()); } handler.element(EL_BORDER_RECT, atts); } catch (SAXException e) { throw new IFException("SAX error in drawBorderRect()", e); } } /** {@inheritDoc} */ public void drawLine(Point start, Point end, int width, Color color, RuleStyle style) throws IFException { try { AttributesImpl atts = new AttributesImpl(); addAttribute(atts, "x1", Integer.toString(start.x)); addAttribute(atts, "y1", Integer.toString(start.y)); addAttribute(atts, "x2", Integer.toString(end.x)); addAttribute(atts, "y2", Integer.toString(end.y)); addAttribute(atts, "stroke-width", Integer.toString(width)); addAttribute(atts, "color", ColorUtil.colorToString(color)); addAttribute(atts, "style", style.getName()); handler.element(EL_LINE, atts); } catch (SAXException e) { throw new IFException("SAX error in drawLine()", e); } } /** {@inheritDoc} */ public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text, String ptr) throws IFException { try { AttributesImpl atts = new AttributesImpl(); XMLUtil.addAttribute(atts, XMLConstants.XML_SPACE, "preserve"); addAttribute(atts, "x", Integer.toString(x)); addAttribute(atts, "y", Integer.toString(y)); if (letterSpacing != 0) { addAttribute(atts, "letter-spacing", Integer.toString(letterSpacing)); } if (wordSpacing != 0) { addAttribute(atts, "word-spacing", Integer.toString(wordSpacing)); } if (dx != null) { addAttribute(atts, "dx", IFUtil.toString(dx)); } if (ptr != null) { addAttribute(atts, "ptr", ptr); // used for accessibility } handler.startElement(EL_TEXT, atts); char[] chars = text.toCharArray(); handler.characters(chars, 0, chars.length); handler.endElement(EL_TEXT); } catch (SAXException e) { throw new IFException("SAX error in setFont()", e); } } /** {@inheritDoc} */ public void setFont(String family, String style, Integer weight, String variant, Integer size, Color color) throws IFException { try { AttributesImpl atts = new AttributesImpl(); boolean changed; if (family != null) { changed = !family.equals(state.getFontFamily()); if (changed) { state.setFontFamily(family); addAttribute(atts, "family", family); } } if (style != null) { changed = !style.equals(state.getFontStyle()); if (changed) { state.setFontStyle(style); addAttribute(atts, "style", style); } } if (weight != null) { changed = (weight.intValue() != state.getFontWeight()); if (changed) { state.setFontWeight(weight.intValue()); addAttribute(atts, "weight", weight.toString()); } } if (variant != null) { changed = !variant.equals(state.getFontVariant()); if (changed) { state.setFontVariant(variant); addAttribute(atts, "variant", variant); } } if (size != null) { changed = (size.intValue() != state.getFontSize()); if (changed) { state.setFontSize(size.intValue()); addAttribute(atts, "size", size.toString()); } } if (color != null) { changed = !color.equals(state.getTextColor()); if (changed) { state.setTextColor(color); addAttribute(atts, "color", toString(color)); } } if (atts.getLength() > 0) { handler.element(EL_FONT, atts); } } catch (SAXException e) { throw new IFException("SAX error in setFont()", e); } } /** {@inheritDoc} */ public void handleExtensionObject(Object extension) throws IFException { if (extension instanceof XMLizable) { try { ((XMLizable)extension).toSAX(this.handler); } catch (SAXException e) { throw new IFException("SAX error while handling extension object", e); } } else { throw new UnsupportedOperationException( "Extension must implement XMLizable: " + extension + " (" + extension.getClass().getName() + ")"); } } /** {@inheritDoc} */ protected RenderingContext createRenderingContext() { throw new IllegalStateException("Should never be called!"); } private void addAttribute(AttributesImpl atts, org.apache.xmlgraphics.util.QName attribute, String value) { XMLUtil.addAttribute(atts, attribute, value); } private void addAttribute(AttributesImpl atts, String localName, String value) { XMLUtil.addAttribute(atts, localName, value); } // ---=== 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 { 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 { 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); } } 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 { 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.remove(action.getID()); if (noted != null) { 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 serializeXMLizable(XMLizable object) throws IFException { try { object.toSAX(handler); } catch (SAXException e) { throw new IFException("SAX error serializing object", e); } } }