Commit e1f28a2c authored by robcast's avatar robcast
Browse files

digilib servlet version 2 ("scaleable digilib")

- first stab at using thread pools to limit resource use
- using Dug Leas util.concurrent
- doesn't mix with tomcat :-(
parent 4ca0fa86
/* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
Digital Image Library servlet components
Digital Image Library servlet components
Copyright (C) 2001, 2002 Robert Casties (robcast@mail.berlios.de)
Copyright (C) 2002, 2003 Robert Casties (robcast@mail.berlios.de)
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
Please read license.txt for the full details. A copy of the GPL
may be found at http://www.gnu.org/copyleft/lgpl.html
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
Please read license.txt for the full details. A copy of the GPL
may be found at http://www.gnu.org/copyleft/lgpl.html
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package digilib.image;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.image.renderable.*;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Iterator;
import javax.imageio.*;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import digilib.*;
import digilib.io.*;
import digilib.io.FileOpException;
import digilib.io.ImageFile;
/** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
public class ImageLoaderDocuImage extends DocuImageImpl {
private BufferedImage img;
public ImageLoaderDocuImage() {
}
public ImageLoaderDocuImage(Utils u) {
util = u;
}
/**
* load image file
*/
public void loadImage(File f) throws FileOpException {
util.dprintln(10, "loadImage!");
System.gc();
try {
for (int i = 0; i < ImageIO.getReaderFormatNames().length; i++) {
System.out.println("ImageLoader reader:"+ImageIO.getReaderFormatNames()[i]);
}
for (int i = 0; i < ImageIO.getWriterFormatNames().length; i++) {
System.out.println("ImageLoader writer:"+ImageIO.getWriterFormatNames()[i]);
}
img = ImageIO.read(f);
if (img == null) {
util.dprintln(3, "ERROR(loadImage): unable to load file");
throw new FileOpException("Unable to load File!");
}
}
catch (IOException e) {
throw new FileOpException("Error reading image.");
}
}
/**
* write image of type mt to Stream
*/
public void writeImage(String mt, ServletResponse res)
throws FileOpException {
util.dprintln(10, "writeImage!");
try {
// setup output
String type = "png";
if (mt == "image/jpeg") {
type = "jpeg";
} else if (mt == "image/png") {
type = "png";
} else {
// unknown mime type
util.dprintln(2, "ERROR(writeImage): Unknown mime type "+mt);
throw new FileOpException("Unknown mime type: "+mt);
}
res.setContentType(mt);
// render output
if (ImageIO.write(img, type, res.getOutputStream())) {
// writing was OK
return;
} else {
throw new FileOpException("Error writing image: Unknown image format!");
}
} catch (IOException e) {
// e.printStackTrace();
throw new FileOpException("Error writing image.");
}
}
public int getWidth() {
if (img != null) {
return img.getWidth();
}
return 0;
}
public int getHeight() {
if (img != null) {
return img.getHeight();
}
return 0;
}
/**
* crop and scale image
* take rectangle width,height at position x_off,y_off
* and scale by scale
*/
public void cropAndScale(int x_off, int y_off, int width, int height,
float scale, int qual) throws ImageOpException {
util.dprintln(10, "cropAndScale!");
int scaleInt = 0;
// setup interpolation quality
if (qual > 0) {
util.dprintln(4, "quality q1");
scaleInt = AffineTransformOp.TYPE_BILINEAR;
} else {
util.dprintln(4, "quality q0");
scaleInt = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
}
// setup Crop
BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
img = null; // free img
util.dprintln(3, "CROP:"+croppedImg.getWidth()+"x"+croppedImg.getHeight()); //DEBUG
// util.dprintln(2, " time "+(System.currentTimeMillis()-startTime)+"ms");
if (croppedImg == null) {
util.dprintln(2, "ERROR(cropAndScale): error in crop");
throw new ImageOpException("Unable to crop");
}
// setup scale
AffineTransformOp scaleOp = new AffineTransformOp(
AffineTransform.getScaleInstance(scale, scale),
scaleInt);
BufferedImage scaledImg = scaleOp.filter(croppedImg, null);
croppedImg = null; // free opCrop
if (scaledImg == null) {
util.dprintln(2, "ERROR(cropAndScale): error in scale");
throw new ImageOpException("Unable to scale");
}
img = scaledImg;
}
/** image object */
protected BufferedImage img;
/** interpolation type */
protected RenderingHints renderHint;
/** ImageIO image reader */
protected ImageReader reader;
/** File that was read */
protected File imgFile;
/* loadSubimage is supported. */
public boolean isSubimageSupported() {
return true;
}
public void setQuality(int qual) {
quality = qual;
renderHint = new RenderingHints(null);
//hint.put(RenderingHints.KEY_ANTIALIASING,
// RenderingHints.VALUE_ANTIALIAS_OFF);
// setup interpolation quality
if (qual > 0) {
logger.debug("quality q1");
renderHint.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
} else {
logger.debug("quality q0");
renderHint.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
}
}
public int getHeight() {
int h = 0;
try {
if (img == null) {
h = reader.getHeight(0);
} else {
h = img.getHeight();
}
} catch (IOException e) {
logger.debug("error in getHeight", e);
}
return h;
}
public int getWidth() {
int w = 0;
try {
if (img == null) {
w = reader.getWidth(0);
} else {
w = img.getWidth();
}
} catch (IOException e) {
logger.debug("error in getHeight", e);
}
return w;
}
/* load image file */
public void loadImage(ImageFile f) throws FileOpException {
logger.debug("loadImage " + f.getFile());
//System.gc();
try {
img = ImageIO.read(f.getFile());
if (img == null) {
throw new FileOpException("Unable to load File!");
}
} catch (IOException e) {
throw new FileOpException("Error reading image.");
}
}
/**
* Get an ImageReader for the image file.
*
* @return
*/
public ImageReader getReader(ImageFile f) throws IOException {
logger.debug("preloadImage " + f.getFile());
if (reader != null) {
logger.debug("Reader was not null!");
// clean up old reader
dispose();
}
//System.gc();
RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
ImageInputStream istream = new FileImageInputStream(rf);
//Iterator readers = ImageIO.getImageReaders(istream);
String mt = f.getMimetype();
logger.debug("File type:" + mt);
Iterator readers = ImageIO.getImageReadersByMIMEType(mt);
if (!readers.hasNext()) {
throw new FileOpException("Unable to load File!");
}
reader = (ImageReader) readers.next();
/* are there more readers? */
logger.debug("ImageIO: this reader: " + reader.getClass());
while (readers.hasNext()) {
logger.debug("ImageIO: next reader: " + readers.next().getClass());
}
//*/
reader.setInput(istream);
imgFile = f.getFile();
return reader;
}
/* Load an image file into the Object. */
public void loadSubimage(ImageFile f, Rectangle region, int prescale)
throws FileOpException {
logger.debug("loadSubimage");
//System.gc();
try {
if ((reader == null) || (imgFile != f.getFile())) {
getReader(f);
}
// set up reader parameters
ImageReadParam readParam = reader.getDefaultReadParam();
readParam.setSourceRegion(region);
if (prescale > 1) {
readParam.setSourceSubsampling(prescale, prescale, 0, 0);
}
// read image
logger.debug("loading..");
img = reader.read(0, readParam);
logger.debug("loaded");
} catch (IOException e) {
throw new FileOpException("Unable to load File!");
}
if (img == null) {
throw new FileOpException("Unable to load File!");
}
}
/* write image of type mt to Stream */
public void writeImage(String mt, OutputStream ostream)
throws FileOpException {
logger.debug("writeImage");
try {
// setup output
String type = "png";
if (mt == "image/jpeg") {
type = "jpeg";
} else if (mt == "image/png") {
type = "png";
} else {
// unknown mime type
throw new FileOpException("Unknown mime type: " + mt);
}
/*
* JPEG doesn't do transparency so we have to convert any RGBA image
* to RGB :-( *Java2D BUG*
*/
if ((type == "jpeg") && (img.getColorModel().hasAlpha())) {
logger.debug("BARF: JPEG with transparency!!");
int w = img.getWidth();
int h = img.getHeight();
// BufferedImage.TYPE_INT_RGB seems to be fastest (JDK1.4.1,
// OSX)
int destType = BufferedImage.TYPE_INT_RGB;
BufferedImage img2 = new BufferedImage(w, h, destType);
img2.createGraphics().drawImage(img, null, 0, 0);
img = img2;
}
// render output
logger.debug("writing");
if (ImageIO.write(img, type, ostream)) {
// writing was OK
return;
} else {
throw new FileOpException(
"Error writing image: Unknown image format!");
}
} catch (IOException e) {
throw new FileOpException("Error writing image.");
}
}
public void scale(double scale, double scaleY) throws ImageOpException {
logger.debug("scale");
/* for downscaling in high quality the image is blurred first */
if ((scale <= 0.5) && (quality > 1)) {
int bl = (int) Math.floor(1 / scale);
blur(bl);
}
/* then scaled */
AffineTransformOp scaleOp = new AffineTransformOp(AffineTransform
.getScaleInstance(scale, scale), renderHint);
BufferedImage scaledImg = null;
// enforce destination image type (*Java2D BUG*)
int type = img.getType();
// FIXME: which type would be best?
if ((quality > 0) && (type != 0)) {
logger.debug("creating destination image");
Rectangle2D dstBounds = scaleOp.getBounds2D(img);
scaledImg = new BufferedImage((int) dstBounds.getWidth(),
(int) dstBounds.getHeight(), type);
}
logger.debug("scaling...");
scaledImg = scaleOp.filter(img, scaledImg);
logger.debug("destination image type " + scaledImg.getType());
if (scaledImg == null) {
throw new ImageOpException("Unable to scale");
}
//DEBUG
logger.debug("SCALE: " + scale + " ->" + scaledImg.getWidth() + "x"
+ scaledImg.getHeight());
img = scaledImg;
scaledImg = null;
}
public void blur(int radius) throws ImageOpException {
//DEBUG
logger.debug("blur: " + radius);
// minimum radius is 2
int klen = Math.max(radius, 2);
// FIXME: use constant kernels for most common sizes
int ksize = klen * klen;
// kernel is constant 1/k
float f = 1f / ksize;
float[] kern = new float[ksize];
for (int i = 0; i < ksize; i++) {
kern[i] = f;
}
Kernel blur = new Kernel(klen, klen, kern);
// blur with convolve operation
ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
renderHint);
// blur needs explicit destination image type for color *Java2D BUG*
BufferedImage blurredImg = null;
if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
blurredImg = new BufferedImage(img.getWidth(), img.getHeight(), img
.getType());
}
blurredImg = blurOp.filter(img, blurredImg);
if (blurredImg == null) {
throw new ImageOpException("Unable to scale");
}
img = blurredImg;
}
public void crop(int x_off, int y_off, int width, int height)
throws ImageOpException {
// setup Crop
BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
logger.debug("CROP:" + croppedImg.getWidth() + "x"
+ croppedImg.getHeight());
//DEBUG
// util.dprintln(2, " time
// "+(System.currentTimeMillis()-startTime)+"ms");
if (croppedImg == null) {
throw new ImageOpException("Unable to crop");
}
img = croppedImg;
}
public void enhance(float mult, float add) throws ImageOpException {
/*
* Only one constant should work regardless of the number of bands
* according to the JDK spec. Doesn't work on JDK 1.4 for OSX and Linux
* (at least). RescaleOp scaleOp = new RescaleOp( (float)mult,
* (float)add, null); scaleOp.filter(img, img);
*/
/* The number of constants must match the number of bands in the image. */
int ncol = img.getColorModel().getNumComponents();
float[] dm = new float[ncol];
float[] da = new float[ncol];
for (int i = 0; i < ncol; i++) {
dm[i] = (float) mult;
da[i] = (float) add;
}
RescaleOp scaleOp = new RescaleOp(dm, da, null);
scaleOp.filter(img, img);
}
public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
/*
* The number of constants must match the number of bands in the image.
* We do only 3 (RGB) bands.
*/
int ncol = img.getColorModel().getNumColorComponents();
if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
logger
.debug("ERROR(enhance): unknown number of color bands or coefficients ("
+ ncol + ")");
return;
}
RescaleOp scaleOp = new RescaleOp(rgbOrdered(rgbm), rgbOrdered(rgba),
null);
scaleOp.filter(img, img);
}
/**
* Ensures that the array f is in the right order to map the images RGB
* components. (not shure what happens
*/
public float[] rgbOrdered(float[] fa) {
/*
* TODO: this is UGLY, UGLY!!
*/
float[] fb;
int t = img.getType();
if (img.getColorModel().hasAlpha()) {
fb = new float[4];
if ((t == BufferedImage.TYPE_INT_ARGB)
|| (t == BufferedImage.TYPE_INT_ARGB_PRE)) {
// RGB Type
fb[0] = fa[0];
fb[1] = fa[1];
fb[2] = fa[2];
fb[3] = 1f;
} else {
// this isn't tested :-(
fb[0] = 1f;
fb[1] = fa[0];
fb[2] = fa[1];
fb[3] = fa[2];
}
} else {
fb = new float[3];
if (t == BufferedImage.TYPE_3BYTE_BGR) {
// BGR Type (actually it looks like RBG...)
fb[0] = fa[0];
fb[1] = fa[2];
fb[2] = fa[1];
} else {
fb[0] = fa[0];
fb[1] = fa[1];
fb[2] = fa[2];
}
}
return fb;
}
public void rotate(double angle) throws ImageOpException {
// setup rotation
double rangle = Math.toRadians(angle);
// create offset to make shure the rotated image has no negative
// coordinates
double w = img.getWidth();
double h = img.getHeight();
AffineTransform trafo = new AffineTransform();
// center of rotation
double x = (w / 2);
double y = (h / 2);
trafo.rotate(rangle, x, y);
// try rotation to see how far we're out of bounds
AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
Rectangle2D rotbounds = rotOp.getBounds2D(img);
double xoff = rotbounds.getX();
double yoff = rotbounds.getY();
// move image back in line
trafo
.preConcatenate(AffineTransform.getTranslateInstance(-xoff,
-yoff));
// transform image
rotOp = new AffineTransformOp(trafo, renderHint);
BufferedImage rotImg = rotOp.filter(img, null);
// calculate new bounding box
//Rectangle2D bounds = rotOp.getBounds2D(img);
if (rotImg == null) {
throw new ImageOpException("Unable to rotate");
}
img = rotImg;
// crop new image (with self-made rounding)
/*
* img = rotImg.getSubimage( (int) (bounds.getX()+0.5), (int)
* (bounds.getY()+0.5), (int) (bounds.getWidth()+0.5), (int)
* (bounds.getHeight()+0.5));
*/
}
public void mirror(double angle) throws ImageOpException {
// setup mirror
double mx = 1;
double my = 1;
double tx = 0;
double ty = 0;
if (Math.abs(angle - 0) < epsilon) { // 0 degree
mx = -1;
tx = getWidth();
} else if (Math.abs(angle - 90) < epsilon) { // 90 degree
my = -1;
ty = getHeight();
} else if (Math.abs(angle - 180) < epsilon) { // 180 degree
mx = -1;
tx = getWidth();