comparison common/src/main/java/digilib/image/ImageLoaderDocuImage.java @ 903:7779b37d1d05

refactored into maven modules per servlet type. can build servlet-api 2.3 and 3.0 via profile now!
author robcast
date Tue, 26 Apr 2011 20:24:31 +0200
parents servlet/src/main/java/digilib/image/ImageLoaderDocuImage.java@ba1eb2d821a2
children 28d007673346
comparison
equal deleted inserted replaced
902:89ba3ffcf552 903:7779b37d1d05
1 /* ImageLoaderDocuImage -- Image class implementation using JDK 1.4 ImageLoader
2
3 Digital Image Library servlet components
4
5 Copyright (C) 2002 - 2011 Robert Casties (robcast@mail.berlios.de)
6
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
14
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 */
19
20 package digilib.image;
21
22 import java.awt.Image;
23 import java.awt.Rectangle;
24 import java.awt.RenderingHints;
25 import java.awt.color.ColorSpace;
26 import java.awt.geom.AffineTransform;
27 import java.awt.geom.Rectangle2D;
28 import java.awt.image.AffineTransformOp;
29 import java.awt.image.BandCombineOp;
30 import java.awt.image.BufferedImage;
31 import java.awt.image.ByteLookupTable;
32 import java.awt.image.ColorConvertOp;
33 import java.awt.image.ColorModel;
34 import java.awt.image.ConvolveOp;
35 import java.awt.image.IndexColorModel;
36 import java.awt.image.Kernel;
37 import java.awt.image.LookupOp;
38 import java.awt.image.LookupTable;
39 import java.awt.image.RescaleOp;
40 import java.io.IOException;
41 import java.io.OutputStream;
42 import java.io.RandomAccessFile;
43 import java.util.Arrays;
44 import java.util.Iterator;
45
46 import javax.imageio.IIOImage;
47 import javax.imageio.ImageIO;
48 import javax.imageio.ImageReadParam;
49 import javax.imageio.ImageReader;
50 import javax.imageio.ImageWriteParam;
51 import javax.imageio.ImageWriter;
52 import javax.imageio.stream.FileImageInputStream;
53 import javax.imageio.stream.ImageInputStream;
54 import javax.imageio.stream.ImageOutputStream;
55 import javax.servlet.ServletException;
56
57 import digilib.io.FileOpException;
58 import digilib.io.FileOps;
59 import digilib.io.ImageInput;
60 import digilib.util.ImageSize;
61
62 /** Implementation of DocuImage using the ImageLoader API of Java 1.4 and Java2D. */
63 public class ImageLoaderDocuImage extends ImageInfoDocuImage {
64
65 /** image object */
66 protected BufferedImage img;
67
68 /** interpolation type */
69 protected RenderingHints renderHint = null;
70
71 /** convolution kernels for blur() */
72 protected static Kernel[] convolutionKernels = {
73 null,
74 new Kernel(1, 1, new float[] {1f}),
75 new Kernel(2, 2, new float[] {0.25f, 0.25f, 0.25f, 0.25f}),
76 new Kernel(3, 3, new float[] {1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f, 1f/9f})
77 };
78
79 /* lookup tables for inverting images (byte) */
80 protected static LookupTable invertSingleByteTable;
81 protected static LookupTable invertRgbaByteTable;
82 protected static boolean needsInvertRgba = false;
83 /* RescaleOp for contrast/brightness operation */
84 protected static boolean needsRescaleRgba = false;
85 /* lookup table for false-color */
86 protected static LookupTable mapBgrByteTable;
87 protected static boolean needsMapBgr = false;
88
89 static {
90 /*
91 * create static lookup tables
92 */
93 byte[] invertByte = new byte[256];
94 byte[] orderedByte = new byte[256];
95 byte[] nullByte = new byte[256];
96 byte[] mapR = new byte[256];
97 byte[] mapG = new byte[256];
98 byte[] mapB = new byte[256];
99 for (int i = 0; i < 256; ++i) {
100 // counting down
101 invertByte[i] = (byte) (256 - i);
102 // counting up
103 orderedByte[i] = (byte) i;
104 // constant 0
105 nullByte[i] = 0;
106 // three overlapping slopes
107 if (i < 64) {
108 mapR[i] = 0;
109 mapG[i] = (byte) (4 * i);
110 mapB[i] = (byte) 255;
111 } else if (i >= 64 && i < 192) {
112 mapR[i] = (byte) (2 * (i - 64));
113 mapG[i] = (byte) 255;
114 mapB[i] = (byte) (255 - 2 * (i - 64));
115 } else {
116 mapR[i] = (byte) 255;
117 mapG[i] = (byte) (255 - (4 * (i - 192)));
118 mapB[i] = 0;
119 }
120 }
121 // should(!) work for all color models
122 invertSingleByteTable = new ByteLookupTable(0, invertByte);
123 // but doesn't work with alpha channel on all platforms
124 String ver = System.getProperty("java.version");
125 String os = System.getProperty("os.name");
126 logger.debug("os="+os+" ver="+ver);
127 if (os.startsWith("Linux") && ver.startsWith("1.6")) {
128 // GRAB(WTF?) works in Linux JDK1.6 with transparency
129 invertRgbaByteTable = new ByteLookupTable(0, new byte[][] {
130 invertByte, invertByte, orderedByte, invertByte});
131 needsInvertRgba = true;
132 needsRescaleRgba = true;
133 needsMapBgr = true;
134 } else {
135 invertRgbaByteTable = invertSingleByteTable;
136 }
137 // this hopefully works for all
138 mapBgrByteTable = new ByteLookupTable(0, new byte[][] {
139 mapR, mapG, mapB});
140 }
141
142 /** the size of the current image */
143 protected ImageSize imageSize;
144
145
146 /* loadSubimage is supported. */
147 public boolean isSubimageSupported() {
148 return true;
149 }
150
151 public void setQuality(int qual) {
152 quality = qual;
153 renderHint = new RenderingHints(null);
154 // hint.put(RenderingHints.KEY_ANTIALIASING,
155 // RenderingHints.VALUE_ANTIALIAS_OFF);
156 // setup interpolation quality
157 if (qual > 0) {
158 logger.debug("quality q1");
159 renderHint.put(RenderingHints.KEY_INTERPOLATION,
160 RenderingHints.VALUE_INTERPOLATION_BICUBIC);
161 } else {
162 logger.debug("quality q0");
163 renderHint.put(RenderingHints.KEY_INTERPOLATION,
164 RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
165 }
166 }
167
168 /* returns the size of the current image */
169 public ImageSize getSize() {
170 if (imageSize == null) {
171 int h = 0;
172 int w = 0;
173 try {
174 if (img == null) {
175 ImageReader reader = getReader(input);
176 // get size from ImageReader
177 h = reader.getHeight(0);
178 w = reader.getWidth(0);
179 } else {
180 // get size from image
181 h = img.getHeight();
182 w = img.getWidth();
183 }
184 imageSize = new ImageSize(w, h);
185 } catch (IOException e) {
186 logger.debug("error in getSize:", e);
187 }
188 }
189 return imageSize;
190 }
191
192 /* returns a list of supported image formats */
193 public Iterator<String> getSupportedFormats() {
194 String[] formats = ImageIO.getReaderFormatNames();
195 return Arrays.asList(formats).iterator();
196 }
197
198 /* Check image size and type and store in ImageInput */
199 public ImageInput identify(ImageInput input) throws IOException {
200 // try parent method first
201 ImageInput ii = super.identify(input);
202 if (ii != null) {
203 return ii;
204 }
205 logger.debug("identifying (ImageIO) " + input);
206 ImageReader reader = null;
207 try {
208 /*
209 * try ImageReader
210 */
211 reader = getReader(input);
212 // set size
213 ImageSize d = new ImageSize(reader.getWidth(0), reader.getHeight(0));
214 input.setSize(d);
215 // set mime type
216 if (input.getMimetype() == null) {
217 if (input.hasFile()) {
218 String t = FileOps.mimeForFile(input.getFile());
219 input.setMimetype(t);
220 } else {
221 // FIXME: is format name a mime type???
222 String t = reader.getFormatName();
223 input.setMimetype(t);
224 }
225 }
226 return input;
227 } catch (FileOpException e) {
228 // maybe just our class doesn't know what to do
229 logger.error("ImageLoaderDocuimage unable to identify:", e);
230 return null;
231 } finally {
232 if (reader != null) {
233 reader.dispose();
234 }
235 }
236 }
237
238 /* load image file */
239 public void loadImage(ImageInput ii) throws FileOpException {
240 logger.debug("loadImage: " + ii);
241 this.input = ii;
242 try {
243 if (ii.hasImageInputStream()) {
244 img = ImageIO.read(ii.getImageInputStream());
245 } else if (ii.hasFile()) {
246 img = ImageIO.read(ii.getFile());
247 }
248 } catch (IOException e) {
249 throw new FileOpException("Error reading image.");
250 }
251 }
252
253 /**
254 * Get an ImageReader for the image file.
255 *
256 * @return
257 */
258 public ImageReader getReader(ImageInput input) throws IOException {
259 logger.debug("get ImageReader for " + input);
260 ImageInputStream istream = null;
261 if (input.hasImageInputStream()) {
262 // stream input
263 istream = input.getImageInputStream();
264 } else if (input.hasFile()) {
265 // file only input
266 RandomAccessFile rf = new RandomAccessFile(input.getFile(), "r");
267 istream = new FileImageInputStream(rf);
268 } else {
269 throw new FileOpException("Unable to get data from ImageInput");
270 }
271 Iterator<ImageReader> readers;
272 String mt = null;
273 if (input.hasMimetype()) {
274 // check hasMimetype first or we might get into a loop
275 mt = input.getMimetype();
276 } else {
277 // try file extension
278 mt = FileOps.mimeForFile(input.getFile());
279 }
280 if (mt == null) {
281 logger.debug("No mime-type. Trying automagic.");
282 readers = ImageIO.getImageReaders(istream);
283 } else {
284 logger.debug("File type:" + mt);
285 readers = ImageIO.getImageReadersByMIMEType(mt);
286 }
287 if (!readers.hasNext()) {
288 throw new FileOpException("Can't find Reader to load File!");
289 }
290 ImageReader reader = readers.next();
291 /* are there more readers? */
292 logger.debug("ImageIO: this reader: " + reader.getClass());
293 /* while (readers.hasNext()) {
294 logger.debug("ImageIO: next reader: " + readers.next().getClass());
295 } */
296 reader.setInput(istream);
297 return reader;
298 }
299
300 /* Load an image file into the Object. */
301 public void loadSubimage(ImageInput ii, Rectangle region, int prescale)
302 throws FileOpException {
303 logger.debug("loadSubimage");
304 this.input = ii;
305 ImageReader reader = null;
306 try {
307 reader = getReader(ii);
308 // set up reader parameters
309 ImageReadParam readParam = reader.getDefaultReadParam();
310 readParam.setSourceRegion(region);
311 if (prescale > 1) {
312 readParam.setSourceSubsampling(prescale, prescale, 0, 0);
313 }
314 // read image
315 logger.debug("loading..");
316 img = reader.read(0, readParam);
317 logger.debug("loaded");
318 /* downconversion of highcolor images seems not to work
319 if (img.getColorModel().getComponentSize(0) > 8) {
320 logger.debug("converting to 8bit");
321 BufferedImage dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
322 dest.createGraphics().drawImage(img, null, 0, 0);
323 img = dest;
324 } */
325 } catch (IOException e) {
326 throw new FileOpException("Unable to load File!");
327 } finally {
328 if (reader != null) {
329 reader.dispose();
330 }
331 }
332 }
333
334 /* write image of type mt to Stream */
335 public void writeImage(String mt, OutputStream ostream)
336 throws ImageOpException, ServletException {
337 logger.debug("writeImage");
338 // setup output
339 ImageWriter writer = null;
340 ImageOutputStream imgout = null;
341 try {
342 imgout = ImageIO.createImageOutputStream(ostream);
343 if (mt == "image/jpeg") {
344 /*
345 * JPEG doesn't do transparency so we have to convert any RGBA
346 * image to RGB or we the client will think its CMYK :-( *Java2D BUG*
347 */
348 if (img.getColorModel().hasAlpha()) {
349 logger.debug("BARF: JPEG with transparency!!");
350 BufferedImage rgbImg = new BufferedImage(img.getWidth(),
351 img.getHeight(), BufferedImage.TYPE_INT_RGB);
352 rgbImg.createGraphics().drawImage(img, null, 0, 0);
353 img = rgbImg;
354 }
355 writer = ImageIO.getImageWritersByFormatName("jpeg").next();
356 if (writer == null) {
357 throw new ImageOpException("Unable to get JPEG writer");
358 }
359 ImageWriteParam param = writer.getDefaultWriteParam();
360 if (quality > 1) {
361 // change JPEG compression quality
362 param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
363 param.setCompressionQuality(0.9f);
364 }
365 writer.setOutput(imgout);
366 // render output
367 logger.debug("writing JPEG");
368 writer.write(null, new IIOImage(img, null, null), param);
369 } else if (mt == "image/png") {
370 // render output
371 writer = ImageIO.getImageWritersByFormatName("png").next();
372 if (writer == null) {
373 throw new ImageOpException("Unable to get PNG writer");
374 }
375 writer.setOutput(imgout);
376 logger.debug("writing PNG");
377 writer.write(img);
378 } else {
379 // unknown mime type
380 throw new ImageOpException("Unknown mime type: " + mt);
381 }
382
383 } catch (IOException e) {
384 logger.error("Error writing image:", e);
385 throw new ServletException("Error writing image:", e);
386 }
387 // TODO: should we: finally { writer.dispose(); }
388 }
389
390 public void scale(double scaleX, double scaleY) throws ImageOpException {
391 logger.debug("scale: " + scaleX);
392 /* for downscaling in high quality the image is blurred first */
393 if ((scaleX <= 0.5) && (quality > 1)) {
394 int bl = (int) Math.floor(1 / scaleX);
395 blur(bl);
396 }
397 /* then scaled */
398 AffineTransformOp scaleOp = new AffineTransformOp(
399 AffineTransform.getScaleInstance(scaleX, scaleY), renderHint);
400 img = scaleOp.filter(img, null);
401 logger.debug("scaled to " + img.getWidth() + "x" + img.getHeight()
402 + " img=" + img);
403 }
404
405 public void blur(int radius) throws ImageOpException {
406 logger.debug("blur: " + radius);
407 // minimum radius is 2
408 int klen = Math.max(radius, 2);
409 Kernel blur = null;
410 if (klen < convolutionKernels.length) {
411 // use precalculated Kernel
412 blur = convolutionKernels[klen];
413 } else {
414 // calculate our own kernel
415 int ksize = klen * klen;
416 // kernel is constant 1/k
417 float f = 1f / ksize;
418 float[] kern = new float[ksize];
419 for (int i = 0; i < ksize; ++i) {
420 kern[i] = f;
421 }
422 blur = new Kernel(klen, klen, kern);
423 }
424 // blur with convolve operation
425 ConvolveOp blurOp = new ConvolveOp(blur, ConvolveOp.EDGE_NO_OP,
426 renderHint);
427 BufferedImage dest = null;
428 // blur needs explicit destination image type for 3BYTE_BGR *Java2D BUG*
429 if (img.getType() == BufferedImage.TYPE_3BYTE_BGR) {
430 logger.debug("blur: fixing destination image type");
431 dest = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
432 }
433 img = blurOp.filter(img, dest);
434 logger.debug("blurred: "+img);
435 }
436
437 public void crop(int x_off, int y_off, int width, int height)
438 throws ImageOpException {
439 // setup Crop
440 img = img.getSubimage(x_off, y_off, width, height);
441 logger.debug("CROP:" + img.getWidth() + "x"
442 + img.getHeight());
443 }
444
445 public void rotate(double angle) throws ImageOpException {
446 logger.debug("rotate: " + angle);
447 // setup rotation
448 double rangle = Math.toRadians(angle);
449 // center of rotation is center of image
450 double w = img.getWidth();
451 double h = img.getHeight();
452 double x = (w / 2);
453 double y = (h / 2);
454 AffineTransform trafo = AffineTransform.getRotateInstance(rangle, x, y);
455 AffineTransformOp rotOp = new AffineTransformOp(trafo, renderHint);
456 // rotate bounds to see how much of the image would be off screen
457 Rectangle2D rotbounds = rotOp.getBounds2D(img);
458 double xoff = rotbounds.getX();
459 double yoff = rotbounds.getY();
460 if (Math.abs(xoff) > epsilon || Math.abs(yoff) > epsilon) {
461 // move image back on screen
462 logger.debug("move rotation: xoff="+xoff+" yoff="+yoff);
463 trafo.preConcatenate(AffineTransform.getTranslateInstance(-xoff, -yoff));
464 rotOp = new AffineTransformOp(trafo, renderHint);
465 }
466 // transform image
467 img = rotOp.filter(img, null);
468 logger.debug("rotated: "+img);
469 }
470
471 public void mirror(double angle) throws ImageOpException {
472 logger.debug("mirror: " + angle);
473 // setup mirror
474 double mx = 1;
475 double my = 1;
476 double tx = 0;
477 double ty = 0;
478 if (Math.abs(angle - 0) < epsilon) { // 0 degree
479 mx = -1;
480 tx = img.getWidth();
481 } else if (Math.abs(angle - 90) < epsilon) { // 90 degree
482 my = -1;
483 ty = img.getHeight();
484 } else if (Math.abs(angle - 180) < epsilon) { // 180 degree
485 mx = -1;
486 tx = img.getWidth();
487 } else if (Math.abs(angle - 270) < epsilon) { // 270 degree
488 my = -1;
489 ty = img.getHeight();
490 } else if (Math.abs(angle - 360) < epsilon) { // 360 degree
491 mx = -1;
492 tx = img.getWidth();
493 } else {
494 logger.error("invalid mirror angle "+angle);
495 return;
496 }
497 AffineTransformOp mirOp = new AffineTransformOp(new AffineTransform(mx,
498 0, 0, my, tx, ty), renderHint);
499 img = mirOp.filter(img, null);
500 }
501
502 public void enhance(float mult, float add) throws ImageOpException {
503 RescaleOp op = null;
504 logger.debug("enhance: img=" + img);
505 if (needsRescaleRgba) {
506 /*
507 * Only one constant should work regardless of the number of bands
508 * according to the JDK spec. Doesn't work on JDK 1.4 for OSX and
509 * Linux (at least).
510 *
511 * The number of constants must match the number of bands in the
512 * image.
513 */
514 int ncol = img.getColorModel().getNumComponents();
515 float[] dm = new float[ncol];
516 float[] da = new float[ncol];
517 for (int i = 0; i < ncol; i++) {
518 dm[i] = (float) mult;
519 da[i] = (float) add;
520 }
521 op = new RescaleOp(dm, da, null);
522 } else {
523 op = new RescaleOp(mult, add, renderHint);
524 }
525 op.filter(img, img);
526 }
527
528 public void enhanceRGB(float[] rgbm, float[] rgba) throws ImageOpException {
529 logger.debug("enhanceRGB: rgbm="+rgbm+" rgba="+rgba);
530 /*
531 * The number of constants must match the number of bands in the image.
532 * We do only 3 (RGB) bands.
533 */
534 int ncol = img.getColorModel().getNumColorComponents();
535 if ((ncol != 3) || (rgbm.length != 3) || (rgba.length != 3)) {
536 logger.error("enhanceRGB: unknown number of color bands or coefficients ("
537 + ncol + ")");
538 return;
539 }
540 if (img.getColorModel().hasAlpha()) {
541 // add constant for alpha
542 rgbm = new float[] {rgbm[0], rgbm[1], rgbm[2], 1};
543 rgba = new float[] {rgba[0], rgba[1], rgba[2], 0};
544 }
545 RescaleOp scaleOp = new RescaleOp(rgbm, rgba, renderHint);
546 scaleOp.filter(img, img);
547 }
548
549 /*
550 * (non-Javadoc)
551 *
552 * @see
553 * digilib.image.DocuImageImpl#colorOp(digilib.image.DocuImage.ColorOps)
554 */
555 public void colorOp(ColorOp colop) throws ImageOpException {
556 if (colop == ColorOp.GRAYSCALE) {
557 /*
558 * convert image to grayscale
559 */
560 logger.debug("Color op: grayscaling");
561 ColorModel cm = img.getColorModel();
562 if (cm.getNumColorComponents() < 3) {
563 // grayscale already
564 logger.debug("Color op: not grayscaling");
565 return;
566 }
567 ColorConvertOp op = new ColorConvertOp(
568 ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
569 // let filter create new image
570 img = op.filter(img, null);
571 } else if (colop == ColorOp.NTSC_GRAY) {
572 /*
573 * convert image to grayscale NTSC-style: luminance = 0.2989*red +
574 * 0.5870*green + 0.1140*blue
575 */
576 logger.debug("Color op: NTSC gray");
577 logger.debug("img="+img);
578 ColorModel cm = img.getColorModel();
579 if (cm.getNumColorComponents() < 3 || cm instanceof IndexColorModel) {
580 // grayscale already or not possible
581 logger.debug("Color op: unable to NTSC gray");
582 return;
583 }
584 float[][] combineFn = new float[1][4];
585 combineFn[0] = new float[] { 0.299f, 0.587f, 0.114f, 0f };
586 BandCombineOp op = new BandCombineOp(combineFn, renderHint);
587 // BandCombineOp only works on Rasters so we create a
588 // new image and use its Raster
589 BufferedImage dest = new BufferedImage(img.getWidth(),
590 img.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
591 op.filter(img.getRaster(), dest.getRaster());
592 img = dest;
593 } else if (colop == ColorOp.INVERT) {
594 /*
595 * invert colors i.e. invert every channel
596 */
597 logger.debug("Color op: inverting");
598 LookupTable invtbl = null;
599 ColorModel cm = img.getColorModel();
600 if (cm instanceof IndexColorModel) {
601 // invert not possible
602 // TODO: should we convert?
603 logger.debug("Color op: unable to invert");
604 return;
605 }
606 if (needsInvertRgba && cm.hasAlpha()) {
607 // fix for some cases
608 invtbl = invertRgbaByteTable;
609 } else {
610 invtbl = invertSingleByteTable;
611 }
612 LookupOp op = new LookupOp(invtbl, renderHint);
613 logger.debug("colop: image=" + img);
614 op.filter(img, img);
615 } else if (colop == ColorOp.MAP_GRAY_BGR) {
616 /*
617 * false color image from grayscale (0: blue, 128: green, 255: red)
618 */
619 logger.debug("Color op: map_gray_bgr");
620 // convert to grayscale
621 ColorConvertOp grayOp = new ColorConvertOp(
622 ColorSpace.getInstance(ColorSpace.CS_GRAY), renderHint);
623 // create new 3-channel image
624 int destType = BufferedImage.TYPE_INT_RGB;
625 if (needsMapBgr) {
626 // special case for broken Java2Ds
627 destType = BufferedImage.TYPE_3BYTE_BGR;
628 }
629 BufferedImage dest = new BufferedImage(img.getWidth(),
630 img.getHeight(), destType);
631 img = grayOp.filter(img, dest);
632 logger.debug("map_gray: image=" + img);
633 // convert to false color
634 LookupOp mapOp = new LookupOp(mapBgrByteTable, renderHint);
635 mapOp.filter(img, img);
636 logger.debug("mapped image=" + img);
637 }
638 }
639
640 public void dispose() {
641 // is this necessary?
642 img = null;
643 }
644
645 public Image getAwtImage(){
646 return (Image) img;
647 }
648
649
650 }