blob: 7d7a92120849adbdb6167daf562f48c48f3d5456 [file]
/*
* 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.
*/
/* $Id$ */
package org.apache.fop.image;
// Java
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// FOP
import org.apache.fop.image.analyser.ImageReaderFactory;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.datatypes.URISpecification;
/**
* Create FopImage objects (with a configuration file - not yet implemented).
* @author Eric SCHAEFFER
*/
public final class ImageFactory {
/**
* logging instance
*/
protected static Log log = LogFactory.getLog(FopImage.class);
private HashMap imageMimeTypes = new HashMap();
private ImageCache cache = new ContextImageCache(true);
/**
* Main constructor for the ImageFactory.
*/
public ImageFactory() {
/* @todo The mappings set up below of image mime types to implementing
* classes should be made externally configurable
*/
ImageProvider jaiImage = new ImageProvider("JAIImage", "org.apache.fop.image.JAIImage");
ImageProvider jimiImage = new ImageProvider("JIMIImage", "org.apache.fop.image.JimiImage");
ImageProvider imageIoImage = new ImageProvider(
"ImageIOImage", "org.apache.fop.image.ImageIOImage");
ImageProvider gifImage = new ImageProvider("GIFImage", "org.apache.fop.image.GifImage");
ImageProvider jpegImage = new ImageProvider("JPEGImage", "org.apache.fop.image.JpegImage");
ImageProvider jpegImageIOImage = new ImageProvider(
"JPEGImage", "org.apache.fop.image.JpegImageIOImage");
ImageProvider bmpImage = new ImageProvider("BMPImage", "org.apache.fop.image.BmpImage");
ImageProvider epsImage = new ImageProvider("EPSImage", "org.apache.fop.image.EPSImage");
ImageProvider pngImage = new ImageProvider("PNGImage", "org.apache.fop.image.PNGImage");
ImageProvider tiffImage = new ImageProvider("TIFFImage", "org.apache.fop.image.TIFFImage");
ImageProvider xmlImage = new ImageProvider("XMLImage", "org.apache.fop.image.XMLImage");
ImageProvider emfImage = new ImageProvider("EMFImage", "org.apache.fop.image.EmfImage");
ImageMimeType imt = new ImageMimeType("image/gif");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(imageIoImage);
imt.addProvider(jaiImage);
imt.addProvider(jimiImage);
imt.addProvider(gifImage);
imt = new ImageMimeType("image/jpeg");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(jpegImageIOImage);
imt.addProvider(jpegImage);
imt = new ImageMimeType("image/bmp");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(bmpImage);
imt = new ImageMimeType("image/eps");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(epsImage);
imt = new ImageMimeType("image/png");
imageMimeTypes.put(imt.getMimeType(), imt);
//Image I/O is faster and more memory-efficient than own codec for PNG
imt.addProvider(imageIoImage);
imt.addProvider(pngImage);
imt = new ImageMimeType("image/tga");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(jaiImage);
imt.addProvider(imageIoImage);
imt.addProvider(jimiImage);
imt = new ImageMimeType("image/tiff");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(tiffImage); //Slower but supports CCITT embedding
imt.addProvider(imageIoImage); //Fast but doesn't support CCITT embedding
imt.addProvider(jaiImage); //Fast but doesn't support CCITT embedding
imt = new ImageMimeType("image/svg+xml");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(xmlImage);
imt = new ImageMimeType("text/xml");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(xmlImage);
imt = new ImageMimeType("image/emf");
imageMimeTypes.put(imt.getMimeType(), imt);
imt.addProvider(emfImage);
}
/**
* Get the url string from a wrapped url.
*
* @param href the input wrapped url
* @return the raw url
*/
public static String getURL(String href) {
return URISpecification.getURL(href);
}
/**
* Get the image from the cache or load.
* If this returns null then the image could not be loaded
* due to an error. Messages should be logged.
* Before calling this the getURL(url) must be used.
*
* @param url the url for the image
* @param context the user agent context
* @return the fop image instance
*/
public FopImage getImage(String url, FOUserAgent context) {
return cache.getImage(url, context);
}
/**
* Release an image from the cache.
* This can be used if the renderer has its own cache of
* the image.
* The image should then be put into the weak cache.
*
* @param url the url for the image
* @param context the user agent context
*/
public void releaseImage(String url, FOUserAgent context) {
cache.releaseImage(url, context);
}
/**
* Release the context and all images in the context.
*
* @param context the context to remove
*/
public void removeContext(FOUserAgent context) {
cache.removeContext(context);
}
/**
* Create an FopImage objects.
* @param href the url for the image
* @param ua the user agent context
* @return the fop image instance
*/
public FopImage loadImage(String href, FOUserAgent ua) {
Source source = ua.resolveURI(href);
if (source == null) {
return null;
}
// Got a valid source, obtain an InputStream from it
InputStream in = null;
if (source instanceof StreamSource) {
in = ((StreamSource)source).getInputStream();
}
if (in == null) {
try {
in = new java.net.URL(source.getSystemId()).openStream();
} catch (Exception ex) {
log.error("Unable to obtain stream from id '"
+ source.getSystemId() + "'");
}
}
if (in == null) {
return null;
}
//Make sure the InputStream is decorated with a BufferedInputStream
if (!(in instanceof java.io.BufferedInputStream)) {
in = new java.io.BufferedInputStream(in);
}
// Check image type
FopImage.ImageInfo imgInfo = null;
try {
imgInfo = ImageReaderFactory.make(source.getSystemId(), in, ua);
} catch (Exception e) {
log.error("Error while recovering image information ("
+ href + ") : " + e.getMessage(), e);
return null;
}
if (imgInfo == null) {
try {
in.close();
in = null;
} catch (Exception e) {
log.debug("Error closing the InputStream for the image", e);
}
log.error("No ImageReader for this type of image (" + href + ")");
return null;
}
// Associate mime-type to FopImage class
String imgMimeType = imgInfo.mimeType;
Class imageClass = getImageClass(imgMimeType);
if (imageClass == null) {
log.error("Unsupported image type (" + href + "): " + imgMimeType);
return null;
} else {
if (log.isDebugEnabled()) {
log.debug("Loading " + imgMimeType + " with " + imageClass.getName()
+ ": " + href);
}
}
// load the right image class
// return new <FopImage implementing class>
Object imageInstance = null;
try {
Class[] imageConstructorParameters = new Class[1];
imageConstructorParameters[0] = org.apache.fop.image.FopImage.ImageInfo.class;
Constructor imageConstructor = imageClass.getDeclaredConstructor(
imageConstructorParameters);
Object[] initArgs = new Object[1];
initArgs[0] = imgInfo;
imageInstance = imageConstructor.newInstance(initArgs);
} catch (java.lang.reflect.InvocationTargetException ex) {
Throwable t = ex.getTargetException();
String msg;
if (t != null) {
msg = t.getMessage();
} else {
msg = ex.getMessage();
}
log.error("Error creating FopImage object ("
+ href + "): " + msg, (t == null) ? ex : t);
return null;
} catch (InstantiationException ie) {
log.error("Error creating FopImage object ("
+ href + "): Could not instantiate " + imageClass.getName() + " instance");
return null;
} catch (Exception ex) {
log.error("Error creating FopImage object ("
+ href + "): " + ex.getMessage(), ex);
return null;
}
if (!(imageInstance instanceof org.apache.fop.image.FopImage)) {
log.error("Error creating FopImage object (" + href + "): " + "class "
+ imageClass.getName()
+ " doesn't implement org.apache.fop.image.FopImage interface");
return null;
}
return (FopImage) imageInstance;
}
private Class getImageClass(String imgMimeType) {
ImageMimeType imt = (ImageMimeType)imageMimeTypes.get(imgMimeType);
if (imt == null) {
return null;
}
return imt.getFirstImplementingClass();
}
/**
* Forces all the image caches to be cleared. This should normally only be used in
* testing environments. If you happen to think that you need to call this yourself
* in a production environment, please notify the development team so we can look
* into the issue. A call like this shouldn't be necessary anymore like it may have
* been with FOP 0.20.5.
*/
public void clearCaches() {
cache.clearAll();
}
}
/**
* Basic image cache.
* This keeps track of invalid images.
*/
class BasicImageCache implements ImageCache {
private Set invalid = Collections.synchronizedSet(new java.util.HashSet());
//private Map contextStore = Collections.synchronizedMap(new java.util.HashMap());
public FopImage getImage(String url, FOUserAgent context) {
if (invalid.contains(url)) {
return null;
}
//TODO Doesn't seem to be fully implemented. Do we need it at all? Not referenced.
return null;
}
public void releaseImage(String url, FOUserAgent context) {
// do nothing
}
public void invalidateImage(String url, FOUserAgent context) {
// cap size of invalid list
if (invalid.size() > 100) {
invalid.clear();
}
invalid.add(url);
}
public void removeContext(FOUserAgent context) {
// do nothing
}
/** @see org.apache.fop.image.ImageCache#clearAll() */
public void clearAll() {
invalid.clear();
}
}
/**
* This is the context image cache.
* This caches images on the basis of the given context.
* Common images in different contexts are currently not handled.
* There are two possiblities, each context handles its own images
* and renderers can cache information or images are shared and
* all information is retained.
* Once a context is removed then all images are placed into a
* weak hashmap so they may be garbage collected.
*/
class ContextImageCache implements ImageCache {
// if this cache is collective then images can be shared
// among contexts, this implies that the base directory
// is either the same or does not effect the images being
// loaded
private boolean collective;
private Map contextStore = Collections.synchronizedMap(new java.util.HashMap());
private Set invalid = null;
private Map refStore = null;
private ReferenceQueue refQueue = new ReferenceQueue();
public ContextImageCache(boolean col) {
collective = col;
if (collective) {
refStore = Collections.synchronizedMap(new java.util.HashMap());
invalid = Collections.synchronizedSet(new java.util.HashSet());
}
}
// sync around lookups and puts
// another sync around load for a particular image
public FopImage getImage(String url, FOUserAgent context) {
ImageLoader im = null;
// this protects the finding or creating of a new
// ImageLoader for multi threads
synchronized (this) {
if (collective && invalid.contains(url)) {
return null;
}
Context con = (Context) contextStore.get(context);
if (con == null) {
con = new Context(context, collective);
contextStore.put(context, con);
} else {
if (con.invalid(url)) {
return null;
}
im = con.getImage(url);
}
if (im == null && collective) {
Iterator i = contextStore.values().iterator();
while (i.hasNext()) {
Context c = (Context)i.next();
if (c != con) {
im = c.getImage(url);
if (im != null) {
break;
}
}
}
if (im == null) {
Reference ref = (Reference)refStore.get(url);
if (ref != null) {
im = (ImageLoader) ref.get();
if (im == null) {
//Remove key if its value has been garbage collected
refStore.remove(url);
}
}
}
}
if (im != null) {
con.putImage(url, im);
} else {
im = con.getImage(url, this);
}
}
// the ImageLoader is synchronized so images with the
// same url will not be loaded at the same time
if (im != null) {
return im.loadImage();
}
return null;
}
public void releaseImage(String url, FOUserAgent context) {
Context con = (Context) contextStore.get(context);
if (con != null) {
if (collective) {
ImageLoader im = con.getImage(url);
refStore.put(url, wrapInReference(im, url));
}
con.releaseImage(url);
}
}
public void invalidateImage(String url, FOUserAgent context) {
if (collective) {
// cap size of invalid list
if (invalid.size() > 100) {
invalid.clear();
}
invalid.add(url);
}
Context con = (Context) contextStore.get(context);
if (con != null) {
con.invalidateImage(url);
}
}
private Reference wrapInReference(Object obj, Object key) {
return new SoftReferenceWithKey(obj, key, refQueue);
}
private static class SoftReferenceWithKey extends SoftReference {
private Object key;
public SoftReferenceWithKey(Object referent, Object key, ReferenceQueue q) {
super(referent, q);
this.key = key;
}
}
public void removeContext(FOUserAgent context) {
Context con = (Context) contextStore.get(context);
if (con != null) {
if (collective) {
Map images = con.getImages();
Iterator iter = images.entrySet().iterator();
while (iter.hasNext()) {
Entry entry = (Entry)iter.next();
refStore.put(entry.getKey(),
wrapInReference(entry.getValue(), entry.getKey()));
}
}
contextStore.remove(context);
}
//House-keeping (remove cleared references)
checkReferenceQueue();
}
/**
* Checks the reference queue if any references have been cleared and removes them from the
* cache.
*/
private void checkReferenceQueue() {
SoftReferenceWithKey ref;
while ((ref = (SoftReferenceWithKey)refQueue.poll()) != null) {
refStore.remove(ref.key);
}
}
class Context {
private Map images = Collections.synchronizedMap(new java.util.HashMap());
private Set invalid = null;
private FOUserAgent userAgent;
public Context(FOUserAgent ua, boolean inv) {
userAgent = ua;
if (inv) {
invalid = Collections.synchronizedSet(new java.util.HashSet());
}
}
public ImageLoader getImage(String url, ImageCache c) {
if (images.containsKey(url)) {
return (ImageLoader) images.get(url);
}
ImageLoader loader = new ImageLoader(url, c, userAgent);
images.put(url, loader);
return loader;
}
public void putImage(String url, ImageLoader image) {
images.put(url, image);
}
public ImageLoader getImage(String url) {
return (ImageLoader) images.get(url);
}
public void releaseImage(String url) {
images.remove(url);
}
public Map getImages() {
return images;
}
public void invalidateImage(String url) {
invalid.add(url);
}
public boolean invalid(String url) {
return invalid.contains(url);
}
}
/** @see org.apache.fop.image.ImageCache#clearAll() */
public void clearAll() {
this.refStore.clear();
this.invalid.clear();
//The context-sensitive caches are not cleared so there are no negative side-effects
//in a multi-threaded environment. Not that it's a good idea to use this method at
//all except in testing environments. If such a calls is necessary in normal environments
//we need to check on memory leaks!
}
}
/**
* Encapsulates a class of type FopImage by holding its class name.
* This allows dynamic loading of the class at runtime.
*/
class ImageProvider {
private String name = null;
private String className = null;
private boolean checked = false;
private Class clazz = null;
/**
* Creates an ImageProvider with a given name and implementing class.
* The class name should refer to a class of type {@link FopImage}.
* However, this is not checked on construction.
* @param name The name of the provider
* @param className The full class name of the class implementing this provider
*/
public ImageProvider(String name, String className) {
setName(name);
setClassName(className);
}
/**
* Returns the provider name.
* @return The provider name
*/
public String getName() {
return name;
}
private void setName(String name) {
this.name = name;
}
/**
* Returns the implementing class name.
* @return The implementing class name
*/
public String getClassName() {
return className;
}
private void setClassName(String className) {
this.className = className;
}
/**
* Returns the implementing class as a {@link Class} object.
* @return The implementing class or null if it couldn't be loaded.
*/
public Class getImplementingClass() {
if (!checked) {
try {
clazz = Class.forName(getClassName());
} catch (ClassNotFoundException cnfe) {
//nop
} catch (LinkageError le) {
// This can happen if fop was build with support for a
// particular provider (e.g. a binary fop distribution)
// but the required support files (e.g. jai, jimi) are not
// available in the current runtime environment.
ImageFactory.log.debug("Image support provider " + getName()
+ " could not be loaded. If " + getName() + " should be"
+ " available please make sure all required external libraries"
+ " are on the classpath.");
}
checked = true;
}
return clazz;
}
}
/**
* Holds a mime type for a particular image format plus a list of
* {@link ImageProvider} objects which support the particular image format.
*/
class ImageMimeType {
private String mimeType = null;
private List providers = null;
/**
* Constructor for a particular mime type.
* @param mimeType The mime type
*/
public ImageMimeType(String mimeType) {
setMimeType(mimeType);
}
/**
* Returns the mime type.
* @return The mime type
*/
public String getMimeType() {
return mimeType;
}
private void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
/**
* Returns the class from the first available provider.
* @return The first available class or null if none can be found
*/
public Class getFirstImplementingClass() {
if (providers == null) {
return null;
}
for (Iterator it = providers.iterator(); it.hasNext();) {
ImageProvider ip = (ImageProvider)it.next();
Class clazz = ip.getImplementingClass();
if (clazz != null) {
return clazz;
}
}
return null;
}
/**
* Adds a new provider.
* The provider is added to the end of the current provider list.
* @param The new provider to add
*/
public void addProvider(ImageProvider provider) {
if (providers == null) {
providers = new ArrayList(4); // Assume we only have a few providers
}
if (!providers.contains(provider)) {
providers.add(provider);
}
}
}