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.Iterator; 061import java.util.logging.Level; 062import java.util.logging.Logger; 063import java.util.zip.GZIPInputStream; 064import javax.imageio.ImageIO; 065import javax.xml.parsers.ParserConfigurationException; 066import javax.xml.parsers.SAXParserFactory; 067import org.xml.sax.EntityResolver; 068import org.xml.sax.InputSource; 069import org.xml.sax.SAXException; 070import org.xml.sax.SAXParseException; 071import org.xml.sax.XMLReader; 072 073/** 074 * Many SVG files can be loaded at one time. These files will quite likely need 075 * to reference one another. The SVG universe provides a container for all these 076 * files and the means for them to relate to each other. 077 * 078 * @author Mark McKay 079 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a> 080 */ 081public class SVGUniverse implements Serializable 082{ 083 084 public static final long serialVersionUID = 0; 085 transient private PropertyChangeSupport changes = new PropertyChangeSupport(this); 086 /** 087 * Maps document URIs to their loaded SVG diagrams. Note that URIs for 088 * documents loaded from URLs will reflect their URLs and URIs for documents 089 * initiated from streams will have the scheme <i>svgSalamander</i>. 090 */ 091 final HashMap loadedDocs = new HashMap(); 092 final HashMap loadedFonts = new HashMap(); 093 final HashMap loadedImages = new HashMap(); 094 public static final String INPUTSTREAM_SCHEME = "svgSalamander"; 095 /** 096 * Current time in this universe. Used for resolving attributes that are 097 * influenced by track information. Time is in milliseconds. Time 0 098 * coresponds to the time of 0 in each member diagram. 099 */ 100 protected double curTime = 0.0; 101 private boolean verbose = false; 102 //Cache reader for efficiency 103 XMLReader cachedReader; 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 (Iterator it = loadedDocs.values().iterator(); it.hasNext();) 154 { 155 SVGDiagram dia = (SVGDiagram) it.next(); 156 dia.updateTime(curTime); 157 } 158 } 159 160 /** 161 * Called by the Font element to let the universe know that a font has been 162 * loaded and is available. 163 */ 164 void registerFont(Font font) 165 { 166 loadedFonts.put(font.getFontFace().getFontFamily(), font); 167 } 168 169 public Font getDefaultFont() 170 { 171 for (Iterator it = loadedFonts.values().iterator(); it.hasNext();) 172 { 173 return (Font) it.next(); 174 } 175 return null; 176 } 177 178 public Font getFont(String fontName) 179 { 180 return (Font) loadedFonts.get(fontName); 181 } 182 183 URL registerImage(URI imageURI) 184 { 185 String scheme = imageURI.getScheme(); 186 if (scheme.equals("data")) 187 { 188 String path = imageURI.getRawSchemeSpecificPart(); 189 int idx = path.indexOf(';'); 190 String mime = path.substring(0, idx); 191 String content = path.substring(idx + 1); 192 193 if (content.startsWith("base64")) 194 { 195 content = content.substring(6); 196 try 197 { 198// byte[] buf = new sun.misc.BASE64Decoder().decodeBuffer(content); 199// ByteArrayInputStream bais = new ByteArrayInputStream(buf); 200 ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes()); 201 Base64InputStream bais = new Base64InputStream(bis); 202 203 BufferedImage img = ImageIO.read(bais); 204 205 URL url; 206 int urlIdx = 0; 207 while (true) 208 { 209 url = new URL("inlineImage", "localhost", "img" + urlIdx); 210 if (!loadedImages.containsKey(url)) 211 { 212 break; 213 } 214 urlIdx++; 215 } 216 217 SoftReference ref = new SoftReference(img); 218 loadedImages.put(url, ref); 219 220 return url; 221 } catch (IOException ex) 222 { 223 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 224 "Could not decode inline image", ex); 225 } 226 } 227 return null; 228 } else 229 { 230 try 231 { 232 URL url = imageURI.toURL(); 233 registerImage(url); 234 return url; 235 } catch (MalformedURLException ex) 236 { 237 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 238 "Bad url", ex); 239 } 240 return null; 241 } 242 } 243 244 void registerImage(URL imageURL) 245 { 246 if (loadedImages.containsKey(imageURL)) 247 { 248 return; 249 } 250 251 SoftReference ref; 252 try 253 { 254 String fileName = imageURL.getFile(); 255 if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase())) 256 { 257 SVGIcon icon = new SVGIcon(); 258 icon.setSvgURI(imageURL.toURI()); 259 260 BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); 261 Graphics2D g = img.createGraphics(); 262 icon.paintIcon(null, g, 0, 0); 263 g.dispose(); 264 ref = new SoftReference(img); 265 } else 266 { 267 BufferedImage img = ImageIO.read(imageURL); 268 ref = new SoftReference(img); 269 } 270 loadedImages.put(imageURL, ref); 271 } catch (Exception e) 272 { 273 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 274 "Could not load image: " + imageURL, e); 275 } 276 } 277 278 BufferedImage getImage(URL imageURL) 279 { 280 SoftReference ref = (SoftReference) loadedImages.get(imageURL); 281 if (ref == null) 282 { 283 return null; 284 } 285 286 BufferedImage img = (BufferedImage) ref.get(); 287 //If image was cleared from memory, reload it 288 if (img == null) 289 { 290 try 291 { 292 img = ImageIO.read(imageURL); 293 } catch (Exception e) 294 { 295 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 296 "Could not load image", e); 297 } 298 ref = new SoftReference(img); 299 loadedImages.put(imageURL, ref); 300 } 301 302 return img; 303 } 304 305 /** 306 * Returns the element of the document at the given URI. If the document is 307 * not already loaded, it will be. 308 */ 309 public SVGElement getElement(URI path) 310 { 311 return getElement(path, true); 312 } 313 314 public SVGElement getElement(URL path) 315 { 316 try 317 { 318 URI uri = new URI(path.toString()); 319 return getElement(uri, true); 320 } catch (Exception e) 321 { 322 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 323 "Could not parse url " + path, e); 324 } 325 return null; 326 } 327 328 /** 329 * Looks up a href within our universe. If the href refers to a document 330 * that is not loaded, it will be loaded. The URL #target will then be 331 * checked against the SVG diagram's index and the coresponding element 332 * returned. If there is no coresponding index, null is returned. 333 */ 334 public SVGElement getElement(URI path, boolean loadIfAbsent) 335 { 336 try 337 { 338 //Strip fragment from URI 339 URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null); 340 341 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 342 if (dia == null && loadIfAbsent) 343 { 344//System.err.println("SVGUnivserse: " + xmlBase.toString()); 345//javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString()); 346 URL url = xmlBase.toURL(); 347 348 loadSVG(url, false); 349 dia = (SVGDiagram) loadedDocs.get(xmlBase); 350 if (dia == null) 351 { 352 return null; 353 } 354 } 355 356 String fragment = path.getFragment(); 357 return fragment == null ? dia.getRoot() : dia.getElement(fragment); 358 } catch (Exception e) 359 { 360 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 361 "Could not parse path " + path, e); 362 return null; 363 } 364 } 365 366 public SVGDiagram getDiagram(URI xmlBase) 367 { 368 return getDiagram(xmlBase, true); 369 } 370 371 /** 372 * Returns the diagram that has been loaded from this root. If diagram is 373 * not already loaded, returns null. 374 */ 375 public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent) 376 { 377 if (xmlBase == null) 378 { 379 return null; 380 } 381 382 SVGDiagram dia = (SVGDiagram) loadedDocs.get(xmlBase); 383 if (dia != null || !loadIfAbsent) 384 { 385 return dia; 386 } 387 388 //Load missing diagram 389 try 390 { 391 URL url; 392 if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/")) 393 { 394 //Workaround for resources stored in jars loaded by Webstart. 395 //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651 396 url = SVGUniverse.class.getResource("xmlBase.getPath()"); 397 } 398 else 399 { 400 url = xmlBase.toURL(); 401 } 402 403 404 loadSVG(url, false); 405 dia = (SVGDiagram) loadedDocs.get(xmlBase); 406 return dia; 407 } catch (Exception e) 408 { 409 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 410 "Could not parse", e); 411 } 412 413 return null; 414 } 415 416 /** 417 * Wraps input stream in a BufferedInputStream. If it is detected that this 418 * input stream is GZIPped, also wraps in a GZIPInputStream for inflation. 419 * 420 * @param is Raw input stream 421 * @return Uncompressed stream of SVG data 422 * @throws java.io.IOException 423 */ 424 private InputStream createDocumentInputStream(InputStream is) throws IOException 425 { 426 BufferedInputStream bin = new BufferedInputStream(is); 427 bin.mark(2); 428 int b0 = bin.read(); 429 int b1 = bin.read(); 430 bin.reset(); 431 432 //Check for gzip magic number 433 if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC) 434 { 435 GZIPInputStream iis = new GZIPInputStream(bin); 436 return iis; 437 } else 438 { 439 //Plain text 440 return bin; 441 } 442 } 443 444 public URI loadSVG(URL docRoot) 445 { 446 return loadSVG(docRoot, false); 447 } 448 449 /** 450 * Loads an SVG file and all the files it references from the URL provided. 451 * If a referenced file already exists in the SVG universe, it is not 452 * reloaded. 453 * 454 * @param docRoot - URL to the location where this SVG file can be found. 455 * @param forceLoad - if true, ignore cached diagram and reload 456 * @return - The URI that refers to the loaded document 457 */ 458 public URI loadSVG(URL docRoot, boolean forceLoad) 459 { 460 try 461 { 462 URI uri = new URI(docRoot.toString()); 463 if (loadedDocs.containsKey(uri) && !forceLoad) 464 { 465 return uri; 466 } 467 468 InputStream is = docRoot.openStream(); 469 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 470 } catch (URISyntaxException ex) 471 { 472 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 473 "Could not parse", ex); 474 } catch (IOException e) 475 { 476 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 477 "Could not parse", e); 478 } 479 480 return null; 481 } 482 483 public URI loadSVG(InputStream is, String name) throws IOException 484 { 485 return loadSVG(is, name, false); 486 } 487 488 public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException 489 { 490 URI uri = getStreamBuiltURI(name); 491 if (uri == null) 492 { 493 return null; 494 } 495 if (loadedDocs.containsKey(uri) && !forceLoad) 496 { 497 return uri; 498 } 499 500 return loadSVG(uri, new InputSource(createDocumentInputStream(is))); 501 } 502 503 public URI loadSVG(Reader reader, String name) 504 { 505 return loadSVG(reader, name, false); 506 } 507 508 /** 509 * This routine allows you to create SVG documents from data streams that 510 * may not necessarily have a URL to load from. Since every SVG document 511 * must be identified by a unique URL, Salamander provides a method to fake 512 * this for streams by defining it's own protocol - svgSalamander - for SVG 513 * documents without a formal URL. 514 * 515 * @param reader - A stream containing a valid SVG document 516 * @param name - <p>A unique name for this document. It will be used to 517 * construct a unique URI to refer to this document and perform resolution 518 * with relative URIs within this document.</p> <p>For example, a name of 519 * "/myScene" will produce the URI svgSalamander:/myScene. 520 * "/maps/canada/toronto" will produce svgSalamander:/maps/canada/toronto. 521 * If this second document then contained the href "../uk/london", it would 522 * resolve by default to svgSalamander:/maps/uk/london. That is, SVG 523 * Salamander defines the URI scheme svgSalamander for it's own internal use 524 * and uses it for uniquely identfying documents loaded by stream.</p> <p>If 525 * you need to link to documents outside of this scheme, you can either 526 * supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)") or 527 * put the xml:base attribute in a tag to change the defaultbase URIs are 528 * resolved against</p> <p>If a name does not start with the character '/', 529 * it will be automatically prefixed to it.</p> 530 * @param forceLoad - if true, ignore cached diagram and reload 531 * 532 * @return - The URI that refers to the loaded document 533 */ 534 public URI loadSVG(Reader reader, String name, boolean forceLoad) 535 { 536//System.err.println(url.toString()); 537 //Synthesize URI for this stream 538 URI uri = getStreamBuiltURI(name); 539 if (uri == null) 540 { 541 return null; 542 } 543 if (loadedDocs.containsKey(uri) && !forceLoad) 544 { 545 return uri; 546 } 547 548 return loadSVG(uri, new InputSource(reader)); 549 } 550 551 /** 552 * Synthesize a URI for an SVGDiagram constructed from a stream. 553 * 554 * @param name - Name given the document constructed from a stream. 555 */ 556 public URI getStreamBuiltURI(String name) 557 { 558 if (name == null || name.length() == 0) 559 { 560 return null; 561 } 562 563 if (name.charAt(0) != '/') 564 { 565 name = '/' + name; 566 } 567 568 try 569 { 570 //Dummy URL for SVG documents built from image streams 571 return new URI(INPUTSTREAM_SCHEME, name, null); 572 } catch (Exception e) 573 { 574 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 575 "Could not parse", e); 576 return null; 577 } 578 } 579 580 private XMLReader getXMLReaderCached() throws SAXException, ParserConfigurationException 581 { 582 if (cachedReader == null) 583 { 584 SAXParserFactory factory = SAXParserFactory.newInstance(); 585 factory.setNamespaceAware(true); 586 cachedReader = factory.newSAXParser().getXMLReader(); 587 } 588 return cachedReader; 589 } 590 591 protected URI loadSVG(URI xmlBase, InputSource is) 592 { 593 // Use an instance of ourselves as the SAX event handler 594 SVGLoader handler = new SVGLoader(xmlBase, this, verbose); 595 596 //Place this docment in the universe before it is completely loaded 597 // so that the load process can refer to references within it's current 598 // document 599 loadedDocs.put(xmlBase, handler.getLoadedDiagram()); 600 601 try 602 { 603 // Parse the input 604 XMLReader reader = getXMLReaderCached(); 605 reader.setEntityResolver( 606 new EntityResolver() 607 { 608 public InputSource resolveEntity(String publicId, String systemId) 609 { 610 //Ignore all DTDs 611 return new InputSource(new ByteArrayInputStream(new byte[0])); 612 } 613 }); 614 reader.setContentHandler(handler); 615 reader.parse(is); 616 617 handler.getLoadedDiagram().updateTime(curTime); 618 return xmlBase; 619 } catch (SAXParseException sex) 620 { 621 System.err.println("Error processing " + xmlBase); 622 System.err.println(sex.getMessage()); 623 624 loadedDocs.remove(xmlBase); 625 return null; 626 } catch (Throwable e) 627 { 628 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING, 629 "Could not load SVG " + xmlBase, e); 630 } 631 632 return null; 633 } 634 635 /** 636 * Get list of uris of all loaded documents and subdocuments. 637 * @return 638 */ 639 public ArrayList getLoadedDocumentURIs() 640 { 641 return new ArrayList(loadedDocs.keySet()); 642 } 643 644 /** 645 * Remove loaded document from cache. 646 * @param uri 647 */ 648 public void removeDocument(URI uri) 649 { 650 loadedDocs.remove(uri); 651 } 652 653 public boolean isVerbose() 654 { 655 return verbose; 656 } 657 658 public void setVerbose(boolean verbose) 659 { 660 this.verbose = verbose; 661 } 662 663 /** 664 * Uses serialization to duplicate this universe. 665 */ 666 public SVGUniverse duplicate() throws IOException, ClassNotFoundException 667 { 668 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 669 ObjectOutputStream os = new ObjectOutputStream(bs); 670 os.writeObject(this); 671 os.close(); 672 673 ByteArrayInputStream bin = new ByteArrayInputStream(bs.toByteArray()); 674 ObjectInputStream is = new ObjectInputStream(bin); 675 SVGUniverse universe = (SVGUniverse) is.readObject(); 676 is.close(); 677 678 return universe; 679 } 680}