diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java index f3efa992e..b0bbe9d2f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadHelper.java @@ -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; - } - /** *

* This {@link ImagingListener} means that Java Advanced Imaging won't dump diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadThumbnailer.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadThumbnailer.java new file mode 100644 index 000000000..4b62f43f4 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ImageUploadThumbnailer.java @@ -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; + } + +}