[VIVO-1236] Add TwelveMonkeys ImageIO library for full OpenJDK image support
This commit is contained in:
parent
721a7fb892
commit
437bb9f515
4 changed files with 233 additions and 7 deletions
|
@ -0,0 +1,208 @@
|
||||||
|
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||||
|
|
||||||
|
package edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio;
|
||||||
|
|
||||||
|
import com.sun.media.jai.codec.MemoryCacheSeekableStream;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.modules.Application;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.modules.ComponentStartupStatus;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.media.jai.JAI;
|
||||||
|
import javax.media.jai.RenderedOp;
|
||||||
|
import javax.media.jai.operator.BandSelectDescriptor;
|
||||||
|
import javax.media.jai.operator.StreamDescriptor;
|
||||||
|
import javax.media.jai.util.ImagingListener;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.AffineTransformOp;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.ColorConvertOp;
|
||||||
|
import java.awt.image.ColorModel;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crop the main image as specified, and scale it to the correct size for a
|
||||||
|
* thumbnail.
|
||||||
|
*
|
||||||
|
* Use the JAI library to read the file because the javax.imageio package
|
||||||
|
* doesn't read extended JPEG properly. Use JAI to remove transparency from
|
||||||
|
* JPEGs and PNGs, simply by removing the alpha channel. Annoyingly, this will
|
||||||
|
* not work with GIFs with transparent pixels.
|
||||||
|
*
|
||||||
|
* The transforms in the JAI library are buggy, so standard AWT operations do
|
||||||
|
* the scaling and cropping. The most obvious problem in the JAI library is the
|
||||||
|
* refusal to crop after scaling an image.
|
||||||
|
*
|
||||||
|
* Scale first to avoid the boundary error that produces black lines along the
|
||||||
|
* edge of the image.
|
||||||
|
*
|
||||||
|
* Use the javax.imagio pacakge to write the thumbnail image as a JPEG file.
|
||||||
|
*/
|
||||||
|
public class IIOImageProcessor implements ImageProcessor {
|
||||||
|
private static final Log log = LogFactory.getLog(IIOImageProcessor.class);
|
||||||
|
|
||||||
|
/** If an image has 3 color bands and 1 alpha band, we want these. */
|
||||||
|
private static final int[] COLOR_BAND_INDEXES = new int[] { 0, 1, 2 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent Java Advanced Imaging from complaining about the lack of
|
||||||
|
* accelerator classes.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void startup(Application application, ComponentStartupStatus ss) {
|
||||||
|
JAI.getDefaultInstance().setImagingListener(
|
||||||
|
new NonNoisyImagingListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown(Application application) {
|
||||||
|
// Nothing to tear down.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimensions getDimensions(InputStream imageStream) throws ImageProcessorException, IOException {
|
||||||
|
MemoryCacheSeekableStream stream = new MemoryCacheSeekableStream(imageStream);
|
||||||
|
BufferedImage image = ImageIO.read(stream);
|
||||||
|
return new Dimensions(image.getWidth(), image.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crop the main image according to this rectangle, and scale it to the
|
||||||
|
* correct size for a thumbnail.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public InputStream cropAndScale(InputStream mainImageStream,
|
||||||
|
CropRectangle crop, Dimensions limits)
|
||||||
|
throws ImageProcessorException, IOException {
|
||||||
|
try {
|
||||||
|
MemoryCacheSeekableStream stream = new MemoryCacheSeekableStream(mainImageStream);
|
||||||
|
BufferedImage mainImage = ImageIO.read(stream);
|
||||||
|
|
||||||
|
BufferedImage bufferedImage = new BufferedImage(mainImage.getWidth(), mainImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR); // BufferedImage.TYPE_INT_RGB
|
||||||
|
new ColorConvertOp(null).filter(mainImage, bufferedImage);
|
||||||
|
|
||||||
|
log.debug("initial image: " + imageSize(bufferedImage));
|
||||||
|
|
||||||
|
log.debug("initial crop: " + crop);
|
||||||
|
CropRectangle boundedCrop = limitCropRectangleToImageBounds(
|
||||||
|
bufferedImage, crop);
|
||||||
|
log.debug("bounded crop: " + boundedCrop);
|
||||||
|
|
||||||
|
float scaleFactor = figureScaleFactor(boundedCrop, limits);
|
||||||
|
log.debug("scale factor: " + scaleFactor);
|
||||||
|
|
||||||
|
BufferedImage scaledImage = scaleImage(bufferedImage, scaleFactor);
|
||||||
|
log.debug("scaled image: " + imageSize(scaledImage));
|
||||||
|
|
||||||
|
CropRectangle rawScaledCrop = adjustCropRectangleToScaledImage(
|
||||||
|
boundedCrop, scaleFactor);
|
||||||
|
log.debug("scaled crop: " + rawScaledCrop);
|
||||||
|
CropRectangle scaledCrop = limitCropRectangleToImageBounds(
|
||||||
|
scaledImage, rawScaledCrop);
|
||||||
|
log.debug("bounded scaled crop: " + scaledCrop);
|
||||||
|
|
||||||
|
BufferedImage croppedImage = cropImage(scaledImage, scaledCrop);
|
||||||
|
log.debug("cropped image: " + imageSize(croppedImage));
|
||||||
|
|
||||||
|
byte[] jpegBytes = encodeAsJpeg(croppedImage);
|
||||||
|
return new ByteArrayInputStream(jpegBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to scale the image", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String imageSize(BufferedImage image) {
|
||||||
|
return image.getWidth() + " by " + image.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CropRectangle limitCropRectangleToImageBounds(BufferedImage image,
|
||||||
|
CropRectangle crop) {
|
||||||
|
|
||||||
|
int imageWidth = image.getWidth();
|
||||||
|
int imageHeight = image.getHeight();
|
||||||
|
|
||||||
|
// Ensure that x and y are at least zero, but not big enough to push the
|
||||||
|
// crop rectangle out of the image.
|
||||||
|
int greatestX = imageWidth - MINIMUM_CROP_SIZE;
|
||||||
|
int greatestY = imageHeight - MINIMUM_CROP_SIZE;
|
||||||
|
int x = Math.max(0, Math.min(greatestX, Math.abs(crop.x)));
|
||||||
|
int y = Math.max(0, Math.min(greatestY, Math.abs(crop.y)));
|
||||||
|
|
||||||
|
// Ensure that width and height are at least as big as the minimum, but
|
||||||
|
// no so big as to extend beyond the image.
|
||||||
|
int greatestW = imageWidth - x;
|
||||||
|
int greatestH = imageHeight - y;
|
||||||
|
int w = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestW, crop.width));
|
||||||
|
int h = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestH, crop.height));
|
||||||
|
|
||||||
|
return new CropRectangle(x, y, h, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float figureScaleFactor(CropRectangle boundedCrop, Dimensions limits) {
|
||||||
|
float horizontalScale = ((float) limits.width)
|
||||||
|
/ ((float) boundedCrop.width);
|
||||||
|
float verticalScale = ((float) limits.height)
|
||||||
|
/ ((float) boundedCrop.height);
|
||||||
|
return Math.min(horizontalScale, verticalScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage scaleImage(BufferedImage image, float scaleFactor) {
|
||||||
|
AffineTransform transform = AffineTransform.getScaleInstance(
|
||||||
|
scaleFactor, scaleFactor);
|
||||||
|
AffineTransformOp atoOp = new AffineTransformOp(transform, null);
|
||||||
|
return atoOp.filter(image, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CropRectangle adjustCropRectangleToScaledImage(CropRectangle crop,
|
||||||
|
float scaleFactor) {
|
||||||
|
int newX = (int) (crop.x * scaleFactor);
|
||||||
|
int newY = (int) (crop.y * scaleFactor);
|
||||||
|
int newHeight = (int) (crop.height * scaleFactor);
|
||||||
|
int newWidth = (int) (crop.width * scaleFactor);
|
||||||
|
return new CropRectangle(newX, newY, newHeight, newWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage cropImage(BufferedImage image, CropRectangle crop) {
|
||||||
|
return image.getSubimage(crop.x, crop.y, crop.width, crop.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] encodeAsJpeg(BufferedImage image) throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(image, "JPG", bytes);
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This ImagingListener means that Java Advanced Imaging won't dump an
|
||||||
|
* exception log to System.out. It writes to the log, instead.
|
||||||
|
*
|
||||||
|
* Further, since the lack of native accelerator classes isn't an error, it
|
||||||
|
* is written as a simple log message.
|
||||||
|
*/
|
||||||
|
static class NonNoisyImagingListener implements ImagingListener {
|
||||||
|
@Override
|
||||||
|
public boolean errorOccurred(String message, Throwable thrown,
|
||||||
|
Object where, boolean isRetryable) throws RuntimeException {
|
||||||
|
if (thrown instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException) thrown;
|
||||||
|
}
|
||||||
|
if ((thrown instanceof NoClassDefFoundError)
|
||||||
|
&& (thrown.getMessage()
|
||||||
|
.contains("com/sun/medialib/mlib/Image"))) {
|
||||||
|
log.info("Java Advanced Imaging: Could not find mediaLib "
|
||||||
|
+ "accelerator wrapper classes. "
|
||||||
|
+ "Continuing in pure Java mode.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.error(thrown, thrown);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
dependencies/pom.xml
vendored
15
dependencies/pom.xml
vendored
|
@ -87,6 +87,21 @@
|
||||||
<artifactId>c3p0</artifactId>
|
<artifactId>c3p0</artifactId>
|
||||||
<version>0.9.2-pre4</version>
|
<version>0.9.2-pre4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||||
|
<artifactId>imageio-jpeg</artifactId>
|
||||||
|
<version>3.2.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.twelvemonkeys.imageio</groupId>
|
||||||
|
<artifactId>imageio-tiff</artifactId>
|
||||||
|
<version>3.2.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.twelvemonkeys.servlet</groupId>
|
||||||
|
<artifactId>servlet</artifactId>
|
||||||
|
<version>3.2.1</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-dbcp</groupId>
|
<groupId>commons-dbcp</groupId>
|
||||||
<artifactId>commons-dbcp</artifactId>
|
<artifactId>commons-dbcp</artifactId>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<java:edu.cornell.mannlib.vitro.webapp.modules.Application> ;
|
<java:edu.cornell.mannlib.vitro.webapp.modules.Application> ;
|
||||||
:hasSearchEngine :instrumentedSearchEngineWrapper ;
|
:hasSearchEngine :instrumentedSearchEngineWrapper ;
|
||||||
:hasSearchIndexer :basicSearchIndexer ;
|
:hasSearchIndexer :basicSearchIndexer ;
|
||||||
:hasImageProcessor :jaiImageProcessor ;
|
:hasImageProcessor :iioImageProcessor ;
|
||||||
:hasFileStorage :ptiFileStorage ;
|
:hasFileStorage :ptiFileStorage ;
|
||||||
:hasContentTripleSource :sdbContentTripleSource ;
|
:hasContentTripleSource :sdbContentTripleSource ;
|
||||||
:hasConfigurationTripleSource :tdbConfigurationTripleSource ;
|
:hasConfigurationTripleSource :tdbConfigurationTripleSource ;
|
||||||
|
@ -32,12 +32,10 @@
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
#
|
#
|
||||||
# Image processor module:
|
# Image processor module:
|
||||||
# The JAI-based implementation is the only standard option.
|
|
||||||
# It requires no parameters.
|
|
||||||
#
|
#
|
||||||
|
|
||||||
:jaiImageProcessor
|
:iioImageProcessor
|
||||||
a <java:edu.cornell.mannlib.vitro.webapp.imageprocessor.jai.JaiImageProcessor> ,
|
a <java:edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio.IIOImageProcessor> ,
|
||||||
<java:edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor> .
|
<java:edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor> .
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|
|
@ -46,6 +46,11 @@
|
||||||
<listener-class>edu.cornell.mannlib.vitro.webapp.startup.StartupManager</listener-class>
|
<listener-class>edu.cornell.mannlib.vitro.webapp.startup.StartupManager</listener-class>
|
||||||
</listener>
|
</listener>
|
||||||
|
|
||||||
|
<!-- TwelveMonkeys ImageIO listener -->
|
||||||
|
<listener>
|
||||||
|
<display-name>ImageIO service provider loader/unloader</display-name>
|
||||||
|
<listener-class>com.twelvemonkeys.servlet.image.IIOProviderContextListener</listener-class>
|
||||||
|
</listener>
|
||||||
|
|
||||||
<!-- Filters ********************************************************** -->
|
<!-- Filters ********************************************************** -->
|
||||||
<!-- in 2.4 spec, filter chain order is first by filter-mapping <url-pattern> order in web.xml,
|
<!-- in 2.4 spec, filter chain order is first by filter-mapping <url-pattern> order in web.xml,
|
||||||
|
|
Loading…
Add table
Reference in a new issue