001: /*
002:
003: Licensed to the Apache Software Foundation (ASF) under one or more
004: contributor license agreements. See the NOTICE file distributed with
005: this work for additional information regarding copyright ownership.
006: The ASF licenses this file to You under the Apache License, Version 2.0
007: (the "License"); you may not use this file except in compliance with
008: the License. You may obtain a copy of the License at
009:
010: http://www.apache.org/licenses/LICENSE-2.0
011:
012: Unless required by applicable law or agreed to in writing, software
013: distributed under the License is distributed on an "AS IS" BASIS,
014: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: See the License for the specific language governing permissions and
016: limitations under the License.
017:
018: */
019: package org.apache.batik.bridge;
020:
021: import java.awt.Cursor;
022: import java.awt.Dimension;
023: import java.awt.Image;
024: import java.awt.Point;
025: import java.awt.Rectangle;
026: import java.awt.Toolkit;
027: import java.awt.geom.AffineTransform;
028: import java.awt.geom.Point2D;
029: import java.awt.image.BufferedImage;
030: import java.awt.image.ColorModel;
031: import java.awt.image.Raster;
032: import java.awt.image.RenderedImage;
033: import java.awt.image.SampleModel;
034: import java.awt.image.WritableRaster;
035: import java.util.Hashtable;
036: import java.util.Map;
037:
038: import org.apache.batik.css.engine.SVGCSSEngine;
039: import org.apache.batik.css.engine.value.Value;
040: import org.apache.batik.dom.AbstractNode;
041: import org.apache.batik.dom.util.XLinkSupport;
042: import org.apache.batik.ext.awt.image.PadMode;
043: import org.apache.batik.ext.awt.image.renderable.AffineRable8Bit;
044: import org.apache.batik.ext.awt.image.renderable.Filter;
045: import org.apache.batik.ext.awt.image.renderable.PadRable8Bit;
046: import org.apache.batik.ext.awt.image.spi.BrokenLinkProvider;
047: import org.apache.batik.ext.awt.image.spi.ImageTagRegistry;
048: import org.apache.batik.gvt.GraphicsNode;
049: import org.apache.batik.util.ParsedURL;
050: import org.apache.batik.util.SVGConstants;
051: import org.apache.batik.util.SoftReferenceCache;
052: import org.w3c.dom.Element;
053: import org.w3c.dom.Node;
054: import org.w3c.dom.css.CSSPrimitiveValue;
055: import org.w3c.dom.css.CSSValue;
056: import org.w3c.dom.svg.SVGDocument;
057: import org.w3c.dom.svg.SVGPreserveAspectRatio;
058:
059: /**
060: * The CursorManager class is a helper class which preloads the cursors
061: * corresponding to the SVG built in cursors.
062: *
063: * @author <a href="mailto:vincent.hardy@sun.com">Vincent Hardy</a>
064: * @version $Id: CursorManager.java 501922 2007-01-31 17:47:47Z dvholten $
065: */
066: public class CursorManager implements SVGConstants, ErrorConstants {
067: /**
068: * Maps SVG Cursor Values to Java Cursors
069: */
070: protected static Map cursorMap;
071:
072: /**
073: * Default cursor when value is not found
074: */
075: public static final Cursor DEFAULT_CURSOR = Cursor
076: .getPredefinedCursor(Cursor.DEFAULT_CURSOR);
077:
078: /**
079: * Cursor used over anchors
080: */
081: public static final Cursor ANCHOR_CURSOR = Cursor
082: .getPredefinedCursor(Cursor.HAND_CURSOR);
083:
084: /**
085: * Cursor used over text
086: */
087: public static final Cursor TEXT_CURSOR = Cursor
088: .getPredefinedCursor(Cursor.TEXT_CURSOR);
089:
090: /**
091: * Default preferred cursor size, used for SVG images
092: */
093: public static final int DEFAULT_PREFERRED_WIDTH = 32;
094: public static final int DEFAULT_PREFERRED_HEIGHT = 32;
095:
096: /**
097: * Static initialization of the cursorMap
098: */
099: static {
100: cursorMap = new Hashtable();
101: cursorMap.put(SVG_CROSSHAIR_VALUE, Cursor
102: .getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
103: cursorMap.put(SVG_DEFAULT_VALUE, Cursor
104: .getPredefinedCursor(Cursor.DEFAULT_CURSOR));
105: cursorMap.put(SVG_POINTER_VALUE, Cursor
106: .getPredefinedCursor(Cursor.HAND_CURSOR));
107: cursorMap.put(SVG_MOVE_VALUE, Cursor
108: .getPredefinedCursor(Cursor.MOVE_CURSOR));
109: cursorMap.put(SVG_E_RESIZE_VALUE, Cursor
110: .getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
111: cursorMap.put(SVG_NE_RESIZE_VALUE, Cursor
112: .getPredefinedCursor(Cursor.NE_RESIZE_CURSOR));
113: cursorMap.put(SVG_NW_RESIZE_VALUE, Cursor
114: .getPredefinedCursor(Cursor.NW_RESIZE_CURSOR));
115: cursorMap.put(SVG_N_RESIZE_VALUE, Cursor
116: .getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
117: cursorMap.put(SVG_SE_RESIZE_VALUE, Cursor
118: .getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
119: cursorMap.put(SVG_SW_RESIZE_VALUE, Cursor
120: .getPredefinedCursor(Cursor.SW_RESIZE_CURSOR));
121: cursorMap.put(SVG_S_RESIZE_VALUE, Cursor
122: .getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
123: cursorMap.put(SVG_W_RESIZE_VALUE, Cursor
124: .getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
125: cursorMap.put(SVG_TEXT_VALUE, Cursor
126: .getPredefinedCursor(Cursor.TEXT_CURSOR));
127: cursorMap.put(SVG_WAIT_VALUE, Cursor
128: .getPredefinedCursor(Cursor.WAIT_CURSOR));
129: cursorMap.put(SVG_HELP_VALUE, Cursor
130: .getPredefinedCursor(Cursor.HAND_CURSOR));
131:
132: }
133:
134: /**
135: * BridgeContext associated with this CursorManager
136: */
137: protected BridgeContext ctx;
138:
139: /**
140: * Cache used to hold references to cursors
141: */
142: protected CursorCache cursorCache = new CursorCache();
143:
144: /**
145: * Creates a new CursorManager object.
146: *
147: * @param ctx the BridgeContext associated to this CursorManager
148: */
149: public CursorManager(BridgeContext ctx) {
150: this .ctx = ctx;
151: }
152:
153: /**
154: * Returns a Cursor object for a given cursor value. This initial
155: * implementation does not handle user-defined cursors, so it
156: * always uses the cursor at the end of the list
157: */
158: public static Cursor getPredefinedCursor(String cursorName) {
159: return (Cursor) cursorMap.get(cursorName);
160: }
161:
162: /**
163: * Returns the Cursor corresponding to the input element's cursor property
164: *
165: * @param e the element on which the cursor property is set
166: */
167: public Cursor convertCursor(Element e) {
168: Value cursorValue = CSSUtilities.getComputedStyle(e,
169: SVGCSSEngine.CURSOR_INDEX);
170:
171: String cursorStr = SVGConstants.SVG_AUTO_VALUE;
172:
173: if (cursorValue != null) {
174: if (cursorValue.getCssValueType() == CSSValue.CSS_PRIMITIVE_VALUE
175: && cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
176: // Single Value : should be one of the predefined cursors or
177: // 'inherit'
178: cursorStr = cursorValue.getStringValue();
179: return convertBuiltInCursor(e, cursorStr);
180: } else if (cursorValue.getCssValueType() == CSSValue.CSS_VALUE_LIST) {
181: int nValues = cursorValue.getLength();
182: if (nValues == 1) {
183: cursorValue = cursorValue.item(0);
184: if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
185: cursorStr = cursorValue.getStringValue();
186: return convertBuiltInCursor(e, cursorStr);
187: }
188: } else if (nValues > 1) {
189: //
190: // Look for the first cursor url we can handle.
191: // That would be a reference to a <cursor> element.
192: //
193: return convertSVGCursor(e, cursorValue);
194: }
195: }
196: }
197:
198: return convertBuiltInCursor(e, cursorStr);
199: }
200:
201: public Cursor convertBuiltInCursor(Element e, String cursorStr) {
202: Cursor cursor = null;
203:
204: // The CSS engine guarantees an non null, non empty string
205: // as the computed value for cursor. Therefore, the following
206: // test is safe.
207: if (cursorStr.charAt(0) == 'a') {
208: //
209: // Handle 'auto' value.
210: //
211: // - <a> The following sets the cursor for <a> element
212: // enclosing text nodes. Setting the proper cursor (i.e.,
213: // depending on the children's 'cursor' property, is
214: // handled in the SVGAElementBridge so as to avoid going
215: // up the tree on mouseover events (looking for an anchor
216: // ancestor.
217: //
218: // - <image> The following does not change the cursor if
219: // the element's cursor property is set to
220: // 'auto'. Otherwise, it takes precedence over any child
221: // (in case of SVG content) cursor setting. This means
222: // that for images referencing SVG content, a cursor
223: // property set to 'auto' on the <image> element will not
224: // override the cursor settings inside the SVG image. Any
225: // other cursor property will take precedence.
226: //
227: // - <use> Same behavior as for <image> except that the
228: // behavior is controlled from the <use> element bridge
229: // (SVGUseElementBridge).
230: //
231: // - <text>, <tref> and <tspan> : a cursor value of auto
232: // will cause the cursor to be set to a text cursor. Note
233: // that text content with an 'auto' cursor and descendant
234: // of an anchor will have its cursor set to the anchor
235: // cursor through the SVGAElementBridge.
236: //
237: String nameSpaceURI = e.getNamespaceURI();
238: if (SVGConstants.SVG_NAMESPACE_URI.equals(nameSpaceURI)) {
239: String tag = e.getLocalName();
240: if (SVGConstants.SVG_A_TAG.equals(tag)) {
241: cursor = CursorManager.ANCHOR_CURSOR;
242: } else if (SVGConstants.SVG_TEXT_TAG.equals(tag)
243: || SVGConstants.SVG_TSPAN_TAG.equals(tag)
244: || SVGConstants.SVG_TREF_TAG.equals(tag)) {
245: cursor = CursorManager.TEXT_CURSOR;
246: } else if (SVGConstants.SVG_IMAGE_TAG.equals(tag)) {
247: // Do not change the cursor
248: return null;
249: } else {
250: cursor = CursorManager.DEFAULT_CURSOR;
251: }
252: } else {
253: cursor = CursorManager.DEFAULT_CURSOR;
254: }
255: } else {
256: // Specific, logical cursor
257: cursor = CursorManager.getPredefinedCursor(cursorStr);
258: }
259:
260: return cursor;
261: }
262:
263: /**
264: * Returns a cursor for the given value list. Note that the
265: * code assumes that the input value has at least two entries.
266: * So the caller should check that before calling the method.
267: * For example, CSSUtilities.convertCursor performs that check.
268: */
269: public Cursor convertSVGCursor(Element e, Value l) {
270: int nValues = l.getLength();
271: Element cursorElement = null;
272: for (int i = 0; i < nValues - 1; i++) {
273: Value cursorValue = l.item(i);
274: if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_URI) {
275: String uri = cursorValue.getStringValue();
276:
277: // If the uri does not resolve to a cursor element,
278: // then, this is not a type of cursor uri we can handle:
279: // go to the next or default to logical cursor
280: try {
281: cursorElement = ctx.getReferencedElement(e, uri);
282: } catch (BridgeException be) {
283: // Be only silent if this is a case where the target
284: // could not be found. Do not catch other errors (e.g,
285: // malformed URIs)
286: if (!ERR_URI_BAD_TARGET.equals(be.getCode())) {
287: throw be;
288: }
289: }
290:
291: if (cursorElement != null) {
292: // We go an element, check it is of type cursor
293: String cursorNS = cursorElement.getNamespaceURI();
294: if (SVGConstants.SVG_NAMESPACE_URI.equals(cursorNS)
295: && SVGConstants.SVG_CURSOR_TAG
296: .equals(cursorElement
297: .getLocalName())) {
298: Cursor c = convertSVGCursorElement(cursorElement);
299: if (c != null) {
300: return c;
301: }
302: }
303: }
304: }
305: }
306:
307: // If we got to that point, it means that no cursorElement
308: // produced a valid cursor, i.e., either a format we support
309: // or a valid referenced image (no broken image).
310: // Fallback on the built in cursor property.
311: Value cursorValue = l.item(nValues - 1);
312: String cursorStr = SVGConstants.SVG_AUTO_VALUE;
313: if (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
314: cursorStr = cursorValue.getStringValue();
315: }
316:
317: return convertBuiltInCursor(e, cursorStr);
318: }
319:
320: /**
321: * Returns a cursor for a given element
322: */
323: public Cursor convertSVGCursorElement(Element cursorElement) {
324: // One of the cursor url resolved to a <cursor> element
325: // Try to handle its image.
326: String uriStr = XLinkSupport.getXLinkHref(cursorElement);
327: if (uriStr.length() == 0) {
328: throw new BridgeException(ctx, cursorElement,
329: ERR_ATTRIBUTE_MISSING,
330: new Object[] { "xlink:href" });
331: }
332:
333: String baseURI = AbstractNode.getBaseURI(cursorElement);
334: ParsedURL purl;
335: if (baseURI == null) {
336: purl = new ParsedURL(uriStr);
337: } else {
338: purl = new ParsedURL(baseURI, uriStr);
339: }
340:
341: //
342: // Convert the cursor's hot spot
343: //
344: UnitProcessor.Context uctx = UnitProcessor.createContext(ctx,
345: cursorElement);
346:
347: String s = cursorElement.getAttributeNS(null, SVG_X_ATTRIBUTE);
348: float x = 0;
349: if (s.length() != 0) {
350: x = UnitProcessor.svgHorizontalCoordinateToUserSpace(s,
351: SVG_X_ATTRIBUTE, uctx);
352: }
353:
354: s = cursorElement.getAttributeNS(null, SVG_Y_ATTRIBUTE);
355: float y = 0;
356: if (s.length() != 0) {
357: y = UnitProcessor.svgVerticalCoordinateToUserSpace(s,
358: SVG_Y_ATTRIBUTE, uctx);
359: }
360:
361: CursorDescriptor desc = new CursorDescriptor(purl, x, y);
362:
363: //
364: // Check if there is a cursor in the cache for this url
365: //
366: Cursor cachedCursor = cursorCache.getCursor(desc);
367:
368: if (cachedCursor != null) {
369: return cachedCursor;
370: }
371:
372: //
373: // Load image into Filter f and transform hotSpot to
374: // cursor space.
375: //
376: Point2D.Float hotSpot = new Point2D.Float(x, y);
377: Filter f = cursorHrefToFilter(cursorElement, purl, hotSpot);
378: if (f == null) {
379: cursorCache.clearCursor(desc);
380: return null;
381: }
382:
383: // The returned Filter is guaranteed to create a
384: // default rendering of the desired size
385: Rectangle cursorSize = f.getBounds2D().getBounds();
386: RenderedImage ri = f.createScaledRendering(cursorSize.width,
387: cursorSize.height, null);
388: Image img = null;
389:
390: if (ri instanceof Image) {
391: img = (Image) ri;
392: } else {
393: img = renderedImageToImage(ri);
394: }
395:
396: // Make sure the not spot does not fall out of the cursor area. If it
397: // does, then clamp the coordinates to the image space.
398: hotSpot.x = hotSpot.x < 0 ? 0 : hotSpot.x;
399: hotSpot.y = hotSpot.y < 0 ? 0 : hotSpot.y;
400: hotSpot.x = hotSpot.x > (cursorSize.width - 1) ? cursorSize.width - 1
401: : hotSpot.x;
402: hotSpot.y = hotSpot.y > (cursorSize.height - 1) ? cursorSize.height - 1
403: : hotSpot.y;
404:
405: //
406: // The cursor image is now into 'img'
407: //
408: Cursor c = Toolkit.getDefaultToolkit()
409: .createCustomCursor(
410: img,
411: new Point(Math.round(hotSpot.x), Math
412: .round(hotSpot.y)), purl.toString());
413:
414: cursorCache.putCursor(desc, c);
415: return c;
416: }
417:
418: /**
419: * Converts the input ParsedURL into a Filter and transforms the
420: * input hotSpot point (in image space) to cursor space
421: */
422: protected Filter cursorHrefToFilter(Element cursorElement,
423: ParsedURL purl, Point2D hotSpot) {
424:
425: AffineRable8Bit f = null;
426: String uriStr = purl.toString();
427: Dimension cursorSize = null;
428:
429: // Try to load as an SVG Document
430: DocumentLoader loader = ctx.getDocumentLoader();
431: SVGDocument svgDoc = (SVGDocument) cursorElement
432: .getOwnerDocument();
433: URIResolver resolver = ctx.createURIResolver(svgDoc, loader);
434: try {
435: Element rootElement = null;
436: Node n = resolver.getNode(uriStr, cursorElement);
437: if (n.getNodeType() == Node.DOCUMENT_NODE) {
438: SVGDocument doc = (SVGDocument) n;
439: // FIXX: really should be subCtx here.
440: ctx.initializeDocument(doc);
441: rootElement = doc.getRootElement();
442: } else {
443: throw new BridgeException(ctx, cursorElement,
444: ERR_URI_IMAGE_INVALID, new Object[] { uriStr });
445: }
446: GraphicsNode node = ctx.getGVTBuilder().build(ctx,
447: rootElement);
448:
449: //
450: // The cursorSize define the viewport into which the
451: // cursor is displayed. That viewport is platform
452: // dependant and is not defined by the SVG content.
453: //
454: float width = DEFAULT_PREFERRED_WIDTH;
455: float height = DEFAULT_PREFERRED_HEIGHT;
456: UnitProcessor.Context uctx = UnitProcessor.createContext(
457: ctx, rootElement);
458:
459: String s = rootElement.getAttribute(SVG_WIDTH_ATTRIBUTE);
460: if (s.length() != 0) {
461: width = UnitProcessor.svgHorizontalLengthToUserSpace(s,
462: SVG_WIDTH_ATTRIBUTE, uctx);
463: }
464:
465: s = rootElement.getAttribute(SVG_HEIGHT_ATTRIBUTE);
466: if (s.length() != 0) {
467: height = UnitProcessor.svgVerticalLengthToUserSpace(s,
468: SVG_HEIGHT_ATTRIBUTE, uctx);
469: }
470:
471: cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(
472: Math.round(width), Math.round(height));
473:
474: // Handle the viewBox transform
475: AffineTransform at = ViewBox
476: .getPreserveAspectRatioTransform(rootElement,
477: cursorSize.width, cursorSize.height, ctx);
478: Filter filter = node.getGraphicsNodeRable(true);
479: f = new AffineRable8Bit(filter, at);
480: } catch (BridgeException ex) {
481: throw ex;
482: } catch (SecurityException ex) {
483: throw new BridgeException(ctx, cursorElement, ex,
484: ERR_URI_UNSECURE, new Object[] { uriStr });
485: } catch (Exception ex) {
486: /* Nothing to do */
487: }
488:
489: // If f is null, it means that we are not dealing with
490: // an SVG image. Try as a raster image.
491: if (f == null) {
492: ImageTagRegistry reg = ImageTagRegistry.getRegistry();
493: Filter filter = reg.readURL(purl);
494: if (filter == null) {
495: return null;
496: }
497:
498: // Check if we got a broken image
499: if (BrokenLinkProvider.hasBrokenLinkProperty(filter)) {
500: return null;
501: }
502:
503: Rectangle preferredSize = filter.getBounds2D().getBounds();
504: cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(
505: preferredSize.width, preferredSize.height);
506:
507: if (preferredSize != null && preferredSize.width > 0
508: && preferredSize.height > 0) {
509: AffineTransform at = new AffineTransform();
510: if (preferredSize.width > cursorSize.width
511: || preferredSize.height > cursorSize.height) {
512: at = ViewBox
513: .getPreserveAspectRatioTransform(
514: new float[] { 0, 0,
515: preferredSize.width,
516: preferredSize.height },
517: SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN,
518: true, cursorSize.width,
519: cursorSize.height);
520: }
521: f = new AffineRable8Bit(filter, at);
522: } else {
523: // Invalid Size
524: return null;
525: }
526: }
527:
528: //
529: // Transform the hot spot from image space to cursor space
530: //
531: AffineTransform at = f.getAffine();
532: at.transform(hotSpot, hotSpot);
533:
534: //
535: // In all cases, clip to the cursor boundaries
536: //
537: Rectangle cursorViewport = new Rectangle(0, 0,
538: cursorSize.width, cursorSize.height);
539:
540: PadRable8Bit cursorImage = new PadRable8Bit(f, cursorViewport,
541: PadMode.ZERO_PAD);
542:
543: return cursorImage;
544:
545: }
546:
547: /**
548: * Implementation helper: converts a RenderedImage to an Image
549: */
550: protected Image renderedImageToImage(RenderedImage ri) {
551: int x = ri.getMinX();
552: int y = ri.getMinY();
553: SampleModel sm = ri.getSampleModel();
554: ColorModel cm = ri.getColorModel();
555: WritableRaster wr = Raster.createWritableRaster(sm, new Point(
556: x, y));
557: ri.copyData(wr);
558:
559: return new BufferedImage(cm, wr, cm.isAlphaPremultiplied(),
560: null);
561: }
562:
563: /**
564: * Simple inner class which holds the information describing
565: * a cursor, i.e., the image it points to and the hot spot point
566: * coordinates.
567: */
568: static class CursorDescriptor {
569: ParsedURL purl;
570: float x;
571: float y;
572: String desc;
573:
574: public CursorDescriptor(ParsedURL purl, float x, float y) {
575: if (purl == null) {
576: throw new IllegalArgumentException();
577: }
578:
579: this .purl = purl;
580: this .x = x;
581: this .y = y;
582:
583: // Desc is used for hascode as well as for toString()
584: this .desc = this .getClass().getName() + "\n\t:["
585: + this .purl + "]\n\t:[" + x + "]:[" + y + "]";
586: }
587:
588: public boolean equals(Object obj) {
589: if (obj == null || !(obj instanceof CursorDescriptor)) {
590: return false;
591: }
592:
593: CursorDescriptor desc = (CursorDescriptor) obj;
594: boolean isEqual = this .purl.equals(desc.purl)
595: && this .x == desc.x && this .y == desc.y;
596:
597: return isEqual;
598: }
599:
600: public String toString() {
601: return this .desc;
602: }
603:
604: public int hashCode() {
605: return desc.hashCode();
606: }
607: }
608:
609: /**
610: * Simple extension of the SoftReferenceCache that
611: * offers typed interface (Kind of needed as SoftReferenceCache
612: * mostly has protected methods).
613: */
614: static class CursorCache extends SoftReferenceCache {
615: public CursorCache() {
616: }
617:
618: public Cursor getCursor(CursorDescriptor desc) {
619: return (Cursor) requestImpl(desc);
620: }
621:
622: public void putCursor(CursorDescriptor desc, Cursor cursor) {
623: putImpl(desc, cursor);
624: }
625:
626: public void clearCursor(CursorDescriptor desc) {
627: clearImpl(desc);
628: }
629: }
630:
631: }
|