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