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 java.awt.Graphics2D; 040import java.awt.image.BufferedImage; 041import java.beans.PropertyChangeListener; 042import java.beans.PropertyChangeSupport; 043import java.io.BufferedInputStream; 044import java.io.ByteArrayInputStream; 045import java.io.ByteArrayOutputStream; 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.ObjectInputStream; 049import java.io.ObjectOutputStream; 050import java.io.Reader; 051import java.io.Serializable; 052import java.lang.ref.SoftReference; 053import java.net.MalformedURLException; 054import java.net.URI; 055import java.net.URISyntaxException; 056import java.net.URL; 057import java.util.ArrayList; 058import java.util.HashMap; 059import java.util.Iterator; 060import java.util.logging.Level; 061import java.util.logging.Logger; 062import java.util.zip.GZIPInputStream; 063import javax.imageio.ImageIO; 064import org.xml.sax.EntityResolver; 065import org.xml.sax.InputSource; 066import org.xml.sax.SAXException; 067import org.xml.sax.SAXParseException; 068import org.xml.sax.XMLReader; 069import org.xml.sax.helpers.XMLReaderFactory; 070 071/** 072 * Many SVG files can be loaded at one time. These files will quite likely need 073 * to reference one another. The SVG universe provides a container for all these 074 * files and the means for them to relate to each other. 075 * 076 * @author Mark McKay 077 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a> 078 */ 079public class SVGUniverse implements Serializable 080{ 081 082 public static final long serialVersionUID = 0; 083 transient private PropertyChangeSupport changes = new PropertyChangeSupport(this); 084 /** 085 * Maps document URIs to their loaded SVG diagrams. Note that URIs for 086 * documents loaded from URLs will reflect their URLs and URIs for documents 087 * initiated from streams will have the scheme <i>svgSalamander</i>. 088 */ 089 final HashMap loadedDocs = new HashMap(); 090 final HashMap loadedFonts = new HashMap(); 091 final HashMap loadedImages = new HashMap(); 092 public static final String INPUTSTREAM_SCHEME = "svgSalamander"; 093 /** 094 * Current time in this universe. Used for resolving attributes that are 095 * influenced by track information. Time is in milliseconds. Time 0 096 * coresponds to the time of 0 in each member diagram. 097 */ 098 protected double curTime = 0.0; 099 private boolean verbose = false; 100 //Cache reader for efficiency 101 XMLReader cachedReader; 102 103 /** 104 * Creates a new instance of SVGUniverse 105 */ 106 public SVGUniverse() 107 { 108 } 109 110 public void addPropertyChangeListener(PropertyChangeListener l) 111 { 112 changes.addPropertyChangeListener(l); 113 } 114 115 public void removePropertyChangeListener(PropertyChangeListener l) 116 { 117 changes.removePropertyChangeListener(l); 118 } 119 120 /** 121 * Release all loaded SVG document from memory 122 */ 123 public void clear() 124 { 125 loadedDocs.clear(); 126 loadedFonts.clear(); 127 loadedImages.clear(); 128 } 129 130 /** 131 * Returns the current animation time in milliseconds. 132 */ 133 public double getCurTime() 134 { 135 return curTime; 136 } 137 138 public void setCurTime(double curTime) 139 { 140 double oldTime = this.curTime; 141 this.curTime = curTime; 142 changes.firePropertyChange("curTime", new Double(oldTime), new Double(curTime)); 143 } 144 145 /** 146 * Updates all time influenced style and presentation attributes in all SVG 147 * documents in this universe. 148 */ 149 public void updateTime() throws SVGException 150 { 151 for (Iterator it = loadedDocs.values().iterator(); it.hasNext();) 152 { 153 SVGDiagram dia = (SVGDiagram) it.next(); 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 (Iterator it = loadedFonts.values().iterator(); it.hasNext();) 170 { 171 return (Font) it.next(); 172 } 173 return null; 174 } 175 176 public Font getFont(String fontName) 177 { 178 return (Font) loadedFonts.get(fontName); 179 } 180 181 URL registerImage(URI imageURI) 182 { 183 String scheme = imageURI.getScheme(); 184 if (scheme.equals("data")) 185 { 186 String path = imageURI.getRawSchemeSpecificPart(); 187 int idx = path.indexOf(';'); 188 String mime = path.substring(0, idx); 189 String content = path.substring(idx + 1); 190 191 if (content.startsWith("base64")) 192 { 193 content = content.substring(6); 194 try 195 { 196 byte[] buf = new sun.misc.BASE64Decoder().decodeBuffer(content); 197 ByteArrayInputStream bais = new ByteArrayInputStream(buf); 198 BufferedImage img = ImageIO.read(bais); 199 200 URL url; 201 int urlIdx = 0; 202 while (true) 203 { 204 url = new URL("inlineImage", "localhost", "img" + urlIdx); 205 if (!loadedImages.containsKey(url)) 206 { 207 break; 208 } 209 urlIdx++; 210 } 211 212 SoftReference ref = new SoftReference(img); 213 loadedImages.put(url, ref); 214 215 return url; 216 } catch (IOException ex) 217 { 218 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 219 "Could not decode inline image", ex); 220 } 221 } 222 return null; 223 } else 224 { 225 try 226 { 227 URL url = imageURI.toURL(); 228 registerImage(url); 229 return url; 230 } catch (MalformedURLException ex) 231 { 232 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 233 "Bad url", ex); 234 } 235 return null; 236 } 237 } 238 239 void registerImage(URL imageURL) 240 { 241 if (loadedImages.containsKey(imageURL)) 242 { 243 return; 244 } 245 246 SoftReference ref; 247 try 248 { 249 String fileName = imageURL.getFile(); 250 if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase())) 251 { 252 SVGIcon icon = new SVGIcon(); 253 icon.setSvgURI(imageURL.toURI()); 254 255 BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); 256 Graphics2D g = img.createGraphics(); 257 icon.paintIcon(null, g, 0, 0); 258 g.dispose(); 259 ref = new SoftReference(img); 260 } else 261 { 262 BufferedImage img = ImageIO.read(imageURL); 263 ref = new SoftReference(img); 264 } 265 loadedImages.put(imageURL, ref); 266 } catch (Exception e) 267 { 268 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 269 "Could not load image: " + imageURL, e); 270 } 271 } 272 273 BufferedImage getImage(URL imageURL) 274 { 275 SoftReference ref = (SoftReference) loadedImages.get(imageURL); 276 if (ref == null) 277 { 278 return null; 279 } 280 281 BufferedImage img = (BufferedImage) ref.get(); 282 //If image was cleared from memory, reload it 283 if (img == null) 284 { 285 try 286 { 287 img = ImageIO.read(imageURL); 288 } catch (Exception e) 289 { 290 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 291 "Could not load image", e); 292 } 293 ref = new SoftReference(img); 294 loadedImages.put(imageURL, ref); 295 } 296 297 return img; 298 } 299 300 /** 301 * Returns the element of the document at the given URI. If the document is 302 * not already loaded, it will be. 303 */ 304 public SVGElement getElement(URI path) 305 { 306 return getElement(path, true); 307 } 308 309 public SVGElement getElement(URL path) 310 { 311 try 312 { 313 URI uri = new URI(path.toString()); 314 return getElement(uri, true); 315 } catch (Exception e) 316 { 317 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 318 "Could not parse url " + path, e); 319 } 320 return null; 321 } 322 323 /** 324 * Looks up a href within our universe. If the href refers to a document 325 * that is not loaded, it will be loaded. The URL #target will then be 326 * checked against the SVG diagram's index and the coresponding element 327 * returned. If there is no coresponding index, null is returned. 328 */ 329 public SVGElement getElement(URI path, boolean loadIfAbsent) 330 { 331 try 332 { 333 //Strip fragment from URI 334 URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null); 335 336 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 337 if (dia == null && loadIfAbsent) 338 { 339//System.err.println("SVGUnivserse: " + xmlBase.toString()); 340//javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString()); 341 URL url = xmlBase.toURL(); 342 343 loadSVG(url, false); 344 dia = (SVGDiagram) loadedDocs.get(xmlBase); 345 if (dia == null) 346 { 347 return null; 348 } 349 } 350 351 String fragment = path.getFragment(); 352 return fragment == null ? dia.getRoot() : dia.getElement(fragment); 353 } catch (Exception e) 354 { 355 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 356 "Could not parse path " + path, e); 357 return null; 358 } 359 } 360 361 public SVGDiagram getDiagram(URI xmlBase) 362 { 363 return getDiagram(xmlBase, true); 364 } 365 366 /** 367 * Returns the diagram that has been loaded from this root. If diagram is 368 * not already loaded, returns null. 369 */ 370 public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent) 371 { 372 if (xmlBase == null) 373 { 374 return null; 375 } 376 377 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 378 if (dia != null || !loadIfAbsent) 379 { 380 return dia; 381 } 382 383 //Load missing diagram 384 try 385 { 386 URL url; 387 if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/")) 388 { 389 //Workaround for resources stored in jars loaded by Webstart. 390 //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651 391 url = SVGUniverse.class.getResource("xmlBase.getPath()"); 392 } 393 else 394 { 395 url = xmlBase.toURL(); 396 } 397 398 399 loadSVG(url, false); 400 dia = (SVGDiagram) loadedDocs.get(xmlBase); 401 return dia; 402 } catch (Exception e) 403 { 404 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 405 "Could not parse", e); 406 } 407 408 return null; 409 } 410 411 /** 412 * Wraps input stream in a BufferedInputStream. If it is detected that this 413 * input stream is GZIPped, also wraps in a GZIPInputStream for inflation. 414 * 415 * @param is Raw input stream 416 * @return Uncompressed stream of SVG data 417 * @throws java.io.IOException 418 */ 419 private InputStream createDocumentInputStream(InputStream is) throws IOException 420 { 421 BufferedInputStream bin = new BufferedInputStream(is); 422 bin.mark(2); 423 int b0 = bin.read(); 424 int b1 = bin.read(); 425 bin.reset(); 426 427 //Check for gzip magic number 428 if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC) 429 { 430 GZIPInputStream iis = new GZIPInputStream(bin); 431 return iis; 432 } else 433 { 434 //Plain text 435 return bin; 436 } 437 } 438 439 public URI loadSVG(URL docRoot) 440 { 441 return loadSVG(docRoot, false); 442 } 443 444 /** 445 * Loads an SVG file and all the files it references from the URL provided. 446 * If a referenced file already exists in the SVG universe, it is not 447 * reloaded. 448 * 449 * @param docRoot - URL to the location where this SVG file can be found. 450 * @param forceLoad - if true, ignore cached diagram and reload 451 * @return - The URI that refers to the loaded document 452 */ 453 public URI loadSVG(URL docRoot, boolean forceLoad) 454 { 455 try 456 { 457 URI uri = new URI(docRoot.toString()); 458 if (loadedDocs.containsKey(uri) && !forceLoad) 459 { 460 return uri; 461 } 462 463 InputStream is = docRoot.openStream(); 464 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 465 } catch (URISyntaxException ex) 466 { 467 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 468 "Could not parse", ex); 469 } catch (IOException e) 470 { 471 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 472 "Could not parse", e); 473 } 474 475 return null; 476 } 477 478 public URI loadSVG(InputStream is, String name) throws IOException 479 { 480 return loadSVG(is, name, false); 481 } 482 483 public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException 484 { 485 URI uri = getStreamBuiltURI(name); 486 if (uri == null) 487 { 488 return null; 489 } 490 if (loadedDocs.containsKey(uri) && !forceLoad) 491 { 492 return uri; 493 } 494 495 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 496 } 497 498 public URI loadSVG(Reader reader, String name) 499 { 500 return loadSVG(reader, name, false); 501 } 502 503 /** 504 * This routine allows you to create SVG documents from data streams that 505 * may not necessarily have a URL to load from. Since every SVG document 506 * must be identified by a unique URL, Salamander provides a method to fake 507 * this for streams by defining it's own protocol - svgSalamander - for SVG 508 * documents without a formal URL. 509 * 510 * @param reader - A stream containing a valid SVG document 511 * @param name - <p>A unique name for this document. It will be used to 512 * construct a unique URI to refer to this document and perform resolution 513 * with relative URIs within this document.</p> <p>For example, a name of 514 * "/myScene" will produce the URI svgSalamander:/myScene. 515 * "/maps/canada/toronto" will produce svgSalamander:/maps/canada/toronto. 516 * If this second document then contained the href "../uk/london", it would 517 * resolve by default to svgSalamander:/maps/uk/london. That is, SVG 518 * Salamander defines the URI scheme svgSalamander for it's own internal use 519 * and uses it for uniquely identfying documents loaded by stream.</p> <p>If 520 * you need to link to documents outside of this scheme, you can either 521 * supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)") or 522 * put the xml:base attribute in a tag to change the defaultbase URIs are 523 * resolved against</p> <p>If a name does not start with the character '/', 524 * it will be automatically prefixed to it.</p> 525 * @param forceLoad - if true, ignore cached diagram and reload 526 * 527 * @return - The URI that refers to the loaded document 528 */ 529 public URI loadSVG(Reader reader, String name, boolean forceLoad) 530 { 531//System.err.println(url.toString()); 532 //Synthesize URI for this stream 533 URI uri = getStreamBuiltURI(name); 534 if (uri == null) 535 { 536 return null; 537 } 538 if (loadedDocs.containsKey(uri) && !forceLoad) 539 { 540 return uri; 541 } 542 543 return loadSVG(uri, new InputSource(reader)); 544 } 545 546 /** 547 * Synthesize a URI for an SVGDiagram constructed from a stream. 548 * 549 * @param name - Name given the document constructed from a stream. 550 */ 551 public URI getStreamBuiltURI(String name) 552 { 553 if (name == null || name.length() == 0) 554 { 555 return null; 556 } 557 558 if (name.charAt(0) != '/') 559 { 560 name = '/' + name; 561 } 562 563 try 564 { 565 //Dummy URL for SVG documents built from image streams 566 return new URI(INPUTSTREAM_SCHEME, name, null); 567 } catch (Exception e) 568 { 569 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 570 "Could not parse", e); 571 return null; 572 } 573 } 574 575 private XMLReader getXMLReaderCached() throws SAXException 576 { 577 if (cachedReader == null) 578 { 579 cachedReader = XMLReaderFactory.createXMLReader(); 580 } 581 return cachedReader; 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 = getXMLReaderCached(); 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 System.err.println("Error processing " + xmlBase); 615 System.err.println(sex.getMessage()); 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 getLoadedDocumentURIs() 633 { 634 return new ArrayList(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}