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