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
26package java.awt.image;
27
28import java.awt.geom.AffineTransform;
29import java.awt.geom.NoninvertibleTransformException;
30import java.awt.geom.Rectangle2D;
31import java.awt.geom.Point2D;
32import java.awt.AlphaComposite;
33import java.awt.GraphicsEnvironment;
34import java.awt.Rectangle;
35import java.awt.RenderingHints;
36import java.awt.Transparency;
37import java.lang.annotation.Native;
38import 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} to 2D coordinates
43 * in the destination image or {@code Raster}.
44 * The type of interpolation that is used is specified through a constructor,
45 * either by a {@code RenderingHints} object or by one of the integer
46 * interpolation types defined in this class.
47 * <p>
48 * If a {@code RenderingHints} 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} 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 */
66public 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} given an affine transform.
89     * The interpolation type is determined from the
90     * {@code RenderingHints} 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} is null),
94     * the interpolation type is {@link #TYPE_NEAREST_NEIGHBOR
95     * TYPE_NEAREST_NEIGHBOR}.
96     *
97     * @param xform The {@code AffineTransform} to use for the
98     * operation.
99     *
100     * @param hints The {@code RenderingHints} 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} given an affine transform
140     * and the interpolation type.
141     *
142     * @param xform The {@code AffineTransform} 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} and stores the results
178     * in the destination {@code BufferedImage}.
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} is created with the source
183     * {@code ColorModel}.
184     * <p>
185     * The coordinates of the rectangle returned by
186     * {@code getBounds2D(BufferedImage)}
187     * are not necessarily the same as the coordinates of the
188     * {@code BufferedImage} 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}.
194     * <p>
195     * An {@code IllegalArgumentException} is thrown if the source is
196     * the same as the destination.
197     *
198     * @param src The {@code BufferedImage} to transform.
199     * @param dst The {@code BufferedImage} in which to store the results
200     * of the transformation.
201     *
202     * @return The filtered {@code BufferedImage}.
203     * @throws IllegalArgumentException if {@code src} and
204     *         {@code dst} 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} and stores the results in
309     * the destination {@code Raster}.  This operation performs the
310     * transform band by band.
311     * <p>
312     * If the destination {@code Raster} is null, a new
313     * {@code Raster} is created.
314     * An {@code IllegalArgumentException} 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)}
321     * are not necessarily the same as the coordinates of the
322     * {@code WritableRaster} 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}.
327     *
328     * @param src The {@code Raster} to transform.
329     * @param dst The {@code Raster} in which to store the results of the
330     * transformation.
331     *
332     * @return The transformed {@code Raster}.
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,&nbsp;0).
370     *
371     * @param src The {@code BufferedImage} to be transformed.
372     *
373     * @return The {@code Rectangle2D} 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,&nbsp;0).
385     *
386     * @param src The {@code Raster} to be transformed.
387     *
388     * @return The {@code Rectangle2D} 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} may be thrown if the
425     * transformed width or height is equal to 0.
426     * <p>
427     * If {@code destCM} is null,
428     * an appropriate {@code ColorModel} is used; this
429     * {@code ColorModel} may have
430     * an alpha channel even if the source {@code ColorModel} is opaque.
431     *
432     * @param src  The {@code BufferedImage} to be transformed.
433     * @param destCM  {@code ColorModel} of the destination.  If null,
434     * an appropriate {@code ColorModel} 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} with the correct size
484     * and number of bands.  A {@code RasterFormatException} may be thrown
485     * if the transformed width or height is equal to 0.
486     *
487     * @param src The {@code Raster} to be transformed.
488     *
489     * @return The zeroed destination {@code Raster}.
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} is specified, it
503     * is used to hold the return value.
504     *
505     * @param srcPt The {@code Point2D} that represents the source
506     *              point.
507     * @param dstPt The {@code Point2D} in which to store the result.
508     *
509     * @return The {@code Point2D} 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} 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} 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}
564