source: josm/trunk/src/com/kitfox/svg/SVGUniverse.java@ 15446

Last change on this file since 15446 was 14328, checked in by Don-vip, 6 years ago

see #14319, see #16838 - update to svgSalamander 1.1.2

  • Property svn:eol-style set to native
File size: 21.3 KB
Line 
1/*
2 * SVG Salamander
3 * Copyright (c) 2004, Mark McKay
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
9 *
10 * - Redistributions of source code must retain the above
11 * copyright notice, this list of conditions and the following
12 * disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials
16 * provided with the distribution.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 *
31 * Mark McKay can be contacted at mark@kitfox.com. Salamander and other
32 * projects can be found at http://www.kitfox.com
33 *
34 * Created on February 18, 2004, 11:43 PM
35 */
36package com.kitfox.svg;
37
38import java.awt.Graphics2D;
39import java.awt.image.BufferedImage;
40import java.beans.PropertyChangeListener;
41import java.beans.PropertyChangeSupport;
42import java.io.BufferedInputStream;
43import java.io.ByteArrayInputStream;
44import java.io.ByteArrayOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.io.ObjectInputStream;
48import java.io.ObjectOutputStream;
49import java.io.Reader;
50import java.io.Serializable;
51import java.lang.ref.SoftReference;
52import java.net.MalformedURLException;
53import java.net.URI;
54import java.net.URISyntaxException;
55import java.net.URL;
56import java.util.ArrayList;
57import java.util.Base64;
58import java.util.HashMap;
59import java.util.logging.Level;
60import java.util.logging.Logger;
61import java.util.zip.GZIPInputStream;
62
63import javax.imageio.ImageIO;
64import javax.xml.parsers.ParserConfigurationException;
65import javax.xml.parsers.SAXParserFactory;
66
67import org.xml.sax.EntityResolver;
68import org.xml.sax.InputSource;
69import org.xml.sax.SAXException;
70import org.xml.sax.SAXParseException;
71import org.xml.sax.XMLReader;
72
73import com.kitfox.svg.app.beans.SVGIcon;
74
75/**
76 * Many SVG files can be loaded at one time. These files will quite likely need
77 * to reference one another. The SVG universe provides a container for all these
78 * files and the means for them to relate to each other.
79 *
80 * @author Mark McKay
81 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a>
82 */
83public class SVGUniverse implements Serializable
84{
85
86 public static final long serialVersionUID = 0;
87 transient private PropertyChangeSupport changes = new PropertyChangeSupport(this);
88 /**
89 * Maps document URIs to their loaded SVG diagrams. Note that URIs for
90 * documents loaded from URLs will reflect their URLs and URIs for documents
91 * initiated from streams will have the scheme <i>svgSalamander</i>.
92 */
93 final HashMap<URI, SVGDiagram> loadedDocs = new HashMap<>();
94 final HashMap<String, Font> loadedFonts = new HashMap<>();
95 final HashMap<URL, SoftReference<BufferedImage>> loadedImages = new HashMap<>();
96 public static final String INPUTSTREAM_SCHEME = "svgSalamander";
97 /**
98 * Current time in this universe. Used for resolving attributes that are
99 * influenced by track information. Time is in milliseconds. Time 0
100 * coresponds to the time of 0 in each member diagram.
101 */
102 protected double curTime = 0.0;
103 private boolean verbose = false;
104 //Cache reader for efficiency
105 XMLReader cachedReader;
106
107 //If true, <imageSVG> elements will only load image data that is included using inline data: uris
108 private boolean imageDataInlineOnly = false;
109
110 /**
111 * Creates a new instance of SVGUniverse
112 */
113 public SVGUniverse()
114 {
115 }
116
117 public void addPropertyChangeListener(PropertyChangeListener l)
118 {
119 changes.addPropertyChangeListener(l);
120 }
121
122 public void removePropertyChangeListener(PropertyChangeListener l)
123 {
124 changes.removePropertyChangeListener(l);
125 }
126
127 /**
128 * Release all loaded SVG document from memory
129 */
130 public void clear()
131 {
132 loadedDocs.clear();
133 loadedFonts.clear();
134 loadedImages.clear();
135 }
136
137 /**
138 * Returns the current animation time in milliseconds.
139 */
140 public double getCurTime()
141 {
142 return curTime;
143 }
144
145 public void setCurTime(double curTime)
146 {
147 double oldTime = this.curTime;
148 this.curTime = curTime;
149 changes.firePropertyChange("curTime", new Double(oldTime), new Double(curTime));
150 }
151
152 /**
153 * Updates all time influenced style and presentation attributes in all SVG
154 * documents in this universe.
155 */
156 public void updateTime() throws SVGException
157 {
158 for (SVGDiagram dia : loadedDocs.values()) {
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 (Font font : loadedFonts.values()) {
175 return font;
176 }
177 return null;
178 }
179
180 public Font getFont(String fontName)
181 {
182 return loadedFonts.get(fontName);
183 }
184
185 URL registerImage(URI imageURI)
186 {
187 String scheme = imageURI.getScheme();
188 if (scheme.equals("data"))
189 {
190 String path = imageURI.getRawSchemeSpecificPart();
191 int idx = path.indexOf(';');
192 String mime = path.substring(0, idx);
193 String content = path.substring(idx + 1);
194
195 if (content.startsWith("base64"))
196 {
197 try
198 {
199 byte[] buf = Base64.getDecoder().decode(content.substring(6));
200 ByteArrayInputStream bais = new ByteArrayInputStream(buf);
201 BufferedImage img = ImageIO.read(bais);
202
203 URL url;
204 int urlIdx = 0;
205 while (true)
206 {
207 url = new URL("inlineImage", "localhost", "img" + urlIdx);
208 if (!loadedImages.containsKey(url))
209 {
210 break;
211 }
212 urlIdx++;
213 }
214
215 SoftReference<BufferedImage> ref = new SoftReference<>(img);
216 loadedImages.put(url, ref);
217
218 return url;
219 }
220 catch (IOException | IllegalArgumentException ex)
221 {
222 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
223 "Could not decode inline image", ex);
224 }
225 }
226 return null;
227 } else
228 {
229 try
230 {
231 URL url = imageURI.toURL();
232 registerImage(url);
233 return url;
234 } catch (MalformedURLException ex)
235 {
236 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
237 "Bad url", ex);
238 }
239 return null;
240 }
241 }
242
243 void registerImage(URL imageURL)
244 {
245 if (loadedImages.containsKey(imageURL))
246 {
247 return;
248 }
249
250 SoftReference<BufferedImage> ref;
251 try
252 {
253 String fileName = imageURL.getFile();
254 if (".svg".equals(fileName.substring(fileName.length() - 4).toLowerCase()))
255 {
256 SVGIcon icon = new SVGIcon();
257 icon.setSvgURI(imageURL.toURI());
258
259 BufferedImage img = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
260 Graphics2D g = img.createGraphics();
261 icon.paintIcon(null, g, 0, 0);
262 g.dispose();
263 ref = new SoftReference<>(img);
264 } else
265 {
266 BufferedImage img = ImageIO.read(imageURL);
267 ref = new SoftReference<>(img);
268 }
269 loadedImages.put(imageURL, ref);
270 } catch (Exception e)
271 {
272 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
273 "Could not load image: " + imageURL, e);
274 }
275 }
276
277 BufferedImage getImage(URL imageURL)
278 {
279 SoftReference<BufferedImage> ref = loadedImages.get(imageURL);
280 if (ref == null)
281 {
282 return null;
283 }
284
285 BufferedImage img = ref.get();
286 //If image was cleared from memory, reload it
287 if (img == null)
288 {
289 try
290 {
291 img = ImageIO.read(imageURL);
292 } catch (Exception e)
293 {
294 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
295 "Could not load image", e);
296 }
297 ref = new SoftReference<>(img);
298 loadedImages.put(imageURL, ref);
299 }
300
301 return img;
302 }
303
304 /**
305 * Returns the element of the document at the given URI. If the document is
306 * not already loaded, it will be.
307 */
308 public SVGElement getElement(URI path)
309 {
310 return getElement(path, true);
311 }
312
313 public SVGElement getElement(URL path)
314 {
315 try
316 {
317 URI uri = new URI(path.toString());
318 return getElement(uri, true);
319 } catch (Exception e)
320 {
321 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
322 "Could not parse url " + path, e);
323 }
324 return null;
325 }
326
327 /**
328 * Looks up a href within our universe. If the href refers to a document
329 * that is not loaded, it will be loaded. The URL #target will then be
330 * checked against the SVG diagram's index and the coresponding element
331 * returned. If there is no coresponding index, null is returned.
332 */
333 public SVGElement getElement(URI path, boolean loadIfAbsent)
334 {
335 try
336 {
337 //Strip fragment from URI
338 URI xmlBase = new URI(path.getScheme(), path.getSchemeSpecificPart(), null);
339
340 SVGDiagram dia = loadedDocs.get(xmlBase);
341 if (dia == null && loadIfAbsent)
342 {
343//System.err.println("SVGUnivserse: " + xmlBase.toString());
344//javax.swing.JOptionPane.showMessageDialog(null, xmlBase.toString());
345 URL url = xmlBase.toURL();
346
347 loadSVG(url, false);
348 dia = loadedDocs.get(xmlBase);
349 if (dia == null)
350 {
351 return null;
352 }
353 }
354
355 String fragment = path.getFragment();
356 return fragment == null ? dia.getRoot() : dia.getElement(fragment);
357 } catch (Exception e)
358 {
359 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
360 "Could not parse path " + path, e);
361 return null;
362 }
363 }
364
365 public SVGDiagram getDiagram(URI xmlBase)
366 {
367 return getDiagram(xmlBase, true);
368 }
369
370 /**
371 * Returns the diagram that has been loaded from this root. If diagram is
372 * not already loaded, returns null.
373 */
374 public SVGDiagram getDiagram(URI xmlBase, boolean loadIfAbsent)
375 {
376 if (xmlBase == null)
377 {
378 return null;
379 }
380
381 SVGDiagram dia = loadedDocs.get(xmlBase);
382 if (dia != null || !loadIfAbsent)
383 {
384 return dia;
385 }
386
387 //Load missing diagram
388 try
389 {
390 URL url;
391 if ("jar".equals(xmlBase.getScheme()) && xmlBase.getPath() != null && !xmlBase.getPath().contains("!/"))
392 {
393 //Workaround for resources stored in jars loaded by Webstart.
394 //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6753651
395 url = SVGUniverse.class.getResource("xmlBase.getPath()");
396 }
397 else
398 {
399 url = xmlBase.toURL();
400 }
401
402
403 loadSVG(url, false);
404 dia = loadedDocs.get(xmlBase);
405 return dia;
406 } catch (Exception e)
407 {
408 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
409 "Could not parse", e);
410 }
411
412 return null;
413 }
414
415 /**
416 * Wraps input stream in a BufferedInputStream. If it is detected that this
417 * input stream is GZIPped, also wraps in a GZIPInputStream for inflation.
418 *
419 * @param is Raw input stream
420 * @return Uncompressed stream of SVG data
421 * @throws java.io.IOException
422 */
423 private InputStream createDocumentInputStream(InputStream is) throws IOException
424 {
425 BufferedInputStream bin = new BufferedInputStream(is);
426 bin.mark(2);
427 int b0 = bin.read();
428 int b1 = bin.read();
429 bin.reset();
430
431 //Check for gzip magic number
432 if ((b1 << 8 | b0) == GZIPInputStream.GZIP_MAGIC)
433 {
434 GZIPInputStream iis = new GZIPInputStream(bin);
435 return iis;
436 } else
437 {
438 //Plain text
439 return bin;
440 }
441 }
442
443 public URI loadSVG(URL docRoot)
444 {
445 return loadSVG(docRoot, false);
446 }
447
448 /**
449 * Loads an SVG file and all the files it references from the URL provided.
450 * If a referenced file already exists in the SVG universe, it is not
451 * reloaded.
452 *
453 * @param docRoot - URL to the location where this SVG file can be found.
454 * @param forceLoad - if true, ignore cached diagram and reload
455 * @return - The URI that refers to the loaded document
456 */
457 public URI loadSVG(URL docRoot, boolean forceLoad)
458 {
459 try
460 {
461 URI uri = new URI(docRoot.toString());
462 if (loadedDocs.containsKey(uri) && !forceLoad)
463 {
464 return uri;
465 }
466
467 InputStream is = docRoot.openStream();
468 return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
469 } catch (URISyntaxException ex)
470 {
471 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
472 "Could not parse", ex);
473 } catch (IOException e)
474 {
475 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
476 "Could not parse", e);
477 }
478
479 return null;
480 }
481
482 public URI loadSVG(InputStream is, String name) throws IOException
483 {
484 return loadSVG(is, name, false);
485 }
486
487 public URI loadSVG(InputStream is, String name, boolean forceLoad) throws IOException
488 {
489 URI uri = getStreamBuiltURI(name);
490 if (uri == null)
491 {
492 return null;
493 }
494 if (loadedDocs.containsKey(uri) && !forceLoad)
495 {
496 return uri;
497 }
498
499 return loadSVG(uri, new InputSource(createDocumentInputStream(is)));
500 }
501
502 public URI loadSVG(Reader reader, String name)
503 {
504 return loadSVG(reader, name, false);
505 }
506
507 /**
508 * This routine allows you to create SVG documents from data streams that
509 * may not necessarily have a URL to load from. Since every SVG document
510 * must be identified by a unique URL, Salamander provides a method to fake
511 * this for streams by defining it's own protocol - svgSalamander - for SVG
512 * documents without a formal URL.
513 *
514 * @param reader - A stream containing a valid SVG document
515 * @param name - <p>A unique name for this document. It will be used to
516 * construct a unique URI to refer to this document and perform resolution
517 * with relative URIs within this document.</p> <p>For example, a name of
518 * "/myScene" will produce the URI svgSalamander:/myScene.
519 * "/maps/canada/toronto" will produce svgSalamander:/maps/canada/toronto.
520 * If this second document then contained the href "../uk/london", it would
521 * resolve by default to svgSalamander:/maps/uk/london. That is, SVG
522 * Salamander defines the URI scheme svgSalamander for it's own internal use
523 * and uses it for uniquely identfying documents loaded by stream.</p> <p>If
524 * you need to link to documents outside of this scheme, you can either
525 * supply full hrefs (eg, href="url(http://www.kitfox.com/index.html)") or
526 * put the xml:base attribute in a tag to change the defaultbase URIs are
527 * resolved against</p> <p>If a name does not start with the character '/',
528 * it will be automatically prefixed to it.</p>
529 * @param forceLoad - if true, ignore cached diagram and reload
530 *
531 * @return - The URI that refers to the loaded document
532 */
533 public URI loadSVG(Reader reader, String name, boolean forceLoad)
534 {
535//System.err.println(url.toString());
536 //Synthesize URI for this stream
537 URI uri = getStreamBuiltURI(name);
538 if (uri == null)
539 {
540 return null;
541 }
542 if (loadedDocs.containsKey(uri) && !forceLoad)
543 {
544 return uri;
545 }
546
547 return loadSVG(uri, new InputSource(reader));
548 }
549
550 /**
551 * Synthesize a URI for an SVGDiagram constructed from a stream.
552 *
553 * @param name - Name given the document constructed from a stream.
554 */
555 public URI getStreamBuiltURI(String name)
556 {
557 if (name == null || name.length() == 0)
558 {
559 return null;
560 }
561
562 if (name.charAt(0) != '/')
563 {
564 name = '/' + name;
565 }
566
567 try
568 {
569 //Dummy URL for SVG documents built from image streams
570 return new URI(INPUTSTREAM_SCHEME, name, null);
571 } catch (Exception e)
572 {
573 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
574 "Could not parse", e);
575 return null;
576 }
577 }
578
579 private XMLReader getXMLReaderCached() throws SAXException, ParserConfigurationException
580 {
581 if (cachedReader == null)
582 {
583 SAXParserFactory factory = SAXParserFactory.newInstance();
584 factory.setNamespaceAware(true);
585 cachedReader = factory.newSAXParser().getXMLReader();
586 }
587 return cachedReader;
588 }
589
590 protected URI loadSVG(URI xmlBase, InputSource is)
591 {
592 // Use an instance of ourselves as the SAX event handler
593 SVGLoader handler = new SVGLoader(xmlBase, this, verbose);
594
595 //Place this docment in the universe before it is completely loaded
596 // so that the load process can refer to references within it's current
597 // document
598 loadedDocs.put(xmlBase, handler.getLoadedDiagram());
599
600 try
601 {
602 // Parse the input
603 XMLReader reader = getXMLReaderCached();
604 reader.setEntityResolver(
605 new EntityResolver()
606 {
607 public InputSource resolveEntity(String publicId, String systemId)
608 {
609 //Ignore all DTDs
610 return new InputSource(new ByteArrayInputStream(new byte[0]));
611 }
612 });
613 reader.setContentHandler(handler);
614 reader.parse(is);
615
616 handler.getLoadedDiagram().updateTime(curTime);
617 return xmlBase;
618 } catch (SAXParseException sex)
619 {
620 System.err.println("Error processing " + xmlBase);
621 System.err.println(sex.getMessage());
622
623 loadedDocs.remove(xmlBase);
624 return null;
625 } catch (Throwable e)
626 {
627 Logger.getLogger(SVGConst.SVG_LOGGER).log(Level.WARNING,
628 "Could not load SVG " + xmlBase, e);
629 }
630
631 return null;
632 }
633
634 /**
635 * Get list of uris of all loaded documents and subdocuments.
636 * @return
637 */
638 public ArrayList<URI> getLoadedDocumentURIs()
639 {
640 return new ArrayList<>(loadedDocs.keySet());
641 }
642
643 /**
644 * Remove loaded document from cache.
645 * @param uri
646 */
647 public void removeDocument(URI uri)
648 {
649 loadedDocs.remove(uri);
650 }
651
652 public boolean isVerbose()
653 {
654 return verbose;
655 }
656
657 public void setVerbose(boolean verbose)
658 {
659 this.verbose = verbose;
660 }
661
662 /**
663 * Uses serialization to duplicate this universe.
664 */
665 public SVGUniverse duplicate() throws IOException, ClassNotFoundException
666 {
667 ByteArrayOutputStream bs = new ByteArrayOutputStream();
668 ObjectOutputStream os = new ObjectOutputStream(bs);
669 os.writeObject(this);
670 os.close();
671
672 ByteArrayInputStream bin = new ByteArrayInputStream(bs.toByteArray());
673 ObjectInputStream is = new ObjectInputStream(bin);
674 SVGUniverse universe = (SVGUniverse) is.readObject();
675 is.close();
676
677 return universe;
678 }
679
680 /**
681 * @return the imageDataInlineOnly
682 */
683 public boolean isImageDataInlineOnly()
684 {
685 return imageDataInlineOnly;
686 }
687
688 /**
689 * @param imageDataInlineOnly the imageDataInlineOnly to set
690 */
691 public void setImageDataInlineOnly(boolean imageDataInlineOnly)
692 {
693 this.imageDataInlineOnly = imageDataInlineOnly;
694 }
695}
Note: See TracBrowser for help on using the repository browser.