NIHVIVO-2477 Change the sequence of operations so scaling is done before cropping, thus avoiding the boundary conditions on rounding error. Use Java AWT operations to do scaling and cropping since the JAI operations are buggy. JAI is still used to read the file and to remove transparency.
This commit is contained in:
parent
753545eb68
commit
e3961cdd20
1 changed files with 82 additions and 45 deletions
|
@ -2,24 +2,23 @@
|
||||||
|
|
||||||
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
|
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
|
||||||
|
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.AffineTransformOp;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.ColorModel;
|
import java.awt.image.ColorModel;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import javax.media.jai.InterpolationBilinear;
|
import javax.imageio.ImageIO;
|
||||||
import javax.media.jai.RenderedOp;
|
import javax.media.jai.RenderedOp;
|
||||||
import javax.media.jai.operator.BandSelectDescriptor;
|
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 javax.media.jai.operator.StreamDescriptor;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
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.MemoryCacheSeekableStream;
|
||||||
|
|
||||||
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.CropRectangle;
|
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadController.CropRectangle;
|
||||||
|
@ -28,9 +27,19 @@ import edu.cornell.mannlib.vitro.webapp.controller.freemarker.ImageUploadControl
|
||||||
* Crop the main image as specified, and scale it to the correct size for a
|
* Crop the main image as specified, and scale it to the correct size for a
|
||||||
* thumbnail.
|
* thumbnail.
|
||||||
*
|
*
|
||||||
* The JAI library has a problem when writing a JPEG from a source image with an
|
* Use the JAI library to read the file because the javax.imageio package
|
||||||
* alpha channel (transparency). The colors come out inverted. We throw in a
|
* doesn't read extended JPEG properly. Use JAI to remove transparency from
|
||||||
* step that will remove transparency from a PNG, but it won't touch a GIF.
|
* 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 ImageUploadThumbnailer {
|
public class ImageUploadThumbnailer {
|
||||||
/** If an image has 3 color bands and 1 alpha band, we want these. */
|
/** If an image has 3 color bands and 1 alpha band, we want these. */
|
||||||
|
@ -59,15 +68,42 @@ public class ImageUploadThumbnailer {
|
||||||
try {
|
try {
|
||||||
RenderedOp mainImage = loadImage(mainImageStream);
|
RenderedOp mainImage = loadImage(mainImageStream);
|
||||||
RenderedOp opaqueImage = makeImageOpaque(mainImage);
|
RenderedOp opaqueImage = makeImageOpaque(mainImage);
|
||||||
RenderedOp croppedImage = cropImage(opaqueImage, crop);
|
|
||||||
RenderedOp scaledImage = scaleImage(croppedImage);
|
BufferedImage bufferedImage = opaqueImage.getAsBufferedImage();
|
||||||
byte[] jpegBytes = encodeAsJpeg(scaledImage);
|
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);
|
||||||
|
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);
|
return new ByteArrayInputStream(jpegBytes);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("Failed to scale the image", e);
|
throw new IllegalStateException("Failed to scale the image", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String imageSize(BufferedImage image) {
|
||||||
|
return image.getWidth() + " by " + image.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
private RenderedOp loadImage(InputStream imageStream) {
|
private RenderedOp loadImage(InputStream imageStream) {
|
||||||
return StreamDescriptor.create(new MemoryCacheSeekableStream(
|
return StreamDescriptor.create(new MemoryCacheSeekableStream(
|
||||||
imageStream), null, null);
|
imageStream), null, null);
|
||||||
|
@ -91,37 +127,8 @@ public class ImageUploadThumbnailer {
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderedOp cropImage(RenderedOp image, CropRectangle crop) {
|
private CropRectangle limitCropRectangleToImageBounds(BufferedImage image,
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CropRectangle limitCropRectangleToImageBounds(RenderedOp image,
|
|
||||||
CropRectangle crop) {
|
CropRectangle crop) {
|
||||||
log.debug("Generating a thumbnail, initial crop info: " + crop);
|
|
||||||
|
|
||||||
int imageWidth = image.getWidth();
|
int imageWidth = image.getWidth();
|
||||||
int imageHeight = image.getHeight();
|
int imageHeight = image.getHeight();
|
||||||
|
@ -140,10 +147,40 @@ public class ImageUploadThumbnailer {
|
||||||
int w = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestW, crop.width));
|
int w = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestW, crop.width));
|
||||||
int h = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestH, crop.height));
|
int h = Math.max(MINIMUM_CROP_SIZE, Math.min(greatestH, crop.height));
|
||||||
|
|
||||||
CropRectangle bounded = new CropRectangle(x, y, h, w);
|
return new CropRectangle(x, y, h, w);
|
||||||
log.debug("Generating a thumbnail, bounded crop info: " + bounded);
|
|
||||||
|
|
||||||
return bounded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float figureScaleFactor(CropRectangle boundedCrop) {
|
||||||
|
float horizontalScale = ((float) thumbnailWidth)
|
||||||
|
/ ((float) boundedCrop.width);
|
||||||
|
float verticalScale = ((float) thumbnailHeight)
|
||||||
|
/ ((float) boundedCrop.height);
|
||||||
|
return Math.min(horizontalScale, verticalScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage cropImage(BufferedImage image, CropRectangle crop) {
|
||||||
|
return image.getSubimage(crop.x, crop.y, crop.width, crop.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 byte[] encodeAsJpeg(BufferedImage image) throws IOException {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(image, "JPG", bytes);
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue