0001: /*
0002:
0003: Licensed to the Apache Software Foundation (ASF) under one or more
0004: contributor license agreements. See the NOTICE file distributed with
0005: this work for additional information regarding copyright ownership.
0006: The ASF licenses this file to You under the Apache License, Version 2.0
0007: (the "License"); you may not use this file except in compliance with
0008: the License. You may obtain a copy of the License at
0009:
0010: http://www.apache.org/licenses/LICENSE-2.0
0011:
0012: Unless required by applicable law or agreed to in writing, software
0013: distributed under the License is distributed on an "AS IS" BASIS,
0014: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0015: See the License for the specific language governing permissions and
0016: limitations under the License.
0017:
0018: */
0019: package org.apache.batik.bridge;
0020:
0021: import java.awt.RenderingHints;
0022: import java.awt.Shape;
0023: import java.awt.color.ColorSpace;
0024: import java.awt.color.ICC_Profile;
0025: import java.awt.geom.AffineTransform;
0026: import java.awt.geom.Rectangle2D;
0027: import java.io.BufferedInputStream;
0028: import java.io.InputStream;
0029: import java.io.InterruptedIOException;
0030: import java.io.IOException;
0031: import java.util.ArrayList;
0032: import java.util.List;
0033:
0034: import org.apache.batik.css.engine.CSSEngine;
0035: import org.apache.batik.css.engine.SVGCSSEngine;
0036: import org.apache.batik.dom.AbstractNode;
0037: import org.apache.batik.dom.events.DOMMouseEvent;
0038: import org.apache.batik.dom.events.NodeEventTarget;
0039: import org.apache.batik.dom.svg.AnimatedLiveAttributeValue;
0040: import org.apache.batik.dom.svg.LiveAttributeException;
0041: import org.apache.batik.dom.svg.SVGOMDocument;
0042: import org.apache.batik.dom.svg.SVGOMElement;
0043: import org.apache.batik.ext.awt.color.ICCColorSpaceExt;
0044: import org.apache.batik.ext.awt.image.renderable.ClipRable8Bit;
0045: import org.apache.batik.ext.awt.image.renderable.Filter;
0046: import org.apache.batik.ext.awt.image.spi.BrokenLinkProvider;
0047: import org.apache.batik.ext.awt.image.spi.ImageTagRegistry;
0048: import org.apache.batik.gvt.CanvasGraphicsNode;
0049: import org.apache.batik.gvt.CompositeGraphicsNode;
0050: import org.apache.batik.gvt.GraphicsNode;
0051: import org.apache.batik.gvt.ImageNode;
0052: import org.apache.batik.gvt.RasterImageNode;
0053: import org.apache.batik.gvt.ShapeNode;
0054: import org.apache.batik.util.HaltingThread;
0055: import org.apache.batik.util.MimeTypeConstants;
0056: import org.apache.batik.util.ParsedURL;
0057: import org.apache.batik.util.XMLConstants;
0058:
0059: import org.w3c.dom.Document;
0060: import org.w3c.dom.Element;
0061: import org.w3c.dom.events.DocumentEvent;
0062: import org.w3c.dom.events.Event;
0063: import org.w3c.dom.events.EventListener;
0064: import org.w3c.dom.events.EventTarget;
0065: import org.w3c.dom.svg.SVGAnimatedPreserveAspectRatio;
0066: import org.w3c.dom.svg.SVGDocument;
0067: import org.w3c.dom.svg.SVGImageElement;
0068: import org.w3c.dom.svg.SVGSVGElement;
0069:
0070: /**
0071: * Bridge class for the <image> element.
0072: *
0073: * @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
0074: * @version $Id: SVGImageElementBridge.java 501922 2007-01-31 17:47:47Z dvholten $
0075: */
0076: public class SVGImageElementBridge extends AbstractGraphicsNodeBridge {
0077:
0078: protected SVGDocument imgDocument;
0079: protected EventListener listener = null;
0080: protected BridgeContext subCtx = null;
0081: protected boolean hitCheckChildren = false;
0082:
0083: /**
0084: * Constructs a new bridge for the <image> element.
0085: */
0086: public SVGImageElementBridge() {
0087: }
0088:
0089: /**
0090: * Returns 'image'.
0091: */
0092: public String getLocalName() {
0093: return SVG_IMAGE_TAG;
0094: }
0095:
0096: /**
0097: * Returns a new instance of this bridge.
0098: */
0099: public Bridge getInstance() {
0100: return new SVGImageElementBridge();
0101: }
0102:
0103: /**
0104: * Creates a graphics node using the specified BridgeContext and for the
0105: * specified element.
0106: *
0107: * @param ctx the bridge context to use
0108: * @param e the element that describes the graphics node to build
0109: * @return a graphics node that represents the specified element
0110: */
0111: public GraphicsNode createGraphicsNode(BridgeContext ctx, Element e) {
0112: ImageNode imageNode = (ImageNode) super .createGraphicsNode(ctx,
0113: e);
0114: if (imageNode == null) {
0115: return null;
0116: }
0117:
0118: associateSVGContext(ctx, e, imageNode);
0119:
0120: hitCheckChildren = false;
0121: GraphicsNode node = buildImageGraphicsNode(ctx, e);
0122:
0123: if (node == null) {
0124: SVGImageElement ie = (SVGImageElement) e;
0125: String uriStr = ie.getHref().getAnimVal();
0126: throw new BridgeException(ctx, e, ERR_URI_IMAGE_INVALID,
0127: new Object[] { uriStr });
0128: }
0129:
0130: imageNode.setImage(node);
0131: imageNode.setHitCheckChildren(hitCheckChildren);
0132:
0133: // 'image-rendering' and 'color-rendering'
0134: RenderingHints hints = null;
0135: hints = CSSUtilities.convertImageRendering(e, hints);
0136: hints = CSSUtilities.convertColorRendering(e, hints);
0137: if (hints != null)
0138: imageNode.setRenderingHints(hints);
0139:
0140: return imageNode;
0141: }
0142:
0143: /**
0144: * Create a Graphics node according to the
0145: * resource pointed by the href : RasterImageNode
0146: * for bitmaps, CompositeGraphicsNode for svg files.
0147: *
0148: * @param ctx : the bridge context to use
0149: * @param e the element that describes the graphics node to build
0150: *
0151: * @return the graphic node that represent the resource
0152: * pointed by the reference
0153: */
0154: protected GraphicsNode buildImageGraphicsNode(BridgeContext ctx,
0155: Element e) {
0156:
0157: SVGImageElement ie = (SVGImageElement) e;
0158:
0159: // 'xlink:href' attribute - required
0160: String uriStr = ie.getHref().getAnimVal();
0161: if (uriStr.length() == 0) {
0162: throw new BridgeException(ctx, e, ERR_ATTRIBUTE_MISSING,
0163: new Object[] { "xlink:href" });
0164: }
0165: if (uriStr.indexOf('#') != -1) {
0166: throw new BridgeException(ctx, e,
0167: ERR_ATTRIBUTE_VALUE_MALFORMED, new Object[] {
0168: "xlink:href", uriStr });
0169: }
0170:
0171: // Build the URL.
0172: String baseURI = AbstractNode.getBaseURI(e);
0173: ParsedURL purl;
0174: if (baseURI == null) {
0175: purl = new ParsedURL(uriStr);
0176: } else {
0177: purl = new ParsedURL(baseURI, uriStr);
0178: }
0179:
0180: return createImageGraphicsNode(ctx, e, purl);
0181: }
0182:
0183: protected GraphicsNode createImageGraphicsNode(BridgeContext ctx,
0184: Element e, ParsedURL purl) {
0185: Rectangle2D bounds = getImageBounds(ctx, e);
0186: if ((bounds.getWidth() == 0) || (bounds.getHeight() == 0)) {
0187: ShapeNode sn = new ShapeNode();
0188: sn.setShape(bounds);
0189: return sn;
0190: }
0191:
0192: SVGDocument svgDoc = (SVGDocument) e.getOwnerDocument();
0193: String docURL = svgDoc.getURL();
0194: ParsedURL pDocURL = null;
0195: if (docURL != null)
0196: pDocURL = new ParsedURL(docURL);
0197:
0198: UserAgent userAgent = ctx.getUserAgent();
0199:
0200: try {
0201: userAgent.checkLoadExternalResource(purl, pDocURL);
0202: } catch (SecurityException secEx) {
0203: throw new BridgeException(ctx, e, secEx, ERR_URI_UNSECURE,
0204: new Object[] { purl });
0205: }
0206:
0207: DocumentLoader loader = ctx.getDocumentLoader();
0208: ImageTagRegistry reg = ImageTagRegistry.getRegistry();
0209: ICCColorSpaceExt colorspace = extractColorSpace(e, ctx);
0210: {
0211: /**
0212: * Before we open the URL we see if we have the
0213: * URL already cached and parsed
0214: */
0215: try {
0216: /* Check the document loader cache */
0217: Document doc = loader.checkCache(purl.toString());
0218: if (doc != null) {
0219: imgDocument = (SVGDocument) doc;
0220: return createSVGImageNode(ctx, e, imgDocument);
0221: }
0222: } catch (BridgeException ex) {
0223: throw ex;
0224: } catch (Exception ex) {
0225: /* Nothing to do */
0226: }
0227:
0228: /* Check the ImageTagRegistry Cache */
0229: Filter img = reg.checkCache(purl, colorspace);
0230: if (img != null) {
0231: return createRasterImageNode(ctx, e, img, purl);
0232: }
0233: }
0234:
0235: /* The Protected Stream ensures that the stream doesn't
0236: * get closed unless we want it to. It is also based on
0237: * a Buffered Reader so in general we can mark the start
0238: * and reset rather than reopening the stream. Finally
0239: * it hides the mark/reset methods so only we get to
0240: * use them.
0241: */
0242: ProtectedStream reference = null;
0243: try {
0244: reference = openStream(e, purl);
0245: } catch (SecurityException secEx) {
0246: throw new BridgeException(ctx, e, secEx, ERR_URI_UNSECURE,
0247: new Object[] { purl });
0248: } catch (IOException ioe) {
0249: return createBrokenImageNode(ctx, e, purl.toString(), ioe
0250: .getLocalizedMessage());
0251: }
0252:
0253: {
0254: /**
0255: * First see if we can id the file as a Raster via magic
0256: * number. This is probably the fastest mechanism.
0257: * We tell the registry what the source purl is but we
0258: * tell it not to open that url.
0259: */
0260: Filter img = reg.readURL(reference, purl, colorspace,
0261: false, false);
0262: if (img != null) {
0263: // It's a bouncing baby Raster...
0264: return createRasterImageNode(ctx, e, img, purl);
0265: }
0266: }
0267:
0268: try {
0269: // Reset the stream for next try.
0270: reference.retry();
0271: } catch (IOException ioe) {
0272: reference.release();
0273: reference = null;
0274: try {
0275: // Couldn't reset stream so reopen it.
0276: reference = openStream(e, purl);
0277: } catch (IOException ioe2) {
0278: // Since we already opened the stream this is unlikely.
0279: return createBrokenImageNode(ctx, e, purl.toString(),
0280: ioe2.getLocalizedMessage());
0281: }
0282: }
0283:
0284: try {
0285: /**
0286: * Next see if it's an XML document.
0287: */
0288: Document doc = loader.loadDocument(purl.toString(),
0289: reference);
0290: imgDocument = (SVGDocument) doc;
0291: return createSVGImageNode(ctx, e, imgDocument);
0292: } catch (BridgeException ex) {
0293: throw ex;
0294: } catch (SecurityException secEx) {
0295: throw new BridgeException(ctx, e, secEx, ERR_URI_UNSECURE,
0296: new Object[] { purl });
0297: } catch (InterruptedIOException iioe) {
0298: if (HaltingThread.hasBeenHalted())
0299: throw new InterruptedBridgeException();
0300:
0301: } catch (InterruptedBridgeException ibe) {
0302: throw ibe;
0303: } catch (Exception ex) {
0304: /* Nothing to do */
0305: // ex.printStackTrace();
0306: }
0307:
0308: try {
0309: reference.retry();
0310: } catch (IOException ioe) {
0311: reference.release();
0312: reference = null;
0313: try {
0314: // Couldn't reset stream so reopen it.
0315: reference = openStream(e, purl);
0316: } catch (IOException ioe2) {
0317: return createBrokenImageNode(ctx, e, purl.toString(),
0318: ioe2.getLocalizedMessage());
0319: }
0320: }
0321:
0322: try {
0323: // Finally try to load the image as a raster image (JPG or
0324: // PNG) allowing the registry to open the url (so the
0325: // JDK readers can be checked).
0326: Filter img = reg.readURL(reference, purl, colorspace, true,
0327: true);
0328: if (img != null) {
0329: // It's a bouncing baby Raster...
0330: return createRasterImageNode(ctx, e, img, purl);
0331: }
0332: } finally {
0333: reference.release();
0334: }
0335: return null;
0336: }
0337:
0338: public static class ProtectedStream extends BufferedInputStream {
0339: static final int BUFFER_SIZE = 8192;
0340:
0341: ProtectedStream(InputStream is) {
0342: super (is, BUFFER_SIZE);
0343: super .mark(BUFFER_SIZE); // Remember start
0344: }
0345:
0346: ProtectedStream(InputStream is, int size) {
0347: super (is, size);
0348: super .mark(size); // Remember start
0349: }
0350:
0351: public boolean markSupported() {
0352: return false;
0353: }
0354:
0355: public void mark(int sz) {
0356: }
0357:
0358: public void reset() throws IOException {
0359: throw new IOException("Reset unsupported");
0360: }
0361:
0362: public void retry() throws IOException {
0363: super .reset();
0364: }
0365:
0366: public void close() throws IOException {
0367: /* do nothing */
0368: }
0369:
0370: public void release() {
0371: try {
0372: super .close();
0373: } catch (IOException ioe) {
0374: // Like Duh! what would you do close it again?
0375: }
0376: }
0377: }
0378:
0379: protected ProtectedStream openStream(Element e, ParsedURL purl)
0380: throws IOException {
0381: List mimeTypes = new ArrayList(ImageTagRegistry.getRegistry()
0382: .getRegisteredMimeTypes());
0383: mimeTypes.add(MimeTypeConstants.MIME_TYPES_SVG);
0384: InputStream reference = purl.openStream(mimeTypes.iterator());
0385: return new ProtectedStream(reference);
0386: }
0387:
0388: /**
0389: * Creates an <tt>ImageNode</tt>.
0390: */
0391: protected GraphicsNode instantiateGraphicsNode() {
0392: return new ImageNode();
0393: }
0394:
0395: /**
0396: * Returns false as image is not a container.
0397: */
0398: public boolean isComposite() {
0399: return false;
0400: }
0401:
0402: // dynamic support
0403:
0404: /**
0405: * This method is invoked during the build phase if the document
0406: * is dynamic. The responsability of this method is to ensure that
0407: * any dynamic modifications of the element this bridge is
0408: * dedicated to, happen on its associated GVT product.
0409: */
0410: protected void initializeDynamicSupport(BridgeContext ctx,
0411: Element e, GraphicsNode node) {
0412: if (!ctx.isInteractive())
0413: return;
0414:
0415: // Bind the nodes for interactive and dynamic
0416: // HACK due to the way images are represented in GVT
0417: ctx.bind(e, node);
0418:
0419: if (ctx.isDynamic()) {
0420: // Only do this for dynamic not interactive.
0421: this .e = e;
0422: this .node = node;
0423: this .ctx = ctx;
0424: ((SVGOMElement) e).setSVGContext(this );
0425: }
0426: }
0427:
0428: // BridgeUpdateHandler implementation //////////////////////////////////
0429:
0430: /**
0431: * Invoked when the animated value of an animatable attribute has changed.
0432: */
0433: public void handleAnimatedAttributeChanged(
0434: AnimatedLiveAttributeValue alav) {
0435: try {
0436: String ns = alav.getNamespaceURI();
0437: String ln = alav.getLocalName();
0438: if (ns == null) {
0439: if (ln.equals(SVG_X_ATTRIBUTE)
0440: || ln.equals(SVG_Y_ATTRIBUTE)) {
0441: updateImageBounds();
0442: return;
0443: } else if (ln.equals(SVG_WIDTH_ATTRIBUTE)
0444: || ln.equals(SVG_HEIGHT_ATTRIBUTE)) {
0445: SVGImageElement ie = (SVGImageElement) e;
0446: ImageNode imageNode = (ImageNode) node;
0447: float val;
0448: if (ln.charAt(0) == 'w') {
0449: val = ie.getWidth().getAnimVal().getValue();
0450: } else {
0451: val = ie.getHeight().getAnimVal().getValue();
0452: }
0453: if (val == 0
0454: || imageNode.getImage() instanceof ShapeNode) {
0455: rebuildImageNode();
0456: } else {
0457: updateImageBounds();
0458: }
0459: return;
0460: } else if (ln
0461: .equals(SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE)) {
0462: updateImageBounds();
0463: return;
0464: }
0465: } else if (ns.equals(XLINK_NAMESPACE_URI)
0466: && ln.equals(XLINK_HREF_ATTRIBUTE)) {
0467: rebuildImageNode();
0468: return;
0469: }
0470: } catch (LiveAttributeException ex) {
0471: throw new BridgeException(ctx, ex);
0472: }
0473: super .handleAnimatedAttributeChanged(alav);
0474: }
0475:
0476: protected void updateImageBounds() {
0477: //retrieve the new bounds of the image tag
0478: Rectangle2D bounds = getImageBounds(ctx, e);
0479: GraphicsNode imageNode = ((ImageNode) node).getImage();
0480: float[] vb = null;
0481: if (imageNode instanceof RasterImageNode) {
0482: //Raster image
0483: Rectangle2D imgBounds = ((RasterImageNode) imageNode)
0484: .getImageBounds();
0485: // create the implicit viewBox for the raster
0486: // image. The viewBox for a raster image is the size
0487: // of the image
0488: vb = new float[4];
0489: vb[0] = 0; // x
0490: vb[1] = 0; // y
0491: vb[2] = (float) imgBounds.getWidth(); // width
0492: vb[3] = (float) imgBounds.getHeight(); // height
0493: } else {
0494: if (imgDocument != null) {
0495: Element svgElement = imgDocument.getRootElement();
0496: String viewBox = svgElement.getAttributeNS(null,
0497: SVG_VIEW_BOX_ATTRIBUTE);
0498: vb = ViewBox.parseViewBoxAttribute(e, viewBox, ctx);
0499: }
0500: }
0501: if (imageNode != null) {
0502: // handles the 'preserveAspectRatio', 'overflow' and
0503: // 'clip' and sets the appropriate AffineTransform to
0504: // the image node
0505: initializeViewport(ctx, e, imageNode, vb, bounds);
0506: }
0507:
0508: }
0509:
0510: protected void rebuildImageNode() {
0511: // Reference copy of the imgDocument
0512: if ((imgDocument != null) && (listener != null)) {
0513: NodeEventTarget tgt = (NodeEventTarget) imgDocument
0514: .getRootElement();
0515:
0516: tgt.removeEventListenerNS(
0517: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0518: SVG_EVENT_CLICK, listener, false);
0519: tgt.removeEventListenerNS(
0520: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0521: SVG_EVENT_KEYDOWN, listener, false);
0522: tgt.removeEventListenerNS(
0523: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0524: SVG_EVENT_KEYPRESS, listener, false);
0525: tgt.removeEventListenerNS(
0526: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0527: SVG_EVENT_KEYUP, listener, false);
0528: tgt.removeEventListenerNS(
0529: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0530: SVG_EVENT_MOUSEDOWN, listener, false);
0531: tgt.removeEventListenerNS(
0532: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0533: SVG_EVENT_MOUSEMOVE, listener, false);
0534: tgt.removeEventListenerNS(
0535: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0536: SVG_EVENT_MOUSEOUT, listener, false);
0537: tgt.removeEventListenerNS(
0538: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0539: SVG_EVENT_MOUSEOVER, listener, false);
0540: tgt.removeEventListenerNS(
0541: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0542: SVG_EVENT_MOUSEUP, listener, false);
0543: listener = null;
0544: }
0545:
0546: if (imgDocument != null) {
0547: SVGSVGElement svgElement = imgDocument.getRootElement();
0548: disposeTree(svgElement);
0549: }
0550:
0551: imgDocument = null;
0552: subCtx = null;
0553:
0554: //update of the reference of the image.
0555: GraphicsNode inode = buildImageGraphicsNode(ctx, e);
0556:
0557: ImageNode imgNode = (ImageNode) node;
0558: imgNode.setImage(inode);
0559:
0560: if (inode == null) {
0561: SVGImageElement ie = (SVGImageElement) e;
0562: String uriStr = ie.getHref().getAnimVal();
0563: throw new BridgeException(ctx, e, ERR_URI_IMAGE_INVALID,
0564: new Object[] { uriStr });
0565: }
0566: }
0567:
0568: /**
0569: * Invoked for each CSS property that has changed.
0570: */
0571: protected void handleCSSPropertyChanged(int property) {
0572: switch (property) {
0573: case SVGCSSEngine.IMAGE_RENDERING_INDEX:
0574: case SVGCSSEngine.COLOR_INTERPOLATION_INDEX:
0575: RenderingHints hints = CSSUtilities.convertImageRendering(
0576: e, null);
0577: hints = CSSUtilities.convertColorRendering(e, hints);
0578: if (hints != null) {
0579: node.setRenderingHints(hints);
0580: }
0581: break;
0582: default:
0583: super .handleCSSPropertyChanged(property);
0584: }
0585: }
0586:
0587: // convenient methods //////////////////////////////////////////////////
0588:
0589: /**
0590: * Returns a GraphicsNode that represents an raster image in JPEG or PNG
0591: * format.
0592: *
0593: * @param ctx the bridge context
0594: * @param e the image element
0595: * @param img the image to use in creating the graphics node
0596: */
0597: protected GraphicsNode createRasterImageNode(BridgeContext ctx,
0598: Element e, Filter img, ParsedURL purl) {
0599: Rectangle2D bounds = getImageBounds(ctx, e);
0600: if ((bounds.getWidth() == 0) || (bounds.getHeight() == 0)) {
0601: ShapeNode sn = new ShapeNode();
0602: sn.setShape(bounds);
0603: return sn;
0604: }
0605:
0606: if (BrokenLinkProvider.hasBrokenLinkProperty(img)) {
0607: Object o = img
0608: .getProperty(BrokenLinkProvider.BROKEN_LINK_PROPERTY);
0609: String msg = "unknown";
0610: if (o instanceof String)
0611: msg = (String) o;
0612: SVGDocument doc = ctx.getUserAgent().getBrokenLinkDocument(
0613: e, purl.toString(), msg);
0614: return createSVGImageNode(ctx, e, doc);
0615: }
0616:
0617: RasterImageNode node = new RasterImageNode();
0618: node.setImage(img);
0619: Rectangle2D imgBounds = img.getBounds2D();
0620:
0621: // create the implicit viewBox for the raster image. The viewBox for a
0622: // raster image is the size of the image
0623: float[] vb = new float[4];
0624: vb[0] = 0; // x
0625: vb[1] = 0; // y
0626: vb[2] = (float) imgBounds.getWidth(); // width
0627: vb[3] = (float) imgBounds.getHeight(); // height
0628:
0629: // handles the 'preserveAspectRatio', 'overflow' and 'clip' and sets the
0630: // appropriate AffineTransform to the image node
0631: initializeViewport(ctx, e, node, vb, bounds);
0632:
0633: return node;
0634: }
0635:
0636: /**
0637: * Returns a GraphicsNode that represents a svg document as an image.
0638: *
0639: * @param ctx the bridge context
0640: * @param e the image element
0641: * @param imgDocument the SVG document that represents the image
0642: */
0643: protected GraphicsNode createSVGImageNode(BridgeContext ctx,
0644: Element e, SVGDocument imgDocument) {
0645: CSSEngine eng = ((SVGOMDocument) imgDocument).getCSSEngine();
0646: subCtx = ctx
0647: .createSubBridgeContext((SVGOMDocument) imgDocument);
0648:
0649: CompositeGraphicsNode result = new CompositeGraphicsNode();
0650: // handles the 'preserveAspectRatio', 'overflow' and 'clip' and
0651: // sets the appropriate AffineTransform to the image node
0652: Rectangle2D bounds = getImageBounds(ctx, e);
0653:
0654: if ((bounds.getWidth() == 0) || (bounds.getHeight() == 0)) {
0655: ShapeNode sn = new ShapeNode();
0656: sn.setShape(bounds);
0657: result.getChildren().add(sn);
0658: return result;
0659: }
0660:
0661: Rectangle2D r = CSSUtilities.convertEnableBackground(e);
0662: if (r != null) {
0663: result.setBackgroundEnable(r);
0664: }
0665:
0666: SVGSVGElement svgElement = imgDocument.getRootElement();
0667: CanvasGraphicsNode node;
0668: node = (CanvasGraphicsNode) subCtx.getGVTBuilder().build(
0669: subCtx, svgElement);
0670:
0671: if ((eng == null) && ctx.isInteractive()) {
0672: // If we "created" this document then add listerns.
0673: subCtx.addUIEventListeners(imgDocument);
0674: }
0675:
0676: // HACK: remove the clip set by the SVGSVGElement as the overflow
0677: // and clip properties must be ignored. The clip will be set later
0678: // using the overflow and clip of the <image> element.
0679: node.setClip(null);
0680: // HACK: remove the viewingTransform set by the SVGSVGElement
0681: // as the viewBox must be ignored. The viewingTransform will
0682: // be set later using the width/height of the image element.
0683: node.setViewingTransform(new AffineTransform());
0684: result.getChildren().add(node);
0685:
0686: // create the implicit viewBox for the SVG image. The viewBox for a
0687: // SVG image is the viewBox of the outermost SVG element of the SVG file
0688: String viewBox = svgElement.getAttributeNS(null,
0689: SVG_VIEW_BOX_ATTRIBUTE);
0690: float[] vb = ViewBox.parseViewBoxAttribute(e, viewBox, ctx);
0691:
0692: initializeViewport(ctx, e, result, vb, bounds);
0693:
0694: // add a listener on the outermost svg element of the SVG image.
0695: // if an event occured inside the SVG image document, send it
0696: // to the <image> element (inside the original document).
0697: if (ctx.isInteractive()) {
0698: listener = new ForwardEventListener(svgElement, e);
0699: NodeEventTarget tgt = (NodeEventTarget) svgElement;
0700:
0701: tgt.addEventListenerNS(
0702: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0703: SVG_EVENT_CLICK, listener, false, null);
0704: subCtx.storeEventListenerNS(tgt,
0705: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0706: SVG_EVENT_CLICK, listener, false);
0707:
0708: tgt.addEventListenerNS(
0709: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0710: SVG_EVENT_KEYDOWN, listener, false, null);
0711: subCtx.storeEventListenerNS(tgt,
0712: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0713: SVG_EVENT_KEYDOWN, listener, false);
0714:
0715: tgt.addEventListenerNS(
0716: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0717: SVG_EVENT_KEYPRESS, listener, false, null);
0718: subCtx.storeEventListenerNS(tgt,
0719: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0720: SVG_EVENT_KEYPRESS, listener, false);
0721:
0722: tgt.addEventListenerNS(
0723: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0724: SVG_EVENT_KEYUP, listener, false, null);
0725: subCtx.storeEventListenerNS(tgt,
0726: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0727: SVG_EVENT_KEYUP, listener, false);
0728:
0729: tgt.addEventListenerNS(
0730: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0731: SVG_EVENT_MOUSEDOWN, listener, false, null);
0732: subCtx.storeEventListenerNS(tgt,
0733: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0734: SVG_EVENT_MOUSEDOWN, listener, false);
0735:
0736: tgt.addEventListenerNS(
0737: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0738: SVG_EVENT_MOUSEMOVE, listener, false, null);
0739: subCtx.storeEventListenerNS(tgt,
0740: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0741: SVG_EVENT_MOUSEMOVE, listener, false);
0742:
0743: tgt.addEventListenerNS(
0744: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0745: SVG_EVENT_MOUSEOUT, listener, false, null);
0746: subCtx.storeEventListenerNS(tgt,
0747: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0748: SVG_EVENT_MOUSEOUT, listener, false);
0749:
0750: tgt.addEventListenerNS(
0751: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0752: SVG_EVENT_MOUSEOVER, listener, false, null);
0753: subCtx.storeEventListenerNS(tgt,
0754: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0755: SVG_EVENT_MOUSEOVER, listener, false);
0756:
0757: tgt.addEventListenerNS(
0758: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0759: SVG_EVENT_MOUSEUP, listener, false, null);
0760: subCtx.storeEventListenerNS(tgt,
0761: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0762: SVG_EVENT_MOUSEUP, listener, false);
0763: }
0764:
0765: return result;
0766: }
0767:
0768: public void dispose() {
0769: if ((imgDocument != null) && (listener != null)) {
0770: NodeEventTarget tgt = (NodeEventTarget) imgDocument
0771: .getRootElement();
0772:
0773: tgt.removeEventListenerNS(
0774: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0775: SVG_EVENT_CLICK, listener, false);
0776: tgt.removeEventListenerNS(
0777: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0778: SVG_EVENT_KEYDOWN, listener, false);
0779: tgt.removeEventListenerNS(
0780: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0781: SVG_EVENT_KEYPRESS, listener, false);
0782: tgt.removeEventListenerNS(
0783: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0784: SVG_EVENT_KEYUP, listener, false);
0785: tgt.removeEventListenerNS(
0786: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0787: SVG_EVENT_MOUSEDOWN, listener, false);
0788: tgt.removeEventListenerNS(
0789: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0790: SVG_EVENT_MOUSEMOVE, listener, false);
0791: tgt.removeEventListenerNS(
0792: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0793: SVG_EVENT_MOUSEOUT, listener, false);
0794: tgt.removeEventListenerNS(
0795: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0796: SVG_EVENT_MOUSEOVER, listener, false);
0797: tgt.removeEventListenerNS(
0798: XMLConstants.XML_EVENTS_NAMESPACE_URI,
0799: SVG_EVENT_MOUSEUP, listener, false);
0800: listener = null;
0801: }
0802:
0803: if (imgDocument != null) {
0804: SVGSVGElement svgElement = imgDocument.getRootElement();
0805: disposeTree(svgElement);
0806: imgDocument = null;
0807: subCtx = null;
0808: }
0809: super .dispose();
0810:
0811: }
0812:
0813: /**
0814: * A simple DOM listener to forward events from the SVG image document to
0815: * the original document.
0816: */
0817: protected static class ForwardEventListener implements
0818: EventListener {
0819:
0820: /**
0821: * The root element of the SVG image.
0822: */
0823: protected Element svgElement;
0824:
0825: /**
0826: * The image element.
0827: */
0828: protected Element imgElement;
0829:
0830: /**
0831: * Constructs a new <tt>ForwardEventListener</tt>
0832: */
0833: public ForwardEventListener(Element svgElement,
0834: Element imgElement) {
0835: this .svgElement = svgElement;
0836: this .imgElement = imgElement;
0837: }
0838:
0839: public void handleEvent(Event e) {
0840: DOMMouseEvent evt = (DOMMouseEvent) e;
0841: DOMMouseEvent newMouseEvent = (DOMMouseEvent)
0842: // DOM Level 2 6.5 cast from Document to DocumentEvent is ok
0843: ((DocumentEvent) imgElement.getOwnerDocument())
0844: .createEvent("MouseEvents");
0845:
0846: newMouseEvent.initMouseEventNS(
0847: XMLConstants.XML_EVENTS_NAMESPACE_URI, evt
0848: .getType(), evt.getBubbles(), evt
0849: .getCancelable(), evt.getView(), evt
0850: .getDetail(), evt.getScreenX(), evt
0851: .getScreenY(), evt.getClientX(), evt
0852: .getClientY(), evt.getButton(),
0853: (EventTarget) imgElement, evt.getModifiersString());
0854: ((EventTarget) imgElement).dispatchEvent(newMouseEvent);
0855: }
0856: }
0857:
0858: /**
0859: * Initializes according to the specified element, the specified graphics
0860: * node with the specified bounds. This method takes into account the
0861: * 'viewBox', 'preserveAspectRatio', and 'clip' properties. According to
0862: * those properties, a AffineTransform and a clip is set.
0863: *
0864: * @param ctx the bridge context
0865: * @param e the image element that defines the properties
0866: * @param node the graphics node
0867: * @param vb the implicit viewBox definition
0868: * @param bounds the bounds of the image element
0869: */
0870: protected static void initializeViewport(BridgeContext ctx,
0871: Element e, GraphicsNode node, float[] vb, Rectangle2D bounds) {
0872:
0873: float x = (float) bounds.getX();
0874: float y = (float) bounds.getY();
0875: float w = (float) bounds.getWidth();
0876: float h = (float) bounds.getHeight();
0877:
0878: try {
0879: SVGImageElement ie = (SVGImageElement) e;
0880: SVGAnimatedPreserveAspectRatio aPAR = ie
0881: .getPreserveAspectRatio();
0882:
0883: AffineTransform at = ViewBox
0884: .getPreserveAspectRatioTransform(e, vb, w, h, aPAR,
0885: ctx);
0886: at.preConcatenate(AffineTransform
0887: .getTranslateInstance(x, y));
0888: node.setTransform(at);
0889:
0890: // 'overflow' and 'clip'
0891: Shape clip = null;
0892: if (CSSUtilities.convertOverflow(e)) { // overflow:hidden
0893: float[] offsets = CSSUtilities.convertClip(e);
0894: if (offsets == null) { // clip:auto
0895: clip = new Rectangle2D.Float(x, y, w, h);
0896: } else { // clip:rect(<x> <y> <w> <h>)
0897: // offsets[0] = top
0898: // offsets[1] = right
0899: // offsets[2] = bottom
0900: // offsets[3] = left
0901: clip = new Rectangle2D.Float(x + offsets[3], y
0902: + offsets[0], w - offsets[1] - offsets[3],
0903: h - offsets[2] - offsets[0]);
0904: }
0905: }
0906:
0907: if (clip != null) {
0908: try {
0909: at = at.createInverse(); // clip in user space
0910: Filter filter = node.getGraphicsNodeRable(true);
0911: clip = at.createTransformedShape(clip);
0912: node.setClip(new ClipRable8Bit(filter, clip));
0913: } catch (java.awt.geom.NoninvertibleTransformException ex) {
0914: }
0915: }
0916: } catch (LiveAttributeException ex) {
0917: throw new BridgeException(ctx, ex);
0918: }
0919: }
0920:
0921: /**
0922: * Analyzes the color-profile property and builds an ICCColorSpaceExt
0923: * object from it.
0924: *
0925: * @param element the element with the color-profile property
0926: * @param ctx the bridge context
0927: */
0928: protected static ICCColorSpaceExt extractColorSpace(
0929: Element element, BridgeContext ctx) {
0930:
0931: String colorProfileProperty = CSSUtilities.getComputedStyle(
0932: element, SVGCSSEngine.COLOR_PROFILE_INDEX)
0933: .getStringValue();
0934:
0935: // The only cases that need special handling are 'sRGB' and 'name'
0936: ICCColorSpaceExt colorSpace = null;
0937: if (CSS_SRGB_VALUE.equalsIgnoreCase(colorProfileProperty)) {
0938:
0939: colorSpace = new ICCColorSpaceExt(ICC_Profile
0940: .getInstance(ColorSpace.CS_sRGB),
0941: ICCColorSpaceExt.AUTO);
0942:
0943: } else if (!CSS_AUTO_VALUE
0944: .equalsIgnoreCase(colorProfileProperty)
0945: && !"".equalsIgnoreCase(colorProfileProperty)) {
0946:
0947: // The value is neither 'sRGB' nor 'auto': it is a profile name.
0948: SVGColorProfileElementBridge profileBridge = (SVGColorProfileElementBridge) ctx
0949: .getBridge(SVG_NAMESPACE_URI, SVG_COLOR_PROFILE_TAG);
0950: if (profileBridge != null) {
0951: colorSpace = profileBridge.createICCColorSpaceExt(ctx,
0952: element, colorProfileProperty);
0953:
0954: }
0955: }
0956: return colorSpace;
0957: }
0958:
0959: /**
0960: * Returns the bounds of the specified image element.
0961: *
0962: * @param ctx the bridge context
0963: * @param element the image element
0964: */
0965: protected static Rectangle2D getImageBounds(BridgeContext ctx,
0966: Element element) {
0967: try {
0968: SVGImageElement ie = (SVGImageElement) element;
0969:
0970: // 'x' attribute - default is 0
0971: float x = ie.getX().getAnimVal().getValue();
0972:
0973: // 'y' attribute - default is 0
0974: float y = ie.getY().getAnimVal().getValue();
0975:
0976: // 'width' attribute - required
0977: float w = ie.getWidth().getAnimVal().getValue();
0978:
0979: // 'height' attribute - required
0980: float h = ie.getHeight().getAnimVal().getValue();
0981:
0982: return new Rectangle2D.Float(x, y, w, h);
0983: } catch (LiveAttributeException ex) {
0984: throw new BridgeException(ctx, ex);
0985: }
0986: }
0987:
0988: GraphicsNode createBrokenImageNode(BridgeContext ctx, Element e,
0989: String uri, String message) {
0990: SVGDocument doc = ctx.getUserAgent().getBrokenLinkDocument(
0991: e,
0992: uri,
0993: Messages.formatMessage(URI_IMAGE_ERROR,
0994: new Object[] { message }));
0995: return createSVGImageNode(ctx, e, doc);
0996: }
0997:
0998: static SVGBrokenLinkProvider brokenLinkProvider = new SVGBrokenLinkProvider();
0999: static {
1000: ImageTagRegistry.setBrokenLinkProvider(brokenLinkProvider);
1001: }
1002: }
|