com.day.image.ResizeOp Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2012 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.image;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
/**
* The ResizeOp class implements a weighted resize filter which produces better
* results than the scaling AffineTransformOp and is faster than a blurring
* ConvolveOp with a following scaling AffineTransformOp.
*
* The RenderingHints
defined at construction time are used if the
* destination color model has to be adapted for the filter operation.
*
* Note that the following constraints have to be met
*
* - The
source
and destination
must be different.
*
*
* @see Implementation of Image
* Resizing
* @version $Revision$
* @author tripod
* @author fmeschbe
* @since coati
* @audience wad
*/
public class ResizeOp extends AbstractBufferedImageOp {
/** The horizontal scale factor */
private final double scaleX;
/** The vertical scale factor */
private final double scaleY;
/**
* use faster algorithm if specified
*/
private boolean fast;
/**
* Creates a new ResizeOp
object.
*
* @param scaleX The horizontal scale factor
* @param scaleY The vertical scale factor
* @param hints The rendering hints. May be null
.
*/
public ResizeOp(double scaleX, double scaleY, RenderingHints hints) {
super(hints);
this.scaleX = scaleX;
this.scaleY = scaleY;
}
/**
* Creates a new ResizeOp
with no rendering hints.
*
* @param scaleX The horizontal scale factor
* @param scaleY The vertical scale factor
*/
public ResizeOp(double scaleX, double scaleY) {
this(scaleX, scaleY, null);
}
/**
* Creates a new ResizeOp
with no rendering hints and the same
* horizontal and vertical scale factor.
*
* @param scale The scale factor used for both horizontal and vertical
* scaling.
*/
public ResizeOp(double scale) {
this(scale, scale);
}
// ---------- BufferedImageOp interface
// -------------------------------------
/**
* Returns the bounding box of the filtered destination image. The
* IllegalArgumentException may be thrown if the source image is
* incompatible with the types of images allowed by the class implementing
* this filter.
*/
public Rectangle2D getBounds2D(BufferedImage src) {
int nw = (int) Math.ceil((src.getWidth() * scaleX));
int nh = (int) Math.ceil((src.getHeight() * scaleY));
return new Rectangle2D.Double(0, 0, nw, nh);
}
/**
* Returns the location of the destination point given a point in the source
* image. If dstPt is non-null, it will be used to hold the return value.
*/
public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
if (dstPt == null) {
dstPt = new Point2D.Float();
}
dstPt.setLocation(srcPt.getX() * scaleX, srcPt.getY() * scaleY);
return dstPt;
}
public boolean isFast() {
return fast;
}
public void setFast(boolean fast) {
this.fast = fast;
}
// ---------- protected
// -----------------------------------------------------
/**
* Implements the resize operation.
*
* @param src The source image to be operated upon.
* @param dst The destination image getting the resulting image. This must
*/
protected void doFilter(BufferedImage src, BufferedImage dst) {
if (fast) {
doFilter_progressive(src, dst);
} else {
doFilter_weighted(src, dst);
}
}
/**
* Implements the resizing using Java2D with bicubic interpolation. note
* that this is only supported in some jdk 1.5 JVMs
*
* @param src The source image to be operated upon.
* @param dst The destination image getting the resulting image. This must
*/
private static void doFilter_bicubic(BufferedImage src, BufferedImage dst) {
Graphics2D g = dst.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.drawImage(src, 0, 0, dst.getWidth(), dst.getHeight(), null);
g.dispose();
}
/**
* Implements the resizing using Java2D with bilinear interpolation.
*
* @param src The source image to be operated upon.
* @param dst The destination image getting the resulting image. This must
*/
private static void doFilter_bilinear(BufferedImage src, BufferedImage dst) {
Graphics2D g = dst.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(src, 0, 0, dst.getWidth(), dst.getHeight(), null);
g.dispose();
}
/**
* Implements the resizing using Java2D with a progressive bilinear
* algorithm.
*
* @param src The source image to be operated upon.
* @param dst The destination image getting the resulting image. This must
*/
static void doFilter_progressive(BufferedImage src,
BufferedImage dst) {
int dw = dst.getWidth() * 2;
int dh = dst.getHeight() * 2;
BufferedImage ori = src;
while (src.getWidth() > dw && src.getHeight() > dh) {
int type = src.getType();
BufferedImage tmp = new BufferedImage(src.getWidth() / 2, src.getHeight() / 2, ((type == 0)
? Layer.IMAGE_TYPE
: type));
doFilter_bilinear(src, tmp);
if (src != ori) {
src.flush();
}
src = tmp;
}
doFilter_bilinear(src, dst);
if (src != ori) {
src.flush();
}
}
/**
* Implements the weighted filter algorithm. This method does not depend on
* the source and destination to have the same color model, as it operates
* on the RGB color value extracted with the getRGB()
method
* and sets the destination with setRGB()
which internally
* convert to the real color model.
*
* The source and destination must not be the same and not null
.
*
* @param src The source image which is to be scaled.
* @param dst The destination image getting the scaled image. This must not
* be null
.
*/
private static void doFilter_weighted(BufferedImage src, BufferedImage dst) {
int ow = src.getWidth();
int oh = src.getHeight();
int nw = dst.getWidth();
int nh = dst.getHeight();
int srcChunkHeight; // height of a pixel block from source
int dstChunkHeight; // height of a pixel block to destination
int blockHeight; // max(srcChunkHeight, dstChunkHeight)
int init_s; // scaling factor for vertical scaling
int blockWidth = (ow > nw) ? ow : nw;
int srcChunkStep; // scan stepping for source block data
int dstChunkStep; // scan stepping for destination block data
int scaleCacheSize = 0; // cacheline size for vertical down-scaling
if (oh > nh) {
// scale down vertically - need more input for less output
srcChunkHeight = divideAndRoundUp(oh,nh); // input lines
srcChunkStep = srcChunkHeight;
dstChunkHeight = dstChunkStep = 1; // output lines
blockHeight = srcChunkHeight;
init_s = nh;
scaleCacheSize = nw;
} else if (oh < nh) {
// scale up vertically - need less input for more output
srcChunkHeight = 1; // input lines
srcChunkStep = 1;
dstChunkHeight = dstChunkStep = divideAndRoundUp(nh,oh); // output lines
blockHeight = dstChunkHeight;
init_s = oh;
scaleCacheSize = nw;
} else {
// no vertical scaling
srcChunkHeight = srcChunkStep = dstChunkHeight = dstChunkStep = 1;
blockHeight = srcChunkHeight;
init_s = nh;
}
// memory buffer for source data - destination of getSample()
int[] sr = new int[blockWidth * blockHeight];
int[] sg = new int[blockWidth * blockHeight];
int[] sb = new int[blockWidth * blockHeight];
int[] sa = new int[blockWidth * blockHeight];
// memory buffer for destination data - source of setSample()
int[] dr = new int[blockWidth * blockHeight];
int[] dg = new int[blockWidth * blockHeight];
int[] db = new int[blockWidth * blockHeight];
int[] da = new int[blockWidth * blockHeight];
// crgba cache for previous parts in vertical down-scaling
int[] ccr = new int[scaleCacheSize];
int[] ccg = new int[scaleCacheSize];
int[] ccb = new int[scaleCacheSize];
int[] cca = new int[scaleCacheSize];
// the rasters
Raster srcRas = src.getRaster();
WritableRaster dstRas = dst.getRaster();
for (int srcChunkTop = 0, dstChunkTop = 0; srcChunkTop < oh; srcChunkTop += srcChunkStep, dstChunkTop += dstChunkStep) {
// ensure input chunk does not overlap end of the image
if (srcChunkTop + srcChunkHeight > oh) {
srcChunkHeight = oh - srcChunkTop;
}
// get a chunk
srcRas.getSamples(0, srcChunkTop, ow, srcChunkHeight, 0, sr);
srcRas.getSamples(0, srcChunkTop, ow, srcChunkHeight, 1, sg);
srcRas.getSamples(0, srcChunkTop, ow, srcChunkHeight, 2, sb);
srcRas.getSamples(0, srcChunkTop, ow, srcChunkHeight, 3, sa);
/**
* Horizontal scaling takes pixel values from the source buffer and
* writes the scaled results into the destination buffer.
*/
// horizontal scaling
if (nw < ow) {
preProcessTransparentPixels(sr, sg, sb, sa);
// scale < 1 --> reduce horizontal size
for (int oy = 0, oi = 0, ni = 0; oy < srcChunkHeight; oy++) {
for (int ox = 0, s = nw, cr = 0, cg = 0, cb = 0, ca = 0; ox < ow; ox++, s += nw, oi++) {
if (s >= ow) {
int a = ow - s + nw;
cr += sr[oi] * a;
cg += sg[oi] * a;
cb += sb[oi] * a;
ca += sa[oi] * a;
dr[ni] = cr / ow;
dg[ni] = cg / ow;
db[ni] = cb / ow;
da[ni] = ca / ow;
s -= ow;
ni++;
cr = sr[oi] * s;
cg = sg[oi] * s;
cb = sb[oi] * s;
ca = sa[oi] * s;
} else {
cr += sr[oi] * nw;
cg += sg[oi] * nw;
cb += sb[oi] * nw;
ca += sa[oi] * nw;
}
}
}
} else if (nw > ow) {
// scale > 1 --> enlarge horizontal size
for (int oy = 0, oi = 0, ni = 0; oy < srcChunkHeight; oy++) {
for (int nx = 0, ox = 0, s = ow; nx < nw; nx++, s += ow, ni++) {
if (s >= nw) {
int a = nw - s + ow;
int oi1 = (ox < ow - 1) ? oi + 1 : oi;
s -= nw;
dr[ni] = (sr[oi] * a + sr[oi1] * s) / ow;
dg[ni] = (sg[oi] * a + sg[oi1] * s) / ow;
db[ni] = (sb[oi] * a + sb[oi1] * s) / ow;
da[ni] = (sa[oi] * a + sa[oi1] * s) / ow;
oi++;
ox++;
} else {
dr[ni] = sr[oi];
dg[ni] = sg[oi];
db[ni] = sb[oi];
da[ni] = sa[oi];
}
}
}
}
/**
* At this point, the destination buffer contains the data if and
* only if horizontal scaling took place, else the data to work on
* is still in the source buffer.
*/
/**
* Exchange source and destination buffers, if either both
* dimensions get adapted or none. If only vertical, the source
* already contains the pixel source and if only horizontal, the
* destination already contains the destination pixels.
*/
if ((nw == ow && nh == oh) || (nw != ow && nh != oh)) {
int[] tr = sr, tg = sg, tb = sb, ta = sa;
sr = dr;
sg = dg;
sb = db;
sa = da;
dr = tr;
dg = tg;
db = tb;
da = ta;
}
/**
* Vertical scaling takes pixel values from the source buffer and
* writes the scaled results into the destination buffer.
*/
// vertical scaling
if (nh < oh) {
// scale < 1 --> reduce
preProcessTransparentPixels(sr, sg, sb, sa);
// might need to fill with first row
if (srcChunkTop == 0) {
for (int i = 0; i < nw; i++) {
ccr[i] = sr[i] * nh;
ccg[i] = sg[i] * nh;
ccb[i] = sb[i] * nh;
cca[i] = sa[i] * nh;
init_s = 2 * nh;
}
}
int start_s = init_s;
for (int ox = 0; ox < nw; ox++) {
// prefill crgba with cached previous values
int cr = ccr[ox];
int cg = ccg[ox];
int cb = ccb[ox];
int ca = cca[ox];
int s = start_s;
for (int oy = 0, oi = ox, ni = ox; oy < dstChunkHeight; s += nh, oi += nw) {
if (s >= oh) {
int a = oh - s + nh;
cr += sr[oi] * a;
cg += sg[oi] * a;
cb += sb[oi] * a;
ca += sa[oi] * a;
dr[ni] = cr / oh;
dg[ni] = cg / oh;
db[ni] = cb / oh;
da[ni] = ca / oh;
oy++;
ni += nw;
s -= oh;
cr = sr[oi] * s;
cg = sg[oi] * s;
cb = sb[oi] * s;
ca = sa[oi] * s;
} else {
cr += sr[oi] * nh;
cg += sg[oi] * nh;
cb += sb[oi] * nh;
ca += sa[oi] * nh;
}
}
// cache current crgba values and s
ccr[ox] = cr;
ccg[ox] = cg;
ccb[ox] = cb;
cca[ox] = ca;
init_s = s;
}
// calculate number of lines needed
srcChunkStep = srcChunkHeight;
int sum = oh - init_s + nh;
srcChunkHeight = sum / nh;
if ((srcChunkHeight * nh) != sum) {
srcChunkHeight++;
};
} else if (nh > oh) {
/**
* The first block is used to initialize the cache row. The real
* data copying starts with the second block, where the cache is
* first copied and after having copied the cache enough the
* newly read line is copied. For each of the destination rows,
* except the last, the cache row is copied into the
* destination, the last row is a mixture of the cache row and
* the next row. At the end the next row is copied into the
* cache for the next block. At the end the last input line has
* only just been copied to the cache has not been used yet, we
* have to force another loop. This is not optimal as the last
* line is read and horizontally scaled twice.
*/
if (srcChunkTop == 0) {
// copy the first row to the cache
System.arraycopy(sr, 0, ccr, 0, nw);
System.arraycopy(sg, 0, ccg, 0, nw);
System.arraycopy(sb, 0, ccb, 0, nw);
System.arraycopy(sa, 0, cca, 0, nw);
// force reading the first line a second time
dstChunkTop -= dstChunkStep;
// go reading next line
continue;
}
// scale > 1 --> enlarge
for (int s = init_s, ny = 0, ni = 0; ny < dstChunkHeight; ny++, s += oh, ni += nw) {
if (s < nh) {
// copy cache row
System.arraycopy(ccr, 0, dr, ni, nw);
System.arraycopy(ccg, 0, dg, ni, nw);
System.arraycopy(ccb, 0, db, ni, nw);
System.arraycopy(cca, 0, da, ni, nw);
} else {
// scale factors
int a = nh - s + oh;
s -= nh;
for (int ox = 0; ox < nw; ox++) {
// mix current (cached) row and next row
dr[ni + ox] = (ccr[ox] * a + sr[ox] * s) / oh;
dg[ni + ox] = (ccg[ox] * a + sg[ox] * s) / oh;
db[ni + ox] = (ccb[ox] * a + sb[ox] * s) / oh;
da[ni + ox] = (cca[ox] * a + sa[ox] * s) / oh;
}
// copy the next row to the cache
System.arraycopy(sr, 0, ccr, 0, nw);
System.arraycopy(sg, 0, ccg, 0, nw);
System.arraycopy(sb, 0, ccb, 0, nw);
System.arraycopy(sa, 0, cca, 0, nw);
// keep scale factor
init_s = s + oh;
// how many rows to copy
dstChunkStep = ny + 1;
// step out here
break;
}
}
// if src ran out of lines, but dst still has space,
// do last source line again
if (srcChunkTop + srcChunkStep >= oh
&& dstChunkTop + dstChunkStep < nh) {
srcChunkTop -= srcChunkStep;
}
}
/**
* At this point we consider the resulting pixel data to be stored
* in the destination buffer, either due to vertical scaling or by
* the correct working of buffer exchanges between the scaling
* steps.
*/
// make sure only the part of the band fitting is written to the
// image
if (dstChunkTop + dstChunkStep > nh) {
dstChunkStep = nh - dstChunkTop;
}
dstRas.setSamples(0, dstChunkTop, nw, dstChunkStep, 0, dr);
dstRas.setSamples(0, dstChunkTop, nw, dstChunkStep, 1, dg);
dstRas.setSamples(0, dstChunkTop, nw, dstChunkStep, 2, db);
dstRas.setSamples(0, dstChunkTop, nw, dstChunkStep, 3, da);
}
}
/**
* Divides 2 integers and rounds up the result to the next integer.
*
* Examples:
* - a/b = 2.32: the result is 3
* - a/b = 2.00: the result is 2
*
* @param a The dividend
* @param b The divisor
* @return The result of a/b rounded up to the next integer
*/
private static int divideAndRoundUp(int a, int b) {
return (a + b - 1) / b;
}
/**
* Converts black and transparent pixels into white and transparent ones.
*
* This processing avoids that scaling down the image creates a visible border line between a transparent area
* and a non transparent one.
*
* Explanation:
* Let's assume that we have two pixels p1 and p2 close to each other in the source image:
* - p1 is black and transparent. Its RGBA values are (0; 0; 0; 0)
* - p2 is light grey and visible. Its RGBA values are (246; 246; 246; 1)
*
* When scaling down the image by a factor 2, the destination pixel will be dark grey and half-visible as the
* RGBA values of p1 and p2 are averaged.
* This results in a dark grey dotted line between the transparent area and the visible one.
*
* After the processing, p1 and p2 are as follows:
* - p1 becomes white and transparent. Its RGBA values are (255; 255; 255; 0)
* - p2 stays light grey and visible. Its RGBA values are (246; 246; 246; 1)
*
* When scaling down the image, the destination pixel will be a bit more light grey and half-visible
* and is not visible any more by human eyes.
*
* @param sr int array containing the R values of a row of pixels of the source image
* @param sg int array containing the G values of a row of pixels of the source image
* @param sb int array containing the B values of a row of pixels of the source image
* @param sa int array containing the A values of a row of pixels of the source image
*
*/
private static void preProcessTransparentPixels(int[] sr, int[] sg, int[] sb, int[] sa) {
if (sr == null || sg == null || sb == null || sa == null) {
return;
}
for (int i = 0; i < sr.length; i++) {
if (sr[i] == 0 && sg[i] == 0 && sb[i] == 0 && sa[i] == 0) {
sr[i] = 255;
sg[i] = 255;
sb[i] = 255;
}
}
}
}