blob: d6eaca0cb6c875f5a24657638f90e3df8a59649e [file] [log] [blame]
/*
* 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.xmlgraphics.image.loader;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import javax.xml.transform.Source;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.cache.ImageCache;
import org.apache.xmlgraphics.image.loader.pipeline.ImageProviderPipeline;
import org.apache.xmlgraphics.image.loader.pipeline.PipelineFactory;
import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry;
import org.apache.xmlgraphics.image.loader.spi.ImagePreloader;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.image.loader.util.Penalty;
import org.apache.xmlgraphics.io.XmlSourceUtil;
/**
* ImageManager is the central starting point for image access.
*/
public class ImageManager {
/** logger */
protected static final Log log = LogFactory.getLog(ImageManager.class);
/** Holds all registered interface implementations for the image package */
private ImageImplRegistry registry;
/** Provides session-independent information */
private ImageContext imageContext;
/** The image cache for this instance */
private ImageCache cache = new ImageCache();
private PipelineFactory pipelineFactory = new PipelineFactory(this);
/**
* Main constructor.
* @param context the session-independent context information
*/
public ImageManager(ImageContext context) {
this(ImageImplRegistry.newInstance(), context);
}
/**
* Constructor for testing purposes.
* @param registry the implementation registry with all plug-ins
* @param context the session-independent context information
*/
public ImageManager(ImageImplRegistry registry, ImageContext context) {
this.registry = registry;
this.imageContext = context;
}
/**
* Returns the ImageImplRegistry in use by the ImageManager.
* @return the ImageImplRegistry
*/
public ImageImplRegistry getRegistry() {
return this.registry;
}
/**
* Returns the ImageContext in use by the ImageManager.
* @return the ImageContext
*/
public ImageContext getImageContext() {
return this.imageContext;
}
/**
* Returns the ImageCache in use by the ImageManager.
* @return the ImageCache
*/
public ImageCache getCache() {
return this.cache;
}
/**
* Returns the PipelineFactory in use by the ImageManager.
* @return the PipelineFactory
*/
public PipelineFactory getPipelineFactory() {
return this.pipelineFactory;
}
/**
* Returns an ImageInfo object containing its intrinsic size for a given URI. The ImageInfo
* is retrieved from an image cache if it has been requested before.
* @param uri the URI of the image
* @param session the session context through which to resolve the URI if the image is not in
* the cache
* @return the ImageInfo object created from the image
* @throws ImageException If no suitable ImagePreloader can be found to load the image or
* if an error occurred while preloading the image.
* @throws IOException If an I/O error occurs while preloading the image
*/
public ImageInfo getImageInfo(String uri, ImageSessionContext session)
throws ImageException, IOException {
if (getCache() != null) {
return getCache().needImageInfo(uri, session, this);
} else {
return preloadImage(uri, session);
}
}
/**
* Preloads an image, i.e. the format of the image is identified and some basic information
* (MIME type, intrinsic size and possibly other values) are loaded and returned as an
* ImageInfo object. Note that the image is not fully loaded normally. Only with certain formats
* the image is already fully loaded and references added to the ImageInfo's custom objects
* (see {@link ImageInfo#getOriginalImage()}).
* <p>
* The reason for the preloading: Apache FOP, for example, only needs the image's intrinsic
* size during layout. Only when the document is rendered to the final format does FOP need
* to load the full image. Like this a lot of memory can be saved.
* @param uri the original URI of the image
* @param session the session context through which to resolve the URI
* @return the ImageInfo object created from the image
* @throws ImageException If no suitable ImagePreloader can be found to load the image or
* if an error occurred while preloading the image.
* @throws IOException If an I/O error occurs while preloading the image
*/
public ImageInfo preloadImage(String uri, ImageSessionContext session)
throws ImageException, IOException {
Source src = session.needSource(uri);
ImageInfo info = preloadImage(uri, src);
session.returnSource(uri, src);
return info;
}
/**
* Preloads an image, i.e. the format of the image is identified and some basic information
* (MIME type, intrinsic size and possibly other values) are loaded and returned as an
* ImageInfo object. Note that the image is not fully loaded normally. Only with certain formats
* the image is already fully loaded and references added to the ImageInfo's custom objects
* (see {@link ImageInfo#getOriginalImage()}).
* <p>
* The reason for the preloading: Apache FOP, for example, only needs the image's intrinsic
* size during layout. Only when the document is rendered to the final format does FOP need
* to load the full image. Like this a lot of memory can be saved.
* @param uri the original URI of the image
* @param src the Source object to load the image from
* @return the ImageInfo object created from the image
* @throws ImageException If no suitable ImagePreloader can be found to load the image or
* if an error occurred while preloading the image.
* @throws IOException If an I/O error occurs while preloading the image
*/
public ImageInfo preloadImage(String uri, Source src)
throws ImageException, IOException {
Iterator iter = registry.getPreloaderIterator();
while (iter.hasNext()) {
ImagePreloader preloader = (ImagePreloader) iter.next();
ImageInfo info = preloader.preloadImage(uri, src, imageContext);
if (info != null) {
return info;
}
}
throw new ImageException("The file format is not supported. No ImagePreloader found for "
+ uri);
}
private Map prepareHints(Map hints, ImageSessionContext sessionContext) {
Map newHints = new java.util.HashMap();
if (hints != null) {
newHints.putAll(hints); //Copy in case an unmodifiable map is passed in
}
if (!newHints.containsKey(ImageProcessingHints.IMAGE_SESSION_CONTEXT)
&& sessionContext != null) {
newHints.put(ImageProcessingHints.IMAGE_SESSION_CONTEXT, sessionContext);
}
if (!newHints.containsKey(ImageProcessingHints.IMAGE_MANAGER)) {
newHints.put(ImageProcessingHints.IMAGE_MANAGER, this);
}
return newHints;
}
/**
* Loads an image. The caller can indicate what kind of image flavor is requested. When this
* method is called the code looks for a suitable ImageLoader and, if necessary, builds
* a conversion pipeline so it can return the image in exactly the form the caller needs.
* <p>
* Optionally, it is possible to pass in Map of hints. These hints may be used by ImageLoaders
* and ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints
* used by the bundled implementations. You can, of course, define your own hints.
* @param info the ImageInfo instance for the image (obtained by
* {@link #getImageInfo(String, ImageSessionContext)})
* @param flavor the requested image flavor.
* @param hints a Map of hints to any of the background components or null
* @param session the session context
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image getImage(ImageInfo info, ImageFlavor flavor, Map hints,
ImageSessionContext session)
throws ImageException, IOException {
hints = prepareHints(hints, session);
Image img = null;
ImageProviderPipeline pipeline = getPipelineFactory().newImageConverterPipeline(
info, flavor);
if (pipeline != null) {
img = pipeline.execute(info, hints, session);
}
if (img == null) {
throw new ImageException(
"Cannot load image (no suitable loader/converter combination available) for "
+ info);
}
XmlSourceUtil.closeQuietly(session.getSource(info.getOriginalURI()));
return img;
}
/**
* Loads an image. The caller can indicate what kind of image flavors are requested. When this
* method is called the code looks for a suitable ImageLoader and, if necessary, builds
* a conversion pipeline so it can return the image in exactly the form the caller needs.
* The array of image flavors is ordered, so the first image flavor is given highest priority.
* <p>
* Optionally, it is possible to pass in Map of hints. These hints may be used by ImageLoaders
* and ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints
* used by the bundled implementations. You can, of course, define your own hints.
* @param info the ImageInfo instance for the image (obtained by
* {@link #getImageInfo(String, ImageSessionContext)})
* @param flavors the requested image flavors (in preferred order).
* @param hints a Map of hints to any of the background components or null
* @param session the session context
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image getImage(ImageInfo info, ImageFlavor[] flavors, Map hints,
ImageSessionContext session)
throws ImageException, IOException {
hints = prepareHints(hints, session);
Image img = null;
ImageProviderPipeline[] candidates = getPipelineFactory().determineCandidatePipelines(
info, flavors);
ImageProviderPipeline pipeline = choosePipeline(candidates);
if (pipeline != null) {
img = pipeline.execute(info, hints, session);
}
if (img == null) {
throw new ImageException(
"Cannot load image (no suitable loader/converter combination available) for "
+ info);
}
XmlSourceUtil.closeQuietly(session.getSource(info.getOriginalURI()));
return img;
}
/**
* Loads an image with no hints. See
* {@link #getImage(ImageInfo, ImageFlavor, Map, ImageSessionContext)} for more
* information.
* @param info the ImageInfo instance for the image (obtained by
* {@link #getImageInfo(String, ImageSessionContext)})
* @param flavor the requested image flavor.
* @param session the session context
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image getImage(ImageInfo info, ImageFlavor flavor, ImageSessionContext session)
throws ImageException, IOException {
return getImage(info, flavor, ImageUtil.getDefaultHints(session), session);
}
/**
* Loads an image with no hints. See
* {@link #getImage(ImageInfo, ImageFlavor[], Map, ImageSessionContext)} for more
* information.
* @param info the ImageInfo instance for the image (obtained by
* {@link #getImageInfo(String, ImageSessionContext)})
* @param flavors the requested image flavors (in preferred order).
* @param session the session context
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image getImage(ImageInfo info, ImageFlavor[] flavors, ImageSessionContext session)
throws ImageException, IOException {
return getImage(info, flavors, ImageUtil.getDefaultHints(session), session);
}
/**
* Closes the resources associated to the given image. This method should be
* used only when none of the {@code getImage} methods is called by the
* client application.
*
* @param uri the URI of the image
* @param session the session context that was used to resolve the URI
*/
public void closeImage(String uri, ImageSessionContext session) {
XmlSourceUtil.closeQuietly(session.getSource(uri));
}
/**
* Converts an image. The caller can indicate what kind of image flavors are requested. When
* this method is called the code looks for a suitable combination of ImageConverters so it
* can return the image in exactly the form the caller needs.
* The array of image flavors is ordered, so the first image flavor is given highest priority.
* <p>
* Optionally, it is possible to pass in Map of hints. These hints may be used by
* ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints
* used by the bundled implementations. You can, of course, define your own hints.
* @param image the image to convert
* @param flavors the requested image flavors (in preferred order).
* @param hints a Map of hints to any of the background components or null
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image convertImage(Image image, ImageFlavor[] flavors, Map hints)
throws ImageException, IOException {
hints = prepareHints(hints, null);
ImageInfo info = image.getInfo();
Image img = null;
for (ImageFlavor flavor : flavors) {
if (image.getFlavor().equals(flavor)) {
//Shortcut (the image is already in one of the requested formats)
return image;
}
}
ImageProviderPipeline[] candidates = getPipelineFactory().determineCandidatePipelines(
image, flavors);
ImageProviderPipeline pipeline = choosePipeline(candidates);
if (pipeline != null) {
img = pipeline.execute(info, image, hints, null);
}
if (img == null) {
throw new ImageException(
"Cannot convert image " + image
+ " (no suitable converter combination available)");
}
return img;
}
/**
* Converts an image with no hints. See
* {@link #convertImage(Image, ImageFlavor[], Map)} for more
* information.
* @param image the image to convert
* @param flavors the requested image flavors (in preferred order).
* @return the fully loaded image
* @throws ImageException If no suitable loader/converter combination is available to fulfill
* the request or if an error occurred while loading the image.
* @throws IOException If an I/O error occurs
*/
public Image convertImage(Image image, ImageFlavor[] flavors)
throws ImageException, IOException {
return convertImage(image, flavors, null);
}
/**
* Chooses the best {@link ImageProviderPipeline} from a set of candidates.
* @param candidates the candidates
* @return the best pipeline
*/
public ImageProviderPipeline choosePipeline(ImageProviderPipeline[] candidates) {
ImageProviderPipeline pipeline = null;
int minPenalty = Integer.MAX_VALUE;
int count = candidates.length;
if (log.isTraceEnabled()) {
log.trace("Candidate Pipelines:");
for (int i = 0; i < count; i++) {
if (candidates[i] == null) {
continue;
}
log.trace(" " + i + ": "
+ candidates[i].getConversionPenalty(getRegistry()) + " for " + candidates[i]);
}
}
for (int i = count - 1; i >= 0; i--) {
if (candidates[i] == null) {
continue;
}
Penalty penalty = candidates[i].getConversionPenalty(getRegistry());
if (penalty.isInfinitePenalty()) {
continue; //Exclude candidate on infinite penalty
}
if (penalty.getValue() <= minPenalty) {
pipeline = candidates[i];
minPenalty = penalty.getValue();
}
}
if (log.isDebugEnabled()) {
log.debug("Chosen pipeline: " + pipeline);
}
return pipeline;
}
}