1
|
1 /* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
|
|
2
|
290
|
3 Digital Image Library servlet components
|
1
|
4
|
290
|
5 Copyright (C) 2002, 2003 Robert Casties (robcast@mail.berlios.de)
|
1
|
6
|
290
|
7 This program is free software; you can redistribute it and/or modify it
|
|
8 under the terms of the GNU General Public License as published by the
|
|
9 Free Software Foundation; either version 2 of the License, or (at your
|
|
10 option) any later version.
|
|
11
|
|
12 Please read license.txt for the full details. A copy of the GPL
|
|
13 may be found at http://www.gnu.org/copyleft/lgpl.html
|
1
|
14
|
290
|
15 You should have received a copy of the GNU General Public License
|
|
16 along with this program; if not, write to the Free Software
|
|
17 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
18 */
|
1
|
19
|
|
20 package digilib.image;
|
|
21
|
290
|
22 import java.awt.Rectangle;
|
|
23 import java.awt.RenderingHints;
|
|
24 import java.awt.geom.AffineTransform;
|
|
25 import java.awt.geom.Rectangle2D;
|
|
26 import java.awt.image.AffineTransformOp;
|
|
27 import java.awt.image.BufferedImage;
|
|
28 import java.awt.image.ConvolveOp;
|
|
29 import java.awt.image.Kernel;
|
|
30 import java.awt.image.RescaleOp;
|
|
31 import java.io.File;
|
|
32 import java.io.IOException;
|
|
33 import java.io.OutputStream;
|
|
34 import java.io.RandomAccessFile;
|
|
35 import java.util.Iterator;
|
1
|
36
|
290
|
37 import javax.imageio.ImageIO;
|
|
38 import javax.imageio.ImageReadParam;
|
|
39 import javax.imageio.ImageReader;
|
|
40 import javax.imageio.stream.FileImageInputStream;
|
|
41 import javax.imageio.stream.ImageInputStream;
|
1
|
42
|
290
|
43 import digilib.io.FileOpException;
|
|
44 import digilib.io.ImageFile;
|
1
|
45
|
290
|
46 /** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
|
1
|
47 public class ImageLoaderDocuImage extends DocuImageImpl {
|
|
48
|
290
|
49 /** image object */
|
|
50 protected BufferedImage img;
|
|
51
|
|
52 /** interpolation type */
|
|
53 protected RenderingHints renderHint;
|
|
54
|
|
55 /** ImageIO image reader */
|
|
56 protected ImageReader reader;
|
|
57
|
|
58 /** File that was read */
|
|
59 protected File imgFile;
|
|
60
|
|
61 /* loadSubimage is supported. */
|
|
62 public boolean isSubimageSupported() {
|
|
63 return true;
|
|
64 }
|
|
65
|
|
66 public void setQuality(int qual) {
|
|
67 quality = qual;
|
|
68 renderHint = new RenderingHints(null);
|
|
69 //hint.put(RenderingHints.KEY_ANTIALIASING,
|
|
70 // RenderingHints.VALUE_ANTIALIAS_OFF);
|
|
71 // setup interpolation quality
|
|
72 if (qual > 0) {
|
|
73 logger.debug("quality q1");
|
|
74 renderHint.put(RenderingHints.KEY_INTERPOLATION,
|
|
75 RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
|
76 } else {
|
|
77 logger.debug("quality q0");
|
|
78 renderHint.put(RenderingHints.KEY_INTERPOLATION,
|
|
79 RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
|
80 }
|
|
81 }
|
1
|
82
|
290
|
83 public int getHeight() {
|
|
84 int h = 0;
|
|
85 try {
|
|
86 if (img == null) {
|
|
87 h = reader.getHeight(0);
|
|
88 } else {
|
|
89 h = img.getHeight();
|
|
90 }
|
|
91 } catch (IOException e) {
|
|
92 logger.debug("error in getHeight", e);
|
|
93 }
|
|
94 return h;
|
|
95 }
|
1
|
96
|
290
|
97 public int getWidth() {
|
|
98 int w = 0;
|
|
99 try {
|
|
100 if (img == null) {
|
|
101 w = reader.getWidth(0);
|
|
102 } else {
|
|
103 w = img.getWidth();
|
|
104 }
|
|
105 } catch (IOException e) {
|
|
106 logger.debug("error in getHeight", e);
|
|
107 }
|
|
108 return w;
|
|
109 }
|
|
110
|
|
111 /* load image file */
|
|
112 public void loadImage(ImageFile f) throws FileOpException {
|
|
113 logger.debug("loadImage " + f.getFile());
|
|
114 //System.gc();
|
|
115 try {
|
|
116 img = ImageIO.read(f.getFile());
|
|
117 if (img == null) {
|
|
118 throw new FileOpException("Unable to load File!");
|
|
119 }
|
|
120 } catch (IOException e) {
|
|
121 throw new FileOpException("Error reading image.");
|
|
122 }
|
|
123 }
|
1
|
124
|
290
|
125 /**
|
|
126 * Get an ImageReader for the image file.
|
|
127 *
|
|
128 * @return
|
|
129 */
|
|
130 public ImageReader getReader(ImageFile f) throws IOException {
|
|
131 logger.debug("preloadImage " + f.getFile());
|
|
132 if (reader != null) {
|
|
133 logger.debug("Reader was not null!");
|
|
134 // clean up old reader
|
|
135 dispose();
|
|
136 }
|
|
137 //System.gc();
|
|
138 RandomAccessFile rf = new RandomAccessFile(f.getFile(), "r");
|
|
139 ImageInputStream istream = new FileImageInputStream(rf);
|
|
140 //Iterator readers = ImageIO.getImageReaders(istream);
|
|
141 String mt = f.getMimetype();
|
|
142 logger.debug("File type:" + mt);
|
|
143 Iterator readers = ImageIO.getImageReadersByMIMEType(mt);
|
|
144 if (!readers.hasNext()) {
|
|
145 throw new FileOpException("Unable to load File!");
|
|
146 }
|
|
147 reader = (ImageReader) readers.next();
|
|
148 /* are there more readers? */
|
|
149 logger.debug("ImageIO: this reader: " + reader.getClass());
|
|
150 while (readers.hasNext()) {
|
|
151 logger.debug("ImageIO: next reader: " + readers.next().getClass());
|
|
152 }
|
|
153 //*/
|
|
154 reader.setInput(istream);
|
|
155 imgFile = f.getFile();
|
|
156 return reader;
|
|
157 }
|
|
158
|
|
159 /* Load an image file into the Object. */
|
|
160 public void loadSubimage(ImageFile f, Rectangle region, int prescale)
|
|
161 throws FileOpException {
|
|
162 logger.debug("loadSubimage");
|
|
163 //System.gc();
|
|
164 try {
|
|
165 if ((reader == null) || (imgFile != f.getFile())) {
|
|
166 getReader(f);
|
|
167 }
|
|
168 // set up reader parameters
|
|
169 ImageReadParam readParam = reader.getDefaultReadParam();
|
|
170 readParam.setSourceRegion(region);
|
|
171 if (prescale > 1) {
|
|
172 readParam.setSourceSubsampling(prescale, prescale, 0, 0);
|
|
173 }
|
|
174 // read image
|
|
175 logger.debug("loading..");
|
|
176 img = reader.read(0, readParam);
|
|
177 logger.debug("loaded");
|
|
178 } catch (IOException e) {
|
|
179 throw new FileOpException("Unable to load File!");
|
|
180 }
|
|
181 if (img == null) {
|
|
182 throw new FileOpException("Unable to load File!");
|
|
183 }
|
|
184 }
|
|
185
|
|
186 /* write image of type mt to Stream */
|
|
187 public void writeImage(String mt, OutputStream ostream)
|
|
188 throws FileOpException {
|
|
189 logger.debug("writeImage");
|
|
190 try {
|
|
191 // setup output
|
|
192 String type = "png";
|
|
193 if (mt == "image/jpeg") {
|
|
194 type = "jpeg";
|
|
195 } else if (mt == "image/png") {
|
|
196 type = "png";
|
|
197 } else {
|
|
198 // unknown mime type
|
|
199 throw new FileOpException("Unknown mime type: " + mt);
|
|
200 }
|
|
201
|
|
202 /*
|
|
203 * JPEG doesn't do transparency so we have to convert any RGBA image
|
|
204 * to RGB :-( *Java2D BUG*
|
|
205 */
|
|
206 if ((type == "jpeg") && (img.getColorModel().hasAlpha())) {
|
|
207 logger.debug("BARF: JPEG with transparency!!");
|
|
208 int w = img.getWidth();
|
|
209 int h = img.getHeight();
|
|
210 // BufferedImage.TYPE_INT_RGB seems to be fastest (JDK1.4.1,
|
|
211 // OSX)
|
|
212 int destType = BufferedImage.TYPE_INT_RGB;
|
|
213 BufferedImage img2 = new BufferedImage(w, h, destType);
|
|
214 img2.createGraphics().drawImage(img, null, 0, 0);
|
|
215 img = img2;
|
|
216 }
|
|
217
|
|
218 // render output
|
|
219 logger.debug("writing");
|
|
220 if (ImageIO.write(img, type, ostream)) {
|
|
221 // writing was OK
|
|
222 return;
|
|
223 } else {
|
|
224 throw new FileOpException(
|
|
225 "Error writing image: Unknown image format!");
|
|
226 }
|
|
227 } catch (IOException e) {
|
|
228 throw new FileOpException("Error writing image.");
|
|
229 }
|
|
230 }
|
1
|
231
|
290
|
232 public void scale(double scale, double scaleY) throws ImageOpException {
|
|
233 logger.debug("scale");
|
|
234 /* for downscaling in high quality the image is blurred first */
|
|
235 if ((scale <= 0.5) && (quality > 1)) {
|
|
236 int bl = (int) Math.floor(1 / scale);
|
|
237 blur(bl);
|
|
238 }
|
|
239 /* then scaled */
|
|
240 AffineTransformOp scaleOp = new AffineTransformOp(AffineTransform
|
|
241 .getScaleInstance(scale, scale), renderHint);
|
|
242 BufferedImage scaledImg = null;
|
|
243 // enforce destination image type (*Java2D BUG*)
|
|
244 int type = img.getType();
|
|
245 // FIXME: which type would be best?
|
|
246 if ((quality > 0) && (type != 0)) {
|
|
247 logger.debug("creating destination image");
|
|
248 Rectangle2D dstBounds = scaleOp.getBounds2D(img);
|
|
249 scaledImg = new BufferedImage((int) dstBounds.getWidth(),
|
|
250 (int) dstBounds.getHeight(), type);
|
|
251 }
|
|
252 logger.debug("scaling...");
|
|
253 scaledImg = scaleOp.filter(img, scaledImg);
|
|
254 logger.debug("destination image type " + scaledImg.getType());
|
|
255 if (scaledImg == null) {
|
|
256 throw new ImageOpException("Unable to scale");
|
|
257 }
|
|
258 //DEBUG
|
|
259 logger.debug("SCALE: " + scale + " ->" + scaledImg.getWidth() + "x"
|
|
260 + scaledImg.getHeight());
|
|
261 img = scaledImg;
|
|
262 scaledImg = null;
|
|
263 }
|
1
|
264
|
290
|
265 public void blur(int radius) throws ImageOpException {
|
|
266 //DEBUG
|
|
267 logger.debug("blur: " + radius);
|
|
268 // minimum radius is 2
|
|
269 int klen = Math.max(radius, 2);
|
|
270 // FIXME: use constant kernels for most common sizes
|
|
271 int ksize = klen * klen;
|
|
272 // kernel is constant 1/k
|
|
273 float f = 1f / ksize;
|
|
274 float[] kern = new float[ksize];
|
|
275 for (int i = 0; i < ksize; i++) {
|
|
276 kern[i] = f;
|
|
277 }
|
|
278 Kernel blur = new Kernel(klen, klen, kern);
|
|
279 // blur with convolve operation
|
|
280 ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
|
|
281 renderHint);
|
|
282 // blur needs explicit destination image type for color *Java2D BUG*
|
|
283 BufferedImage blurredImg = null;
|
|
284 if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
|
|
285 blurredImg = new BufferedImage(img.getWidth(), img.getHeight(), img
|
|
286 .getType());
|
|
287 }
|
|
288 blurredImg = blurOp.filter(img, blurredImg);
|
|
289 if (blurredImg == null) {
|
|
290 throw new ImageOpException("Unable to scale");
|
|
291 }
|
|
292 img = blurredImg;
|
|
293 }
|
|
294
|
|
295 public void crop(int x_off, int y_off, int width, int height)
|
|
296 throws ImageOpException {
|
|
297 // setup Crop
|
|
298 BufferedImage croppedImg = img.getSubimage(x_off, y_off, width, height);
|
|
299 logger.debug("CROP:" + croppedImg.getWidth() + "x"
|
|
300 + croppedImg.getHeight());
|
|
301 //DEBUG
|
|
302 // util.dprintln(2, " time
|
|
303 // "+(System.currentTimeMillis()-startTime)+"ms");
|
|
304 if (croppedImg == null) {
|
|
305 throw new ImageOpException("Unable to crop");
|
|
306 }
|
|
307 img = croppedImg;
|
|
308 }
|
|
309
|
|
310 public void enhance(float mult, float add) throws ImageOpException {
|
|
311 /*
|
|
312 * Only one constant should work regardless of the number of bands
|
|
313 * according to the JDK spec. Doesn't work on JDK 1.4 for OSX and Linux
|
|
314 * (at least). RescaleOp scaleOp = new RescaleOp( (float)mult,
|
|
315 * (float)add, null); scaleOp.filter(img, img);
|
|
316 */
|
|
317
|
|
318 /* The number of constants must match the number of bands in the image. */
|
|
319 int ncol = img.getColorModel().getNumComponents();
|
|
320 float[] dm = new float[ncol];
|
|
321 float[] da = new float[ncol];
|
|
322 for (int i = 0; i < ncol; i++) {
|
|
323 dm[i] = (float) mult;
|
|
324 da[i] = (float) add;
|
|
325 }
|
|
326 RescaleOp scaleOp = new RescaleOp(dm, da, null);
|
|
327 scaleOp.filter(img, img);
|
|
328 }
|
|
329
|
|
330 public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
|
|
331
|
|
332 /*
|
|
333 * The number of constants must match the number of bands in the image.
|
|
334 * We do only 3 (RGB) bands.
|
|
335 */
|
|
336
|
|
337 int ncol = img.getColorModel().getNumColorComponents();
|
|
338 if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
|
|
339 logger
|
|
340 .debug("ERROR(enhance): unknown number of color bands or coefficients ("
|
|
341 + ncol + ")");
|
|
342 return;
|
|
343 }
|
|
344 RescaleOp scaleOp = new RescaleOp(rgbOrdered(rgbm), rgbOrdered(rgba),
|
|
345 null);
|
|
346 scaleOp.filter(img, img);
|
|
347 }
|
1
|
348
|
290
|
349 /**
|
|
350 * Ensures that the array f is in the right order to map the images RGB
|
|
351 * components. (not shure what happens
|
|
352 */
|
|
353 public float[] rgbOrdered(float[] fa) {
|
|
354 /*
|
|
355 * TODO: this is UGLY, UGLY!!
|
|
356 */
|
|
357 float[] fb;
|
|
358 int t = img.getType();
|
|
359 if (img.getColorModel().hasAlpha()) {
|
|
360 fb = new float[4];
|
|
361 if ((t == BufferedImage.TYPE_INT_ARGB)
|
|
362 || (t == BufferedImage.TYPE_INT_ARGB_PRE)) {
|
|
363 // RGB Type
|
|
364 fb[0] = fa[0];
|
|
365 fb[1] = fa[1];
|
|
366 fb[2] = fa[2];
|
|
367 fb[3] = 1f;
|
|
368 } else {
|
|
369 // this isn't tested :-(
|
|
370 fb[0] = 1f;
|
|
371 fb[1] = fa[0];
|
|
372 fb[2] = fa[1];
|
|
373 fb[3] = fa[2];
|
|
374 }
|
|
375 } else {
|
|
376 fb = new float[3];
|
|
377 if (t == BufferedImage.TYPE_3BYTE_BGR) {
|
|
378 // BGR Type (actually it looks like RBG...)
|
|
379 fb[0] = fa[0];
|
|
380 fb[1] = fa[2];
|
|
381 fb[2] = fa[1];
|
|
382 } else {
|
|
383 fb[0] = fa[0];
|
|
384 fb[1] = fa[1];
|
|
385 fb[2] = fa[2];
|
|
386 }
|
|
387 }
|
|
388 return fb;
|
|
389 }
|
1
|
390
|
290
|
391 public void rotate(double angle) throws ImageOpException {
|
|
392 // setup rotation
|
|
393 double rangle = Math.toRadians(angle);
|
|
394 // create offset to make shure the rotated image has no negative
|
|
395 // coordinates
|
|
396 double w = img.getWidth();
|
|
397 double h = img.getHeight();
|
|
398 AffineTransform trafo = new AffineTransform();
|
|
399 // center of rotation
|
|
400 double x = (w / 2);
|
|
401 double y = (h / 2);
|
|
402 trafo.rotate(rangle, x, y);
|
|
403 // try rotation to see how far we're out of bounds
|
|
404 AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
|
|
405 Rectangle2D rotbounds = rotOp.getBounds2D(img);
|
|
406 double xoff = rotbounds.getX();
|
|
407 double yoff = rotbounds.getY();
|
|
408 // move image back in line
|
|
409 trafo
|
|
410 .preConcatenate(AffineTransform.getTranslateInstance(-xoff,
|
|
411 -yoff));
|
|
412 // transform image
|
|
413 rotOp = new AffineTransformOp(trafo, renderHint);
|
|
414 BufferedImage rotImg = rotOp.filter(img, null);
|
|
415 // calculate new bounding box
|
|
416 //Rectangle2D bounds = rotOp.getBounds2D(img);
|
|
417 if (rotImg == null) {
|
|
418 throw new ImageOpException("Unable to rotate");
|
|
419 }
|
|
420 img = rotImg;
|
|
421 // crop new image (with self-made rounding)
|
|
422 /*
|
|
423 * img = rotImg.getSubimage( (int) (bounds.getX()+0.5), (int)
|
|
424 * (bounds.getY()+0.5), (int) (bounds.getWidth()+0.5), (int)
|
|
425 * (bounds.getHeight()+0.5));
|
|
426 */
|
|
427 }
|
1
|
428
|
290
|
429 public void mirror(double angle) throws ImageOpException {
|
|
430 // setup mirror
|
|
431 double mx = 1;
|
|
432 double my = 1;
|
|
433 double tx = 0;
|
|
434 double ty = 0;
|
|
435 if (Math.abs(angle - 0) < epsilon) { // 0 degree
|
|
436 mx = -1;
|
|
437 tx = getWidth();
|
|
438 } else if (Math.abs(angle - 90) < epsilon) { // 90 degree
|
|
439 my = -1;
|
|
440 ty = getHeight();
|
|
441 } else if (Math.abs(angle - 180) < epsilon) { // 180 degree
|
|
442 mx = -1;
|
|
443 tx = getWidth();
|
|
444 } else if (Math.abs(angle - 270) < epsilon) { // 270 degree
|
|
445 my = -1;
|
|
446 ty = getHeight();
|
|
447 } else if (Math.abs(angle - 360) < epsilon) { // 360 degree
|
|
448 mx = -1;
|
|
449 tx = getWidth();
|
|
450 }
|
|
451 AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
|
|
452 0, 0, my, tx, ty), renderHint);
|
|
453 BufferedImage mirImg = mirOp.filter(img, null);
|
|
454 if (mirImg == null) {
|
|
455 throw new ImageOpException("Unable to mirror");
|
|
456 }
|
|
457 img = mirImg;
|
|
458 }
|
1
|
459
|
290
|
460 /*
|
|
461 * (non-Javadoc)
|
|
462 *
|
|
463 * @see java.lang.Object#finalize()
|
|
464 */
|
|
465 protected void finalize() throws Throwable {
|
|
466 dispose();
|
|
467 super.finalize();
|
|
468 }
|
|
469
|
|
470 public void dispose() {
|
|
471 // we must dispose the ImageReader because it keeps the filehandle
|
|
472 // open!
|
|
473 if (reader != null) {
|
|
474 reader.dispose();
|
|
475 reader = null;
|
|
476 }
|
|
477 img = null;
|
|
478 }
|
1
|
479
|
|
480 }
|