/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.bridge;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Hashtable;
import java.util.Map;
import org.apache.batik.css.engine.SVGCSSEngine;
import org.apache.batik.css.engine.value.ListValue;
import org.apache.batik.css.engine.value.Value;
import org.apache.batik.dom.svg.XMLBaseSupport;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.ext.awt.image.PadMode;
import org.apache.batik.ext.awt.image.renderable.AffineRable8Bit;
import org.apache.batik.ext.awt.image.renderable.Filter;
import org.apache.batik.ext.awt.image.renderable.PadRable8Bit;
import org.apache.batik.ext.awt.image.spi.ImageTagRegistry;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.SoftReferenceCache;
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.svg.SVGDocument;
import org.w3c.dom.svg.SVGPreserveAspectRatio;
/**
* The CursorManager class is a helper class which preloads the cursors
* corresponding to the SVG built in cursors.
*
* @author <a href="mailto:vincent.hardy@sun.com">Vincent Hardy</a>
* @version $Id: CursorManager.java,v 1.7 2003/04/11 13:54:42 vhardy Exp $
*/
public class CursorManager implements SVGConstants, ErrorConstants {
/**
* Maps SVG Cursor Values to Java Cursors
*/
protected static Map cursorMap;
/**
* Default cursor when value is not found
*/
public static final Cursor DEFAULT_CURSOR
= Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
/**
* Cursor used over anchors
*/
public static final Cursor ANCHOR_CURSOR
= Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
/**
* Cursor used over text
*/
public static final Cursor TEXT_CURSOR
= Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
/**
* Default preferred cursor size, used for SVG images
*/
public static final int DEFAULT_PREFERRED_WIDTH = 32;
public static final int DEFAULT_PREFERRED_HEIGHT = 32;
/**
* Static initialization of the cursorMap
*/
static {
cursorMap = new Hashtable();
cursorMap.put(SVG_CROSSHAIR_VALUE,
Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
cursorMap.put(SVG_DEFAULT_VALUE,
Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
cursorMap.put(SVG_POINTER_VALUE,
Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
cursorMap.put(SVG_MOVE_VALUE,
Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
cursorMap.put(SVG_E_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
cursorMap.put(SVG_NE_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR));
cursorMap.put(SVG_NW_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR));
cursorMap.put(SVG_N_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
cursorMap.put(SVG_SE_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
cursorMap.put(SVG_SW_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR));
cursorMap.put(SVG_S_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
cursorMap.put(SVG_W_RESIZE_VALUE,
Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
cursorMap.put(SVG_TEXT_VALUE,
Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
cursorMap.put(SVG_WAIT_VALUE,
Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
cursorMap.put(SVG_HELP_VALUE,
Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
/**
* BridgeContext associated with this CursorManager
*/
protected BridgeContext ctx;
/**
* Cache used to hold references to cursors
*/
protected CursorCache cursorCache = new CursorCache();
/**
* Constructor
*
* @param BridgeContext ctx, the BridgeContext associated to this CursorManager
*/
public CursorManager(BridgeContext ctx) {
this.ctx = ctx;
}
/**
* Returns a Cursor object for a given cursor value. This initial implementation
* does not handle user-defined cursors, so it always uses the cursor at the
* end of the list
*/
public static Cursor getPredefinedCursor(String cursorName){
return (Cursor)cursorMap.get(cursorName);
}
/**
* Returns the Cursor corresponding to the input element's cursor property
*
* @param e the element on which the cursor property is set
*/
public Cursor convertCursor(Element e) {
Value cursorValue
= CSSUtilities.getComputedStyle(e,
SVGCSSEngine.CURSOR_INDEX);
String cursorStr = SVGConstants.SVG_AUTO_VALUE;
if (cursorValue != null) {
if (cursorValue.getCssValueType() == CSSValue.CSS_PRIMITIVE_VALUE
&&
cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
// Single Value : should be one of the predefined cursors or
// 'inherit'
cursorStr = cursorValue.getStringValue();
return convertBuiltInCursor(e, cursorStr);
} else if (cursorValue.getCssValueType() == CSSValue.CSS_VALUE_LIST) {
ListValue l = (ListValue)cursorValue;
int nValues = l.getLength();
if (nValues == 1) {
cursorValue = l.item(nValues-1);
if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
cursorStr = cursorValue.getStringValue();
return convertBuiltInCursor(e, cursorStr);
}
} else if (nValues > 1) {
//
// Look for the first cursor url we can handle.
// That would be a reference to a <cursor> element.
//
return convertSVGCursor(e, l);
}
}
}
return convertBuiltInCursor(e, cursorStr);
}
public Cursor convertBuiltInCursor(Element e, String cursorStr) {
Cursor cursor = null;
// The CSS engine guarantees an non null, non empty string
// as the computed value for cursor. Therefore, the following
// test is safe.
if (cursorStr.charAt(0) == 'a') {
//
// Handle 'auto' value.
//
// - <a> The following sets the cursor for <a> element enclosing
// text nodes. Setting the proper cursor (i.e., depending on the
// children's 'cursor' property, is handled in the SVGAElementBridge
// so as to avoid going up the tree on mouseover events (looking for
// an anchor ancestor.
//
// - <image> The following does not change the cursor if the
// element's cursor property is set to 'auto'. Otherwise, it takes
// precedence over any child (in case of SVG content) cursor setting.
// This means that for images referencing SVG content, a cursor
// property set to 'auto' on the <image> element will not override
// the cursor settings inside the SVG image. Any other cursor property
// will take precedence.
//
// - <use> Same behavior as for <image> except that the behavior
// is controlled from the <use> element bridge (SVGUseElementBridge).
//
// - <text>, <tref> and <tspan> : a cursor value of auto will cause the
// cursor to be set to a text cursor. Note that text content with an
// 'auto' cursor and descendant of an anchor will have its cursor
// set to the anchor cursor through the SVGAElementBridge.
//
String nameSpaceURI = e.getNamespaceURI();
String tag = e.getNodeName();
if (SVGConstants.SVG_NAMESPACE_URI.equals(nameSpaceURI)) {
if (SVGConstants.SVG_A_TAG.equals(tag)) {
cursor = CursorManager.ANCHOR_CURSOR;
} else if (SVGConstants.SVG_TEXT_TAG.equals(tag) ||
SVGConstants.SVG_TSPAN_TAG.equals(tag) ||
SVGConstants.SVG_TREF_TAG.equals(tag) ) {
cursor = CursorManager.TEXT_CURSOR;
} else if (SVGConstants.SVG_IMAGE_TAG.equals(tag)) {
// Do not change the cursor
return null;
} else {
cursor = CursorManager.DEFAULT_CURSOR;
}
} else {
cursor = CursorManager.DEFAULT_CURSOR;
}
} else {
// Specific, logical cursor
cursor = CursorManager.getPredefinedCursor(cursorStr);
}
return cursor;
}
/**
* Returns a cursor for the given value list. Note that the
* code assumes that the input value has at least two entries.
* So the caller should check that before calling the method.
* For example, CSSUtilities.convertCursor performs that check.
*/
public Cursor convertSVGCursor(Element e, ListValue l) {
int nValues = l.getLength();
Element cursorElement = null;
for (int i=0; i<nValues-1; i++) {
Value cursorValue = l.item(i);
if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_URI) {
String uri = cursorValue.getStringValue();
// If the uri does not resolve to a cursor element,
// then, this is not a type of cursor uri we can handle:
// go to the next or default to logical cursor
try {
cursorElement = ctx.getReferencedElement(e, uri);
} catch (BridgeException be) {
// Be only silent if this is a case where the target
// could not be found. Do not catch other errors (e.g,
// malformed URIs)
if (!ERR_URI_BAD_TARGET.equals(be.getCode())) {
throw be;
}
}
if (cursorElement != null) {
// We go an element, check it is of type cursor
String cursorNS = cursorElement.getNamespaceURI();
if (SVGConstants.SVG_NAMESPACE_URI.equals(cursorNS)
&&
SVGConstants.SVG_CURSOR_TAG.equals(cursorElement.getNodeName())) {
Cursor c = convertSVGCursorElement(cursorElement);
if (c != null) {
return c;
}
}
}
}
}
// If we got to that point, it means that no cursorElement
// produced a valid cursor, i.e., either a format we support
// or a valid referenced image (no broken image).
// Fallback on the built in cursor property.
Value cursorValue = l.item(nValues-1);
String cursorStr = SVGConstants.SVG_AUTO_VALUE;
if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
cursorStr = cursorValue.getStringValue();
}
return convertBuiltInCursor(e, cursorStr);
}
/**
* Returns a cursor for a given element
*/
public Cursor convertSVGCursorElement(Element cursorElement) {
// One of the cursor url resolved to a <cursor> element
// Try to handle its image.
String uriStr = XLinkSupport.getXLinkHref(cursorElement);
if (uriStr.length() == 0) {
throw new BridgeException(cursorElement, ERR_ATTRIBUTE_MISSING,
new Object[] {"xlink:href"});
}
String baseURI = XMLBaseSupport.getCascadedXMLBase(cursorElement);
ParsedURL purl;
if (baseURI == null) {
purl = new ParsedURL(uriStr);
} else {
purl = new ParsedURL(baseURI, uriStr);
}
//
// Convert the cursor's hot spot
//
UnitProcessor.Context uctx
= UnitProcessor.createContext(ctx, cursorElement);
String s = cursorElement.getAttributeNS(null, SVG_X_ATTRIBUTE);
float x = 0;
if (s.length() != 0) {
x = UnitProcessor.svgHorizontalCoordinateToUserSpace
(s, SVG_X_ATTRIBUTE, uctx);
}
s = cursorElement.getAttributeNS(null, SVG_Y_ATTRIBUTE);
float y = 0;
if (s.length() != 0) {
y = UnitProcessor.svgVerticalCoordinateToUserSpace
(s, SVG_Y_ATTRIBUTE, uctx);
}
CursorDescriptor desc = new CursorDescriptor(purl, x, y);
//
// Check if there is a cursor in the cache for this url
//
Cursor cachedCursor = cursorCache.getCursor(desc);
if (cachedCursor != null) {
return cachedCursor;
}
//
// Load image into Filter f and transform hotSpot to
// cursor space.
//
Point2D.Float hotSpot = new Point2D.Float(x, y);
Filter f = cursorHrefToFilter(cursorElement,
purl,
hotSpot);
if (f == null) {
cursorCache.clearCursor(desc);
return null;
}
// The returned Filter is guaranteed to create a
// default rendering of the desired size
Rectangle cursorSize = f.getBounds2D().getBounds();
RenderedImage ri = f.createScaledRendering(cursorSize.width,
cursorSize.height,
null);
Image img = null;
if (ri instanceof Image) {
img = (Image)ri;
} else {
img = renderedImageToImage(ri);
}
// Make sure the not spot does not fall out of the cursor area. If it
// does, then clamp the coordinates to the image space.
hotSpot.x = hotSpot.x < 0 ? 0 : hotSpot.x;
hotSpot.y = hotSpot.y < 0 ? 0 : hotSpot.y;
hotSpot.x = hotSpot.x > (cursorSize.width-1) ? cursorSize.width - 1 : hotSpot.x;
hotSpot.y = hotSpot.y > (cursorSize.height-1) ? cursorSize.height - 1: hotSpot.y;
//
// The cursor image is now into 'img'
//
Cursor c = Toolkit.getDefaultToolkit()
.createCustomCursor(img,
new Point((int)Math.round(hotSpot.x),
(int)Math.round(hotSpot.y)),
purl.toString());
cursorCache.putCursor(desc, c);
return c;
}
/**
* Converts the input ParsedURL into a Filter and transforms the
* input hotSpot point (in image space) to cursor space
*/
protected Filter cursorHrefToFilter(Element cursorElement,
ParsedURL purl,
Point2D hotSpot) {
AffineRable8Bit f = null;
String uriStr = purl.toString();
Dimension cursorSize = null;
// Try to load as an SVG Document
DocumentLoader loader = (DocumentLoader)ctx.getDocumentLoader();
SVGDocument svgDoc = (SVGDocument)cursorElement.getOwnerDocument();
URIResolver resolver = new URIResolver(svgDoc, loader);
try {
Element rootElement = null;
Node n = resolver.getNode(uriStr, cursorElement);
if (n.getNodeType() == n.DOCUMENT_NODE) {
rootElement = ((SVGDocument)n).getRootElement();
} else {
throw new BridgeException
(cursorElement, ERR_URI_IMAGE_INVALID,
new Object[] {uriStr});
}
GraphicsNode node = ctx.getGVTBuilder().build(ctx, rootElement);
//
// The cursorSize define the viewport into which the
// cursor is displayed. That viewport is platform
// dependant and is not defined by the SVG content.
//
float width = DEFAULT_PREFERRED_WIDTH;
float height = DEFAULT_PREFERRED_HEIGHT;
UnitProcessor.Context uctx
= UnitProcessor.createContext(ctx, rootElement);
String s = rootElement.getAttribute(SVG_WIDTH_ATTRIBUTE);
if (s.length() != 0) {
width = UnitProcessor.svgHorizontalLengthToUserSpace
(s, SVG_WIDTH_ATTRIBUTE, uctx);
}
s = rootElement.getAttribute(SVG_HEIGHT_ATTRIBUTE);
if (s.length() != 0) {
height = UnitProcessor.svgVerticalLengthToUserSpace
(s, SVG_HEIGHT_ATTRIBUTE, uctx);
}
cursorSize
= Toolkit.getDefaultToolkit().getBestCursorSize
((int)Math.round(width), (int)Math.round(height));
// Handle the viewBox transform
AffineTransform at
= ViewBox.getPreserveAspectRatioTransform(rootElement,
cursorSize.width,
cursorSize.height);
Filter filter = node.getGraphicsNodeRable(true);
f = new AffineRable8Bit(filter, at);
} catch (BridgeException ex) {
throw ex;
} catch (SecurityException ex) {
throw new BridgeException(cursorElement, ERR_URI_UNSECURE,
new Object[] {uriStr});
} catch (Exception ex) {
/* Nothing to do */
}
// If f is null, it means that we are not dealing with
// an SVG image. Try as a raster image.
if (f == null) {
ImageTagRegistry reg = ImageTagRegistry.getRegistry();
Filter filter = reg.readURL(purl);
if (filter == null) {
return null;
}
// Check if we got a broken image
if (filter.getProperty
(SVGBrokenLinkProvider.SVG_BROKEN_LINK_DOCUMENT_PROPERTY) != null) {
return null;
}
Rectangle preferredSize = filter.getBounds2D().getBounds();
cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize
(preferredSize.width, preferredSize.height);
if (preferredSize != null && preferredSize.width >0
&& preferredSize.height > 0 ) {
AffineTransform at = new AffineTransform();
if (preferredSize.width > cursorSize.width
||
preferredSize.height > cursorSize.height) {
at = ViewBox.getPreserveAspectRatioTransform
(new float[] {0, 0, preferredSize.width, preferredSize.height},
SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN,
true,
cursorSize.width,
cursorSize.height);
}
f = new AffineRable8Bit(filter, at);
} else {
// Invalid Size
return null;
}
}
//
// Transform the hot spot from image space to cursor space
//
AffineTransform at = f.getAffine();
at.transform(hotSpot, hotSpot);
//
// In all cases, clip to the cursor boundaries
//
Rectangle cursorViewport
= new Rectangle(0, 0, cursorSize.width, cursorSize.height);
PadRable8Bit cursorImage
= new PadRable8Bit(f, cursorViewport,
PadMode.ZERO_PAD);
return cursorImage;
}
/**
* Implementation helper: converts a RenderedImage to an Image
*/
protected Image renderedImageToImage(RenderedImage ri) {
int x = ri.getMinX();
int y = ri.getMinY();
SampleModel sm = ri.getSampleModel();
ColorModel cm = ri.getColorModel();
WritableRaster wr = Raster.createWritableRaster(sm, new Point(x,y));
ri.copyData(wr);
return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
}
/**
* Simple inner class which holds the information describing
* a cursor, i.e., the image it points to and the hot spot point
* coordinates.
*/
static class CursorDescriptor {
ParsedURL purl;
float x;
float y;
String desc;
public CursorDescriptor(ParsedURL purl,
float x, float y) {
if (purl == null) {
throw new IllegalArgumentException();
}
this.purl = purl;
this.x = x;
this.y = y;
// Desc is used for hascode as well as for toString()
this.desc = this.getClass().getName() +
"\n\t:[" + this.purl + "]\n\t:[" + x + "]:[" + y + "]";
}
public boolean equals(Object obj) {
if (obj == null
||
!(obj instanceof CursorDescriptor)) {
return false;
}
CursorDescriptor desc = (CursorDescriptor)obj;
boolean isEqual =
this.purl.equals(desc.purl)
&&
this.x == desc.x
&&
this.y == desc.y;
return isEqual;
}
public String toString() {
return this.desc;
}
public int hashCode() {
return desc.hashCode();
}
}
/**
* Simple extension of the SoftReferenceCache that
* offers typed interface (Kind of needed as SoftReferenceCache
* mostly has protected methods).
*/
static class CursorCache extends SoftReferenceCache {
public CursorCache() {
}
public Cursor getCursor(CursorDescriptor desc) {
return (Cursor)requestImpl(desc);
}
public void putCursor(CursorDescriptor desc,
Cursor cursor) {
putImpl(desc, cursor);
}
public void clearCursor(CursorDescriptor desc) {
clearImpl(desc);
}
}
}