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