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