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