1. /*
  2. * @(#)RescaleOp.java 1.43 03/12/19
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package java.awt.image;
  8. import java.awt.color.ColorSpace;
  9. import java.awt.geom.Rectangle2D;
  10. import java.awt.Rectangle;
  11. import java.awt.geom.Point2D;
  12. import java.awt.RenderingHints;
  13. import sun.awt.image.ImagingLib;
  14. /**
  15. * This class performs a pixel-by-pixel rescaling of the data in the
  16. * source image by multiplying the sample values for each pixel by a scale
  17. * factor and then adding an offset. The scaled sample values are clipped
  18. * to the minimum/maximum representable in the destination image.
  19. * <p>
  20. * The pseudo code for the rescaling operation is as follows:
  21. * <pre>
  22. *for each pixel from Source object {
  23. * for each band/component of the pixel {
  24. * dstElement = (srcElement*scaleFactor) + offset
  25. * }
  26. *}
  27. * </pre>
  28. * <p>
  29. * For Rasters, rescaling operates on bands. The number of
  30. * sets of scaling constants may be one, in which case the same constants
  31. * are applied to all bands, or it must equal the number of Source
  32. * Raster bands.
  33. * <p>
  34. * For BufferedImages, rescaling operates on color and alpha components.
  35. * The number of sets of scaling constants may be one, in which case the
  36. * same constants are applied to all color (but not alpha) components.
  37. * Otherwise, the number of sets of scaling constants may
  38. * equal the number of Source color components, in which case no
  39. * rescaling of the alpha component (if present) is performed.
  40. * If neither of these cases apply, the number of sets of scaling constants
  41. * must equal the number of Source color components plus alpha components,
  42. * in which case all color and alpha components are rescaled.
  43. * <p>
  44. * BufferedImage sources with premultiplied alpha data are treated in the same
  45. * manner as non-premultiplied images for purposes of rescaling. That is,
  46. * the rescaling is done per band on the raw data of the BufferedImage source
  47. * without regard to whether the data is premultiplied. If a color conversion
  48. * is required to the destination ColorModel, the premultiplied state of
  49. * both source and destination will be taken into account for this step.
  50. * <p>
  51. * Images with an IndexColorModel cannot be rescaled.
  52. * <p>
  53. * If a RenderingHints object is specified in the constructor, the
  54. * color rendering hint and the dithering hint may be used when color
  55. * conversion is required.
  56. * <p>
  57. * Note that in-place operation is allowed (i.e. the source and destination can
  58. * be the same object).
  59. * @version 10 Feb 1997
  60. * @see java.awt.RenderingHints#KEY_COLOR_RENDERING
  61. * @see java.awt.RenderingHints#KEY_DITHERING
  62. */
  63. public class RescaleOp implements BufferedImageOp, RasterOp {
  64. float[] scaleFactors;
  65. float[] offsets;
  66. int length = 0;
  67. RenderingHints hints;
  68. private int srcNbits;
  69. private int dstNbits;
  70. /**
  71. * Constructs a new RescaleOp with the desired scale factors
  72. * and offsets. The length of the scaleFactor and offset arrays
  73. * must meet the restrictions stated in the class comments above.
  74. * The RenderingHints argument may be null.
  75. * @param scaleFactors the specified scale factors
  76. * @param offsets the specified offsets
  77. * @param hints the specified <code>RenderingHints</code>, or
  78. * <code>null</code>
  79. */
  80. public RescaleOp (float[] scaleFactors, float[] offsets,
  81. RenderingHints hints) {
  82. length = scaleFactors.length;
  83. if (length > offsets.length) length = offsets.length;
  84. this.scaleFactors = new float[length];
  85. this.offsets = new float[length];
  86. for (int i=0; i < length; i++) {
  87. this.scaleFactors[i] = scaleFactors[i];
  88. this.offsets[i] = offsets[i];
  89. }
  90. this.hints = hints;
  91. }
  92. /**
  93. * Constructs a new RescaleOp with the desired scale factor
  94. * and offset. The scaleFactor and offset will be applied to
  95. * all bands in a source Raster and to all color (but not alpha)
  96. * components in a BufferedImage.
  97. * The RenderingHints argument may be null.
  98. * @param scaleFactor the specified scale factor
  99. * @param offset the specified offset
  100. * @param hints the specified <code>RenderingHints</code>, or
  101. * <code>null</code>
  102. */
  103. public RescaleOp (float scaleFactor, float offset, RenderingHints hints) {
  104. length = 1;
  105. this.scaleFactors = new float[1];
  106. this.offsets = new float[1];
  107. this.scaleFactors[0] = scaleFactor;
  108. this.offsets[0] = offset;
  109. this.hints = hints;
  110. }
  111. /**
  112. * Returns the scale factors in the given array. The array is also
  113. * returned for convenience. If scaleFactors is null, a new array
  114. * will be allocated.
  115. * @param scaleFactors the array to contain the scale factors of
  116. * this <code>RescaleOp</code>
  117. * @return the scale factors of this <code>RescaleOp</code>.
  118. */
  119. final public float[] getScaleFactors (float scaleFactors[]) {
  120. if (scaleFactors == null) {
  121. return (float[]) this.scaleFactors.clone();
  122. }
  123. System.arraycopy (this.scaleFactors, 0, scaleFactors, 0,
  124. Math.min(this.scaleFactors.length,
  125. scaleFactors.length));
  126. return scaleFactors;
  127. }
  128. /**
  129. * Returns the offsets in the given array. The array is also returned
  130. * for convenience. If offsets is null, a new array
  131. * will be allocated.
  132. * @param offsets the array to contain the offsets of
  133. * this <code>RescaleOp</code>
  134. * @return the offsets of this <code>RescaleOp</code>.
  135. */
  136. final public float[] getOffsets(float offsets[]) {
  137. if (offsets == null) {
  138. return (float[]) this.offsets.clone();
  139. }
  140. System.arraycopy (this.offsets, 0, offsets, 0,
  141. Math.min(this.offsets.length, offsets.length));
  142. return offsets;
  143. }
  144. /**
  145. * Returns the number of scaling factors and offsets used in this
  146. * RescaleOp.
  147. * @return the number of scaling factors and offsets of this
  148. * <code>RescaleOp</code>.
  149. */
  150. final public int getNumFactors() {
  151. return length;
  152. }
  153. /**
  154. * Creates a ByteLookupTable to implement the rescale.
  155. * The table may have either a SHORT or BYTE input.
  156. * @param nElems Number of elements the table is to have.
  157. * This will generally be 256 for byte and
  158. * 65536 for short.
  159. */
  160. private ByteLookupTable createByteLut(float scale[],
  161. float off[],
  162. int nBands,
  163. int nElems) {
  164. byte[][] lutData = new byte[scale.length][nElems];
  165. for (int band=0; band<scale.length; band++) {
  166. float bandScale = scale[band];
  167. float bandOff = off[band];
  168. byte[] bandLutData = lutData[band];
  169. for (int i=0; i<nElems; i++) {
  170. int val = (int)(i*bandScale + bandOff);
  171. if ((val & 0xffffff00) != 0) {
  172. if (val < 0) {
  173. val = 0;
  174. } else {
  175. val = 255;
  176. }
  177. }
  178. bandLutData[i] = (byte)val;
  179. }
  180. }
  181. return new ByteLookupTable(0, lutData);
  182. }
  183. /**
  184. * Creates a ShortLookupTable to implement the rescale.
  185. * The table may have either a SHORT or BYTE input.
  186. * @param nElems Number of elements the table is to have.
  187. * This will generally be 256 for byte and
  188. * 65536 for short.
  189. */
  190. private ShortLookupTable createShortLut(float scale[],
  191. float off[],
  192. int nBands,
  193. int nElems) {
  194. short[][] lutData = new short[scale.length][nElems];
  195. for (int band=0; band<scale.length; band++) {
  196. float bandScale = scale[band];
  197. float bandOff = off[band];
  198. short[] bandLutData = lutData[band];
  199. for (int i=0; i<nElems; i++) {
  200. int val = (int)(i*bandScale + bandOff);
  201. if ((val & 0xffff0000) != 0) {
  202. if (val < 0) {
  203. val = 0;
  204. } else {
  205. val = 65535;
  206. }
  207. }
  208. bandLutData[i] = (short)val;
  209. }
  210. }
  211. return new ShortLookupTable(0, lutData);
  212. }
  213. /**
  214. * Determines if the rescale can be performed as a lookup.
  215. * The dst must be a byte or short type.
  216. * The src must be less than 16 bits.
  217. * All source band sizes must be the same and all dst band sizes
  218. * must be the same.
  219. */
  220. private boolean canUseLookup(Raster src, Raster dst) {
  221. //
  222. // Check that the src datatype is either a BYTE or SHORT
  223. //
  224. int datatype = src.getDataBuffer().getDataType();
  225. if(datatype != DataBuffer.TYPE_BYTE &&
  226. datatype != DataBuffer.TYPE_USHORT) {
  227. return false;
  228. }
  229. //
  230. // Check dst sample sizes. All must be 8 or 16 bits.
  231. //
  232. SampleModel dstSM = dst.getSampleModel();
  233. dstNbits = dstSM.getSampleSize(0);
  234. if (!(dstNbits == 8 || dstNbits == 16)) {
  235. return false;
  236. }
  237. for (int i=1; i<src.getNumBands(); i++) {
  238. int bandSize = dstSM.getSampleSize(i);
  239. if (bandSize != dstNbits) {
  240. return false;
  241. }
  242. }
  243. //
  244. // Check src sample sizes. All must be the same size
  245. //
  246. SampleModel srcSM = src.getSampleModel();
  247. srcNbits = srcSM.getSampleSize(0);
  248. if (srcNbits > 16) {
  249. return false;
  250. }
  251. for (int i=1; i<src.getNumBands(); i++) {
  252. int bandSize = srcSM.getSampleSize(i);
  253. if (bandSize != srcNbits) {
  254. return false;
  255. }
  256. }
  257. return true;
  258. }
  259. /**
  260. * Rescales the source BufferedImage.
  261. * If the color model in the source image is not the same as that
  262. * in the destination image, the pixels will be converted
  263. * in the destination. If the destination image is null,
  264. * a BufferedImage will be created with the source ColorModel.
  265. * An IllegalArgumentException may be thrown if the number of
  266. * scaling factors/offsets in this object does not meet the
  267. * restrictions stated in the class comments above, or if the
  268. * source image has an IndexColorModel.
  269. * @param src the <code>BufferedImage</code> to be filtered
  270. * @param dst the destination for the filtering operation
  271. * or <code>null</code>
  272. * @return the filtered <code>BufferedImage</code>.
  273. * @throws IllegalArgumentException if the <code>ColorModel</code>
  274. * of <code>src</code> is an <code>IndexColorModel</code>,
  275. * or if the number of scaling factors and offsets in this
  276. * <code>RescaleOp</code> do not meet the requirements
  277. * stated in the class comments.
  278. */
  279. public final BufferedImage filter (BufferedImage src, BufferedImage dst) {
  280. ColorModel srcCM = src.getColorModel();
  281. ColorModel dstCM;
  282. int numBands = srcCM.getNumColorComponents();
  283. if (srcCM instanceof IndexColorModel) {
  284. throw new
  285. IllegalArgumentException("Rescaling cannot be "+
  286. "performed on an indexed image");
  287. }
  288. if (length != 1 && length != numBands &&
  289. length != srcCM.getNumComponents())
  290. {
  291. throw new IllegalArgumentException("Number of scaling constants "+
  292. "does not equal the number of"+
  293. " of color or color/alpha "+
  294. " components");
  295. }
  296. boolean needToConvert = false;
  297. // Include alpha
  298. if (length > numBands && srcCM.hasAlpha()) {
  299. length = numBands+1;
  300. }
  301. int width = src.getWidth();
  302. int height = src.getHeight();
  303. if (dst == null) {
  304. dst = createCompatibleDestImage(src, null);
  305. dstCM = srcCM;
  306. }
  307. else {
  308. if (width != dst.getWidth()) {
  309. throw new
  310. IllegalArgumentException("Src width ("+width+
  311. ") not equal to dst width ("+
  312. dst.getWidth()+")");
  313. }
  314. if (height != dst.getHeight()) {
  315. throw new
  316. IllegalArgumentException("Src height ("+height+
  317. ") not equal to dst height ("+
  318. dst.getHeight()+")");
  319. }
  320. dstCM = dst.getColorModel();
  321. if(srcCM.getColorSpace().getType() !=
  322. dstCM.getColorSpace().getType()) {
  323. needToConvert = true;
  324. dst = createCompatibleDestImage(src, null);
  325. }
  326. }
  327. BufferedImage origDst = dst;
  328. //
  329. // Try to use a native BI rescale operation first
  330. //
  331. if (ImagingLib.filter(this, src, dst) == null) {
  332. //
  333. // Native BI rescale failed - convert to rasters
  334. //
  335. WritableRaster srcRaster = src.getRaster();
  336. WritableRaster dstRaster = dst.getRaster();
  337. if (srcCM.hasAlpha()) {
  338. if (numBands-1 == length || length == 1) {
  339. int minx = srcRaster.getMinX();
  340. int miny = srcRaster.getMinY();
  341. int[] bands = new int[numBands-1];
  342. for (int i=0; i < numBands-1; i++) {
  343. bands[i] = i;
  344. }
  345. srcRaster =
  346. srcRaster.createWritableChild(minx, miny,
  347. srcRaster.getWidth(),
  348. srcRaster.getHeight(),
  349. minx, miny,
  350. bands);
  351. }
  352. }
  353. if (dstCM.hasAlpha()) {
  354. int dstNumBands = dstRaster.getNumBands();
  355. if (dstNumBands-1 == length || length == 1) {
  356. int minx = dstRaster.getMinX();
  357. int miny = dstRaster.getMinY();
  358. int[] bands = new int[numBands-1];
  359. for (int i=0; i < numBands-1; i++) {
  360. bands[i] = i;
  361. }
  362. dstRaster =
  363. dstRaster.createWritableChild(minx, miny,
  364. dstRaster.getWidth(),
  365. dstRaster.getHeight(),
  366. minx, miny,
  367. bands);
  368. }
  369. }
  370. //
  371. // Call the raster filter method
  372. //
  373. filter(srcRaster, dstRaster);
  374. }
  375. if (needToConvert) {
  376. // ColorModels are not the same
  377. ColorConvertOp ccop = new ColorConvertOp(hints);
  378. ccop.filter(dst, origDst);
  379. }
  380. return origDst;
  381. }
  382. /**
  383. * Rescales the pixel data in the source Raster.
  384. * If the destination Raster is null, a new Raster will be created.
  385. * The source and destination must have the same number of bands.
  386. * Otherwise, an IllegalArgumentException is thrown.
  387. * Note that the number of scaling factors/offsets in this object must
  388. * meet the restrictions stated in the class comments above.
  389. * Otherwise, an IllegalArgumentException is thrown.
  390. * @param src the <code>Raster</code> to be filtered
  391. * @param dst the destination for the filtering operation
  392. * or <code>null</code>
  393. * @return the filtered <code>WritableRaster</code>.
  394. * @throws IllegalArgumentException if <code>src</code> and
  395. * <code>dst</code> do not have the same number of bands,
  396. * or if the number of scaling factors and offsets in this
  397. * <code>RescaleOp</code> do not meet the requirements
  398. * stated in the class comments.
  399. */
  400. public final WritableRaster filter (Raster src, WritableRaster dst) {
  401. int numBands = src.getNumBands();
  402. int width = src.getWidth();
  403. int height = src.getHeight();
  404. int[] srcPix = null;
  405. int step = 0;
  406. int tidx = 0;
  407. // Create a new destination Raster, if needed
  408. if (dst == null) {
  409. dst = createCompatibleDestRaster(src);
  410. }
  411. else if (height != dst.getHeight() || width != dst.getWidth()) {
  412. throw new
  413. IllegalArgumentException("Width or height of Rasters do not "+
  414. "match");
  415. }
  416. else if (numBands != dst.getNumBands()) {
  417. // Make sure that the number of bands are equal
  418. throw new IllegalArgumentException("Number of bands in src "
  419. + numBands
  420. + " does not equal number of bands in dest "
  421. + dst.getNumBands());
  422. }
  423. // Make sure that the arrays match
  424. // Make sure that the low/high/constant arrays match
  425. if (length != 1 && length != src.getNumBands()) {
  426. throw new IllegalArgumentException("Number of scaling constants "+
  427. "does not equal the number of"+
  428. " of bands in the src raster");
  429. }
  430. //
  431. // Try for a native raster rescale first
  432. //
  433. if (ImagingLib.filter(this, src, dst) != null) {
  434. return dst;
  435. }
  436. //
  437. // Native raster rescale failed.
  438. // Try to see if a lookup operation can be used
  439. //
  440. if (canUseLookup(src, dst)) {
  441. int srcNgray = (1 << srcNbits);
  442. int dstNgray = (1 << dstNbits);
  443. if (dstNgray == 256) {
  444. ByteLookupTable lut = createByteLut(scaleFactors, offsets,
  445. numBands, srcNgray);
  446. LookupOp op = new LookupOp(lut, hints);
  447. op.filter(src, dst);
  448. } else {
  449. ShortLookupTable lut = createShortLut(scaleFactors, offsets,
  450. numBands, srcNgray);
  451. LookupOp op = new LookupOp(lut, hints);
  452. op.filter(src, dst);
  453. }
  454. } else {
  455. //
  456. // Fall back to the slow code
  457. //
  458. if (length > 1) {
  459. step = 1;
  460. }
  461. int sminX = src.getMinX();
  462. int sY = src.getMinY();
  463. int dminX = dst.getMinX();
  464. int dY = dst.getMinY();
  465. int sX;
  466. int dX;
  467. //
  468. // Determine bits per band to determine maxval for clamps.
  469. // The min is assumed to be zero.
  470. // REMIND: This must change if we ever support signed data types.
  471. //
  472. int nbits;
  473. int dstMax[] = new int[numBands];
  474. int dstMask[] = new int[numBands];
  475. SampleModel dstSM = dst.getSampleModel();
  476. for (int z=0; z<numBands; z++) {
  477. nbits = dstSM.getSampleSize(z);
  478. dstMax[z] = (1 << nbits) - 1;
  479. dstMask[z] = ~(dstMax[z]);
  480. }
  481. int val;
  482. for (int y=0; y < height; y++, sY++, dY++) {
  483. dX = dminX;
  484. sX = sminX;
  485. for (int x = 0; x < width; x++, sX++, dX++) {
  486. // Get data for all bands at this x,y position
  487. srcPix = src.getPixel(sX, sY, srcPix);
  488. tidx = 0;
  489. for (int z=0; z<numBands; z++, tidx += step) {
  490. val = (int)(srcPix[z]*scaleFactors[tidx]
  491. + offsets[tidx]);
  492. // Clamp
  493. if ((val & dstMask[z]) != 0) {
  494. if (val < 0) {
  495. val = 0;
  496. } else {
  497. val = dstMax[z];
  498. }
  499. }
  500. srcPix[z] = val;
  501. }
  502. // Put it back for all bands
  503. dst.setPixel(dX, dY, srcPix);
  504. }
  505. }
  506. }
  507. return dst;
  508. }
  509. /**
  510. * Returns the bounding box of the rescaled destination image. Since
  511. * this is not a geometric operation, the bounding box does not
  512. * change.
  513. */
  514. public final Rectangle2D getBounds2D (BufferedImage src) {
  515. return getBounds2D(src.getRaster());
  516. }
  517. /**
  518. * Returns the bounding box of the rescaled destination Raster. Since
  519. * this is not a geometric operation, the bounding box does not
  520. * change.
  521. * @param src the rescaled destination <code>Raster</code>
  522. * @return the bounds of the specified <code>Raster</code>.
  523. */
  524. public final Rectangle2D getBounds2D (Raster src) {
  525. return src.getBounds();
  526. }
  527. /**
  528. * Creates a zeroed destination image with the correct size and number of
  529. * bands.
  530. * @param src Source image for the filter operation.
  531. * @param destCM ColorModel of the destination. If null, the
  532. * ColorModel of the source will be used.
  533. * @return the zeroed-destination image.
  534. */
  535. public BufferedImage createCompatibleDestImage (BufferedImage src,
  536. ColorModel destCM) {
  537. BufferedImage image;
  538. if (destCM == null) {
  539. ColorModel cm = src.getColorModel();
  540. image = new BufferedImage(cm,
  541. src.getRaster().createCompatibleWritableRaster(),
  542. cm.isAlphaPremultiplied(),
  543. null);
  544. }
  545. else {
  546. int w = src.getWidth();
  547. int h = src.getHeight();
  548. image = new BufferedImage (destCM,
  549. destCM.createCompatibleWritableRaster(w, h),
  550. destCM.isAlphaPremultiplied(), null);
  551. }
  552. return image;
  553. }
  554. /**
  555. * Creates a zeroed-destination <code>Raster</code> with the correct
  556. * size and number of bands, given this source.
  557. * @param src the source <code>Raster</code>
  558. * @return the zeroed-destination <code>Raster</code>.
  559. */
  560. public WritableRaster createCompatibleDestRaster (Raster src) {
  561. return src.createCompatibleWritableRaster(src.getWidth(), src.getHeight());
  562. }
  563. /**
  564. * Returns the location of the destination point given a
  565. * point in the source. If dstPt is non-null, it will
  566. * be used to hold the return value. Since this is not a geometric
  567. * operation, the srcPt will equal the dstPt.
  568. * @param srcPt a point in the source image
  569. * @param dstPt the destination point or <code>null</code>
  570. * @return the location of the destination point.
  571. */
  572. public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) {
  573. if (dstPt == null) {
  574. dstPt = new Point2D.Float();
  575. }
  576. dstPt.setLocation(srcPt.getX(), srcPt.getY());
  577. return dstPt;
  578. }
  579. /**
  580. * Returns the rendering hints for this op.
  581. * @return the rendering hints of this <code>RescaleOp</code>.
  582. */
  583. public final RenderingHints getRenderingHints() {
  584. return hints;
  585. }
  586. }