Replace JAI image processor with IIO image processor and log warnings to update configuration

This commit is contained in:
Graham Triggs 2016-12-19 10:52:29 +00:00
parent 45eef7e9f7
commit 6351e6ba1d

View file

@ -2,234 +2,44 @@
package edu.cornell.mannlib.vitro.webapp.imageprocessor.jai; package edu.cornell.mannlib.vitro.webapp.imageprocessor.jai;
import java.awt.geom.AffineTransform; import edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio.IIOImageProcessor;
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;
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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.sun.media.jai.codec.MemoryCacheSeekableStream;
import edu.cornell.mannlib.vitro.webapp.modules.Application; import edu.cornell.mannlib.vitro.webapp.modules.Application;
import edu.cornell.mannlib.vitro.webapp.modules.ComponentStartupStatus; import edu.cornell.mannlib.vitro.webapp.modules.ComponentStartupStatus;
import edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor; import edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.InputStream;
/** /**
* Crop the main image as specified, and scale it to the correct size for a * Re-implemented as ImageIO / TwelveMonkeys plugin for better OpenJDK compatibility and to remove
* thumbnail. * JAI codec dependencies that are not distributed via Maven
* *
* Use the JAI library to read the file because the javax.imageio package * This transitional stub extends the new class, and logs warnings for people to update their config
* 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 JaiImageProcessor implements ImageProcessor { @Deprecated
public class JaiImageProcessor extends IIOImageProcessor {
private static final Log log = LogFactory.getLog(JaiImageProcessor.class); private static final Log log = LogFactory.getLog(JaiImageProcessor.class);
/** If an image has 3 color bands and 1 alpha band, we want these. */ @Deprecated
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 @Override
public void startup(Application application, ComponentStartupStatus ss) { public void startup(Application application, ComponentStartupStatus ss) {
JAI.getDefaultInstance().setImagingListener( log.warn("JaiImageProcessor is deprecated and will be removed - please update config/applicationSetup.n3 to use edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio.IIOImageProcessor");
new NonNoisyImagingListener()); super.startup(application, ss);
} }
@Deprecated
@Override @Override
public void shutdown(Application application) { public Dimensions getDimensions(InputStream imageStream) throws ImageProcessorException, IOException {
// Nothing to tear down. log.warn("JaiImageProcessor is deprecated and will be removed - please update config/applicationSetup.n3 to use edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio.IIOImageProcessor");
return super.getDimensions(imageStream);
} }
@Deprecated
@Override @Override
public Dimensions getDimensions(InputStream imageStream) public InputStream cropAndScale(InputStream mainImageStream, CropRectangle crop, Dimensions limits) throws ImageProcessorException, IOException {
throws ImageProcessorException, IOException { log.warn("JaiImageProcessor is deprecated and will be removed - please update config/applicationSetup.n3 to use edu.cornell.mannlib.vitro.webapp.imageprocessor.imageio.IIOImageProcessor");
MemoryCacheSeekableStream stream = new MemoryCacheSeekableStream( return super.cropAndScale(mainImageStream, crop, limits);
imageStream);
RenderedOp image = JAI.create("stream", 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 {
RenderedOp mainImage = loadImage(mainImageStream);
BufferedImage bufferedImage = new BufferedImage(mainImage.getWidth(), mainImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR); // BufferedImage.TYPE_INT_RGB
new ColorConvertOp(null).filter(mainImage.getAsBufferedImage(), 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 RenderedOp loadImage(InputStream imageStream) {
return StreamDescriptor.create(new MemoryCacheSeekableStream(
imageStream), null, null);
}
private RenderedOp makeImageOpaque(RenderedOp image) {
ColorModel colorModel = image.getColorModel();
if (!colorModel.hasAlpha()) {
// The image is already opaque.
return image;
}
if (image.getNumBands() == 4) {
// The image has a separate alpha channel. Drop the alpha channel.
return BandSelectDescriptor.create(image, COLOR_BAND_INDEXES, null);
}
// Don't know how to handle it. Probably a GIF with a transparent
// background. Give up.
return image;
}
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;
}
}
}