001/* 002 * SVG Salamander 003 * Copyright (c) 2004, Mark McKay 004 * All rights reserved. 005 * 006 * Redistribution and use in source and binary forms, with or 007 * without modification, are permitted provided that the following 008 * conditions are met: 009 * 010 * - Redistributions of source code must retain the above 011 * copyright notice, this list of conditions and the following 012 * disclaimer. 013 * - Redistributions in binary form must reproduce the above 014 * copyright notice, this list of conditions and the following 015 * disclaimer in the documentation and/or other materials 016 * provided with the distribution. 017 * 018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 019 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 020 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 021 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 022 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 025 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 026 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 027 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 029 * OF THE POSSIBILITY OF SUCH DAMAGE. 030 * 031 * Mark McKay can be contacted at mark@kitfox.com. Salamander and other 032 * projects can be found at http://www.kitfox.com 033 * 034 * Created on February 18, 2004, 11:43 PM 035 */ 036package com.kitfox.svg; 037 038import com.kitfox.svg.app.beans.SVGIcon; 039import com.kitfox.svg.util.Base64InputStream; 040import java.awt.Graphics2D; 041import java.awt.image.BufferedImage; 042import java.beans.PropertyChangeListener; 043import java.beans.PropertyChangeSupport; 044import java.io.BufferedInputStream; 045import java.io.ByteArrayInputStream; 046import java.io.ByteArrayOutputStream; 047import java.io.IOException; 048import java.io.InputStream; 049import java.io.ObjectInputStream; 050import java.io.ObjectOutputStream; 051import java.io.Reader; 052import java.io.Serializable; 053import java.lang.ref.SoftReference; 054import java.net.MalformedURLException; 055import java.net.URI; 056import java.net.URISyntaxException; 057import java.net.URL; 058import java.util.ArrayList; 059import java.util.HashMap; 060import java.util.logging.Level; 061import java.util.logging.Logger; 062import java.util.zip.GZIPInputStream; 063import javax.imageio.ImageIO; 064import javax.xml.parsers.ParserConfigurationException; 065import javax.xml.parsers.SAXParserFactory; 066import org.xml.sax.EntityResolver; 067import org.xml.sax.InputSource; 068import org.xml.sax.SAXException; 069import org.xml.sax.SAXParseException; 070import org.xml.sax.XMLReader; 071 072/** 073 * Many SVG files can be loaded at one time. These files will quite likely need 074 * to reference one another. The SVG universe provides a container for all these 075 * files and the means for them to relate to each other. 076 * 077 * @author Mark McKay 078 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a> 079 */ 080public class SVGUniverse implements Serializable 081{ 082 083 public static final long serialVersionUID = 0; 084 transient private PropertyChangeSupport changes = new PropertyChangeSupport(this); 085 /** 086 * Maps document URIs to their loaded SVG diagrams. Note that URIs for 087 * documents loaded from URLs will reflect their URLs and URIs for documents 088 * initiated from streams will have the scheme <i>svgSalamander</i>. 089 */ 090 final HashMap<URI, SVGDiagram> loadedDocs = new HashMap<URI, SVGDiagram>(); 091 final HashMap<String, Font> loadedFonts = new HashMap<String, Font>(); 092 final HashMap<URL, SoftReference<BufferedImage>> loadedImages = new HashMap<URL, SoftReference<BufferedImage>>(); 093 public static final String INPUTSTREAM_SCHEME = "svgSalamander"; 094 /** 095 * Current time in this universe. Used for resolving attributes that are 096 * influenced by track information. Time is in milliseconds. Time 0 097 * coresponds to the time of 0 in each member diagram. 098 */ 099 protected double curTime = 0.0; 100 private boolean verbose = false; 101 102 //If true, <imageSVG> elements will only load image data that is included using inline data: uris 103 private boolean imageDataInlineOnly = false; 104 105 /** 106 * Creates a new instance of SVGUniverse 107 */ 108 public SVGUniverse() 109 { 110 } 111 112 public void addPropertyChangeListener(PropertyChangeListener l) 113 { 114 changes.addPropertyChangeListener(l); 115 } 116 117 public void removePropertyChangeListener(PropertyChangeListener l) 118 { 119 changes.removePropertyChangeListener(l); 120 } 121 122 /** 123 * Release all loaded SVG document from memory 124 */ 125 public void clear() 126 { 127 loadedDocs.clear(); 128 loadedFonts.clear(); 129 loadedImages.clear(); 130 } 131 132 /** 133 * Returns the current animation time in milliseconds. 134 */ 135 public double getCurTime() 136 { 137 return curTime; 138 } 139 140 public void setCurTime(double curTime) 141 { 142 double oldTime = this.curTime; 143 this.curTime = curTime; 144 changes.firePropertyChange("curTime", new Double(oldTime), new Double(curTime)); 145 } 146 147 /** 148 * Updates all time influenced style and presentation attributes in all SVG 149 * documents in this universe. 150 */ 151 public void updateTime() throws SVGException 152 { 153 for (SVGDiagram dia : loadedDocs.values()) { 154 dia.updateTime(curTime); 155 } 156 } 157 158 /** 159 * Called by the Font element to let the universe know that a font has been 160 * loaded and is available. 161 */ 162 void registerFont(Font font) 163 { 164 loadedFonts.put(font.getFontFace().getFontFamily(), font); 165 } 166 167 public Font getDefaultFont() 168 { 169 for (Font font : loadedFonts.values()) { 170 return font; 171 } 172 return null; 173 } 174 175 public Font getFont(String fontName) 176 { 177 return (Font) loadedFonts.get(fontName); 178 } 179 180 URL registerImage(URI imageURI) 181 { 182 String scheme = imageURI.getScheme(); 183 if (scheme.equals("data")) 184 { 185 String path = imageURI.getRawSchemeSpecificPart(); 186 int idx = path.indexOf(';'); 187 String mime = path.substring(0, idx); 188 String content = path.substring(idx + 1); 189 190 if (content.startsWith("base64")) 191 { 192 content = content.substring(6); 193 try 194 { 195// byte[] buf = new sun.misc.BASE64Decoder().decodeBuffer(content); 196// ByteArrayInputStream bais = new ByteArrayInputStream(buf); 197 ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes()); 198 Base64InputStream bais = new Base64InputStream(bis); 199 200 BufferedImage img = ImageIO.read(bais); 201 202 URL url; 203 int urlIdx = 0; 204 while (true) 205 { 206 url = new URL("inlineImage", "localhost", "img" + urlIdx); 207 if (!loadedImages.containsKey(url)) 208 { 209 break; 210 } 211 urlIdx++; 212 } 213 214 SoftReference<BufferedImage> ref = new SoftReference<BufferedImage>(img); 215 loadedImages.put(url, ref); 216 217 return url; 218 } catch (IOException ex) 219 { 220 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 221 "Could not decode inline image", ex); 222 } 223 } 224 return null; 225 } else 226 { 227 try 228 { 229 URL url = imageURI.toURL(); 230 registerImage(url); 231 return url; 232 } catch (MalformedURLException ex) 233 { 234 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 235 "Bad url", ex); 236 } 237 return null; 238 } 239 } 240 241 void registerImage(URL imageURL) 242 { 243 if (loadedImages.containsKey(imageURL)) 244 { 245 return; 246 } 247 248 SoftReference<BufferedImage> ref; 249 try 250 { 251 String fileName = imageURL.getFile(); 252 if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase())) 253 { 254 SVGIcon icon = new SVGIcon(); 255 icon.setSvgURI(imageURL.toURI()); 256 257 BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); 258 Graphics2D g = img.createGraphics(); 259 icon.paintIcon(null, g, 0, 0); 260 g.dispose(); 261 ref = new SoftReference<BufferedImage>(img); 262 } else 263 { 264 BufferedImage img = ImageIO.read(imageURL); 265 ref = new SoftReference<BufferedImage>(img); 266 } 267 loadedImages.put(imageURL, ref); 268 } catch (Exception e) 269 { 270 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 271 "Could not load image: " + imageURL, e); 272 } 273 } 274 275 BufferedImage getImage(URL imageURL) 276 { 277 SoftReference<BufferedImage> ref = (SoftReference<BufferedImage>) loadedImages.get(imageURL); 278 if (ref == null) 279 { 280 return null; 281 } 282 283 BufferedImage img = (BufferedImage) ref.get(); 284 //If image was cleared from memory, reload it 285 if (img == null) 286 { 287 try 288 { 289 img = ImageIO.read(imageURL); 290 } catch (Exception e) 291 { 292 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 293 "Could not load image", e); 294 } 295 ref = new SoftReference<BufferedImage>(img); 296 loadedImages.put(imageURL, ref); 297 } 298 299 return img; 300 } 301 302 /** 303 * Returns the element of the document at the given URI. If the document is 304 * not already loaded, it will be. 305 */ 306 public SVGElement getElement(URI path) 307 { 308 return getElement(path, true); 309 } 310 311 public SVGElement getElement(URL path) 312 { 313 try 314 { 315 URI uri = new URI(path.toString()); 316 return getElement(uri, true); 317 } catch (Exception e) 318 { 319 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 320 "Could not parse url " + path, e); 321 } 322 return null; 323 } 324 325 /** 326 * Looks up a href within our universe. If the href refers to a document 327 * that is not loaded, it will be loaded. The URL #target will then be 328 * checked against the SVG diagram's index and the coresponding element 329 * returned. If there is no coresponding index, null is returned. 330 */ 331 public SVGElement getElement(URI path, boolean loadIfAbsent) 332 { 333 try 334 { 335 //Strip fragment from URI 336 URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null); 337 338 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 339 if (dia == null && loadIfAbsent) 340 { 341//System.err.println("SVGUnivserse: " + xmlBase.toString()); 342//javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString()); 343 URL url = xmlBase.toURL(); 344 345 loadSVG(url, false); 346 dia = (SVGDiagram) loadedDocs.get(xmlBase); 347 if (dia == null) 348 { 349 return null; 350 } 351 } 352 353 String fragment = path.getFragment(); 354 return fragment == null ? dia.getRoot() : dia.getElement(fragment); 355 } catch (Exception e) 356 { 357 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 358 "Could not parse path " + path, e); 359 return null; 360 } 361 } 362 363 public SVGDiagram getDiagram(URI xmlBase) 364 { 365 return getDiagram(xmlBase, true); 366 } 367 368 /** 369 * Returns the diagram that has been loaded from this root. If diagram is 370 * not already loaded, returns null. 371 */ 372 public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent) 373 { 374 if (xmlBase == null) 375 { 376 return null; 377 } 378 379 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 380 if (dia != null || !loadIfAbsent) 381 { 382 return dia; 383 } 384 385 //Load missing diagram 386 try 387 { 388 URL url; 389 if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/")) 390 { 391 //Workaround for resources stored in jars loaded by Webstart. 392 //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651 393 url = SVGUniverse.class.getResource("xmlBase.getPath()"); 394 } 395 else 396 { 397 url = xmlBase.toURL(); 398 } 399 400 401 loadSVG(url, false); 402 dia = (SVGDiagram) loadedDocs.get(xmlBase); 403 return dia; 404 } catch (Exception e) 405 { 406 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 407 "Could not parse", e); 408 } 409 410 return null; 411 } 412 413 /** 414 * Wraps input stream in a BufferedInputStream. If it is detected that this 415 * input stream is GZIPped, also wraps in a GZIPInputStream for inflation. 416 * 417 * @param is Raw input stream 418 * @return Uncompressed stream of SVG data 419 * @throws java.io.IOException 420 */ 421 private InputStream createDocumentInputStream(InputStream is) throws IOException 422 { 423 BufferedInputStream bin = new BufferedInputStream(is); 424 bin.mark(2); 425 int b0 = bin.read(); 426 int b1 = bin.read(); 427 bin.reset(); 428 429 //Check for gzip magic number 430 if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC) 431 { 432 GZIPInputStream iis = new GZIPInputStream(bin); 433 return iis; 434 } else 435 { 436 //Plain text 437 return bin; 438 } 439 } 440 441 public URI loadSVG(URL docRoot) 442 { 443 return loadSVG(docRoot, false); 444 } 445 446 /** 447 * Loads an SVG file and all the files it references from the URL provided. 448 * If a referenced file already exists in the SVG universe, it is not 449 * reloaded. 450 * 451 * @param docRoot - URL to the location where this SVG file can be found. 452 * @param forceLoad - if true, ignore cached diagram and reload 453 * @return - The URI that refers to the loaded document 454 */ 455 public URI loadSVG(URL docRoot, boolean forceLoad) 456 { 457 try 458 { 459 URI uri = new URI(docRoot.toString()); 460 if (loadedDocs.containsKey(uri) && !forceLoad) 461 { 462 return uri; 463 } 464 465 InputStream is = docRoot.openStream(); 466 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 467 } catch (URISyntaxException ex) 468 { 469 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 470 "Could not parse", ex); 471 } catch (IOException e) 472 { 473 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 474 "Could not parse", e); 475 } 476 477 return null; 478 } 479 480 public URI loadSVG(InputStream is, String name) throws IOException 481 { 482 return loadSVG(is, name, false); 483 } 484 485 public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException 486 { 487 URI uri = getStreamBuiltURI(name); 488 if (uri == null) 489 { 490 return null; 491 } 492 if (loadedDocs.containsKey(uri) && !forceLoad) 493 { 494 return uri; 495 } 496 497 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 498 } 499 500 public URI loadSVG(Reader reader, String name) 501 { 502 return loadSVG(reader, name, false); 503 } 504 505 /** 506 * This routine allows you to create SVG documents from data streams that 507 * may not necessarily have a URL to load from. Since every SVG document 508 * must be identified by a unique URL, Salamander provides a method to fake 509 * this for streams by defining it's own protocol - svgSalamander - for SVG 510 * documents without a formal URL. 511 * 512 * @param reader - A stream containing a valid SVG document 513 * @param name - <p>A unique name for this document. It will be used to 514 * construct a unique URI to refer to this document and perform resolution 515 * with relative URIs within this document.</p> <p>For example, a name of 516 * "/myScene" will produce the URI svgSalamander:/myScene. 517 * "/maps/canada/toronto" will produce svgSalamander:/maps/canada/toronto. 518 * If this second document then contained the href "../uk/london", it would 519 * resolve by default to svgSalamander:/maps/uk/london. That is, SVG 520 * Salamander defines the URI scheme svgSalamander for it's own internal use 521 * and uses it for uniquely identfying documents loaded by stream.</p> <p>If 522 * you need to link to documents outside of this scheme, you can either 523 * supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)") or 524 * put the xml:base attribute in a tag to change the defaultbase URIs are 525 * resolved against</p> <p>If a name does not start with the character '/', 526 * it will be automatically prefixed to it.</p> 527 * @param forceLoad - if true, ignore cached diagram and reload 528 * 529 * @return - The URI that refers to the loaded document 530 */ 531 public URI loadSVG(Reader reader, String name, boolean forceLoad) 532 { 533//System.err.println(url.toString()); 534 //Synthesize URI for this stream 535 URI uri = getStreamBuiltURI(name); 536 if (uri == null) 537 { 538 return null; 539 } 540 if (loadedDocs.containsKey(uri) && !forceLoad) 541 { 542 return uri; 543 } 544 545 return loadSVG(uri, new InputSource(reader)); 546 } 547 548 /** 549 * Synthesize a URI for an SVGDiagram constructed from a stream. 550 * 551 * @param name - Name given the document constructed from a stream. 552 */ 553 public URI getStreamBuiltURI(String name) 554 { 555 if (name == null || name.length() == 0) 556 { 557 return null; 558 } 559 560 if (name.charAt(0) != '/') 561 { 562 name = '/' + name; 563 } 564 565 try 566 { 567 //Dummy URL for SVG documents built from image streams 568 return new URI(INPUTSTREAM_SCHEME, name, null); 569 } catch (Exception e) 570 { 571 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 572 "Could not parse", e); 573 return null; 574 } 575 } 576 577 private XMLReader getXMLReader() throws SAXException, ParserConfigurationException 578 { 579 SAXParserFactory factory = SAXParserFactory.newInstance(); 580 factory.setNamespaceAware(true); 581 return factory.newSAXParser().getXMLReader(); 582 } 583 584 protected URI loadSVG(URI xmlBase, InputSource is) 585 { 586 // Use an instance of ourselves as the SAX event handler 587 SVGLoader handler = new SVGLoader(xmlBase, this, verbose); 588 589 //Place this docment in the universe before it is completely loaded 590 // so that the load process can refer to references within it's current 591 // document 592 loadedDocs.put(xmlBase, handler.getLoadedDiagram()); 593 594 try 595 { 596 // Parse the input 597 XMLReader reader = getXMLReader(); 598 reader.setEntityResolver( 599 new EntityResolver() 600 { 601 public InputSource resolveEntity(String publicId, String systemId) 602 { 603 //Ignore all DTDs 604 return new InputSource(new ByteArrayInputStream(new byte[0])); 605 } 606 }); 607 reader.setContentHandler(handler); 608 reader.parse(is); 609 610 handler.getLoadedDiagram().updateTime(curTime); 611 return xmlBase; 612 } catch (SAXParseException sex) 613 { 614 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 615 "Error processing " + xmlBase, sex); 616 617 loadedDocs.remove(xmlBase); 618 return null; 619 } catch (Throwable e) 620 { 621 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 622 "Could not load SVG " + xmlBase, e); 623 } 624 625 return null; 626 } 627 628 /** 629 * Get list of uris of all loaded documents and subdocuments. 630 * @return 631 */ 632 public ArrayList<URI> getLoadedDocumentURIs() 633 { 634 return new ArrayList<URI>(loadedDocs.keySet()); 635 } 636 637 /** 638 * Remove loaded document from cache. 639 * @param uri 640 */ 641 public void removeDocument(URI uri) 642 { 643 loadedDocs.remove(uri); 644 } 645 646 public boolean isVerbose() 647 { 648 return verbose; 649 } 650 651 public void setVerbose(boolean verbose) 652 { 653 this.verbose = verbose; 654 } 655 656 /** 657 * Uses serialization to duplicate this universe. 658 */ 659 public SVGUniverse duplicate() throws IOException, ClassNotFoundException 660 { 661 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 662 ObjectOutputStream os = new ObjectOutputStream(bs); 663 os.writeObject(this); 664 os.close(); 665 666 ByteArrayInputStream bin = new ByteArrayInputStream(bs.toByteArray()); 667 ObjectInputStream is = new ObjectInputStream(bin); 668 SVGUniverse universe = (SVGUniverse) is.readObject(); 669 is.close(); 670 671 return universe; 672 } 673 674 /** 675 * @return the imageDataInlineOnly 676 */ 677 public boolean isImageDataInlineOnly() 678 { 679 return imageDataInlineOnly; 680 } 681 682 /** 683 * @param imageDataInlineOnly the imageDataInlineOnly to set 684 */ 685 public void setImageDataInlineOnly(boolean imageDataInlineOnly) 686 { 687 this.imageDataInlineOnly = imageDataInlineOnly; 688 } 689}