blob: 96defb310b1f93b789101afcbece9651cfa5607e [file] [log] [blame]
/*
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 &lt;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.
* &lt;title&gt;, &lt;desc&gt; and &lt;metadata&gt;
* are non displayable elements.
*
* @return true if the parent of the node is &lt;text&gt;,
* &lt;tspan&gt;, &lt;tref&gt;, &lt;textPath&gt;, &lt;a&gt;,
* &lt;altGlyph&gt;
*/
protected boolean isParentDisplayed(Node childNode) {
Node parentNode = getParentNode(childNode);
return isTextElement((Element)parentNode);
}
/**
* Recompute the layout of the &lt;text&gt; 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 &lt;text&gt;
*/
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 &lt;text&gt;.
* 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 &lt;tref&gt; 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 &lt;textPath&gt; element.
*/
protected class TextPathBridge
extends AbstractTextChildTextContent{
protected TextPathBridge(BridgeContext ctx,
SVGTextElementBridge parent,
Element e){
super(ctx,parent,e);
}
}
/**
* BridgeUpdateHandle for &lt;tspan&gt; 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;
}
}
}