| /* |
| |
| 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. |
| |
| */ |
| package org.apache.batik.bridge; |
| |
| import java.awt.AlphaComposite; |
| import java.awt.Color; |
| import java.awt.RenderingHints; |
| import java.awt.Shape; |
| import java.awt.font.TextAttribute; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.lang.ref.SoftReference; |
| import java.text.AttributedCharacterIterator; |
| import java.text.AttributedCharacterIterator.Attribute; |
| import java.text.AttributedString; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.WeakHashMap; |
| |
| import org.apache.batik.css.engine.CSSEngineEvent; |
| import org.apache.batik.css.engine.CSSStylableElement; |
| import org.apache.batik.css.engine.SVGCSSEngine; |
| import org.apache.batik.css.engine.StyleMap; |
| import org.apache.batik.css.engine.value.ListValue; |
| import org.apache.batik.css.engine.value.Value; |
| import org.apache.batik.dom.events.NodeEventTarget; |
| import org.apache.batik.dom.svg.AbstractSVGAnimatedLength; |
| import org.apache.batik.dom.svg.AnimatedLiveAttributeValue; |
| import org.apache.batik.dom.svg.LiveAttributeException; |
| import org.apache.batik.dom.svg.SVGContext; |
| import org.apache.batik.dom.svg.SVGOMAnimatedEnumeration; |
| import org.apache.batik.dom.svg.SVGOMAnimatedLengthList; |
| import org.apache.batik.dom.svg.SVGOMAnimatedNumberList; |
| import org.apache.batik.dom.svg.SVGOMElement; |
| import org.apache.batik.dom.svg.SVGOMTextPositioningElement; |
| import org.apache.batik.dom.svg.SVGTextContent; |
| import org.apache.batik.dom.util.XLinkSupport; |
| import org.apache.batik.dom.util.XMLSupport; |
| import org.apache.batik.gvt.GraphicsNode; |
| import org.apache.batik.gvt.TextNode; |
| import org.apache.batik.gvt.font.GVTFont; |
| import org.apache.batik.gvt.font.GVTFontFamily; |
| import org.apache.batik.gvt.font.GVTGlyphMetrics; |
| import org.apache.batik.gvt.font.GVTGlyphVector; |
| import org.apache.batik.gvt.font.UnresolvedFontFamily; |
| import org.apache.batik.gvt.renderer.StrokingTextPainter; |
| import org.apache.batik.gvt.text.GVTAttributedCharacterIterator; |
| import org.apache.batik.gvt.text.Mark; |
| import org.apache.batik.gvt.text.TextHit; |
| import org.apache.batik.gvt.text.TextPaintInfo; |
| import org.apache.batik.gvt.text.TextPath; |
| import org.apache.batik.gvt.text.TextSpanLayout; |
| import org.apache.batik.util.XMLConstants; |
| |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.css.CSSPrimitiveValue; |
| import org.w3c.dom.css.CSSValue; |
| import org.w3c.dom.events.Event; |
| import org.w3c.dom.events.EventListener; |
| import org.w3c.dom.events.MutationEvent; |
| import org.w3c.dom.svg.SVGLengthList; |
| import org.w3c.dom.svg.SVGNumberList; |
| import org.w3c.dom.svg.SVGTextContentElement; |
| import org.w3c.dom.svg.SVGTextPositioningElement; |
| |
| /** |
| * Bridge class for the <text> element. |
| * |
| * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a> |
| * @author <a href="mailto:bill.haneman@ireland.sun.com">Bill Haneman</a> |
| * @version $Id$ |
| */ |
| public class SVGTextElementBridge extends AbstractGraphicsNodeBridge |
| implements SVGTextContent { |
| |
| protected static final Integer ZERO = new Integer(0); |
| |
| public static final |
| AttributedCharacterIterator.Attribute TEXT_COMPOUND_DELIMITER = |
| GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER; |
| |
| public static final |
| AttributedCharacterIterator.Attribute TEXT_COMPOUND_ID = |
| GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_ID; |
| |
| public static final AttributedCharacterIterator.Attribute PAINT_INFO = |
| GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO; |
| |
| public static final |
| AttributedCharacterIterator.Attribute ALT_GLYPH_HANDLER = |
| GVTAttributedCharacterIterator.TextAttribute.ALT_GLYPH_HANDLER; |
| |
| public static final |
| AttributedCharacterIterator.Attribute TEXTPATH |
| = GVTAttributedCharacterIterator.TextAttribute.TEXTPATH; |
| |
| public static final |
| AttributedCharacterIterator.Attribute ANCHOR_TYPE |
| = GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE; |
| |
| public static final |
| AttributedCharacterIterator.Attribute GVT_FONT_FAMILIES |
| = GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES; |
| |
| public static final |
| AttributedCharacterIterator.Attribute GVT_FONTS |
| = GVTAttributedCharacterIterator.TextAttribute.GVT_FONTS; |
| |
| public static final |
| AttributedCharacterIterator.Attribute BASELINE_SHIFT |
| = GVTAttributedCharacterIterator.TextAttribute.BASELINE_SHIFT; |
| |
| protected AttributedString laidoutText; |
| |
| // This is used to track the TextPainterInfo for each element |
| // in this text element. |
| protected WeakHashMap elemTPI = new WeakHashMap(); |
| |
| // This is true if any of the spans of this text element |
| // use a 'complex' SVG font (meaning the font uses more |
| // and just the 'd' attribute on the glyph element. |
| // In this case we need to recreate the font when ever |
| // CSS attributes change on the text - so we can capture |
| // the effects of CSS inheritence. |
| protected boolean usingComplexSVGFont = false; |
| |
| /** |
| * Constructs a new bridge for the <text> element. |
| */ |
| public SVGTextElementBridge() {} |
| |
| /** |
| * Returns 'text'. |
| */ |
| public String getLocalName() { |
| return SVG_TEXT_TAG; |
| } |
| |
| /** |
| * Returns a new instance of this bridge. |
| */ |
| public Bridge getInstance() { |
| return new SVGTextElementBridge(); |
| } |
| |
| protected TextNode getTextNode() { |
| return (TextNode)node; |
| } |
| /** |
| * Creates a <code>GraphicsNode</code> according to the specified parameters. |
| * |
| * @param ctx the bridge context to use |
| * @param e the element that describes the graphics node to build |
| * @return a graphics node that represents the specified element |
| */ |
| public GraphicsNode createGraphicsNode(BridgeContext ctx, Element e) { |
| TextNode node = (TextNode)super.createGraphicsNode(ctx, e); |
| if (node == null) |
| return null; |
| |
| associateSVGContext(ctx, e, node); |
| |
| // traverse the children to add context on |
| // <tspan>, <tref> and <textPath> |
| Node child = getFirstChild(e); |
| while (child != null) { |
| if (child.getNodeType() == Node.ELEMENT_NODE) { |
| addContextToChild(ctx,(Element)child); |
| } |
| child = getNextSibling(child); |
| } |
| |
| // specify the text painter to use |
| if (ctx.getTextPainter() != null) |
| node.setTextPainter(ctx.getTextPainter()); |
| |
| // 'text-rendering' and 'color-rendering' |
| RenderingHints hints = null; |
| hints = CSSUtilities.convertColorRendering(e, hints); |
| hints = CSSUtilities.convertTextRendering (e, hints); |
| if (hints != null) |
| node.setRenderingHints(hints); |
| |
| node.setLocation(getLocation(ctx, e)); |
| |
| return node; |
| } |
| |
| /** |
| * Creates the GraphicsNode depending on the GraphicsNodeBridge |
| * implementation. |
| */ |
| protected GraphicsNode instantiateGraphicsNode() { |
| return new TextNode(); |
| } |
| |
| /** |
| * Returns the text node location according to the 'x' and 'y' |
| * attributes of the specified text element. |
| * |
| * @param ctx the bridge context to use |
| * @param e the text element |
| */ |
| protected Point2D getLocation(BridgeContext ctx, Element e) { |
| try { |
| SVGOMTextPositioningElement te = (SVGOMTextPositioningElement) e; |
| |
| // 'x' attribute - default is 0 |
| SVGOMAnimatedLengthList _x = (SVGOMAnimatedLengthList) te.getX(); |
| _x.check(); |
| SVGLengthList xs = _x.getAnimVal(); |
| float x = 0; |
| if (xs.getNumberOfItems() > 0) { |
| x = xs.getItem(0).getValue(); |
| } |
| |
| // 'y' attribute - default is 0 |
| SVGOMAnimatedLengthList _y = (SVGOMAnimatedLengthList) te.getY(); |
| _y.check(); |
| SVGLengthList ys = _y.getAnimVal(); |
| float y = 0; |
| if (ys.getNumberOfItems() > 0) { |
| y = ys.getItem(0).getValue(); |
| } |
| |
| return new Point2D.Float(x, y); |
| } catch (LiveAttributeException ex) { |
| throw new BridgeException(ctx, ex); |
| } |
| } |
| |
| protected boolean isTextElement(Element e) { |
| if (!SVG_NAMESPACE_URI.equals(e.getNamespaceURI())) |
| return false; |
| String nodeName = e.getLocalName(); |
| return (nodeName.equals(SVG_TEXT_TAG) || |
| nodeName.equals(SVG_TSPAN_TAG) || |
| nodeName.equals(SVG_ALT_GLYPH_TAG) || |
| nodeName.equals(SVG_A_TAG) || |
| nodeName.equals(SVG_TEXT_PATH_TAG) || |
| nodeName.equals(SVG_TREF_TAG)); |
| } |
| |
| protected boolean isTextChild(Element e) { |
| if (!SVG_NAMESPACE_URI.equals(e.getNamespaceURI())) |
| return false; |
| String nodeName = e.getLocalName(); |
| return (nodeName.equals(SVG_TSPAN_TAG) || |
| nodeName.equals(SVG_ALT_GLYPH_TAG) || |
| nodeName.equals(SVG_A_TAG) || |
| nodeName.equals(SVG_TEXT_PATH_TAG) || |
| nodeName.equals(SVG_TREF_TAG)); |
| } |
| |
| /** |
| * Builds using the specified BridgeContext and element, the |
| * specified graphics node. |
| * |
| * @param ctx the bridge context to use |
| * @param e the element that describes the graphics node to build |
| * @param node the graphics node to build |
| */ |
| public void buildGraphicsNode(BridgeContext ctx, |
| Element e, |
| GraphicsNode node) { |
| e.normalize(); |
| computeLaidoutText(ctx, e, node); |
| |
| // |
| // DO NOT CALL super, 'opacity' is handle during addPaintAttributes() |
| // |
| // 'opacity' |
| node.setComposite(CSSUtilities.convertOpacity(e)); |
| // 'filter' |
| node.setFilter(CSSUtilities.convertFilter(e, node, ctx)); |
| // 'mask' |
| node.setMask(CSSUtilities.convertMask(e, node, ctx)); |
| // 'clip-path' |
| node.setClip(CSSUtilities.convertClipPath(e, node, ctx)); |
| // 'pointer-events' |
| node.setPointerEventType(CSSUtilities.convertPointerEvents(e)); |
| |
| initializeDynamicSupport(ctx, e, node); |
| if (!ctx.isDynamic()) { |
| elemTPI.clear(); |
| } |
| } |
| |
| /** |
| * Returns false as text is not a container. |
| */ |
| public boolean isComposite() { |
| return false; |
| } |
| |
| // Tree navigation ------------------------------------------------------ |
| |
| /** |
| * Returns the first child node of the given node that should be |
| * processed by the text bridge. |
| */ |
| protected Node getFirstChild(Node n) { |
| return n.getFirstChild(); |
| } |
| |
| /** |
| * Returns the next sibling node of the given node that should be |
| * processed by the text bridge. |
| */ |
| protected Node getNextSibling(Node n) { |
| return n.getNextSibling(); |
| } |
| |
| /** |
| * Returns the parent node of the given node that should be |
| * processed by the text bridge. |
| */ |
| protected Node getParentNode(Node n) { |
| return n.getParentNode(); |
| } |
| |
| // Listener implementation ---------------------------------------------- |
| |
| /** |
| * The DOM EventListener to receive 'DOMNodeRemoved' event. |
| */ |
| protected DOMChildNodeRemovedEventListener childNodeRemovedEventListener; |
| |
| /** |
| * The DOM EventListener invoked when a node is removed. |
| */ |
| protected class DOMChildNodeRemovedEventListener implements EventListener { |
| |
| /** |
| * Handles 'DOMNodeRemoved' event type. |
| */ |
| public void handleEvent(Event evt) { |
| handleDOMChildNodeRemovedEvent((MutationEvent)evt); |
| } |
| } |
| |
| /** |
| * The DOM EventListener to receive 'DOMSubtreeModified' event. |
| */ |
| protected DOMSubtreeModifiedEventListener subtreeModifiedEventListener; |
| |
| /** |
| * The DOM EventListener invoked when the subtree is modified. |
| */ |
| protected class DOMSubtreeModifiedEventListener implements EventListener { |
| |
| /** |
| * Handles 'DOMSubtreeModified' event type. |
| */ |
| public void handleEvent(Event evt) { |
| handleDOMSubtreeModifiedEvent((MutationEvent)evt); |
| } |
| } |
| |
| // BridgeUpdateHandler implementation ----------------------------------- |
| |
| /** |
| * This method ensures that any modification to a text |
| * element and its children is going to be reflected |
| * into the GVT tree. |
| */ |
| protected void initializeDynamicSupport(BridgeContext ctx, |
| Element e, |
| GraphicsNode node) { |
| super.initializeDynamicSupport(ctx, e, node); |
| |
| if (ctx.isDynamic()) { |
| // Only add the listeners if we are dynamic. |
| addTextEventListeners(ctx, (NodeEventTarget) e); |
| } |
| } |
| |
| /** |
| * Adds the DOM listeners for this text bridge. |
| */ |
| protected void addTextEventListeners(BridgeContext ctx, NodeEventTarget e) { |
| if (childNodeRemovedEventListener == null) { |
| childNodeRemovedEventListener = |
| new DOMChildNodeRemovedEventListener(); |
| } |
| if (subtreeModifiedEventListener == null) { |
| subtreeModifiedEventListener = |
| new DOMSubtreeModifiedEventListener(); |
| } |
| |
| //to be notified when a child is removed from the |
| //<text> element. |
| e.addEventListenerNS |
| (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeRemoved", |
| childNodeRemovedEventListener, true, null); |
| ctx.storeEventListenerNS |
| (e, XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeRemoved", |
| childNodeRemovedEventListener, true); |
| |
| //to be notified when the modification of the subtree |
| //of the <text> element is done |
| e.addEventListenerNS |
| (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMSubtreeModified", |
| subtreeModifiedEventListener, false, null); |
| ctx.storeEventListenerNS |
| (e, XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMSubtreeModified", |
| subtreeModifiedEventListener, false); |
| } |
| |
| /** |
| * Removes the DOM listeners for this text bridge. |
| */ |
| protected void removeTextEventListeners(BridgeContext ctx, |
| NodeEventTarget e) { |
| e.removeEventListenerNS |
| (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeRemoved", |
| childNodeRemovedEventListener, true); |
| e.removeEventListenerNS |
| (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMSubtreeModified", |
| subtreeModifiedEventListener, false); |
| } |
| |
| /** |
| * Disposes this text element bridge by removing the text event listeners |
| * that were added in {@link #initializeDynamicSupport}. |
| */ |
| public void dispose() { |
| removeTextEventListeners(ctx, (NodeEventTarget) e); |
| super.dispose(); |
| } |
| |
| /** |
| * Add to the element children of the node, an |
| * <code>SVGContext</code> to support dynamic update. This is |
| * recursive, the children of the nodes are also traversed to add |
| * to the support elements their context |
| * |
| * @param ctx a <code>BridgeContext</code> value |
| * @param e an <code>Element</code> value |
| * |
| * @see org.apache.batik.dom.svg.SVGContext |
| * @see org.apache.batik.bridge.BridgeUpdateHandler |
| */ |
| protected void addContextToChild(BridgeContext ctx, Element e) { |
| if (SVG_NAMESPACE_URI.equals(e.getNamespaceURI())) { |
| if (e.getLocalName().equals(SVG_TSPAN_TAG)) { |
| ((SVGOMElement)e).setSVGContext |
| (new TspanBridge(ctx, this, e)); |
| } else if (e.getLocalName().equals(SVG_TEXT_PATH_TAG)) { |
| ((SVGOMElement)e).setSVGContext |
| (new TextPathBridge(ctx, this, e)); |
| } else if (e.getLocalName().equals(SVG_TREF_TAG)) { |
| ((SVGOMElement)e).setSVGContext |
| (new TRefBridge(ctx, this, e)); |
| } |
| } |
| |
| Node child = getFirstChild(e); |
| while (child != null) { |
| if (child.getNodeType() == Node.ELEMENT_NODE) { |
| addContextToChild(ctx, (Element)child); |
| } |
| child = getNextSibling(child); |
| } |
| } |
| |
| /** |
| * From the <code>SVGContext</code> from the element children of the node. |
| * |
| * @param ctx the <code>BridgeContext</code> for the document |
| * @param e the <code>Element</code> whose subtree's elements will have |
| * threir <code>SVGContext</code>s removed |
| * |
| * @see org.apache.batik.dom.svg.SVGContext |
| * @see org.apache.batik.bridge.BridgeUpdateHandler |
| */ |
| protected void removeContextFromChild(BridgeContext ctx, Element e) { |
| if (SVG_NAMESPACE_URI.equals(e.getNamespaceURI())) { |
| if (e.getLocalName().equals(SVG_TSPAN_TAG)) { |
| ((AbstractTextChildBridgeUpdateHandler) |
| ((SVGOMElement) e).getSVGContext()).dispose(); |
| } else if (e.getLocalName().equals(SVG_TEXT_PATH_TAG)) { |
| ((AbstractTextChildBridgeUpdateHandler) |
| ((SVGOMElement) e).getSVGContext()).dispose(); |
| } else if (e.getLocalName().equals(SVG_TREF_TAG)) { |
| ((AbstractTextChildBridgeUpdateHandler) |
| ((SVGOMElement) e).getSVGContext()).dispose(); |
| } |
| } |
| |
| Node child = getFirstChild(e); |
| while (child != null) { |
| if (child.getNodeType() == Node.ELEMENT_NODE) { |
| removeContextFromChild(ctx, (Element)child); |
| } |
| child = getNextSibling(child); |
| } |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMNodeInserted' is fired. |
| */ |
| public void handleDOMNodeInsertedEvent(MutationEvent evt) { |
| Node childNode = (Node)evt.getTarget(); |
| |
| //check the type of the node inserted before discard the layout |
| //in the case of <title> or <desc> or <metadata>, the layout |
| //is unchanged |
| switch(childNode.getNodeType()) { |
| case Node.TEXT_NODE: // fall-through is intended |
| case Node.CDATA_SECTION_NODE: |
| laidoutText = null; |
| break; |
| case Node.ELEMENT_NODE: { |
| Element childElement = (Element)childNode; |
| if (isTextChild(childElement)) { |
| addContextToChild(ctx, childElement); |
| laidoutText = null; |
| } |
| break; |
| } |
| } |
| if (laidoutText == null) { |
| computeLaidoutText(ctx, e, getTextNode()); |
| } |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMNodeRemoved' is fired. |
| */ |
| public void handleDOMChildNodeRemovedEvent(MutationEvent evt) { |
| Node childNode = (Node)evt.getTarget(); |
| |
| //check the type of the node inserted before discard the layout |
| //in the case of <title> or <desc> or <metadata>, the layout |
| //is unchanged |
| switch (childNode.getNodeType()) { |
| case Node.TEXT_NODE: // fall-through is intended |
| case Node.CDATA_SECTION_NODE: |
| //the parent has to be a displayed node |
| if (isParentDisplayed(childNode)) { |
| laidoutText = null; |
| } |
| break; |
| case Node.ELEMENT_NODE: { |
| Element childElt = (Element) childNode; |
| if (isTextChild(childElt)) { |
| laidoutText = null; |
| removeContextFromChild(ctx, childElt); |
| } |
| break; |
| } |
| default: |
| } |
| //if the laidoutText was set to null, |
| //then wait for DOMSubtreeChange to recompute it. |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMSubtree' is fired. |
| */ |
| public void handleDOMSubtreeModifiedEvent(MutationEvent evt) { |
| //an operation occured onto the children of the |
| //text element, check if the layout was discarded |
| if (laidoutText == null) { |
| computeLaidoutText(ctx, e, getTextNode()); |
| } |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMCharacterDataModified' |
| * is fired. |
| */ |
| public void handleDOMCharacterDataModified(MutationEvent evt){ |
| Node childNode = (Node)evt.getTarget(); |
| //if the parent is displayed, then discard the layout. |
| if (isParentDisplayed(childNode)) { |
| laidoutText = null; |
| } |
| } |
| |
| /** |
| * Indicate of the parent of a node is |
| * a displayed element. |
| * <title>, <desc> and <metadata> |
| * are non displayable elements. |
| * |
| * @return true if the parent of the node is <text>, |
| * <tspan>, <tref>, <textPath>, <a>, |
| * <altGlyph> |
| */ |
| protected boolean isParentDisplayed(Node childNode) { |
| Node parentNode = getParentNode(childNode); |
| return isTextElement((Element)parentNode); |
| } |
| |
| /** |
| * Recompute the layout of the <text> node. |
| * |
| * Assign onto the TextNode pending to the element |
| * the new recomputed AttributedString. Also |
| * update <code>laidoutText</code> with the new |
| * value. |
| */ |
| protected void computeLaidoutText(BridgeContext ctx, |
| Element e, |
| GraphicsNode node) { |
| TextNode tn = (TextNode)node; |
| elemTPI.clear(); |
| |
| AttributedString as = buildAttributedString(ctx, e); |
| if (as == null) { |
| tn.setAttributedCharacterIterator(null); |
| return; |
| } |
| |
| addGlyphPositionAttributes(as, e, ctx); |
| if (ctx.isDynamic()) { |
| laidoutText = new AttributedString(as.getIterator()); |
| } |
| |
| // Install the ACI in the text node. |
| tn.setAttributedCharacterIterator(as.getIterator()); |
| |
| // Now get the real paint into - this needs to |
| // wait until the text node is laidout so we can get |
| // objectBoundingBox info. |
| TextPaintInfo pi = new TextPaintInfo(); |
| setBaseTextPaintInfo(pi, e, node, ctx); |
| // This get's Overline/underline info. |
| setDecorationTextPaintInfo(pi, e); |
| // Install the attributes. |
| addPaintAttributes(as, e, tn, pi, ctx); |
| |
| if (usingComplexSVGFont) { |
| // Force Complex SVG fonts to be recreated, if we have them. |
| tn.setAttributedCharacterIterator(as.getIterator()); |
| } |
| |
| if (ctx.isDynamic()) { |
| checkBBoxChange(); |
| } |
| } |
| |
| /** |
| * This flag bit indicates if a new ACI has been created in |
| * response to a CSSEngineEvent. |
| * Avoid creating one ShapePainter per CSS property change |
| */ |
| private boolean hasNewACI; |
| |
| /** |
| * This is the element a CSS property has changed. |
| */ |
| private Element cssProceedElement; |
| |
| /** |
| * Invoked when the animated value of an animatable attribute has changed. |
| */ |
| public void handleAnimatedAttributeChanged |
| (AnimatedLiveAttributeValue alav) { |
| if (alav.getNamespaceURI() == null) { |
| String ln = alav.getLocalName(); |
| if (ln.equals(SVG_X_ATTRIBUTE) |
| || ln.equals(SVG_Y_ATTRIBUTE) |
| || ln.equals(SVG_DX_ATTRIBUTE) |
| || ln.equals(SVG_DY_ATTRIBUTE) |
| || ln.equals(SVG_ROTATE_ATTRIBUTE) |
| || ln.equals(SVG_TEXT_LENGTH_ATTRIBUTE) |
| || ln.equals(SVG_LENGTH_ADJUST_ATTRIBUTE)) { |
| char c = ln.charAt(0); |
| if (c == 'x' || c == 'y') { |
| getTextNode().setLocation(getLocation(ctx, e)); |
| } |
| computeLaidoutText(ctx, e, getTextNode()); |
| return; |
| } |
| } |
| super.handleAnimatedAttributeChanged(alav); |
| } |
| |
| /** |
| * Invoked when CSS properties have changed on an element. |
| * |
| * @param evt the CSSEngine event that describes the update |
| */ |
| public void handleCSSEngineEvent(CSSEngineEvent evt) { |
| hasNewACI = false; |
| int [] properties = evt.getProperties(); |
| // first try to find CSS properties that change the layout |
| for (int i=0; i < properties.length; ++i) { |
| switch(properties[i]) { // fall-through is intended |
| case SVGCSSEngine.BASELINE_SHIFT_INDEX: |
| case SVGCSSEngine.DIRECTION_INDEX: |
| case SVGCSSEngine.DISPLAY_INDEX: |
| case SVGCSSEngine.FONT_FAMILY_INDEX: |
| case SVGCSSEngine.FONT_SIZE_INDEX: |
| case SVGCSSEngine.FONT_STRETCH_INDEX: |
| case SVGCSSEngine.FONT_STYLE_INDEX: |
| case SVGCSSEngine.FONT_WEIGHT_INDEX: |
| case SVGCSSEngine.GLYPH_ORIENTATION_HORIZONTAL_INDEX: |
| case SVGCSSEngine.GLYPH_ORIENTATION_VERTICAL_INDEX: |
| case SVGCSSEngine.KERNING_INDEX: |
| case SVGCSSEngine.LETTER_SPACING_INDEX: |
| case SVGCSSEngine.TEXT_ANCHOR_INDEX: |
| case SVGCSSEngine.UNICODE_BIDI_INDEX: |
| case SVGCSSEngine.WORD_SPACING_INDEX: |
| case SVGCSSEngine.WRITING_MODE_INDEX: { |
| if (!hasNewACI) { |
| hasNewACI = true; |
| computeLaidoutText(ctx, e, getTextNode()); |
| } |
| break; |
| } |
| } |
| } |
| //optimize the calculation of |
| //the painting attributes and decoration |
| //by only recomputing the section for the element |
| cssProceedElement = evt.getElement(); |
| // go for the other CSS properties |
| super.handleCSSEngineEvent(evt); |
| cssProceedElement = null; |
| } |
| |
| /** |
| * Invoked for each CSS property that has changed. |
| */ |
| protected void handleCSSPropertyChanged(int property) { |
| switch(property) { // fall-through is intended |
| case SVGCSSEngine.FILL_INDEX: |
| case SVGCSSEngine.FILL_OPACITY_INDEX: |
| case SVGCSSEngine.STROKE_INDEX: |
| case SVGCSSEngine.STROKE_OPACITY_INDEX: |
| case SVGCSSEngine.STROKE_WIDTH_INDEX: |
| case SVGCSSEngine.STROKE_LINECAP_INDEX: |
| case SVGCSSEngine.STROKE_LINEJOIN_INDEX: |
| case SVGCSSEngine.STROKE_MITERLIMIT_INDEX: |
| case SVGCSSEngine.STROKE_DASHARRAY_INDEX: |
| case SVGCSSEngine.STROKE_DASHOFFSET_INDEX: |
| case SVGCSSEngine.TEXT_DECORATION_INDEX: |
| rebuildACI(); |
| break; |
| |
| case SVGCSSEngine.VISIBILITY_INDEX: |
| rebuildACI(); |
| super.handleCSSPropertyChanged(property); |
| break; |
| case SVGCSSEngine.TEXT_RENDERING_INDEX: { |
| RenderingHints hints = node.getRenderingHints(); |
| hints = CSSUtilities.convertTextRendering(e, hints); |
| if (hints != null) { |
| node.setRenderingHints(hints); |
| } |
| break; |
| } |
| case SVGCSSEngine.COLOR_RENDERING_INDEX: { |
| RenderingHints hints = node.getRenderingHints(); |
| hints = CSSUtilities.convertColorRendering(e, hints); |
| if (hints != null) { |
| node.setRenderingHints(hints); |
| } |
| break; |
| } |
| default: |
| super.handleCSSPropertyChanged(property); |
| } |
| } |
| |
| protected void rebuildACI() { |
| if (hasNewACI) |
| return; |
| |
| TextNode textNode = getTextNode(); |
| if (textNode.getAttributedCharacterIterator() == null) |
| return; |
| |
| TextPaintInfo pi, oldPI; |
| if ( cssProceedElement == e ){ |
| pi = new TextPaintInfo(); |
| setBaseTextPaintInfo(pi, e, node, ctx); |
| setDecorationTextPaintInfo(pi, e); |
| oldPI = (TextPaintInfo)elemTPI.get(e); |
| } else { |
| //if a child CSS property has changed, then |
| //retrieve the parent text decoration |
| //and only update the section of the AtrtibutedString of |
| //the child |
| TextPaintInfo parentPI; |
| parentPI = getParentTextPaintInfo(cssProceedElement); |
| pi = getTextPaintInfo(cssProceedElement, textNode, parentPI, ctx); |
| oldPI = (TextPaintInfo)elemTPI.get(cssProceedElement); |
| } |
| if (oldPI == null) return; |
| |
| textNode.swapTextPaintInfo(pi, oldPI); |
| if (usingComplexSVGFont) |
| // Force Complex SVG fonts to be recreated |
| textNode.setAttributedCharacterIterator |
| (textNode.getAttributedCharacterIterator()); |
| } |
| |
| int getElementStartIndex(Element element) { |
| TextPaintInfo tpi = (TextPaintInfo)elemTPI.get(element); |
| if (tpi == null) return -1; |
| return tpi.startChar; |
| } |
| |
| int getElementEndIndex(Element element) { |
| TextPaintInfo tpi = (TextPaintInfo)elemTPI.get(element); |
| if (tpi == null) return -1; |
| return tpi.endChar; |
| } |
| |
| // ----------------------------------------------------------------------- |
| // ----------------------------------------------------------------------- |
| // ----------------------------------------------------------------------- |
| // ----------------------------------------------------------------------- |
| |
| /** |
| * Creates the attributed string which represents the given text |
| * element children. |
| * |
| * @param ctx the bridge context to use |
| * @param element the text element |
| */ |
| protected AttributedString buildAttributedString(BridgeContext ctx, |
| Element element) { |
| |
| AttributedStringBuffer asb = new AttributedStringBuffer(); |
| fillAttributedStringBuffer(ctx, element, true, null, null, null, asb); |
| return asb.toAttributedString(); |
| } |
| |
| |
| /** |
| * This is used to store the end of the last piece of text |
| * content from an element with xml:space="preserve". When |
| * we are stripping trailing spaces we need to make sure |
| * we don't strip anything before this point. |
| */ |
| protected int endLimit; |
| |
| /** |
| * Fills the given AttributedStringBuffer. |
| */ |
| protected void fillAttributedStringBuffer(BridgeContext ctx, |
| Element element, |
| boolean top, |
| TextPath textPath, |
| Integer bidiLevel, |
| Map initialAttributes, |
| AttributedStringBuffer asb) { |
| // 'requiredFeatures', 'requiredExtensions', 'systemLanguage' & |
| // 'display="none". |
| if ((!SVGUtilities.matchUserAgent(element, ctx.getUserAgent())) || |
| (!CSSUtilities.convertDisplay(element))) { |
| return; |
| } |
| |
| String s = XMLSupport.getXMLSpace(element); |
| boolean preserve = s.equals(SVG_PRESERVE_VALUE); |
| boolean prevEndsWithSpace; |
| Element nodeElement = element; |
| int elementStartChar = asb.length(); |
| |
| if (top) { |
| endLimit = 0; |
| } |
| if (preserve) { |
| endLimit = asb.length(); |
| } |
| |
| Map map = initialAttributes == null |
| ? new HashMap() |
| : new HashMap(initialAttributes); |
| initialAttributes = |
| getAttributeMap(ctx, element, textPath, bidiLevel, map); |
| Object o = map.get(TextAttribute.BIDI_EMBEDDING); |
| Integer subBidiLevel = bidiLevel; |
| if (o != null) { |
| subBidiLevel = (Integer) o; |
| } |
| |
| for (Node n = getFirstChild(element); |
| n != null; |
| n = getNextSibling(n)) { |
| |
| if (preserve) { |
| prevEndsWithSpace = false; |
| } else { |
| if (asb.length() == 0) { |
| prevEndsWithSpace = true; |
| } else { |
| prevEndsWithSpace = (asb.getLastChar() == ' '); |
| } |
| } |
| |
| switch (n.getNodeType()) { |
| case Node.ELEMENT_NODE: |
| if (!SVG_NAMESPACE_URI.equals(n.getNamespaceURI())) |
| break; |
| |
| nodeElement = (Element)n; |
| |
| String ln = n.getLocalName(); |
| |
| if (ln.equals(SVG_TSPAN_TAG) || |
| ln.equals(SVG_ALT_GLYPH_TAG)) { |
| int before = asb.count; |
| fillAttributedStringBuffer(ctx, |
| nodeElement, |
| false, |
| textPath, |
| subBidiLevel, |
| initialAttributes, |
| asb); |
| if (asb.count != before) { |
| initialAttributes = null; |
| } |
| } else if (ln.equals(SVG_TEXT_PATH_TAG)) { |
| SVGTextPathElementBridge textPathBridge |
| = (SVGTextPathElementBridge)ctx.getBridge(nodeElement); |
| TextPath newTextPath |
| = textPathBridge.createTextPath(ctx, nodeElement); |
| if (newTextPath != null) { |
| int before = asb.count; |
| fillAttributedStringBuffer(ctx, |
| nodeElement, |
| false, |
| newTextPath, |
| subBidiLevel, |
| initialAttributes, |
| asb); |
| if (asb.count != before) { |
| initialAttributes = null; |
| } |
| } |
| } else if (ln.equals(SVG_TREF_TAG)) { |
| String uriStr = XLinkSupport.getXLinkHref((Element)n); |
| Element ref = ctx.getReferencedElement((Element)n, uriStr); |
| s = TextUtilities.getElementContent(ref); |
| s = normalizeString(s, preserve, prevEndsWithSpace); |
| if (s.length() != 0) { |
| int trefStart = asb.length(); |
| Map m = initialAttributes == null |
| ? new HashMap() |
| : new HashMap(initialAttributes); |
| getAttributeMap |
| (ctx, nodeElement, textPath, bidiLevel, m); |
| asb.append(s, m); |
| int trefEnd = asb.length() - 1; |
| TextPaintInfo tpi; |
| tpi = (TextPaintInfo)elemTPI.get(nodeElement); |
| tpi.startChar = trefStart; |
| tpi.endChar = trefEnd; |
| initialAttributes = null; |
| } |
| } else if (ln.equals(SVG_A_TAG)) { |
| NodeEventTarget target = (NodeEventTarget)nodeElement; |
| UserAgent ua = ctx.getUserAgent(); |
| SVGAElementBridge.CursorHolder ch; |
| ch = new SVGAElementBridge.CursorHolder |
| (CursorManager.DEFAULT_CURSOR); |
| EventListener l; |
| l = new SVGAElementBridge.AnchorListener(ua, ch); |
| target.addEventListenerNS |
| (XMLConstants.XML_EVENTS_NAMESPACE_URI, |
| SVG_EVENT_CLICK, l, false, null); |
| ctx.storeEventListenerNS |
| (target, XMLConstants.XML_EVENTS_NAMESPACE_URI, |
| SVG_EVENT_CLICK, l, false); |
| |
| int before = asb.count; |
| fillAttributedStringBuffer(ctx, |
| nodeElement, |
| false, |
| textPath, |
| subBidiLevel, |
| initialAttributes, |
| asb); |
| if (asb.count != before) { |
| initialAttributes = null; |
| } |
| } |
| break; |
| case Node.TEXT_NODE: // fall-through is intended |
| case Node.CDATA_SECTION_NODE: |
| s = n.getNodeValue(); |
| s = normalizeString(s, preserve, prevEndsWithSpace); |
| if (s.length() != 0) { |
| asb.append(s, map); |
| if (preserve) { |
| endLimit = asb.length(); |
| } |
| initialAttributes = null; |
| } |
| } |
| } |
| |
| if (top) { |
| boolean strippedSome = false; |
| while ((endLimit < asb.length()) && (asb.getLastChar() == ' ')) { |
| asb.stripLast(); |
| strippedSome = true; |
| } |
| if (strippedSome) { |
| Iterator iter = elemTPI.values().iterator(); |
| while (iter.hasNext()) { |
| TextPaintInfo tpi = (TextPaintInfo)iter.next(); |
| if (tpi.endChar >= asb.length()) { |
| tpi.endChar = asb.length()-1; |
| if (tpi.startChar > tpi.endChar) |
| tpi.startChar = tpi.endChar; |
| } |
| } |
| } |
| } |
| int elementEndChar = asb.length()-1; |
| TextPaintInfo tpi = (TextPaintInfo)elemTPI.get(element); |
| tpi.startChar = elementStartChar; |
| tpi.endChar = elementEndChar; |
| } |
| |
| /** |
| * Normalizes the given string. |
| */ |
| protected String normalizeString(String s, |
| boolean preserve, |
| boolean stripfirst) { |
| StringBuffer sb = new StringBuffer(s.length()); |
| if (preserve) { |
| for (int i = 0; i < s.length(); i++) { |
| char c = s.charAt(i); |
| switch (c) { // fall-through is intended |
| case 10: |
| case 13: |
| case '\t': |
| sb.append(' '); |
| break; |
| default: |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| int idx = 0; |
| if (stripfirst) { |
| loop: while (idx < s.length()) { |
| switch (s.charAt(idx)) { |
| default: |
| break loop; |
| case 10: // fall-through is intended |
| case 13: |
| case ' ': |
| case '\t': |
| idx++; |
| } |
| } |
| } |
| |
| boolean space = false; |
| for (int i = idx; i < s.length(); i++) { |
| char c = s.charAt(i); |
| switch (c) { |
| case 10: // fall-through is intended |
| case 13: |
| break; |
| case ' ': // fall-through is intended |
| case '\t': |
| if (!space) { |
| sb.append(' '); |
| space = true; |
| } |
| break; |
| default: |
| sb.append(c); |
| space = false; |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * This class is used to build an AttributedString. |
| */ |
| protected static class AttributedStringBuffer { |
| |
| /** |
| * The strings. |
| */ |
| protected List strings; |
| |
| /** |
| * The attributes. |
| */ |
| protected List attributes; |
| |
| /** |
| * The number of items. |
| */ |
| protected int count; |
| |
| /** |
| * The length of the attributed string. |
| */ |
| protected int length; |
| |
| /** |
| * Creates a new empty AttributedStringBuffer. |
| */ |
| public AttributedStringBuffer() { |
| strings = new ArrayList(); |
| attributes = new ArrayList(); |
| count = 0; |
| length = 0; |
| } |
| |
| /** |
| * Tells whether this AttributedStringBuffer is empty. |
| */ |
| public boolean isEmpty() { |
| return count == 0; |
| } |
| |
| /** |
| * Returns the length in chars of the current Attributed String |
| */ |
| public int length() { |
| return length; |
| } |
| |
| /** |
| * Appends a String and its associated attributes. |
| */ |
| public void append(String s, Map m) { |
| if (s.length() == 0) return; |
| strings.add(s); |
| attributes.add(m); |
| count++; |
| length += s.length(); |
| } |
| |
| /** |
| * Returns the value of the last char or -1. |
| */ |
| public int getLastChar() { |
| if (count == 0) { |
| return -1; |
| } |
| String s = (String)strings.get(count - 1); |
| return s.charAt(s.length() - 1); |
| } |
| |
| /** |
| * Strips the last string character. |
| */ |
| public void stripFirst() { |
| String s = (String)strings.get(0); |
| if (s.charAt(s.length() - 1) != ' ') |
| return; |
| |
| length--; |
| |
| if (s.length() == 1) { |
| attributes.remove(0); |
| strings.remove(0); |
| count--; |
| return; |
| } |
| |
| strings.set(0, s.substring(1)); |
| } |
| |
| /** |
| * Strips the last string character. |
| */ |
| public void stripLast() { |
| String s = (String)strings.get(count - 1); |
| if (s.charAt(s.length() - 1) != ' ') |
| return; |
| |
| length--; |
| |
| if (s.length() == 1) { |
| attributes.remove(--count); |
| strings.remove(count); |
| return; |
| } |
| |
| strings.set(count-1, s.substring(0, s.length() - 1)); |
| } |
| |
| /** |
| * Builds an attributed string from the content of this |
| * buffer. |
| */ |
| public AttributedString toAttributedString() { |
| switch (count) { |
| case 0: |
| return null; |
| case 1: |
| return new AttributedString((String)strings.get(0), |
| (Map)attributes.get(0)); |
| } |
| |
| StringBuffer sb = new StringBuffer( strings.size() * 5 ); |
| Iterator it = strings.iterator(); |
| while (it.hasNext()) { |
| sb.append((String)it.next()); |
| } |
| |
| AttributedString result = new AttributedString(sb.toString()); |
| |
| // Set the attributes |
| |
| Iterator sit = strings.iterator(); |
| Iterator ait = attributes.iterator(); |
| int idx = 0; |
| while (sit.hasNext()) { |
| String s = (String)sit.next(); |
| int nidx = idx + s.length(); |
| Map m = (Map)ait.next(); |
| Iterator kit = m.keySet().iterator(); |
| Iterator vit = m.values().iterator(); |
| while (kit.hasNext()) { |
| Attribute attr = (Attribute)kit.next(); |
| Object val = vit.next(); |
| result.addAttribute(attr, val, idx, nidx); |
| } |
| idx = nidx; |
| } |
| |
| return result; |
| } |
| |
| public String toString() { |
| switch (count) { |
| case 0: |
| return ""; |
| case 1: |
| return (String)strings.get(0); |
| } |
| |
| StringBuffer sb = new StringBuffer( strings.size() * 5 ); |
| Iterator it = strings.iterator(); |
| while (it.hasNext()) { |
| sb.append((String)it.next()); |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Returns true if node1 is an ancestor of node2 |
| */ |
| protected boolean nodeAncestorOf(Node node1, Node node2) { |
| if (node2 == null || node1 == null) { |
| return false; |
| } |
| Node parent = getParentNode(node2); |
| while (parent != null && parent != node1) { |
| parent = getParentNode(parent); |
| } |
| return (parent == node1); |
| } |
| |
| |
| /** |
| * Adds glyph position attributes to an AttributedString. |
| */ |
| protected void addGlyphPositionAttributes(AttributedString as, |
| Element element, |
| BridgeContext ctx) { |
| |
| // 'requiredFeatures', 'requiredExtensions' and 'systemLanguage' |
| if ((!SVGUtilities.matchUserAgent(element, ctx.getUserAgent())) || |
| (!CSSUtilities.convertDisplay(element))) { |
| return; |
| } |
| if (element.getLocalName().equals(SVG_TEXT_PATH_TAG)) { |
| // 'textPath' doesn't support position attributes. |
| addChildGlyphPositionAttributes(as, element, ctx); |
| return; |
| } |
| |
| // calculate which chars in the string belong to this element |
| int firstChar = getElementStartIndex(element); |
| // No match so no chars to annotate. |
| if (firstChar == -1) return; |
| |
| int lastChar = getElementEndIndex(element); |
| |
| // 'a' elements aren't SVGTextPositioningElements, so don't process |
| // their positioning attributes on them. |
| if (!(element instanceof SVGTextPositioningElement)) { |
| addChildGlyphPositionAttributes(as, element, ctx); |
| return; |
| } |
| |
| // get all of the glyph position attribute values |
| SVGTextPositioningElement te = (SVGTextPositioningElement) element; |
| |
| try { |
| SVGOMAnimatedLengthList _x = |
| (SVGOMAnimatedLengthList) te.getX(); |
| _x.check(); |
| SVGOMAnimatedLengthList _y = |
| (SVGOMAnimatedLengthList) te.getY(); |
| _y.check(); |
| SVGOMAnimatedLengthList _dx = |
| (SVGOMAnimatedLengthList) te.getDx(); |
| _dx.check(); |
| SVGOMAnimatedLengthList _dy = |
| (SVGOMAnimatedLengthList) te.getDy(); |
| _dy.check(); |
| SVGOMAnimatedNumberList _rotate = |
| (SVGOMAnimatedNumberList) te.getRotate(); |
| _rotate.check(); |
| |
| SVGLengthList xs = _x.getAnimVal(); |
| SVGLengthList ys = _y.getAnimVal(); |
| SVGLengthList dxs = _dx.getAnimVal(); |
| SVGLengthList dys = _dy.getAnimVal(); |
| SVGNumberList rs = _rotate.getAnimVal(); |
| |
| int len; |
| |
| // process the x attribute |
| len = xs.getNumberOfItems(); |
| for (int i = 0; i < len && firstChar + i <= lastChar; i++) { |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.X, |
| new Float(xs.getItem(i).getValue()), firstChar + i, |
| firstChar + i + 1); |
| } |
| |
| // process the y attribute |
| len = ys.getNumberOfItems(); |
| for (int i = 0; i < len && firstChar + i <= lastChar; i++) { |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.Y, |
| new Float(ys.getItem(i).getValue()), firstChar + i, |
| firstChar + i + 1); |
| } |
| |
| // process dx attribute |
| len = dxs.getNumberOfItems(); |
| for (int i = 0; i < len && firstChar + i <= lastChar; i++) { |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.DX, |
| new Float(dxs.getItem(i).getValue()), firstChar + i, |
| firstChar + i + 1); |
| } |
| |
| // process dy attribute |
| len = dys.getNumberOfItems(); |
| for (int i = 0; i < len && firstChar + i <= lastChar; i++) { |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.DY, |
| new Float(dys.getItem(i).getValue()), firstChar + i, |
| firstChar + i + 1); |
| } |
| |
| // process rotate attribute |
| len = rs.getNumberOfItems(); |
| if (len == 1) { // not a list |
| // each char will have the same rotate value |
| Float rad = new Float(Math.toRadians(rs.getItem(0).getValue())); |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.ROTATION, |
| rad, firstChar, lastChar + 1); |
| |
| } else if (len > 1) { // it's a list |
| // set each rotate value from the list |
| for (int i = 0; i < len && firstChar + i <= lastChar; i++) { |
| Float rad = new Float(Math.toRadians(rs.getItem(i).getValue())); |
| as.addAttribute |
| (GVTAttributedCharacterIterator.TextAttribute.ROTATION, |
| rad, firstChar + i, firstChar + i + 1); |
| } |
| } |
| |
| addChildGlyphPositionAttributes(as, element, ctx); |
| } catch (LiveAttributeException ex) { |
| throw new BridgeException(ctx, ex); |
| } |
| } |
| |
| protected void addChildGlyphPositionAttributes(AttributedString as, |
| Element element, |
| BridgeContext ctx) { |
| // do the same for each child element |
| for (Node child = getFirstChild(element); |
| child != null; |
| child = getNextSibling(child)) { |
| if (child.getNodeType() != Node.ELEMENT_NODE) continue; |
| |
| Element childElement = (Element)child; |
| if (isTextChild(childElement)) { |
| addGlyphPositionAttributes(as, childElement, ctx); |
| } |
| } |
| } |
| |
| /** |
| * Adds painting attributes to an AttributedString. |
| */ |
| protected void addPaintAttributes(AttributedString as, |
| Element element, |
| TextNode node, |
| TextPaintInfo pi, |
| BridgeContext ctx) { |
| // 'requiredFeatures', 'requiredExtensions' and 'systemLanguage' |
| if ((!SVGUtilities.matchUserAgent(element, ctx.getUserAgent())) || |
| (!CSSUtilities.convertDisplay(element))) { |
| return; |
| } |
| Object o = elemTPI.get(element); |
| if (o != null) { |
| node.swapTextPaintInfo(pi, (TextPaintInfo)o); |
| } |
| addChildPaintAttributes(as, element, node, pi, ctx); |
| } |
| |
| protected void addChildPaintAttributes(AttributedString as, |
| Element element, |
| TextNode node, |
| TextPaintInfo parentPI, |
| BridgeContext ctx) { |
| // Add Paint attributres for children of text element |
| for (Node child = getFirstChild(element); |
| child != null; |
| child = getNextSibling(child)) { |
| if (child.getNodeType() != Node.ELEMENT_NODE) { |
| continue; |
| } |
| Element childElement = (Element)child; |
| if (isTextChild(childElement)) { |
| TextPaintInfo pi = getTextPaintInfo(childElement, node, |
| parentPI, ctx); |
| addPaintAttributes(as, childElement, node, pi, ctx); |
| } |
| } |
| } |
| |
| /** |
| * This method adds all the font related properties to <code>result</code> |
| * It also builds a List of the GVTFonts and returns it. |
| */ |
| protected List getFontList(BridgeContext ctx, |
| Element element, |
| Map result) { |
| |
| // Unique value for text element - used for run identification. |
| result.put(TEXT_COMPOUND_ID, new SoftReference(element)); |
| |
| Float fsFloat = TextUtilities.convertFontSize(element); |
| float fontSize = fsFloat.floatValue(); |
| // Font size. |
| result.put(TextAttribute.SIZE, fsFloat); |
| |
| // Font stretch |
| result.put(TextAttribute.WIDTH, |
| TextUtilities.convertFontStretch(element)); |
| |
| // Font style |
| result.put(TextAttribute.POSTURE, |
| TextUtilities.convertFontStyle(element)); |
| |
| // Font weight |
| result.put(TextAttribute.WEIGHT, |
| TextUtilities.convertFontWeight(element)); |
| |
| // Font weight |
| Value v = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.FONT_WEIGHT_INDEX); |
| String fontWeightString = v.getCssText(); |
| |
| // Font style |
| String fontStyleString = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.FONT_STYLE_INDEX).getStringValue(); |
| |
| // Needed for SVG fonts (also for dynamic documents). |
| result.put(TEXT_COMPOUND_DELIMITER, element); |
| |
| // make a list of GVTFont objects |
| Value val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.FONT_FAMILY_INDEX); |
| List fontFamilyList = new ArrayList(); |
| List fontList = new ArrayList(); |
| int len = val.getLength(); |
| for (int i = 0; i < len; i++) { |
| Value it = val.item(i); |
| String fontFamilyName = it.getStringValue(); |
| GVTFontFamily fontFamily; |
| fontFamily = SVGFontUtilities.getFontFamily(element, ctx, fontFamilyName, |
| fontWeightString, fontStyleString); |
| if (fontFamily != null && fontFamily instanceof UnresolvedFontFamily) { |
| fontFamily = ctx.getFontFamilyResolver().resolve(fontFamily.getFamilyName()); |
| } |
| if (fontFamily == null) { |
| continue; |
| } |
| fontFamilyList.add(fontFamily); |
| if (fontFamily.isComplex()) { |
| usingComplexSVGFont = true; |
| } |
| GVTFont ft = fontFamily.deriveFont(fontSize, result); |
| fontList.add(ft); |
| } |
| |
| // Eventually this will need to go for SVG fonts it |
| // holds hard ref to DOM. |
| result.put(GVT_FONT_FAMILIES, fontFamilyList); |
| |
| if (!ctx.isDynamic()) { |
| // Only leave this in the map for dynamic documents. |
| // Otherwise it will cause the whole DOM to stay when |
| // we don't really need it. |
| result.remove(TEXT_COMPOUND_DELIMITER); |
| } |
| return fontList; |
| } |
| |
| /** |
| * Returns the map to pass to the current characters. |
| * |
| * @param ctx the BridgeContext to use for throwing exceptions |
| * @param element the text element whose attributes are being collected |
| * @param textPath the text path that the characters of <code>element</code> |
| * will be placed along |
| * @param bidiLevel the bidi level of <code>element</code> |
| * @param result a Map into which the attributes of <code>element</code>'s |
| * characters will be stored |
| * @return a new Map that contains the attributes that must be inherited |
| * into a child element if the given element has no characters before |
| * the child element |
| */ |
| protected Map getAttributeMap(BridgeContext ctx, |
| Element element, |
| TextPath textPath, |
| Integer bidiLevel, |
| Map result) { |
| SVGTextContentElement tce = null; |
| if (element instanceof SVGTextContentElement) { |
| // 'a' elements aren't SVGTextContentElements, so they shouldn't |
| // be checked for 'textLength' or 'lengthAdjust' attributes. |
| tce = (SVGTextContentElement) element; |
| } |
| |
| Map inheritMap = null; |
| String s; |
| |
| if (SVG_NAMESPACE_URI.equals(element.getNamespaceURI()) && |
| element.getLocalName().equals(SVG_ALT_GLYPH_TAG)) { |
| result.put(ALT_GLYPH_HANDLER, |
| new SVGAltGlyphHandler(ctx, element)); |
| } |
| |
| // Add null TPI objects to the text (after we set it on the |
| // Text we will swap in the correct values. |
| TextPaintInfo pi = new TextPaintInfo(); |
| // Set some basic props so we can get bounds info for complex paints. |
| pi.visible = true; |
| pi.fillPaint = Color.black; |
| result.put(PAINT_INFO, pi); |
| elemTPI.put(element, pi); |
| |
| if (textPath != null) { |
| result.put(TEXTPATH, textPath); |
| } |
| |
| // Text-anchor |
| TextNode.Anchor a = TextUtilities.convertTextAnchor(element); |
| result.put(ANCHOR_TYPE, a); |
| |
| // Font family |
| List fontList = getFontList(ctx, element, result); |
| result.put(GVT_FONTS, fontList); |
| |
| |
| // Text baseline adjustment. |
| Object bs = TextUtilities.convertBaselineShift(element); |
| if (bs != null) { |
| result.put(BASELINE_SHIFT, bs); |
| } |
| |
| // Unicode-bidi mode |
| Value val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.UNICODE_BIDI_INDEX); |
| s = val.getStringValue(); |
| if (s.charAt(0) == 'n') { |
| if (bidiLevel != null) |
| result.put(TextAttribute.BIDI_EMBEDDING, bidiLevel); |
| } else { |
| |
| // Text direction |
| // XXX: this needs to coordinate with the unicode-bidi |
| // property, so that when an explicit reversal |
| // occurs, the BIDI_EMBEDDING level is |
| // appropriately incremented or decremented. |
| // Note that direction is implicitly handled by unicode |
| // BiDi algorithm in most cases, this property |
| // is only needed when one wants to override the |
| // normal writing direction for a string/substring. |
| |
| val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.DIRECTION_INDEX); |
| String rs = val.getStringValue(); |
| int cbidi = 0; |
| if (bidiLevel != null) cbidi = bidiLevel.intValue(); |
| |
| // We don't care if it was embed or override we just want |
| // it's level here. So map override to positive value. |
| if (cbidi < 0) cbidi = -cbidi; |
| |
| switch (rs.charAt(0)) { |
| case 'l': |
| result.put(TextAttribute.RUN_DIRECTION, |
| TextAttribute.RUN_DIRECTION_LTR); |
| if ((cbidi & 0x1) == 1) cbidi++; // was odd now even |
| else cbidi+=2; // next greater even number |
| break; |
| case 'r': |
| result.put(TextAttribute.RUN_DIRECTION, |
| TextAttribute.RUN_DIRECTION_RTL); |
| if ((cbidi & 0x1) == 1) cbidi+=2; // next greater odd number |
| else cbidi++; // was even now odd |
| break; |
| } |
| |
| switch (s.charAt(0)) { |
| case 'b': // bidi-override |
| cbidi = -cbidi; // For bidi-override we want a negative number. |
| break; |
| } |
| |
| result.put(TextAttribute.BIDI_EMBEDDING, new Integer(cbidi)); |
| } |
| |
| // Writing mode |
| |
| val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.WRITING_MODE_INDEX); |
| s = val.getStringValue(); |
| switch (s.charAt(0)) { |
| case 'l': |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE, |
| GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE_LTR); |
| break; |
| case 'r': |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE, |
| GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE_RTL); |
| break; |
| case 't': |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE, |
| GVTAttributedCharacterIterator. |
| TextAttribute.WRITING_MODE_TTB); |
| break; |
| } |
| |
| // glyph-orientation-vertical |
| |
| val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.GLYPH_ORIENTATION_VERTICAL_INDEX); |
| int primitiveType = val.getPrimitiveType(); |
| switch ( primitiveType ) { |
| case CSSPrimitiveValue.CSS_IDENT: // auto |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ORIENTATION_AUTO); |
| break; |
| case CSSPrimitiveValue.CSS_DEG: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ORIENTATION_ANGLE); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION_ANGLE, |
| new Float(val.getFloatValue())); |
| break; |
| case CSSPrimitiveValue.CSS_RAD: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ORIENTATION_ANGLE); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION_ANGLE, |
| new Float( Math.toDegrees( val.getFloatValue() ) )); |
| break; |
| case CSSPrimitiveValue.CSS_GRAD: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ORIENTATION_ANGLE); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.VERTICAL_ORIENTATION_ANGLE, |
| new Float(val.getFloatValue() * 9 / 5)); |
| break; |
| default: |
| // Cannot happen |
| throw new IllegalStateException("unexpected primitiveType (V):" + primitiveType ); |
| } |
| |
| // glyph-orientation-horizontal |
| |
| val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.GLYPH_ORIENTATION_HORIZONTAL_INDEX); |
| primitiveType = val.getPrimitiveType(); |
| switch ( primitiveType ) { |
| case CSSPrimitiveValue.CSS_DEG: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.HORIZONTAL_ORIENTATION_ANGLE, |
| new Float(val.getFloatValue())); |
| break; |
| case CSSPrimitiveValue.CSS_RAD: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.HORIZONTAL_ORIENTATION_ANGLE, |
| new Float( Math.toDegrees( val.getFloatValue() ) )); |
| break; |
| case CSSPrimitiveValue.CSS_GRAD: |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.HORIZONTAL_ORIENTATION_ANGLE, |
| new Float(val.getFloatValue() * 9 / 5)); |
| break; |
| default: |
| // Cannot happen |
| throw new IllegalStateException("unexpected primitiveType (H):" + primitiveType ); |
| } |
| |
| // text spacing properties... |
| |
| // Letter Spacing |
| Float sp = TextUtilities.convertLetterSpacing(element); |
| if (sp != null) { |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.LETTER_SPACING, |
| sp); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.CUSTOM_SPACING, |
| Boolean.TRUE); |
| } |
| |
| // Word spacing |
| sp = TextUtilities.convertWordSpacing(element); |
| if (sp != null) { |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.WORD_SPACING, |
| sp); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.CUSTOM_SPACING, |
| Boolean.TRUE); |
| } |
| |
| // Kerning |
| sp = TextUtilities.convertKerning(element); |
| if (sp != null) { |
| result.put(GVTAttributedCharacterIterator.TextAttribute.KERNING, |
| sp); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.CUSTOM_SPACING, |
| Boolean.TRUE); |
| } |
| |
| if (tce == null) { |
| return inheritMap; |
| } |
| |
| try { |
| // textLength |
| AbstractSVGAnimatedLength textLength = |
| (AbstractSVGAnimatedLength) tce.getTextLength(); |
| if (textLength.isSpecified()) { |
| if (inheritMap == null) { |
| inheritMap = new HashMap(); |
| } |
| |
| Object value = new Float(textLength.getCheckedValue()); |
| result.put |
| (GVTAttributedCharacterIterator.TextAttribute.BBOX_WIDTH, |
| value); |
| inheritMap.put |
| (GVTAttributedCharacterIterator.TextAttribute.BBOX_WIDTH, |
| value); |
| |
| // lengthAdjust |
| SVGOMAnimatedEnumeration _lengthAdjust = |
| (SVGOMAnimatedEnumeration) tce.getLengthAdjust(); |
| if (_lengthAdjust.getCheckedVal() == |
| SVGTextContentElement.LENGTHADJUST_SPACINGANDGLYPHS) { |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.LENGTH_ADJUST, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ADJUST_ALL); |
| inheritMap.put(GVTAttributedCharacterIterator. |
| TextAttribute.LENGTH_ADJUST, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ADJUST_ALL); |
| } else { |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.LENGTH_ADJUST, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ADJUST_SPACING); |
| inheritMap.put(GVTAttributedCharacterIterator. |
| TextAttribute.LENGTH_ADJUST, |
| GVTAttributedCharacterIterator. |
| TextAttribute.ADJUST_SPACING); |
| result.put(GVTAttributedCharacterIterator. |
| TextAttribute.CUSTOM_SPACING, |
| Boolean.TRUE); |
| inheritMap.put(GVTAttributedCharacterIterator. |
| TextAttribute.CUSTOM_SPACING, |
| Boolean.TRUE); |
| } |
| } |
| } catch (LiveAttributeException ex) { |
| throw new BridgeException(ctx, ex); |
| } |
| |
| return inheritMap; |
| } |
| |
| |
| /** |
| * Retrieve in the AttributeString the closest parent |
| * of the node 'child' and extract the text decorations |
| * of the parent. |
| * |
| * @param child an <code>Element</code> value |
| * @return a <code>TextDecoration</code> value |
| */ |
| protected TextPaintInfo getParentTextPaintInfo(Element child) { |
| Node parent = getParentNode(child); |
| while (parent != null) { |
| TextPaintInfo tpi = (TextPaintInfo)elemTPI.get(parent); |
| if (tpi != null) return tpi; |
| parent = getParentNode(parent); |
| } |
| return null; |
| } |
| |
| /** |
| * Constructs a TextDecoration object for the specified element. This will |
| * contain all of the decoration properties to be used when drawing the |
| * text. |
| */ |
| protected TextPaintInfo getTextPaintInfo(Element element, |
| GraphicsNode node, |
| TextPaintInfo parentTPI, |
| BridgeContext ctx) { |
| // Force the engine to update stuff.. |
| CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.TEXT_DECORATION_INDEX); |
| |
| TextPaintInfo pi = new TextPaintInfo(parentTPI); |
| |
| // Was text-decoration explicity set on this element? |
| StyleMap sm = ((CSSStylableElement)element).getComputedStyleMap(null); |
| if ((sm.isNullCascaded(SVGCSSEngine.TEXT_DECORATION_INDEX)) && |
| (sm.isNullCascaded(SVGCSSEngine.FILL_INDEX)) && |
| (sm.isNullCascaded(SVGCSSEngine.STROKE_INDEX)) && |
| (sm.isNullCascaded(SVGCSSEngine.STROKE_WIDTH_INDEX)) && |
| (sm.isNullCascaded(SVGCSSEngine.OPACITY_INDEX))) { |
| // If not, keep the same decorations. |
| return pi; |
| } |
| |
| setBaseTextPaintInfo(pi, element, node, ctx); |
| |
| if (!sm.isNullCascaded(SVGCSSEngine.TEXT_DECORATION_INDEX)) |
| setDecorationTextPaintInfo(pi, element); |
| |
| return pi; |
| } |
| |
| public void setBaseTextPaintInfo(TextPaintInfo pi, Element element, |
| GraphicsNode node, BridgeContext ctx) { |
| if (!element.getLocalName().equals(SVG_TEXT_TAG)) |
| pi.composite = CSSUtilities.convertOpacity (element); |
| else |
| pi.composite = AlphaComposite.SrcOver; |
| |
| pi.visible = CSSUtilities.convertVisibility(element); |
| pi.fillPaint = PaintServer.convertFillPaint (element, node, ctx); |
| pi.strokePaint = PaintServer.convertStrokePaint(element, node, ctx); |
| pi.strokeStroke = PaintServer.convertStroke (element); |
| } |
| |
| public void setDecorationTextPaintInfo(TextPaintInfo pi, Element element) { |
| Value val = CSSUtilities.getComputedStyle |
| (element, SVGCSSEngine.TEXT_DECORATION_INDEX); |
| |
| switch (val.getCssValueType()) { |
| case CSSValue.CSS_VALUE_LIST: |
| ListValue lst = (ListValue)val; |
| |
| int len = lst.getLength(); |
| for (int i = 0; i < len; i++) { |
| Value v = lst.item(i); |
| String s = v.getStringValue(); |
| switch (s.charAt(0)) { |
| case 'u': |
| if (pi.fillPaint != null) { |
| pi.underlinePaint = pi.fillPaint; |
| } |
| if (pi.strokePaint != null) { |
| pi.underlineStrokePaint = pi.strokePaint; |
| } |
| if (pi.strokeStroke != null) { |
| pi.underlineStroke = pi.strokeStroke; |
| } |
| break; |
| case 'o': |
| if (pi.fillPaint != null) { |
| pi.overlinePaint = pi.fillPaint; |
| } |
| if (pi.strokePaint != null) { |
| pi.overlineStrokePaint = pi.strokePaint; |
| } |
| if (pi.strokeStroke != null) { |
| pi.overlineStroke = pi.strokeStroke; |
| } |
| break; |
| case 'l': |
| if (pi.fillPaint != null) { |
| pi.strikethroughPaint = pi.fillPaint; |
| } |
| if (pi.strokePaint != null) { |
| pi.strikethroughStrokePaint = pi.strokePaint; |
| } |
| if (pi.strokeStroke != null) { |
| pi.strikethroughStroke = pi.strokeStroke; |
| } |
| break; |
| } |
| } |
| break; |
| |
| default: // None |
| pi.underlinePaint = null; |
| pi.underlineStrokePaint = null; |
| pi.underlineStroke = null; |
| |
| pi.overlinePaint = null; |
| pi.overlineStrokePaint = null; |
| pi.overlineStroke = null; |
| |
| pi.strikethroughPaint = null; |
| pi.strikethroughStrokePaint = null; |
| pi.strikethroughStroke = null; |
| break; |
| } |
| } |
| |
| /** |
| * Implementation of <code>SVGContext</code> for |
| * the children of <text> |
| */ |
| public abstract class AbstractTextChildSVGContext |
| extends AnimatableSVGBridge { |
| |
| /** Text bridge parent */ |
| protected SVGTextElementBridge textBridge; |
| |
| /** |
| * Initialize the <code>SVGContext</code> implementation |
| * with the bridgeContext, the parent bridge, and the |
| * element supervised by this context |
| */ |
| public AbstractTextChildSVGContext(BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e) { |
| this.ctx = ctx; |
| this.textBridge = parent; |
| this.e = e; |
| } |
| |
| /** |
| * Returns the namespace URI of the element this <code>Bridge</code> is |
| * dedicated to. |
| */ |
| public String getNamespaceURI() { |
| return null; |
| } |
| |
| /** |
| * Returns the local name of the element this <code>Bridge</code> is dedicated |
| * to. |
| */ |
| public String getLocalName() { |
| return null; |
| } |
| |
| /** |
| * Returns a new instance of this bridge. |
| */ |
| public Bridge getInstance() { |
| return null; |
| } |
| |
| public SVGTextElementBridge getTextBridge() { return textBridge; } |
| |
| /** |
| * Returns the size of a px CSS unit in millimeters. |
| */ |
| public float getPixelUnitToMillimeter() { |
| return ctx.getUserAgent().getPixelUnitToMillimeter(); |
| } |
| |
| /** |
| * Returns the size of a px CSS unit in millimeters. |
| * This will be removed after next release. |
| * @see #getPixelUnitToMillimeter() |
| */ |
| public float getPixelToMM() { |
| return getPixelUnitToMillimeter(); |
| |
| } |
| /** |
| * Returns the tight bounding box in current user space (i.e., |
| * after application of the transform attribute, if any) on the |
| * geometry of all contained graphics elements, exclusive of |
| * stroke-width and filter effects). |
| */ |
| public Rectangle2D getBBox() { |
| //text children does not support getBBox |
| //return textBridge.getBBox(); |
| return null; |
| } |
| |
| /** |
| * Returns the transformation matrix from current user units |
| * (i.e., after application of the transform attribute, if any) to |
| * the viewport coordinate system for the nearestViewportElement. |
| */ |
| public AffineTransform getCTM() { |
| // text children does not support transform attribute |
| //return textBridge.getCTM(); |
| return null; |
| } |
| |
| /** |
| * Returns the global transformation matrix from the current |
| * element to the root. |
| */ |
| public AffineTransform getGlobalTransform() { |
| //return node.getGlobalTransform(); |
| return null; |
| } |
| |
| /** |
| * Returns the transformation matrix from the userspace of |
| * the root element to the screen. |
| */ |
| public AffineTransform getScreenTransform() { |
| //return node.getScreenTransform(); |
| return null; |
| } |
| |
| /** |
| * Sets the transformation matrix to be used from the |
| * userspace of the root element to the screen. |
| */ |
| public void setScreenTransform(AffineTransform at) { |
| //return node.setScreenTransform(at); |
| return; |
| } |
| |
| /** |
| * Returns the width of the viewport which directly contains the |
| * given element. |
| */ |
| public float getViewportWidth() { |
| return ctx.getBlockWidth(e); |
| } |
| |
| /** |
| * Returns the height of the viewport which directly contains the |
| * given element. |
| */ |
| public float getViewportHeight() { |
| return ctx.getBlockHeight(e); |
| } |
| |
| /** |
| * Returns the font-size on the associated element. |
| */ |
| public float getFontSize() { |
| return CSSUtilities.getComputedStyle |
| (e, SVGCSSEngine.FONT_SIZE_INDEX).getFloatValue(); |
| } |
| } |
| |
| /** |
| * Implementation for the <code>BridgeUpdateHandler</code> |
| * for the child elements of <text>. |
| * This implementation relies on the parent bridge |
| * which contains the <code>TextNode</code> |
| * representing the node this context supervised. |
| * All operations are done by the parent bridge |
| * <code>SVGTextElementBridge</code> which can determine |
| * the impact of a change of one of its children for the others. |
| */ |
| protected abstract class AbstractTextChildBridgeUpdateHandler |
| extends AbstractTextChildSVGContext implements BridgeUpdateHandler { |
| |
| /** |
| * Initialize the BridgeUpdateHandler implementation. |
| */ |
| protected AbstractTextChildBridgeUpdateHandler |
| (BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e) { |
| |
| super(ctx,parent,e); |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMAttrModified' is fired. |
| */ |
| public void handleDOMAttrModifiedEvent(MutationEvent evt) { |
| //nothing to do |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMNodeInserted' is fired. |
| */ |
| public void handleDOMNodeInsertedEvent(MutationEvent evt) { |
| textBridge.handleDOMNodeInsertedEvent(evt); |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMNodeRemoved' is fired. |
| */ |
| public void handleDOMNodeRemovedEvent(MutationEvent evt) { |
| } |
| |
| /** |
| * Invoked when an MutationEvent of type 'DOMCharacterDataModified' |
| * is fired. |
| */ |
| public void handleDOMCharacterDataModified(MutationEvent evt) { |
| textBridge.handleDOMCharacterDataModified(evt); |
| } |
| |
| /** |
| * Invoked when an CSSEngineEvent is fired. |
| */ |
| public void handleCSSEngineEvent(CSSEngineEvent evt) { |
| textBridge.handleCSSEngineEvent(evt); |
| } |
| |
| /** |
| * Invoked when the animated value of an animatable attribute has |
| * changed. |
| */ |
| public void handleAnimatedAttributeChanged |
| (AnimatedLiveAttributeValue alav) { |
| } |
| |
| /** |
| * Invoked when an 'other' animation value has changed. |
| */ |
| public void handleOtherAnimationChanged(String type) { |
| } |
| |
| /** |
| * Disposes this BridgeUpdateHandler and releases all resources. |
| */ |
| public void dispose(){ |
| ((SVGOMElement)e).setSVGContext(null); |
| elemTPI.remove(e); |
| } |
| } |
| |
| protected class AbstractTextChildTextContent |
| extends AbstractTextChildBridgeUpdateHandler |
| implements SVGTextContent { |
| |
| /** |
| * Initialize the AbstractTextChildBridgeUpdateHandler implementation. |
| */ |
| protected AbstractTextChildTextContent |
| (BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e) { |
| |
| super(ctx,parent,e); |
| } |
| |
| //Implementation of TextContent |
| |
| public int getNumberOfChars(){ |
| return textBridge.getNumberOfChars(e); |
| } |
| |
| public Rectangle2D getExtentOfChar(int charnum ){ |
| return textBridge.getExtentOfChar(e,charnum); |
| } |
| |
| public Point2D getStartPositionOfChar(int charnum){ |
| return textBridge.getStartPositionOfChar(e,charnum); |
| } |
| |
| public Point2D getEndPositionOfChar(int charnum){ |
| return textBridge.getEndPositionOfChar(e,charnum); |
| } |
| |
| public void selectSubString(int charnum, int nchars){ |
| textBridge.selectSubString(e,charnum,nchars); |
| } |
| |
| public float getRotationOfChar(int charnum){ |
| return textBridge.getRotationOfChar(e,charnum); |
| } |
| |
| public float getComputedTextLength(){ |
| return textBridge.getComputedTextLength(e); |
| } |
| |
| public float getSubStringLength(int charnum, int nchars){ |
| return textBridge.getSubStringLength(e,charnum,nchars); |
| } |
| |
| public int getCharNumAtPosition(float x , float y){ |
| return textBridge.getCharNumAtPosition(e,x,y); |
| } |
| } |
| |
| /** |
| * BridgeUpdateHandle for <tref> element. |
| */ |
| protected class TRefBridge |
| extends AbstractTextChildTextContent { |
| |
| protected TRefBridge(BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e) { |
| super(ctx,parent,e); |
| } |
| |
| /** |
| * Invoked when the animated value of an animatable attribute has |
| * changed on a 'tref' element. |
| */ |
| public void handleAnimatedAttributeChanged |
| (AnimatedLiveAttributeValue alav) { |
| if (alav.getNamespaceURI() == null) { |
| String ln = alav.getLocalName(); |
| if (ln.equals(SVG_X_ATTRIBUTE) |
| || ln.equals(SVG_Y_ATTRIBUTE) |
| || ln.equals(SVG_DX_ATTRIBUTE) |
| || ln.equals(SVG_DY_ATTRIBUTE) |
| || ln.equals(SVG_ROTATE_ATTRIBUTE) |
| || ln.equals(SVG_TEXT_LENGTH_ATTRIBUTE) |
| || ln.equals(SVG_LENGTH_ADJUST_ATTRIBUTE)) { |
| // Recompute the layout of the text node. |
| textBridge.computeLaidoutText(ctx, textBridge.e, |
| textBridge.getTextNode()); |
| return; |
| } |
| } |
| super.handleAnimatedAttributeChanged(alav); |
| } |
| } |
| |
| /** |
| * BridgeUpdateHandle for <textPath> element. |
| */ |
| protected class TextPathBridge |
| extends AbstractTextChildTextContent{ |
| |
| protected TextPathBridge(BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e){ |
| super(ctx,parent,e); |
| } |
| } |
| |
| /** |
| * BridgeUpdateHandle for <tspan> element. |
| */ |
| protected class TspanBridge |
| extends AbstractTextChildTextContent { |
| |
| protected TspanBridge(BridgeContext ctx, |
| SVGTextElementBridge parent, |
| Element e){ |
| super(ctx,parent,e); |
| } |
| |
| /** |
| * Invoked when the animated value of an animatable attribute has |
| * changed on a 'tspan' element. |
| */ |
| public void handleAnimatedAttributeChanged |
| (AnimatedLiveAttributeValue alav) { |
| if (alav.getNamespaceURI() == null) { |
| String ln = alav.getLocalName(); |
| if (ln.equals(SVG_X_ATTRIBUTE) |
| || ln.equals(SVG_Y_ATTRIBUTE) |
| || ln.equals(SVG_DX_ATTRIBUTE) |
| || ln.equals(SVG_DY_ATTRIBUTE) |
| || ln.equals(SVG_ROTATE_ATTRIBUTE) |
| || ln.equals(SVG_TEXT_LENGTH_ATTRIBUTE) |
| || ln.equals(SVG_LENGTH_ADJUST_ATTRIBUTE)) { |
| // Recompute the layout of the text node. |
| textBridge.computeLaidoutText(ctx, textBridge.e, |
| textBridge.getTextNode()); |
| return; |
| } |
| } |
| super.handleAnimatedAttributeChanged(alav); |
| } |
| } |
| |
| //Implementation of TextContent |
| public int getNumberOfChars(){ |
| return getNumberOfChars(e); |
| } |
| |
| public Rectangle2D getExtentOfChar(int charnum ){ |
| return getExtentOfChar(e,charnum); |
| } |
| |
| public Point2D getStartPositionOfChar(int charnum){ |
| return getStartPositionOfChar(e,charnum); |
| } |
| |
| public Point2D getEndPositionOfChar(int charnum){ |
| return getEndPositionOfChar(e,charnum); |
| } |
| |
| public void selectSubString(int charnum, int nchars){ |
| selectSubString(e,charnum,nchars); |
| } |
| |
| public float getRotationOfChar(int charnum){ |
| return getRotationOfChar(e,charnum); |
| } |
| |
| public float getComputedTextLength(){ |
| return getComputedTextLength(e); |
| } |
| |
| public float getSubStringLength(int charnum, int nchars){ |
| return getSubStringLength(e,charnum,nchars); |
| } |
| |
| public int getCharNumAtPosition(float x , float y){ |
| return getCharNumAtPosition(e,x,y); |
| } |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getNumberOfChars()}. |
| */ |
| protected int getNumberOfChars(Element element){ |
| |
| AttributedCharacterIterator aci; |
| aci = getTextNode().getAttributedCharacterIterator(); |
| if (aci == null) |
| return 0; |
| |
| //get the index in the aci for the first character |
| //of the element |
| int firstChar = getElementStartIndex(element); |
| |
| if (firstChar == -1) |
| return 0; // Element not part of aci (no chars in elem usually) |
| |
| int lastChar = getElementEndIndex(element); |
| |
| return( lastChar - firstChar + 1 ); |
| } |
| |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getExtentOfChar(int charnum)}. |
| */ |
| protected Rectangle2D getExtentOfChar(Element element,int charnum ){ |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) return null; |
| |
| int firstChar = getElementStartIndex(element); |
| |
| if ( firstChar == -1 ) |
| return null; |
| |
| //retrieve the text run for the text node |
| List list = getTextRuns(textNode); |
| |
| //find the character 'charnum' in the text run |
| CharacterInformation info; |
| info = getCharacterInformation(list, firstChar,charnum, aci); |
| |
| if ( info == null ) |
| return null; |
| |
| //retrieve the glyphvector containing the glyph |
| //for 'charnum' |
| GVTGlyphVector it = info.layout.getGlyphVector(); |
| |
| Shape b = null; |
| |
| if (info.glyphIndexStart == info.glyphIndexEnd) { |
| if (it.isGlyphVisible(info.glyphIndexStart)) { |
| b = it.getGlyphCellBounds(info.glyphIndexStart); |
| } |
| } else { |
| GeneralPath path = null; |
| for (int k = info.glyphIndexStart; k <= info.glyphIndexEnd; k++) { |
| if (it.isGlyphVisible(k)) { |
| Rectangle2D gb = it.getGlyphCellBounds(k); |
| if (path == null) { |
| path = new GeneralPath(gb); |
| } else { |
| path.append(gb, false); |
| } |
| } |
| } |
| b = path; |
| } |
| |
| if (b == null) { |
| return null; |
| } |
| |
| //return the bounding box of the outline |
| return b.getBounds2D(); |
| } |
| |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getStartPositionOfChar(int charnum)}. |
| */ |
| protected Point2D getStartPositionOfChar(Element element,int charnum){ |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return null; |
| |
| int firstChar = getElementStartIndex(element); |
| if ( firstChar == -1 ) |
| return null; |
| |
| //retrieve the text run for the text node |
| List list = getTextRuns(textNode); |
| |
| //find the character 'charnum' in the text run |
| CharacterInformation info; |
| info = getCharacterInformation(list, firstChar,charnum, aci); |
| |
| if ( info == null ) |
| return null; |
| |
| return getStartPoint( info ); |
| } |
| |
| protected Point2D getStartPoint(CharacterInformation info){ |
| |
| GVTGlyphVector it = info.layout.getGlyphVector(); |
| if (!it.isGlyphVisible(info.glyphIndexStart)) |
| return null; |
| |
| Point2D b = it.getGlyphPosition(info.glyphIndexStart); |
| |
| AffineTransform glyphTransform; |
| glyphTransform = it.getGlyphTransform(info.glyphIndexStart); |
| |
| |
| //glyph are defined starting at position (0,0) |
| Point2D.Float result = new Point2D.Float(0, 0); |
| if ( glyphTransform != null ) |
| //apply the glyph transformation to the start point |
| glyphTransform.transform(result,result); |
| |
| result.x += b.getX(); |
| result.y += b.getY(); |
| return result; |
| } |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getEndPositionOfChar(int charnum)}. |
| */ |
| protected Point2D getEndPositionOfChar(Element element,int charnum ){ |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return null; |
| |
| int firstChar = getElementStartIndex(element); |
| if ( firstChar == -1 ) |
| return null; |
| |
| //retrieve the text run for the text node |
| List list = getTextRuns(textNode); |
| |
| //find the glyph information for the character 'charnum' |
| CharacterInformation info; |
| info = getCharacterInformation(list, firstChar,charnum, aci); |
| |
| if ( info == null ) |
| return null; |
| return getEndPoint(info); |
| } |
| |
| protected Point2D getEndPoint(CharacterInformation info){ |
| |
| GVTGlyphVector it = info.layout.getGlyphVector(); |
| if (!it.isGlyphVisible(info.glyphIndexEnd)) |
| return null; |
| |
| Point2D b = it.getGlyphPosition(info.glyphIndexEnd); |
| |
| AffineTransform glyphTransform; |
| glyphTransform = it.getGlyphTransform(info.glyphIndexEnd); |
| |
| GVTGlyphMetrics metrics = it.getGlyphMetrics(info.glyphIndexEnd); |
| |
| |
| Point2D.Float result = new Point2D.Float |
| (metrics.getHorizontalAdvance(), 0); |
| |
| if ( glyphTransform != null ) |
| glyphTransform.transform(result,result); |
| |
| result.x += b.getX(); |
| result.y += b.getY(); |
| return result; |
| } |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getRotationOfChar(int charnum)}. |
| */ |
| protected float getRotationOfChar(Element element, int charnum){ |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return 0; |
| |
| //first the first character for the element |
| int firstChar = getElementStartIndex(element); |
| if ( firstChar == -1 ) |
| return 0; |
| |
| //retrieve the text run for the text node |
| List list = getTextRuns(textNode); |
| |
| //find the glyph information for the character 'charnum' |
| CharacterInformation info; |
| info = getCharacterInformation(list, firstChar,charnum, aci); |
| |
| double angle = 0.0; |
| int nbGlyphs = 0; |
| |
| if ( info != null ){ |
| GVTGlyphVector it = info.layout.getGlyphVector(); |
| |
| for( int k = info.glyphIndexStart ; |
| k <= info.glyphIndexEnd ; |
| k++ ){ |
| if (!it.isGlyphVisible(k)) continue; |
| |
| nbGlyphs++; |
| |
| //the glyph transform contains only a scale and a rotate. |
| AffineTransform glyphTransform = it.getGlyphTransform(k); |
| if ( glyphTransform == null ) continue; |
| |
| double glyphAngle = 0.0; |
| double cosTheta = glyphTransform.getScaleX(); |
| double sinTheta = glyphTransform.getShearX(); |
| |
| //extract the angle |
| if ( cosTheta == 0.0 ){ |
| if ( sinTheta > 0 ) glyphAngle = Math.PI; |
| else glyphAngle = -Math.PI; |
| } else { |
| glyphAngle = Math.atan(sinTheta/cosTheta); // todo is this safe?? |
| if ( cosTheta < 0 ) |
| glyphAngle += Math.PI; |
| } |
| //get a degrees value for the angle |
| //SVG angle are clock wise java anticlockwise |
| |
| glyphAngle = (Math.toDegrees( - glyphAngle ) ) % 360.0; |
| |
| //remove the orientation from the value |
| angle += glyphAngle - info.getComputedOrientationAngle(); |
| } |
| } |
| if (nbGlyphs == 0) return 0; |
| return (float)(angle / nbGlyphs ); |
| } |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getComputedTextLength()}. |
| */ |
| protected float getComputedTextLength(Element e) { |
| return getSubStringLength(e,0,getNumberOfChars(e)); |
| } |
| |
| /** |
| * Implementation of {@link |
| * org.w3c.dom.svg.SVGTextContentElement#getSubStringLength(int charnum,int nchars)}. |
| */ |
| protected float getSubStringLength(Element element, |
| int charnum, |
| int nchars){ |
| if (nchars == 0) { |
| return 0; |
| } |
| |
| float length = 0; |
| |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return -1; |
| |
| int firstChar = getElementStartIndex(element); |
| |
| if ( firstChar == -1 ) |
| return -1; |
| |
| List list = getTextRuns(textNode); |
| |
| CharacterInformation currentInfo; |
| currentInfo = getCharacterInformation(list, firstChar,charnum,aci); |
| CharacterInformation lastCharacterInRunInfo = null; |
| int chIndex = currentInfo.characterIndex+1; |
| GVTGlyphVector vector = currentInfo.layout.getGlyphVector(); |
| float [] advs = currentInfo.layout.getGlyphAdvances(); |
| boolean [] glyphTrack = new boolean[advs.length]; |
| for( int k = charnum +1; k < charnum +nchars ; k++) { |
| if (currentInfo.layout.isOnATextPath() ){ |
| for (int gi = currentInfo.glyphIndexStart; |
| gi <= currentInfo.glyphIndexEnd; gi++) { |
| if ((vector.isGlyphVisible(gi)) && !glyphTrack[gi]) |
| length += advs[gi+1]-advs[gi]; |
| glyphTrack[gi] = true; |
| } |
| CharacterInformation newInfo; |
| newInfo = getCharacterInformation(list, firstChar, k, aci); |
| if (newInfo.layout != currentInfo.layout) { |
| vector = newInfo.layout.getGlyphVector(); |
| advs = newInfo.layout.getGlyphAdvances(); |
| glyphTrack = new boolean[advs.length]; |
| chIndex = currentInfo.characterIndex+1; |
| } |
| currentInfo = newInfo; |
| } else { |
| //reach the next run |
| if ( currentInfo.layout.hasCharacterIndex(chIndex) ){ |
| chIndex++; |
| continue; |
| } |
| |
| lastCharacterInRunInfo = getCharacterInformation |
| (list,firstChar,k-1,aci); |
| |
| //if the text run change compute the distance between the |
| //first character of the run and the last |
| length += distanceFirstLastCharacterInRun |
| (currentInfo,lastCharacterInRunInfo); |
| |
| currentInfo = getCharacterInformation(list,firstChar,k,aci); |
| chIndex = currentInfo.characterIndex+1; |
| vector = currentInfo.layout.getGlyphVector(); |
| advs = currentInfo.layout.getGlyphAdvances(); |
| glyphTrack = new boolean[advs.length]; |
| lastCharacterInRunInfo = null; |
| } |
| } |
| |
| if (currentInfo.layout.isOnATextPath() ){ |
| for (int gi = currentInfo.glyphIndexStart; |
| gi <= currentInfo.glyphIndexEnd; gi++) { |
| if ((vector.isGlyphVisible(gi)) && !glyphTrack[gi]) |
| length += advs[gi+1]-advs[gi]; |
| glyphTrack[gi] = true; |
| } |
| } else { |
| if ( lastCharacterInRunInfo == null ){ |
| lastCharacterInRunInfo = getCharacterInformation |
| (list,firstChar,charnum+nchars-1,aci); |
| } |
| //add the length between the end position of the last character |
| //and the first character in the run |
| length += distanceFirstLastCharacterInRun |
| (currentInfo,lastCharacterInRunInfo); |
| } |
| |
| return length; |
| } |
| |
| protected float distanceFirstLastCharacterInRun |
| (CharacterInformation first, CharacterInformation last){ |
| |
| float [] advs = first.layout.getGlyphAdvances(); |
| |
| int firstStart = first.glyphIndexStart; |
| int firstEnd = first.glyphIndexEnd; |
| int lastStart = last.glyphIndexStart; |
| int lastEnd = last.glyphIndexEnd; |
| |
| int start = (firstStart<lastStart)?firstStart:lastStart; |
| int end = (firstEnd<lastEnd)?lastEnd:firstEnd; |
| return advs[end+1] - advs[start]; |
| } |
| |
| protected float distanceBetweenRun |
| (CharacterInformation last, CharacterInformation first){ |
| |
| float distance; |
| Point2D startPoint; |
| Point2D endPoint; |
| CharacterInformation info = new CharacterInformation(); |
| |
| //determine where the last run stops |
| |
| info.layout = last.layout; |
| info.glyphIndexEnd = last.layout.getGlyphCount()-1; |
| |
| startPoint = getEndPoint(info); |
| |
| //determine where the next run starts |
| info.layout = first.layout; |
| info.glyphIndexStart = 0; |
| |
| endPoint = getStartPoint(info); |
| |
| if( first.isVertical() ){ |
| distance = (float)(endPoint.getY() - startPoint.getY()); |
| } |
| else{ |
| distance = (float)(endPoint.getX() - startPoint.getX()); |
| } |
| |
| return distance; |
| } |
| |
| |
| /** |
| * Select an ensemble of characters for that element. |
| * |
| * TODO : report the selection to the selection |
| * manager in JSVGComponent. |
| */ |
| protected void selectSubString(Element element, int charnum, int nchars) { |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return; |
| |
| int firstChar = getElementStartIndex(element); |
| |
| if ( firstChar == -1 ) |
| return; |
| |
| List list = getTextRuns(textNode); |
| |
| int lastChar = getElementEndIndex(element); |
| |
| CharacterInformation firstInfo, lastInfo; |
| firstInfo = getCharacterInformation(list, firstChar,charnum,aci); |
| lastInfo = getCharacterInformation(list, firstChar,charnum+nchars-1,aci); |
| |
| Mark firstMark, lastMark; |
| firstMark = textNode.getMarkerForChar(firstInfo.characterIndex,true); |
| |
| if ( lastInfo != null && lastInfo.characterIndex <= lastChar ){ |
| lastMark = textNode.getMarkerForChar(lastInfo.characterIndex,false); |
| } |
| else{ |
| lastMark = textNode.getMarkerForChar(lastChar,false); |
| } |
| |
| ctx.getUserAgent().setTextSelection(firstMark,lastMark); |
| } |
| |
| protected int getCharNumAtPosition(Element e, float x, float y){ |
| TextNode textNode = getTextNode(); |
| |
| AttributedCharacterIterator aci; |
| aci = textNode.getAttributedCharacterIterator(); |
| if (aci == null) |
| return -1; |
| |
| //check if there is an hit |
| List list = getTextRuns(textNode); |
| |
| //going backward in the list to catch the last character |
| // displayed at that position |
| TextHit hit = null; |
| |
| for( int i = list.size()-1 ; i>= 0 && hit == null; i-- ){ |
| StrokingTextPainter.TextRun textRun; |
| textRun = (StrokingTextPainter.TextRun)list.get(i); |
| hit = textRun.getLayout().hitTestChar(x,y); |
| } |
| |
| if ( hit == null ) |
| return -1; |
| |
| |
| //found an hit, check if it belong to the element |
| int first = getElementStartIndex( e ); |
| int last = getElementEndIndex( e ); |
| |
| int hitIndex = hit.getCharIndex(); |
| |
| if ( hitIndex >= first && hitIndex <= last ) |
| return hitIndex - first; |
| |
| return -1; |
| } |
| |
| /** |
| * Retrieve the list of layout for the |
| * text node. |
| */ |
| protected List getTextRuns(TextNode node){ |
| //System.out.println(node.getTextRuns()); |
| if ( node.getTextRuns() == null ){ |
| //TODO : need to work out a solution |
| //to compute the text runs |
| node.getPrimitiveBounds(); |
| } |
| //System.out.println(node.getTextRuns()); |
| return node.getTextRuns(); |
| } |
| |
| /** |
| * Retrieve the information about a character |
| * of en element. The element first character in |
| * the ACI is 'firstChar' and the character |
| * look for is the charnum th character in the |
| * element |
| * |
| * @param list list of the layouts |
| * @param startIndex index in the ACI of the first |
| * character for the element |
| * @param charnum index of the character (among the |
| * characters of the element) looked for. |
| * |
| * @return information about the glyph representing the |
| * character |
| */ |
| protected CharacterInformation getCharacterInformation |
| (List list,int startIndex, int charnum, |
| AttributedCharacterIterator aci) |
| { |
| CharacterInformation info = new CharacterInformation(); |
| info.characterIndex = startIndex+charnum; |
| |
| for (int i = 0; i < list.size(); i++) { |
| StrokingTextPainter.TextRun run; |
| run = (StrokingTextPainter.TextRun)list.get(i); |
| |
| if (!run.getLayout().hasCharacterIndex(info.characterIndex) ) |
| continue; |
| |
| info.layout = run.getLayout(); |
| |
| aci.setIndex(info.characterIndex); |
| |
| //check is it is a altGlyph |
| if (aci.getAttribute(ALT_GLYPH_HANDLER) != null){ |
| info.glyphIndexStart = 0; |
| info.glyphIndexEnd = info.layout.getGlyphCount()-1; |
| } else { |
| info.glyphIndexStart = info.layout.getGlyphIndex |
| (info.characterIndex); |
| |
| //special case when the glyph does not have a unicode |
| //associated to it, it will return -1 |
| if ( info.glyphIndexStart == -1 ){ |
| info.glyphIndexStart = 0; |
| info.glyphIndexEnd = info.layout.getGlyphCount()-1; |
| } |
| else{ |
| info.glyphIndexEnd = info.glyphIndexStart; |
| } |
| } |
| return info; |
| } |
| return null; |
| } |
| |
| /** |
| * Helper class to collect information about one Glyph |
| * in the GlyphVector |
| */ |
| protected static class CharacterInformation{ |
| ///layout associated to the Glyph |
| TextSpanLayout layout; |
| ///GlyphIndex in the vector |
| int glyphIndexStart; |
| |
| int glyphIndexEnd; |
| |
| ///Character index in the ACI. |
| int characterIndex; |
| |
| /// Indicates is the glyph is vertical |
| public boolean isVertical(){ |
| return layout.isVertical(); |
| } |
| /// Retrieve the orientation angle for the Glyph |
| public double getComputedOrientationAngle(){ |
| return layout.getComputedOrientationAngle(characterIndex); |
| } |
| } |
| |
| |
| public Set getTextIntersectionSet(AffineTransform at, |
| Rectangle2D rect) { |
| Set elems = new HashSet(); |
| |
| TextNode tn = getTextNode(); |
| |
| List list = tn.getTextRuns(); |
| if (list == null) |
| return elems; |
| |
| for (int i = 0 ; i < list.size(); i++) { |
| StrokingTextPainter.TextRun run; |
| run = (StrokingTextPainter.TextRun)list.get(i); |
| TextSpanLayout layout = run.getLayout(); |
| AttributedCharacterIterator aci = run.getACI(); |
| aci.first(); |
| SoftReference sr; |
| sr =(SoftReference)aci.getAttribute(TEXT_COMPOUND_ID); |
| Element elem = (Element)sr.get(); |
| |
| if (elem == null) continue; |
| if (elems.contains(elem)) continue; |
| if (!isTextSensitive(elem)) continue; |
| |
| Rectangle2D glBounds = layout.getBounds2D(); |
| if (glBounds != null) { |
| glBounds = at.createTransformedShape(glBounds).getBounds2D(); |
| |
| if (!rect.intersects(glBounds)) { |
| continue; |
| } |
| } |
| |
| GVTGlyphVector gv = layout.getGlyphVector(); |
| for (int g = 0; g < gv.getNumGlyphs(); g++) { |
| Shape gBounds = gv.getGlyphLogicalBounds(g); |
| if (gBounds != null) { |
| gBounds = at.createTransformedShape |
| (gBounds).getBounds2D(); |
| |
| if (gBounds.intersects(rect)) { |
| elems.add(elem); |
| break; |
| } |
| } |
| } |
| } |
| return elems; |
| } |
| |
| public Set getTextEnclosureSet(AffineTransform at, |
| Rectangle2D rect) { |
| TextNode tn = getTextNode(); |
| |
| Set elems = new HashSet(); |
| List list = tn.getTextRuns(); |
| if (list == null) |
| return elems; |
| |
| Set reject = new HashSet(); |
| for (int i = 0 ; i < list.size(); i++) { |
| StrokingTextPainter.TextRun run; |
| run = (StrokingTextPainter.TextRun)list.get(i); |
| TextSpanLayout layout = run.getLayout(); |
| AttributedCharacterIterator aci = run.getACI(); |
| aci.first(); |
| SoftReference sr; |
| sr =(SoftReference)aci.getAttribute(TEXT_COMPOUND_ID); |
| Element elem = (Element)sr.get(); |
| |
| if (elem == null) continue; |
| if (reject.contains(elem)) continue; |
| if (!isTextSensitive(elem)) { |
| reject.add(elem); |
| continue; |
| } |
| |
| Rectangle2D glBounds = layout.getBounds2D(); |
| if ( glBounds == null ){ |
| continue; |
| } |
| |
| glBounds = at.createTransformedShape( glBounds ).getBounds2D(); |
| |
| if (rect.contains(glBounds)) { |
| elems.add(elem); |
| } else { |
| reject.add(elem); |
| elems.remove(elem); |
| } |
| } |
| |
| return elems; |
| } |
| |
| public static boolean getTextIntersection(BridgeContext ctx, |
| Element elem, |
| AffineTransform ati, |
| Rectangle2D rect, |
| boolean checkSensitivity) { |
| SVGContext svgCtx = null; |
| if (elem instanceof SVGOMElement) |
| svgCtx = ((SVGOMElement)elem).getSVGContext(); |
| if (svgCtx == null) return false; |
| |
| SVGTextElementBridge txtBridge = null; |
| if (svgCtx instanceof SVGTextElementBridge) |
| txtBridge = (SVGTextElementBridge)svgCtx; |
| else if (svgCtx instanceof AbstractTextChildSVGContext) { |
| AbstractTextChildSVGContext childCtx; |
| childCtx = (AbstractTextChildSVGContext)svgCtx; |
| txtBridge = childCtx.getTextBridge(); |
| } |
| if (txtBridge == null) return false; |
| |
| TextNode tn = txtBridge.getTextNode(); |
| List list = tn.getTextRuns(); |
| if (list == null) |
| return false; |
| |
| Element txtElem = txtBridge.e; |
| |
| AffineTransform at = tn.getGlobalTransform(); |
| at.preConcatenate(ati); |
| |
| Rectangle2D tnRect; |
| tnRect = tn.getBounds(); |
| tnRect = at.createTransformedShape(tnRect).getBounds2D(); |
| if (!rect.intersects(tnRect)) return false; |
| |
| for (int i = 0 ; i < list.size(); i++) { |
| StrokingTextPainter.TextRun run; |
| run = (StrokingTextPainter.TextRun)list.get(i); |
| TextSpanLayout layout = run.getLayout(); |
| AttributedCharacterIterator aci = run.getACI(); |
| aci.first(); |
| SoftReference sr; |
| sr =(SoftReference)aci.getAttribute(TEXT_COMPOUND_ID); |
| Element runElem = (Element)sr.get(); |
| if (runElem == null) continue; |
| |
| // Only consider runElem if it is sensitive. |
| if (checkSensitivity && !isTextSensitive(runElem)) continue; |
| |
| Element p = runElem; |
| while ((p != null) && (p != txtElem) && (p != elem)) { |
| p = (Element) txtBridge.getParentNode(p); |
| } |
| if (p != elem) continue; |
| |
| // runElem is a child of elem so check it out. |
| Rectangle2D glBounds = layout.getBounds2D(); |
| if (glBounds == null) continue; |
| glBounds = at.createTransformedShape(glBounds).getBounds2D(); |
| if (!rect.intersects(glBounds)) continue; |
| |
| GVTGlyphVector gv = layout.getGlyphVector(); |
| for (int g = 0; g < gv.getNumGlyphs(); g++) { |
| Shape gBounds = gv.getGlyphLogicalBounds(g); |
| if (gBounds != null) { |
| gBounds = at.createTransformedShape |
| (gBounds).getBounds2D(); |
| |
| if (gBounds.intersects(rect)){ |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| public static Rectangle2D getTextBounds(BridgeContext ctx, Element elem, |
| boolean checkSensitivity) { |
| SVGContext svgCtx = null; |
| if (elem instanceof SVGOMElement) |
| svgCtx = ((SVGOMElement)elem).getSVGContext(); |
| if (svgCtx == null) return null; |
| |
| SVGTextElementBridge txtBridge = null; |
| if (svgCtx instanceof SVGTextElementBridge) |
| txtBridge = (SVGTextElementBridge)svgCtx; |
| else if (svgCtx instanceof AbstractTextChildSVGContext) { |
| AbstractTextChildSVGContext childCtx; |
| childCtx = (AbstractTextChildSVGContext)svgCtx; |
| txtBridge = childCtx.getTextBridge(); |
| } |
| if (txtBridge == null) return null; |
| |
| TextNode tn = txtBridge.getTextNode(); |
| List list = tn.getTextRuns(); |
| if (list == null) |
| return null; |
| |
| Element txtElem = txtBridge.e; |
| Rectangle2D ret = null; |
| |
| for (int i = 0 ; i < list.size(); i++) { |
| StrokingTextPainter.TextRun run; |
| run = (StrokingTextPainter.TextRun)list.get(i); |
| TextSpanLayout layout = run.getLayout(); |
| AttributedCharacterIterator aci = run.getACI(); |
| aci.first(); |
| SoftReference sr; |
| sr =(SoftReference)aci.getAttribute(TEXT_COMPOUND_ID); |
| Element runElem = (Element)sr.get(); |
| if (runElem == null) continue; |
| |
| // Only consider runElem if it is sensitive. |
| if (checkSensitivity && !isTextSensitive(runElem)) continue; |
| |
| Element p = runElem; |
| while ((p != null) && (p != txtElem) && (p != elem)) { |
| p = (Element) txtBridge.getParentNode(p); |
| } |
| if (p != elem) continue; |
| |
| // runElem is a child of elem so include it's bounds. |
| Rectangle2D glBounds = layout.getBounds2D(); |
| if (glBounds != null) { |
| if (ret == null) ret = (Rectangle2D)glBounds.clone(); |
| else ret.add(glBounds); |
| } |
| } |
| return ret; |
| } |
| |
| |
| public static boolean isTextSensitive(Element e) { |
| int ptrEvts = CSSUtilities.convertPointerEvents(e); |
| switch (ptrEvts) { |
| case GraphicsNode.VISIBLE_PAINTED: // fall-through is intended |
| case GraphicsNode.VISIBLE_FILL: |
| case GraphicsNode.VISIBLE_STROKE: |
| case GraphicsNode.VISIBLE: |
| return CSSUtilities.convertVisibility(e); |
| case GraphicsNode.PAINTED: |
| case GraphicsNode.FILL: // fall-through is intended |
| case GraphicsNode.STROKE: |
| case GraphicsNode.ALL: |
| return true; |
| case GraphicsNode.NONE: |
| default: |
| return false; |
| } |
| } |
| } |