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