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