001: /*
002: * Helma License Notice
003: *
004: * The contents of this file are subject to the Helma License
005: * Version 2.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://adele.helma.org/download/helma/license.txt
008: *
009: * Copyright 1998-2003 Helma Software. All Rights Reserved.
010: *
011: * $RCSfile$
012: * $Author: root $
013: * $Revision: 8604 $
014: * $Date: 2007-09-28 15:16:38 +0200 (Fre, 28 Sep 2007) $
015: */
016:
017: /*
018: * A few explanations:
019: *
020: * - this.image is either an AWT Image or a BufferedImage.
021: * It depends on the ImageGenerator in what form the Image initially is.
022: * (the ImageIO implementation only uses BufferedImages for example.)
023: *
024: * As soon as some action that needs the graphics object is performed and the
025: * image is still in AWT format, it is converted to a BufferedImage
026: *
027: * Any internal function that performs graphical actions needs to call
028: * getGraphics, never rely on this.graphics being set correctly!
029: *
030: * - ImageWrapper objects are created and safed by the ImageGenerator class
031: * all different implementations of Imaging functionallity are implemented
032: * as a ImageGenerator extending class.
033: *
034: */
035:
036: package helma.image;
037:
038: import java.awt.*;
039: import java.awt.geom.AffineTransform;
040: import java.awt.image.*;
041: import java.io.*;
042:
043: /**
044: * Abstract base class for Image Wrappers.
045: */
046: public class ImageWrapper {
047: protected Image image;
048: protected int width;
049: protected int height;
050: protected ImageGenerator generator;
051: private Graphics2D graphics;
052:
053: /**
054: * Creates a new ImageWrapper object.
055: *
056: * @param image ...
057: * @param width ...
058: * @param height ...
059: */
060: public ImageWrapper(Image image, int width, int height,
061: ImageGenerator generator) {
062: this .image = image;
063: this .width = width;
064: this .height = height;
065: this .generator = generator;
066: // graphics are turned off by default. getGraphics activates it if necessary.
067: this .graphics = null;
068: }
069:
070: public ImageWrapper(Image image, ImageGenerator generator) {
071: this (image, image.getWidth(null), image.getHeight(null),
072: generator);
073: }
074:
075: /**
076: * Converts the internal image object to a BufferedImage (if it's not
077: * already) and returns it. this is necessary as not all images are of type
078: * BufferedImage. e.g. images loaded from a resource with the Toolkit are
079: * not. By using getBufferedImage, images are only converted to a
080: * getBufferedImage when this is actually needed, which is better than
081: * storing images as BufferedImage in general.
082: *
083: * @return the Image object as a BufferedImage
084: */
085: public BufferedImage getBufferedImage() {
086: if (!(image instanceof BufferedImage)) {
087: BufferedImage buffered = new BufferedImage(width, height,
088: BufferedImage.TYPE_INT_ARGB);
089: Graphics2D g2d = buffered.createGraphics();
090: g2d.drawImage(image, 0, 0, null);
091: g2d.dispose();
092: setImage(buffered);
093: }
094: return (BufferedImage) image;
095: }
096:
097: /**
098: * Returns the Graphics object to directly paint to this Image. Converts the
099: * internal image to a BufferedImage if necessary.
100: *
101: * @return the Graphics object for drawing into this image
102: */
103: public Graphics2D getGraphics() {
104: if (graphics == null) {
105: // make sure the image is a BufferedImage and then create a graphics object
106: BufferedImage img = getBufferedImage();
107: graphics = img.createGraphics();
108: }
109: return graphics;
110: }
111:
112: /**
113: * Sets the internal image and clears the stored graphics object.
114: * Any code that is changing the internal image should do it through this function
115: * to make sure getGraphcis() returns a valid graphics object the next time it is called.
116: */
117: protected void setImage(Image img) {
118: // flush image and dispose graphics before updating them
119: if (graphics != null) {
120: graphics.dispose();
121: graphics = null;
122: }
123: if (image != null) {
124: image.flush();
125: }
126: image = img;
127: width = image.getWidth(null);
128: height = image.getHeight(null);
129: }
130:
131: /**
132: * Creates and returns a copy of this image.
133: *
134: * @return a clone of this image.
135: */
136: public Object clone() {
137: ImageWrapper wrapper = generator.createImage(this .width,
138: this .height);
139: wrapper.getGraphics().drawImage(image, 0, 0, null);
140: return wrapper;
141: }
142:
143: /**
144: * Returns the Image object represented by this ImageWrapper.
145: *
146: * @return the image object
147: */
148: public Image getImage() {
149: return image;
150: }
151:
152: /**
153: * Returns the ImageProducer of the wrapped image
154: *
155: * @return the images's ImageProducer
156: */
157: public ImageProducer getSource() {
158: return image.getSource();
159: }
160:
161: /**
162: * Dispose the Graphics context and null out the image.
163: */
164: public void dispose() {
165: if (image != null) {
166: image.flush();
167: image = null;
168: }
169: if (graphics != null) {
170: graphics.dispose();
171: graphics = null;
172: }
173: }
174:
175: /**
176: * Set the font used to write on this image.
177: */
178: public void setFont(String name, int style, int size) {
179: getGraphics().setFont(new Font(name, style, size));
180: }
181:
182: /**
183: * Sets the color used to write/paint to this image.
184: *
185: * @param red ...
186: * @param green ...
187: * @param blue ...
188: */
189: public void setColor(int red, int green, int blue) {
190: getGraphics().setColor(new Color(red, green, blue));
191: }
192:
193: /**
194: * Sets the color used to write/paint to this image.
195: *
196: * @param color ...
197: */
198: public void setColor(int color) {
199: getGraphics().setColor(new Color(color));
200: }
201:
202: /**
203: * Sets the color used to write/paint to this image.
204: *
205: * @param color ...
206: */
207: public void setColor(Color color) {
208: getGraphics().setColor(color);
209: }
210:
211: /**
212: * Sets the color used to write/paint to this image.
213: *
214: * @param color ...
215: */
216: public void setColor(String color) {
217: getGraphics().setColor(Color.decode(color));
218: }
219:
220: /**
221: * Draws a string to this image at the given coordinates.
222: *
223: * @param str ...
224: * @param x ...
225: * @param y ...
226: */
227: public void drawString(String str, int x, int y) {
228: getGraphics().drawString(str, x, y);
229: }
230:
231: /**
232: * Draws a line to this image from x1/y1 to x2/y2.
233: *
234: * @param x1 ...
235: * @param y1 ...
236: * @param x2 ...
237: * @param y2 ...
238: */
239: public void drawLine(int x1, int y1, int x2, int y2) {
240: getGraphics().drawLine(x1, y1, x2, y2);
241: }
242:
243: /**
244: * Draws a rectangle to this image.
245: *
246: * @param x ...
247: * @param y ...
248: * @param w ...
249: * @param h ...
250: */
251: public void drawRect(int x, int y, int w, int h) {
252: getGraphics().drawRect(x, y, w, h);
253: }
254:
255: /**
256: * Draws another image to this image.
257: *
258: * @param filename ...
259: * @param x ...
260: * @param y ...
261: */
262: public void drawImage(String filename, int x, int y)
263: throws IOException {
264: Image img = generator.read(filename);
265: if (img != null)
266: getGraphics().drawImage(img, x, y, null);
267: }
268:
269: /**
270: * Draws another image to this image.
271: *
272: * @param image ...
273: * @param x ...
274: * @param y ...
275: */
276: public void drawImage(ImageWrapper image, int x, int y)
277: throws IOException {
278: getGraphics().drawImage(image.getImage(), x, y, null);
279: }
280:
281: /**
282: * Draws another image to this image.
283: *
284: * @param image ...
285: * @param at ...
286: */
287: public void drawImage(ImageWrapper image, AffineTransform at)
288: throws IOException {
289: getGraphics().drawImage(image.getImage(), at, null);
290: }
291:
292: /**
293: * Draws a filled rectangle to this image.
294: *
295: * @param x ...
296: * @param y ...
297: * @param w ...
298: * @param h ...
299: */
300: public void fillRect(int x, int y, int w, int h) {
301: getGraphics().fillRect(x, y, w, h);
302: }
303:
304: /**
305: * Returns the width of this image.
306: *
307: * @return the width of this image
308: */
309: public int getWidth() {
310: return width;
311: }
312:
313: /**
314: * Returns the height of this image.
315: *
316: * @return the height of this image
317: */
318: public int getHeight() {
319: return height;
320: }
321:
322: /**
323: * Crops the image.
324: *
325: * @param x ...
326: * @param y ...
327: * @param w ...
328: * @param h ...
329: */
330: public void crop(int x, int y, int w, int h) {
331: // do not use the CropFilter any longer:
332: if (image instanceof BufferedImage && x + w <= width
333: && y + h <= height) {
334: // BufferedImages define their own function for cropping:
335: setImage(((BufferedImage) image).getSubimage(x, y, w, h));
336: } else {
337: // The internal image will be a BufferedImage after this.
338: // Simply create it with the cropped dimensions and draw the image into it:
339: BufferedImage buffered = new BufferedImage(w, h,
340: BufferedImage.TYPE_INT_ARGB);
341: Graphics2D g2d = buffered.createGraphics();
342: g2d.drawImage(image, -x, -y, null);
343: g2d.dispose();
344: setImage(buffered);
345: }
346: }
347:
348: /**
349: * Trims the image.
350: *
351: * @param x the x-coordinate of the pixel specifying the background color
352: * @param y the y-coordinate of the pixel specifying the background color
353: */
354:
355: public void trim(int x, int y) {
356: trim(x, y, true, true, true, true);
357: }
358:
359: /**
360: * Trims the image.
361: *
362: * @param x
363: * @param y
364: * @param trimLeft
365: * @param trimTop
366: * @param trimRight
367: * @param trimBottom
368: */
369: public void trim(int x, int y, boolean trimLeft, boolean trimTop,
370: boolean trimRight, boolean trimBottom) {
371: BufferedImage bi = this .getBufferedImage();
372: int color = bi.getRGB(x, y), pixel;
373: int left = 0, top = 0, right = width - 1, bottom = height - 1;
374:
375: // create a BufferedImage of only 1 pixel height for fetching the rows of the image in the correct format (ARGB)
376: // This speeds up things by more than factor 2, compared to the standard BufferedImage.getRGB solution,
377: // which is supposed to be fast too. This is probably the case because drawing to BufferedImages uses
378: // very optimized code which may even be hardware accelerated.
379: if (trimTop || trimBottom) {
380: BufferedImage row = new BufferedImage(width, 1,
381: BufferedImage.TYPE_INT_ARGB);
382: Graphics2D g2d = row.createGraphics();
383: int pixels[] = ((DataBufferInt) row.getRaster()
384: .getDataBuffer()).getData();
385: // make sure alpha values do not add up for each row:
386: g2d.setComposite(AlphaComposite.Src);
387: if (trimTop) {
388: // top:
389: for (top = 0; top < height; top++) {
390: g2d.drawImage(bi, null, 0, -top);
391: // now pixels contains the rgb values of the row y!
392: // scan this row now:
393: for (x = 0; x < width; x++) {
394: if (pixels[x] != color)
395: break;
396: }
397: if (x < width)
398: break;
399: }
400: }
401: if (trimBottom) {
402: // bottom:
403: for (bottom = height - 1; bottom > top; bottom--) {
404: g2d.drawImage(bi, null, 0, -bottom);
405: // now pixels contains the rgb values of the row y!
406: // scan this row now:
407: for (x = 0; x < width; x++) {
408: if (pixels[x] != color)
409: break;
410: }
411: if (x < width)
412: break;
413: }
414: }
415: g2d.dispose();
416: }
417: if (trimLeft || trimRight) {
418: BufferedImage column = new BufferedImage(1, height,
419: BufferedImage.TYPE_INT_ARGB);
420: Graphics2D g2d = column.createGraphics();
421: int pixels[] = ((DataBufferInt) column.getRaster()
422: .getDataBuffer()).getData();
423: // make sure alpha values do not add up for each row:
424: g2d.setComposite(AlphaComposite.Src);
425: if (trimLeft) {
426: // left:
427: for (left = 0; left < width; left++) {
428: g2d.drawImage(bi, null, -left, 0);
429: // now pixels contains the rgb values of the row y!
430: // scan this row now:
431: for (y = 0; y < height; y++) {
432: if (pixels[y] != color)
433: break;
434: }
435: if (y < height)
436: break;
437: }
438: }
439: if (trimRight) {
440: // right:
441: for (right = width - 1; right > left; right--) {
442: g2d.drawImage(bi, null, -right, 0);
443: // now pixels contains the rgb values of the row y!
444: // scan this row now:
445: for (y = 0; y < height; y++) {
446: if (pixels[y] != color)
447: break;
448: }
449: if (y < height)
450: break;
451: }
452: }
453: g2d.dispose();
454: }
455: crop(left, top, right - left + 1, bottom - top + 1);
456: }
457:
458: /**
459: * resizes the image using the Graphics2D approach
460: */
461: protected void resize(int w, int h, boolean smooth) {
462: BufferedImage buffered = new BufferedImage(w, h,
463: BufferedImage.TYPE_INT_ARGB);
464: Graphics2D g2d = buffered.createGraphics();
465:
466: g2d
467: .setRenderingHint(
468: RenderingHints.KEY_INTERPOLATION,
469: smooth ? RenderingHints.VALUE_INTERPOLATION_BICUBIC
470: : RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
471:
472: g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
473: smooth ? RenderingHints.VALUE_RENDER_QUALITY
474: : RenderingHints.VALUE_RENDER_SPEED);
475:
476: AffineTransform at = AffineTransform.getScaleInstance(
477: (double) w / width, (double) h / height);
478: g2d.drawImage(image, at, null);
479: g2d.dispose();
480: setImage(buffered);
481: }
482:
483: /**
484: * Resize the image
485: *
486: * @param w ...
487: * @param h ...
488: */
489: public void resize(int w, int h) {
490: double factor = Math.max((double) w / width, (double) h
491: / height);
492: // if the image is scaled, used the Graphcis2D method, otherwise use AWT:
493: if (factor > 1f) {
494: // scale it with the Graphics2D approach for supperiour quality.
495: resize(w, h, true);
496: } else {
497: // Area averaging has the best results for shrinking of images:
498:
499: // as getScaledInstance is asynchronous, the ImageWaiter is needed here too:
500: // Image scaled = ImageWaiter.waitForImage(image.getScaledInstance(w, h, Image.SCALE_AREA_AVERAGING));
501: // if (scaled == null)
502: // throw new RuntimeException("Image cannot be resized.");
503:
504: // this version is up to 4 times faster than getScaledInstance:
505: ImageFilterOp filter = new ImageFilterOp(
506: new AreaAveragingScaleFilter(w, h));
507: setImage(filter.filter(getBufferedImage(), null));
508: }
509: }
510:
511: /**
512: * Resize the image, using a fast and cheap algorithm
513: *
514: * @param w ...
515: * @param h ...
516: */
517: public void resizeFast(int w, int h) {
518: resize(w, h, false);
519: }
520:
521: /**
522: * Reduces the colors used in the image. Necessary before saving as GIF.
523: *
524: * @param colors colors the number of colors to use, usually <= 256.
525: */
526: public void reduceColors(int colors) {
527: reduceColors(colors, false);
528: }
529:
530: /**
531: * Reduces the colors used in the image. Necessary before saving as GIF.
532: *
533: * @param colors colors the number of colors to use, usually <= 256.
534: * @param dither ...
535: */
536: public void reduceColors(int colors, boolean dither) {
537: reduceColors(colors, dither, true);
538: }
539:
540: /**
541: * Reduce the colors used in this image. Useful and necessary before saving
542: * the image as GIF file.
543: *
544: * @param colors the number of colors to use, usually <= 256.
545: * @param dither ...
546: * @param alphaToBitmask ...
547: */
548:
549: public void reduceColors(int colors, boolean dither,
550: boolean alphaToBitmask) {
551: setImage(ColorQuantizer.quantizeImage(getBufferedImage(),
552: colors, dither, alphaToBitmask));
553: }
554:
555: /**
556: * Save the image. Image format is deduced from filename.
557: *
558: * @param filename ...
559: * @throws IOException
560: */
561: public void saveAs(String filename) throws IOException {
562: saveAs(filename, -1f, false); // -1 means default quality
563: }
564:
565: /**
566: * Saves the image. Image format is deduced from filename.
567: *
568: * @param filename ...
569: * @param quality ...
570: * @throws IOException
571: */
572: public void saveAs(String filename, float quality)
573: throws IOException {
574: saveAs(filename, quality, false);
575: }
576:
577: /**
578: * Saves the image. Image format is deduced from filename.
579: *
580: * @param filename ...
581: * @param quality ...
582: * @param alpha ...
583: * @throws IOException
584: */
585: public void saveAs(String filename, float quality, boolean alpha)
586: throws IOException {
587: generator.write(this , checkFilename(filename), quality, alpha);
588: }
589:
590: /**
591: * Saves the image. Image format is deduced from mimeType.
592: *
593: * @param out ...
594: * @param mimeType ...
595: * @throws IOException
596: */
597: public void saveAs(OutputStream out, String mimeType)
598: throws IOException {
599: generator.write(this , out, mimeType, -1f, false); // -1 means default quality
600: }
601:
602: /**
603: * Saves the image. Image format is deduced from mimeType.
604: *
605: * @param out ...
606: * @param mimeType ...
607: * @param quality ...
608: * @throws IOException
609: */
610: public void saveAs(OutputStream out, String mimeType, float quality)
611: throws IOException {
612: generator.write(this , out, mimeType, quality, false);
613: }
614:
615: /**
616: * Saves the image. Image format is deduced from mimeType.
617: *
618: * @param out ...
619: * @param mimeType ...
620: * @param quality ...
621: * @param alpha ...
622: * @throws IOException
623: */
624: public void saveAs(OutputStream out, String mimeType,
625: float quality, boolean alpha) throws IOException {
626: generator.write(this , out, mimeType, quality, alpha);
627: }
628:
629: /**
630: * Sets the palette index of the transparent color for Images with an
631: * IndexColorModel. This can be used together with
632: * {@link helma.image.ImageWrapper#getPixel}.
633: */
634: public void setTransparentPixel(int trans) {
635: BufferedImage bi = this .getBufferedImage();
636: ColorModel cm = bi.getColorModel();
637: if (!(cm instanceof IndexColorModel))
638: throw new RuntimeException("Image is not indexed!");
639: IndexColorModel icm = (IndexColorModel) cm;
640: int mapSize = icm.getMapSize();
641: byte reds[] = new byte[mapSize];
642: byte greens[] = new byte[mapSize];
643: byte blues[] = new byte[mapSize];
644: icm.getReds(reds);
645: icm.getGreens(greens);
646: icm.getBlues(blues);
647: // create the new IndexColorModel with the changed transparentPixel:
648: icm = new IndexColorModel(icm.getPixelSize(), mapSize, reds,
649: greens, blues, trans);
650: // create a new BufferedImage with the new IndexColorModel and the old
651: // raster:
652: setImage(new BufferedImage(icm, bi.getRaster(), false, null));
653: }
654:
655: /**
656: * Returns the pixel at x, y. If the image is indexed, it returns the
657: * palette index, otherwise the rgb code of the color is returned.
658: *
659: * @param x the x coordinate of the pixel
660: * @param y the y coordinate of the pixel
661: * @return the pixel at x, y
662: */
663: public int getPixel(int x, int y) {
664: BufferedImage bi = this .getBufferedImage();
665: if (bi.getColorModel() instanceof IndexColorModel)
666: return bi.getRaster().getSample(x, y, 0);
667: else
668: return bi.getRGB(x, y);
669: }
670:
671: /**
672: * Utility method to be used by write().
673: * Converts file name to absolute path and creates parent directories.
674: * @param filename the file name
675: * @return the absolute path for the file name
676: * @throws IOException if missing directories could not be created
677: */
678: String checkFilename(String filename) throws IOException {
679: File file = new File(filename).getAbsoluteFile();
680: File parent = file.getParentFile();
681: if (parent != null && !parent.exists() && !parent.mkdirs()) {
682: throw new IOException("Error creating directories for "
683: + filename);
684: }
685: return file.getPath();
686: }
687: }
|