001 /*
002 * Copyright 1997-2005 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025
026 package java.awt.image;
027
028 import java.awt.geom.AffineTransform;
029 import java.awt.geom.NoninvertibleTransformException;
030 import java.awt.geom.Rectangle2D;
031 import java.awt.geom.Point2D;
032 import java.awt.AlphaComposite;
033 import java.awt.GraphicsEnvironment;
034 import java.awt.Rectangle;
035 import java.awt.RenderingHints;
036 import java.awt.Transparency;
037 import sun.awt.image.ImagingLib;
038
039 /**
040 * This class uses an affine transform to perform a linear mapping from
041 * 2D coordinates in the source image or <CODE>Raster</CODE> to 2D coordinates
042 * in the destination image or <CODE>Raster</CODE>.
043 * The type of interpolation that is used is specified through a constructor,
044 * either by a <CODE>RenderingHints</CODE> object or by one of the integer
045 * interpolation types defined in this class.
046 * <p>
047 * If a <CODE>RenderingHints</CODE> object is specified in the constructor, the
048 * interpolation hint and the rendering quality hint are used to set
049 * the interpolation type for this operation. The color rendering hint
050 * and the dithering hint can be used when color conversion is required.
051 * <p>
052 * Note that the following constraints have to be met:
053 * <ul>
054 * <li>The source and destination must be different.
055 * <li>For <CODE>Raster</CODE> objects, the number of bands in the source must
056 * be equal to the number of bands in the destination.
057 * </ul>
058 * @see AffineTransform
059 * @see BufferedImageFilter
060 * @see java.awt.RenderingHints#KEY_INTERPOLATION
061 * @see java.awt.RenderingHints#KEY_RENDERING
062 * @see java.awt.RenderingHints#KEY_COLOR_RENDERING
063 * @see java.awt.RenderingHints#KEY_DITHERING
064 * @version 16 Apr 1998
065 */
066 public class AffineTransformOp implements BufferedImageOp, RasterOp {
067 private AffineTransform xform;
068 RenderingHints hints;
069
070 /**
071 * Nearest-neighbor interpolation type.
072 */
073 public static final int TYPE_NEAREST_NEIGHBOR = 1;
074
075 /**
076 * Bilinear interpolation type.
077 */
078 public static final int TYPE_BILINEAR = 2;
079
080 /**
081 * Bicubic interpolation type.
082 */
083 public static final int TYPE_BICUBIC = 3;
084
085 int interpolationType = TYPE_NEAREST_NEIGHBOR;
086
087 /**
088 * Constructs an <CODE>AffineTransformOp</CODE> given an affine transform.
089 * The interpolation type is determined from the
090 * <CODE>RenderingHints</CODE> object. If the interpolation hint is
091 * defined, it will be used. Otherwise, if the rendering quality hint is
092 * defined, the interpolation type is determined from its value. If no
093 * hints are specified (<CODE>hints</CODE> is null),
094 * the interpolation type is {@link #TYPE_NEAREST_NEIGHBOR
095 * TYPE_NEAREST_NEIGHBOR}.
096 *
097 * @param xform The <CODE>AffineTransform</CODE> to use for the
098 * operation.
099 *
100 * @param hints The <CODE>RenderingHints</CODE> object used to specify
101 * the interpolation type for the operation.
102 *
103 * @throws ImagingOpException if the transform is non-invertible.
104 * @see java.awt.RenderingHints#KEY_INTERPOLATION
105 * @see java.awt.RenderingHints#KEY_RENDERING
106 */
107 public AffineTransformOp(AffineTransform xform, RenderingHints hints) {
108 validateTransform(xform);
109 this .xform = (AffineTransform) xform.clone();
110 this .hints = hints;
111
112 if (hints != null) {
113 Object value = hints.get(hints.KEY_INTERPOLATION);
114 if (value == null) {
115 value = hints.get(hints.KEY_RENDERING);
116 if (value == hints.VALUE_RENDER_SPEED) {
117 interpolationType = TYPE_NEAREST_NEIGHBOR;
118 } else if (value == hints.VALUE_RENDER_QUALITY) {
119 interpolationType = TYPE_BILINEAR;
120 }
121 } else if (value == hints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
122 interpolationType = TYPE_NEAREST_NEIGHBOR;
123 } else if (value == hints.VALUE_INTERPOLATION_BILINEAR) {
124 interpolationType = TYPE_BILINEAR;
125 } else if (value == hints.VALUE_INTERPOLATION_BICUBIC) {
126 interpolationType = TYPE_BICUBIC;
127 }
128 } else {
129 interpolationType = TYPE_NEAREST_NEIGHBOR;
130 }
131 }
132
133 /**
134 * Constructs an <CODE>AffineTransformOp</CODE> given an affine transform
135 * and the interpolation type.
136 *
137 * @param xform The <CODE>AffineTransform</CODE> to use for the operation.
138 * @param interpolationType One of the integer
139 * interpolation type constants defined by this class:
140 * {@link #TYPE_NEAREST_NEIGHBOR TYPE_NEAREST_NEIGHBOR},
141 * {@link #TYPE_BILINEAR TYPE_BILINEAR},
142 * {@link #TYPE_BICUBIC TYPE_BICUBIC}.
143 * @throws ImagingOpException if the transform is non-invertible.
144 */
145 public AffineTransformOp(AffineTransform xform,
146 int interpolationType) {
147 validateTransform(xform);
148 this .xform = (AffineTransform) xform.clone();
149 switch (interpolationType) {
150 case TYPE_NEAREST_NEIGHBOR:
151 case TYPE_BILINEAR:
152 case TYPE_BICUBIC:
153 break;
154 default:
155 throw new IllegalArgumentException(
156 "Unknown interpolation type: " + interpolationType);
157 }
158 this .interpolationType = interpolationType;
159 }
160
161 /**
162 * Returns the interpolation type used by this op.
163 * @return the interpolation type.
164 * @see #TYPE_NEAREST_NEIGHBOR
165 * @see #TYPE_BILINEAR
166 * @see #TYPE_BICUBIC
167 */
168 public final int getInterpolationType() {
169 return interpolationType;
170 }
171
172 /**
173 * Transforms the source <CODE>BufferedImage</CODE> and stores the results
174 * in the destination <CODE>BufferedImage</CODE>.
175 * If the color models for the two images do not match, a color
176 * conversion into the destination color model is performed.
177 * If the destination image is null,
178 * a <CODE>BufferedImage</CODE> is created with the source
179 * <CODE>ColorModel</CODE>.
180 * <p>
181 * The coordinates of the rectangle returned by
182 * <code>getBounds2D(BufferedImage)</code>
183 * are not necessarily the same as the coordinates of the
184 * <code>BufferedImage</code> returned by this method. If the
185 * upper-left corner coordinates of the rectangle are
186 * negative then this part of the rectangle is not drawn. If the
187 * upper-left corner coordinates of the rectangle are positive
188 * then the filtered image is drawn at that position in the
189 * destination <code>BufferedImage</code>.
190 * <p>
191 * An <CODE>IllegalArgumentException</CODE> is thrown if the source is
192 * the same as the destination.
193 *
194 * @param src The <CODE>BufferedImage</CODE> to transform.
195 * @param dst The <CODE>BufferedImage</CODE> in which to store the results
196 * of the transformation.
197 *
198 * @return The filtered <CODE>BufferedImage</CODE>.
199 * @throws IllegalArgumentException if <code>src</code> and
200 * <code>dst</code> are the same
201 * @throws ImagingOpException if the image cannot be transformed
202 * because of a data-processing error that might be
203 * caused by an invalid image format, tile format, or
204 * image-processing operation, or any other unsupported
205 * operation.
206 */
207 public final BufferedImage filter(BufferedImage src,
208 BufferedImage dst) {
209
210 if (src == null) {
211 throw new NullPointerException("src image is null");
212 }
213 if (src == dst) {
214 throw new IllegalArgumentException(
215 "src image cannot be the "
216 + "same as the dst image");
217 }
218
219 boolean needToConvert = false;
220 ColorModel srcCM = src.getColorModel();
221 ColorModel dstCM;
222 BufferedImage origDst = dst;
223
224 if (dst == null) {
225 dst = createCompatibleDestImage(src, null);
226 dstCM = srcCM;
227 origDst = dst;
228 } else {
229 dstCM = dst.getColorModel();
230 if (srcCM.getColorSpace().getType() != dstCM
231 .getColorSpace().getType()) {
232 int type = xform.getType();
233 boolean needTrans = ((type & (xform.TYPE_MASK_ROTATION | xform.TYPE_GENERAL_TRANSFORM)) != 0);
234 if (!needTrans && type != xform.TYPE_TRANSLATION
235 && type != xform.TYPE_IDENTITY) {
236 double[] mtx = new double[4];
237 xform.getMatrix(mtx);
238 // Check out the matrix. A non-integral scale will force ARGB
239 // since the edge conditions can't be guaranteed.
240 needTrans = (mtx[0] != (int) mtx[0] || mtx[3] != (int) mtx[3]);
241 }
242
243 if (needTrans
244 && srcCM.getTransparency() == Transparency.OPAQUE) {
245 // Need to convert first
246 ColorConvertOp ccop = new ColorConvertOp(hints);
247 BufferedImage tmpSrc = null;
248 int sw = src.getWidth();
249 int sh = src.getHeight();
250 if (dstCM.getTransparency() == Transparency.OPAQUE) {
251 tmpSrc = new BufferedImage(sw, sh,
252 BufferedImage.TYPE_INT_ARGB);
253 } else {
254 WritableRaster r = dstCM
255 .createCompatibleWritableRaster(sw, sh);
256 tmpSrc = new BufferedImage(dstCM, r, dstCM
257 .isAlphaPremultiplied(), null);
258 }
259 src = ccop.filter(src, tmpSrc);
260 } else {
261 needToConvert = true;
262 dst = createCompatibleDestImage(src, null);
263 }
264 }
265
266 }
267
268 if (interpolationType != TYPE_NEAREST_NEIGHBOR
269 && dst.getColorModel() instanceof IndexColorModel) {
270 dst = new BufferedImage(dst.getWidth(), dst.getHeight(),
271 BufferedImage.TYPE_INT_ARGB);
272 }
273 if (ImagingLib.filter(this , src, dst) == null) {
274 throw new ImagingOpException(
275 "Unable to transform src image");
276 }
277
278 if (needToConvert) {
279 ColorConvertOp ccop = new ColorConvertOp(hints);
280 ccop.filter(dst, origDst);
281 } else if (origDst != dst) {
282 java.awt.Graphics2D g = origDst.createGraphics();
283 try {
284 g.setComposite(AlphaComposite.Src);
285 g.drawImage(dst, 0, 0, null);
286 } finally {
287 g.dispose();
288 }
289 }
290
291 return origDst;
292 }
293
294 /**
295 * Transforms the source <CODE>Raster</CODE> and stores the results in
296 * the destination <CODE>Raster</CODE>. This operation performs the
297 * transform band by band.
298 * <p>
299 * If the destination <CODE>Raster</CODE> is null, a new
300 * <CODE>Raster</CODE> is created.
301 * An <CODE>IllegalArgumentException</CODE> may be thrown if the source is
302 * the same as the destination or if the number of bands in
303 * the source is not equal to the number of bands in the
304 * destination.
305 * <p>
306 * The coordinates of the rectangle returned by
307 * <code>getBounds2D(Raster)</code>
308 * are not necessarily the same as the coordinates of the
309 * <code>WritableRaster</code> returned by this method. If the
310 * upper-left corner coordinates of rectangle are negative then
311 * this part of the rectangle is not drawn. If the coordinates
312 * of the rectangle are positive then the filtered image is drawn at
313 * that position in the destination <code>Raster</code>.
314 * <p>
315 * @param src The <CODE>Raster</CODE> to transform.
316 * @param dst The <CODE>Raster</CODE> in which to store the results of the
317 * transformation.
318 *
319 * @return The transformed <CODE>Raster</CODE>.
320 *
321 * @throws ImagingOpException if the raster cannot be transformed
322 * because of a data-processing error that might be
323 * caused by an invalid image format, tile format, or
324 * image-processing operation, or any other unsupported
325 * operation.
326 */
327 public final WritableRaster filter(Raster src, WritableRaster dst) {
328 if (src == null) {
329 throw new NullPointerException("src image is null");
330 }
331 if (dst == null) {
332 dst = createCompatibleDestRaster(src);
333 }
334 if (src == dst) {
335 throw new IllegalArgumentException(
336 "src image cannot be the "
337 + "same as the dst image");
338 }
339 if (src.getNumBands() != dst.getNumBands()) {
340 throw new IllegalArgumentException("Number of src bands ("
341 + src.getNumBands() + ") does not match number of "
342 + " dst bands (" + dst.getNumBands() + ")");
343 }
344
345 if (ImagingLib.filter(this , src, dst) == null) {
346 throw new ImagingOpException(
347 "Unable to transform src image");
348 }
349 return dst;
350 }
351
352 /**
353 * Returns the bounding box of the transformed destination. The
354 * rectangle returned is the actual bounding box of the
355 * transformed points. The coordinates of the upper-left corner
356 * of the returned rectangle might not be (0, 0).
357 *
358 * @param src The <CODE>BufferedImage</CODE> to be transformed.
359 *
360 * @return The <CODE>Rectangle2D</CODE> representing the destination's
361 * bounding box.
362 */
363 public final Rectangle2D getBounds2D(BufferedImage src) {
364 return getBounds2D(src.getRaster());
365 }
366
367 /**
368 * Returns the bounding box of the transformed destination. The
369 * rectangle returned will be the actual bounding box of the
370 * transformed points. The coordinates of the upper-left corner
371 * of the returned rectangle might not be (0, 0).
372 *
373 * @param src The <CODE>Raster</CODE> to be transformed.
374 *
375 * @return The <CODE>Rectangle2D</CODE> representing the destination's
376 * bounding box.
377 */
378 public final Rectangle2D getBounds2D(Raster src) {
379 int w = src.getWidth();
380 int h = src.getHeight();
381
382 // Get the bounding box of the src and transform the corners
383 float[] pts = { 0, 0, w, 0, w, h, 0, h };
384 xform.transform(pts, 0, pts, 0, 4);
385
386 // Get the min, max of the dst
387 float fmaxX = pts[0];
388 float fmaxY = pts[1];
389 float fminX = pts[0];
390 float fminY = pts[1];
391 for (int i = 2; i < 8; i += 2) {
392 if (pts[i] > fmaxX) {
393 fmaxX = pts[i];
394 } else if (pts[i] < fminX) {
395 fminX = pts[i];
396 }
397 if (pts[i + 1] > fmaxY) {
398 fmaxY = pts[i + 1];
399 } else if (pts[i + 1] < fminY) {
400 fminY = pts[i + 1];
401 }
402 }
403
404 return new Rectangle2D.Float(fminX, fminY, fmaxX - fminX, fmaxY
405 - fminY);
406 }
407
408 /**
409 * Creates a zeroed destination image with the correct size and number of
410 * bands. A <CODE>RasterFormatException</CODE> may be thrown if the
411 * transformed width or height is equal to 0.
412 * <p>
413 * If <CODE>destCM</CODE> is null,
414 * an appropriate <CODE>ColorModel</CODE> is used; this
415 * <CODE>ColorModel</CODE> may have
416 * an alpha channel even if the source <CODE>ColorModel</CODE> is opaque.
417 *
418 * @param src The <CODE>BufferedImage</CODE> to be transformed.
419 * @param destCM <CODE>ColorModel</CODE> of the destination. If null,
420 * an appropriate <CODE>ColorModel</CODE> is used.
421 *
422 * @return The zeroed destination image.
423 */
424 public BufferedImage createCompatibleDestImage(BufferedImage src,
425 ColorModel destCM) {
426 BufferedImage image;
427 Rectangle r = getBounds2D(src).getBounds();
428
429 // If r.x (or r.y) is < 0, then we want to only create an image
430 // that is in the positive range.
431 // If r.x (or r.y) is > 0, then we need to create an image that
432 // includes the translation.
433 int w = r.x + r.width;
434 int h = r.y + r.height;
435 if (w <= 0) {
436 throw new RasterFormatException("Transformed width (" + w
437 + ") is less than or equal to 0.");
438 }
439 if (h <= 0) {
440 throw new RasterFormatException("Transformed height (" + h
441 + ") is less than or equal to 0.");
442 }
443
444 if (destCM == null) {
445 ColorModel cm = src.getColorModel();
446 if (interpolationType != TYPE_NEAREST_NEIGHBOR
447 && (cm instanceof IndexColorModel || cm
448 .getTransparency() == Transparency.OPAQUE)) {
449 image = new BufferedImage(w, h,
450 BufferedImage.TYPE_INT_ARGB);
451 } else {
452 image = new BufferedImage(cm, src.getRaster()
453 .createCompatibleWritableRaster(w, h), cm
454 .isAlphaPremultiplied(), null);
455 }
456 } else {
457 image = new BufferedImage(destCM, destCM
458 .createCompatibleWritableRaster(w, h), destCM
459 .isAlphaPremultiplied(), null);
460 }
461
462 return image;
463 }
464
465 /**
466 * Creates a zeroed destination <CODE>Raster</CODE> with the correct size
467 * and number of bands. A <CODE>RasterFormatException</CODE> may be thrown
468 * if the transformed width or height is equal to 0.
469 *
470 * @param src The <CODE>Raster</CODE> to be transformed.
471 *
472 * @return The zeroed destination <CODE>Raster</CODE>.
473 */
474 public WritableRaster createCompatibleDestRaster(Raster src) {
475 Rectangle2D r = getBounds2D(src);
476
477 return src
478 .createCompatibleWritableRaster((int) r.getX(), (int) r
479 .getY(), (int) r.getWidth(), (int) r
480 .getHeight());
481 }
482
483 /**
484 * Returns the location of the corresponding destination point given a
485 * point in the source. If <CODE>dstPt</CODE> is specified, it
486 * is used to hold the return value.
487 *
488 * @param srcPt The <code>Point2D</code> that represents the source
489 * point.
490 * @param dstPt The <CODE>Point2D</CODE> in which to store the result.
491 *
492 * @return The <CODE>Point2D</CODE> in the destination that corresponds to
493 * the specified point in the source.
494 */
495 public final Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
496 return xform.transform(srcPt, dstPt);
497 }
498
499 /**
500 * Returns the affine transform used by this transform operation.
501 *
502 * @return The <CODE>AffineTransform</CODE> associated with this op.
503 */
504 public final AffineTransform getTransform() {
505 return (AffineTransform) xform.clone();
506 }
507
508 /**
509 * Returns the rendering hints used by this transform operation.
510 *
511 * @return The <CODE>RenderingHints</CODE> object associated with this op.
512 */
513 public final RenderingHints getRenderingHints() {
514 if (hints == null) {
515 Object val;
516 switch (interpolationType) {
517 case TYPE_NEAREST_NEIGHBOR:
518 val = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
519 break;
520 case TYPE_BILINEAR:
521 val = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
522 break;
523 case TYPE_BICUBIC:
524 val = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
525 break;
526 default:
527 // Should never get here
528 throw new InternalError("Unknown interpolation type "
529 + interpolationType);
530
531 }
532 hints = new RenderingHints(
533 RenderingHints.KEY_INTERPOLATION, val);
534 }
535
536 return hints;
537 }
538
539 // We need to be able to invert the transform if we want to
540 // transform the image. If the determinant of the matrix is 0,
541 // then we can't invert the transform.
542 void validateTransform(AffineTransform xform) {
543 if (Math.abs(xform.getDeterminant()) <= Double.MIN_VALUE) {
544 throw new ImagingOpException("Unable to invert transform "
545 + xform);
546 }
547 }
548 }
|