/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.apache.batik.svggen;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.HashMap;
import java.util.zip.Adler32;
import java.util.zip.Checksum;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
This class implements caching functionality for raster images.
Author: Paul Evenblij Version: $Id: ImageCacher.java 1802297 2017-07-18 13:58:12Z ssteiner $
/**
* This class implements caching functionality for raster images.
*
* @author <a href="mailto:paul_evenblij@compuware.com">Paul Evenblij</a>
* @version $Id: ImageCacher.java 1802297 2017-07-18 13:58:12Z ssteiner $
*/
public abstract class ImageCacher implements SVGSyntax, ErrorConstants {
DOMTreeManager domTreeManager = null;
Map imageCache;
Checksum checkSum;
Creates an ImageCacher.
/**
* Creates an ImageCacher.
*/
public ImageCacher() {
imageCache = new HashMap();
checkSum = new Adler32();
}
Creates an ImageCacher.
Params: - domTreeManager – the DOMTreeManager for the tree this cacher works on
/**
* Creates an ImageCacher.
*
* @param domTreeManager the DOMTreeManager for the tree this cacher works on
*/
public ImageCacher(DOMTreeManager domTreeManager) {
this();
setDOMTreeManager(domTreeManager);
}
Sets the DOMTreeManager this cacher should work on.
Params: - domTreeManager – the DOMTreeManager for the tree this cacher works on
/**
* Sets the DOMTreeManager this cacher should work on.
*
* @param domTreeManager the DOMTreeManager for the tree this cacher works on
*/
public void setDOMTreeManager(DOMTreeManager domTreeManager) {
if (domTreeManager == null){
throw new IllegalArgumentException();
}
this.domTreeManager = domTreeManager;
}
public DOMTreeManager getDOMTreeManager(){
return domTreeManager;
}
Checks if the image is already in the cache, and
adds it if not. Returns a unique id for the entry.
Params: - os – the image as a byte stream
- width – the width of the image
- height – the height of the image
- ctx – the SVGGeneratorContext
Throws: - SVGGraphics2DIOException – if an error occurs during image file i/o
Returns: a URI for the image
/**
* Checks if the image is already in the cache, and
* adds it if not. Returns a unique id for the entry.
*
* @param os the image as a byte stream
* @param width the width of the image
* @param height the height of the image
* @param ctx the SVGGeneratorContext
*
* @return a URI for the image
* @throws SVGGraphics2DIOException if an error occurs during image file i/o
*/
public String lookup(ByteArrayOutputStream os,
int width, int height,
SVGGeneratorContext ctx)
throws SVGGraphics2DIOException {
// We determine a checksum value for the byte data, and use it
// as hash key for the image. This may not be unique, so we
// need to check on actual byte-for-byte equality as well.
// The checksum will be sufficient in most cases.
int checksum = getChecksum(os.toByteArray());
Integer key = checksum;
String href = null;
Object data = getCacheableData(os);
LinkedList list = (LinkedList) imageCache.get(key);
if(list == null) {
// Key not found: make a new key/value pair
list = new LinkedList();
imageCache.put(key, list);
} else {
// Key found: check if the image is already in the list
for(ListIterator i = list.listIterator(0); i.hasNext(); ) {
ImageCacheEntry entry = (ImageCacheEntry) i.next();
if(entry.checksum == checksum && imagesMatch(entry.src, data)) {
href = entry.href;
break;
}
}
}
if(href == null) {
// Still no hit: add our own
ImageCacheEntry newEntry = createEntry(checksum, data,
width, height,
ctx);
list.add(newEntry);
href = newEntry.href;
}
return href;
}
Returns an object which can be cached.
Implementation must determine which information
should actually be stored.
Params: - os – the byte stream which is to be coerced
/**
* Returns an object which can be cached.
* Implementation must determine which information
* should actually be stored.
*
* @param os the byte stream which is to be coerced
*/
abstract Object getCacheableData(ByteArrayOutputStream os);
Determines if two images are equal.
Interpretation of the objects referred to by
o1
and o2
is entirely
implementation-dependent.
Params: - o1 – object referring to one image
- o2 – object referring to the other image
/**
* Determines if two images are equal.
* Interpretation of the objects referred to by
* <code>o1</code> and <code>o2</code> is entirely
* implementation-dependent.
*
* @param o1 object referring to one image
* @param o2 object referring to the other image
*/
abstract boolean imagesMatch(Object o1, Object o2)
throws SVGGraphics2DIOException;
Creates a new entry for keeping in the cache.
Params: - checksum – the checksum from which the hash key is derived
- data – the data to be cached
- width – image width
- height – image height
- ctx – the SVGGeneratorContext
/**
* Creates a new entry for keeping in the cache.
*
* @param checksum the checksum from which the hash key is derived
* @param data the data to be cached
* @param width image width
* @param height image height
* @param ctx the SVGGeneratorContext
*/
abstract ImageCacheEntry createEntry(int checksum,
Object data,
int width, int height,
SVGGeneratorContext ctx)
throws SVGGraphics2DIOException;
Calculates a checksum value for the given data.
/**
* Calculates a checksum value for the given data.
*/
int getChecksum(byte[] data) {
checkSum.reset();
checkSum.update(data, 0, data.length);
return (int) checkSum.getValue();
}
Instances of this class are created to keep track of the
set of images processed by the ImageHandler. Each entry
corresponds to one unique member of this set.
/**
* Instances of this class are created to keep track of the
* set of images processed by the ImageHandler. Each entry
* corresponds to one unique member of this set.
*/
private static class ImageCacheEntry {
A checksum calculated for the data cached /** A checksum calculated for the data cached */
public int checksum;
An implementation-dependent object referring to the data /** An implementation-dependent object referring to the data */
public Object src;
A uri identifying the data /** A uri identifying the data */
public String href;
Creates a new entry
/**
* Creates a new entry
*/
ImageCacheEntry(int checksum,
Object src,
String href) {
this.checksum = checksum;
this.src = src;
this.href = href;
}
}
Cache implementation for images embedded in the SVG file.
/**
* Cache implementation for images embedded in the SVG file.
*/
public static class Embedded extends ImageCacher {
Sets the DOMTreeManager this cacher should work on.
Params: - domTreeManager – the DOMTreeManager for the tree this cacher works on
/**
* Sets the DOMTreeManager this cacher should work on.
*
* @param domTreeManager the DOMTreeManager for the tree this cacher works on
*/
public void setDOMTreeManager(DOMTreeManager domTreeManager) {
// A new DOMTreeManager implies a new cache, because we cache
// images in the SVG tree itself
if(this.domTreeManager != domTreeManager) {
this.domTreeManager = domTreeManager;
this.imageCache = new HashMap();
}
}
Object getCacheableData(ByteArrayOutputStream os) {
// In order to have only one instance of the image data
// in memory, we cache the entire xlink:href attribute value,
// so we can just pass a reference to the tree manager.
return DATA_PROTOCOL_PNG_PREFIX + os.toString();
}
boolean imagesMatch(Object o1, Object o2) {
return o1.equals(o2);
}
ImageCacheEntry createEntry(int checksum, Object data,
int width, int height,
SVGGeneratorContext ctx) {
// Get a new unique id
String id = ctx.idGenerator.generateID(ID_PREFIX_IMAGE);
// Add the image data reference to the <defs> section
addToTree(id, (String) data, width, height, ctx);
// Create new cache entry
return new ImageCacheEntry(checksum, data, SIGN_POUND + id); // <<<<<<<<<< error ??
}
Adds a new image element to the defs section for cached images.
/**
* Adds a new image element to the defs section for cached images.
*/
private void addToTree(String id,
String href,
int width, int height,
SVGGeneratorContext ctx) {
Document domFactory = domTreeManager.getDOMFactory();
// Element imageDefs = getImageDefs(domFactory, ctx);
// Create and initialize the new image element
Element imageElement = domFactory.createElementNS(SVG_NAMESPACE_URI,
SVG_IMAGE_TAG);
imageElement.setAttributeNS(null, SVG_ID_ATTRIBUTE,
id);
imageElement.setAttributeNS(null, SVG_WIDTH_ATTRIBUTE,
Integer.toString(width));
imageElement.setAttributeNS(null, SVG_HEIGHT_ATTRIBUTE,
Integer.toString(height));
imageElement.setAttributeNS(DefaultImageHandler.XLINK_NAMESPACE_URI,
XLINK_HREF_QNAME,
href);
// imageDefs.appendChild(imageElement);
domTreeManager.addOtherDef(imageElement);
}
/**
* Returns the top level defs section dedicated to cached
* embedded images, creating one if necessary.
* This one very simply creates a new defs section for each image,
* causing them to be spread throughout the entire SVG tree.
* A nicer implementation would group all imageDefs sections into
* one.
*/
/*private Element getImageDefs(Document domFactory,
SVGGeneratorContext ctx) {
Element imageDefs = domFactory.createElementNS(SVG_NAMESPACE_URI,
SVG_DEFS_TAG);
String id = ctx.idGenerator.generateID(ID_PREFIX_IMAGE_DEFS);
imageDefs.setAttributeNS(null, SVG_ID_ATTRIBUTE, id);
domTreeManager.appendGroup(imageDefs, null);
return imageDefs;
}*/
}
Cache implementation for file-based images.
/**
* Cache implementation for file-based images.
*/
public static class External extends ImageCacher {
private String imageDir;
private String prefix;
private String suffix;
public External(String imageDir, String prefix, String suffix) {
super();
this.imageDir = imageDir;
this.prefix = prefix;
this.suffix = suffix;
}
Object getCacheableData(ByteArrayOutputStream os) {
return os;
}
boolean imagesMatch(Object o1, Object o2)
throws SVGGraphics2DIOException {
boolean match = false;
FileInputStream imageStream = null;
try {
imageStream = new FileInputStream((File) o1);
int imageLen = imageStream.available();
byte[] imageBytes = new byte[imageLen];
byte[] candidateBytes =
((ByteArrayOutputStream) o2).toByteArray();
int bytesRead = 0;
while (bytesRead != imageLen) {
bytesRead += imageStream.read
(imageBytes, bytesRead, imageLen-bytesRead);
}
match = Arrays.equals(imageBytes, candidateBytes);
} catch(IOException e) {
throw new SVGGraphics2DIOException(
ERR_READ+((File) o1).getName());
} finally {
try {
if (imageStream != null)
imageStream.close();
} catch (IOException e) {
}
}
return match;
}
ImageCacheEntry createEntry(int checksum, Object data,
int width, int height,
SVGGeneratorContext ctx)
throws SVGGraphics2DIOException {
// Create a new file in image directory
File imageFile = null;
try {
// While the files we are generating exist, try to create
// another unique id.
while (imageFile == null) {
String fileId = ctx.idGenerator.generateID(prefix);
imageFile = new File(imageDir, fileId + suffix);
if (imageFile.exists())
imageFile = null;
}
// Write data to file
OutputStream outputStream = new FileOutputStream(imageFile);
((ByteArrayOutputStream) data).writeTo(outputStream);
((ByteArrayOutputStream) data).close();
} catch(IOException e) {
throw new SVGGraphics2DIOException(ERR_WRITE+imageFile.getName());
}
// Create new cache entry
return new ImageCacheEntry(checksum, imageFile, imageFile.getName()); // <<<<<<<<<< error ??
}
}
}