NIHVIVO-1208 Moved the thumbnailing code into its own class, revised to use the more Object Oriented methods in JAI. Still works for JPEG, but not for GIF.

This commit is contained in:
jeb228 2010-10-11 21:58:35 +00:00
parent 0fe786390e
commit d0846f2348
2 changed files with 148 additions and 107 deletions

View file

@ -9,9 +9,6 @@ import static edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUpload
import static edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest.FILE_ITEM_MAP;
import static edu.cornell.mannlib.vitro.webapp.filestorage.uploadrequest.FileUploadServletRequest.FILE_UPLOAD_EXCEPTION;
import java.awt.image.renderable.ParameterBlock;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@ -20,7 +17,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import javax.media.jai.util.ImagingListener;
@ -31,9 +27,7 @@ import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sun.media.jai.codec.JPEGEncodeParam;
import com.sun.media.jai.codec.MemoryCacheSeekableStream;
import com.sun.media.jai.codec.PNGDecodeParam;
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
@ -285,7 +279,8 @@ public class ImageUploadHelper {
mainStream = fileStorage.getInputStream(mainBytestreamUri,
mainFilename);
thumbStream = scaleImageForThumbnail(mainStream, crop, newImage);
thumbStream = new ImageUploadThumbnailer(THUMBNAIL_HEIGHT,
THUMBNAIL_WIDTH).cropAndScale(mainStream, crop);
String mimeType = RECOGNIZED_FILE_TYPES.get(".jpg");
String filename = createThumbnailFilename(mainFilename);
@ -388,106 +383,6 @@ public class ImageUploadHelper {
}
}
/**
* Create a thumbnail from a source image, given a cropping rectangle (x, y,
* width, height).
*/
private InputStream scaleImageForThumbnail(InputStream source,
CropRectangle crop, FileInfo newImage) throws IOException {
try {
// Read the main image.
MemoryCacheSeekableStream stream = new MemoryCacheSeekableStream(
source);
ParameterBlock streamParams = new ParameterBlock();
streamParams.add(stream);
addDecodeParametersAsNeeded(newImage, streamParams);
RenderedOp mainImage = JAI.create("stream", streamParams);
int imageWidth = mainImage.getWidth();
int imageHeight = mainImage.getHeight();
// Adjust the crop rectangle, if needed, to compensate for scaling
// and to limit to the image size.
crop = adjustCropRectangle(crop, imageWidth, imageHeight);
// Crop the image.
ParameterBlock cropParams = new ParameterBlock();
cropParams.addSource(mainImage);
cropParams.add((float) crop.x);
cropParams.add((float) crop.y);
cropParams.add((float) crop.width);
cropParams.add((float) crop.height);
RenderedOp croppedImage = JAI.create("crop", cropParams);
// Figure the scales.
float scaleWidth = ((float) THUMBNAIL_WIDTH) / ((float) crop.width);
float scaleHeight = ((float) THUMBNAIL_HEIGHT)
/ ((float) crop.height);
log.debug("Generating a thumbnail, scales: " + scaleWidth + ", "
+ scaleHeight);
// Create the parameters for the scaling operation.
Interpolation interpolation = Interpolation
.getInstance(Interpolation.INTERP_BILINEAR);
ParameterBlock scaleParams = new ParameterBlock();
scaleParams.addSource(croppedImage);
scaleParams.add(scaleWidth); // x scale factor
scaleParams.add(scaleHeight); // y scale factor
scaleParams.add(0.0F); // x translate
scaleParams.add(0.0F); // y translate
scaleParams.add(interpolation);
RenderedOp image2 = JAI.create("scale", scaleParams);
JPEGEncodeParam encodeParam = new JPEGEncodeParam();
encodeParam.setQuality(1.0F);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
JAI.create("encode", image2, bytes, "JPEG", encodeParam);
bytes.close();
return new ByteArrayInputStream(bytes.toByteArray());
} catch (Exception e) {
throw new IllegalStateException("Failed to scale the image", e);
}
}
/**
* The JAI 1.1.3 package has a known bug writing JPEG images from sources
* that have transparency (alpha channel) enabled.
*
* For PNG images, we can add a parameter that will disable the alpha
* channel.
*/
private void addDecodeParametersAsNeeded(FileInfo newImage,
ParameterBlock streamParams) {
if ("image/png".equals(newImage.getMimeType())) {
PNGDecodeParam pdp = new PNGDecodeParam();
pdp.setSuppressAlpha(true);
streamParams.add(pdp);
}
}
/**
* The bounds of the cropping rectangle should be limited to the bounds of
* the image.
*/
private CropRectangle adjustCropRectangle(CropRectangle crop,
int imageWidth, int imageHeight) {
log.debug("Generating a thumbnail, initial crop info: " + crop);
// Insure that x and y fall within the image dimensions.
int x = Math.max(0, Math.min(imageWidth, Math.abs(crop.x)));
int y = Math.max(0, Math.min(imageHeight, Math.abs(crop.y)));
// Insure that width and height are reasonable.
int w = Math.max(5, Math.min(imageWidth - x, crop.width));
int h = Math.max(5, Math.min(imageHeight - y, crop.height));
CropRectangle bounded = new CropRectangle(x, y, h, w);
log.debug("Generating a thumbnail, bounded crop info: " + bounded);
return bounded;
}
/**
* <p>
* This {@link ImagingListener} means that Java Advanced Imaging won't dump

View file

@ -0,0 +1,146 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
import java.awt.image.ColorModel;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import javax.media.jai.InterpolationBilinear;
import javax.media.jai.RenderedOp;
import javax.media.jai.operator.BandSelectDescriptor;
import javax.media.jai.operator.CropDescriptor;
import javax.media.jai.operator.EncodeDescriptor;
import javax.media.jai.operator.ScaleDescriptor;
import javax.media.jai.operator.StreamDescriptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sun.media.jai.codec.JPEGEncodeParam;
import com.sun.media.jai.codec.MemoryCacheSeekableStream;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.CropRectangle;
/**
* Crop the main image as specified, and scale it to the correct size for a
* thumbnail.
*/
public class ImageUploadThumbnailer {
private static final Log log = LogFactory
.getLog(ImageUploadThumbnailer.class);
/** We won't let you crop to smaller than this many pixels wide or high. */
private static final int MINIMUM_CROP_SIZE = 5;
private final int thumbnailHeight;
private final int thumbnailWidth;
public ImageUploadThumbnailer(int thumbnailHeight, int thumbnailWidth) {
this.thumbnailHeight = thumbnailHeight;
this.thumbnailWidth = thumbnailWidth;
}
/**
* Crop the main image according to this rectangle, and scale it to the
* correct size for a thumbnail.
*/
public InputStream cropAndScale(InputStream mainImageStream,
CropRectangle crop) {
try {
RenderedOp mainImage = loadImage(mainImageStream);
RenderedOp opaqueImage = makeImageOpaque(mainImage);
RenderedOp croppedImage = cropImage(opaqueImage, crop);
RenderedOp scaledImage = scaleImage(croppedImage);
byte[] jpegBytes = encodeAsJpeg(scaledImage);
return new ByteArrayInputStream(jpegBytes);
} catch (Exception e) {
throw new IllegalStateException("Failed to scale the image", e);
}
}
private RenderedOp loadImage(InputStream imageStream) {
return StreamDescriptor.create(new MemoryCacheSeekableStream(
imageStream), null, null);
}
private RenderedOp makeImageOpaque(RenderedOp image) {
ColorModel colorModel = image.getColorModel();
if (!colorModel.hasAlpha()) {
return image;
}
return BandSelectDescriptor.create(image, figureBandIndices(image),
null);
}
private RenderedOp cropImage(RenderedOp image, CropRectangle crop) {
CropRectangle boundedCrop = limitCropRectangleToImageBounds(image, crop);
return CropDescriptor.create(image, (float) boundedCrop.x,
(float) boundedCrop.y, (float) boundedCrop.width,
(float) boundedCrop.height, null);
}
private RenderedOp scaleImage(RenderedOp image) {
float horizontalScale = ((float) thumbnailWidth)
/ ((float) image.getWidth());
float verticalScale = ((float) thumbnailHeight)
/ ((float) image.getHeight());
log.debug("Generating a thumbnail, scales: " + horizontalScale + ", "
+ verticalScale);
return ScaleDescriptor.create(image, horizontalScale, verticalScale,
0.0F, 0.0F, new InterpolationBilinear(), null);
}
private byte[] encodeAsJpeg(RenderedOp image) throws IOException {
JPEGEncodeParam encodeParam = new JPEGEncodeParam();
encodeParam.setQuality(1.0F);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
EncodeDescriptor.create(image, bytes, "JPEG", encodeParam, null);
return bytes.toByteArray();
}
/** Build an array holding the indexes of the color bands in this image. */
private int[] figureBandIndices(RenderedOp image) {
int howMany = Math.min(image.getColorModel().getNumColorComponents(),
image.getNumBands());
int[] bandIndices = new int[howMany];
for (int i = 0; i < bandIndices.length; i++) {
bandIndices[i] = i;
}
log.debug("Selecting these bands: " + Arrays.toString(bandIndices));
return bandIndices;
}
private CropRectangle limitCropRectangleToImageBounds(RenderedOp image,
CropRectangle crop) {
log.debug("Generating a thumbnail, initial crop info: " + 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));
CropRectangle bounded = new CropRectangle(x, y, h, w);
log.debug("Generating a thumbnail, bounded crop info: " + bounded);
return bounded;
}
}