/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.ext.awt.image.renderable;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.color.ColorSpace;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ConvolveOp;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.Kernel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderContext;
import java.awt.image.WritableRaster;
import org.apache.batik.ext.awt.image.GraphicsUtil;
import org.apache.batik.ext.awt.image.PadMode;
import org.apache.batik.ext.awt.image.rendered.CachableRed;
import org.apache.batik.ext.awt.image.rendered.PadRed;
import org.apache.batik.ext.awt.image.rendered.BufferedImageCachableRed;
import org.apache.batik.ext.awt.image.rendered.AffineRed;
/**
* Convolves an image with a convolution matrix.
*
* Known limitations:
* Does not support bias other than zero - pending 16bit pathway
* Does not support edgeMode="wrap" - pending Tile code.
*
* @author <a href="mailto:Thomas.DeWeeese@Kodak.com">Thomas DeWeese</a>
* @version $Id: ConvolveMatrixRable8Bit.java,v 1.5 2001/08/07 17:28:21 deweese Exp $
*/
public class ConvolveMatrixRable8Bit
extends AbstractColorInterpolationRable
implements ConvolveMatrixRable {
Kernel kernel;
Point target;
float bias;
PadMode edgeMode;
float [] kernelUnitLength = new float[2];
boolean preserveAlpha = false;;
public ConvolveMatrixRable8Bit(Filter source) {
super(source);
}
public Filter getSource() {
return (Filter)getSources().get(0);
}
public void setSource(Filter src) {
init(src);
}
/**
* Returns the Convolution Kernel in use
*/
public Kernel getKernel() {
return kernel;
}
/**
* Sets the Convolution Kernel to use.
* @param k Kernel to use for convolution.
*/
public void setKernel(Kernel k) {
touch();
this.kernel = k;
}
public Point getTarget() {
return (Point)target.clone();
}
public void setTarget(Point pt) {
touch();
this.target = (Point)pt.clone();
}
/**
* Returns the shift value to apply to the result of convolution
*/
public double getBias() {
return bias;
}
/**
* Returns the shift value to apply to the result of convolution
*/
public void setBias(double bias) {
touch();
this.bias = (float)bias;
}
/**
* Returns the current edge handling mode.
*/
public PadMode getEdgeMode() {
return edgeMode;
}
/**
* Sets the current edge handling mode.
*/
public void setEdgeMode(PadMode edgeMode) {
touch();
this.edgeMode = edgeMode;
}
/**
* Returns the [x,y] distance in user space between kernel values
*/
public double [] getKernelUnitLength() {
if (kernelUnitLength == null)
return null;
double [] ret = new double[2];
ret[0] = kernelUnitLength[0];
ret[1] = kernelUnitLength[1];
return ret;
}
/**
* Sets the [x,y] distance in user space between kernel values
* If set to zero then device space will be used.
*/
public void setKernelUnitLength(double [] kernelUnitLength) {
touch();
if (kernelUnitLength == null) {
this.kernelUnitLength = null;
return;
}
if (this.kernelUnitLength == null)
this.kernelUnitLength = new float[2];
this.kernelUnitLength[0] = (float)kernelUnitLength[0];
this.kernelUnitLength[1] = (float)kernelUnitLength[1];
}
/**
* Returns false if the convolution should affect the Alpha channel
*/
public boolean getPreserveAlpha() {
return preserveAlpha;
}
/**
* Sets Alpha channel handling.
* A value of False indicates that the convolution should apply to
* the Alpha Channel
*/
public void setPreserveAlpha(boolean preserveAlpha) {
touch();
this.preserveAlpha = preserveAlpha;
}
public RenderedImage createRendering(RenderContext rc) {
// Just copy over the rendering hints.
RenderingHints rh = rc.getRenderingHints();
if (rh == null) rh = new RenderingHints(null);
// update the current affine transform
AffineTransform at = rc.getTransform();
// This splits out the scale and applies it
// prior to the Gaussian. Then after appying the gaussian
// it applies the shear (rotation) and translation components.
double sx = at.getScaleX();
double sy = at.getScaleY();
double shx = at.getShearX();
double shy = at.getShearY();
double tx = at.getTranslateX();
double ty = at.getTranslateY();
// The Scale is the "hypotonose" of the matrix vectors. This
// represents the complete scaling value from user to an
// intermediate space that is scaled similarly to device
// space.
double scaleX = Math.sqrt(sx*sx + shy*shy);
double scaleY = Math.sqrt(sy*sy + shx*shx);
// These values represent the scale factor to the intermediate
// coordinate system where we will apply our convolution.
if (kernelUnitLength != null) {
if (kernelUnitLength[0] > 0.0)
scaleX = 1/kernelUnitLength[0];
if (kernelUnitLength[1] > 0.0)
scaleY = 1/kernelUnitLength[1];
}
Shape aoi = rc.getAreaOfInterest();
if(aoi == null)
aoi = getBounds2D();
Rectangle2D r = aoi.getBounds2D();
int kw = kernel.getWidth();
int kh = kernel.getHeight();
int kx = target.x;
int ky = target.y;
// Grow the region in usr space.
{
double rx0 = r.getX() -(kx/scaleX);
double ry0 = r.getY() -(ky/scaleY);
double rx1 = rx0 + r.getWidth() + (kw-1)/scaleX;
double ry1 = ry0 + r.getHeight() + (kh-1)/scaleY;
r = new Rectangle2D.Double(Math.floor(rx0),
Math.floor(ry0),
Math.ceil (rx1-Math.floor(rx0)),
Math.ceil (ry1-Math.floor(ry0)));
}
// This will be the affine transform between our usr space and
// an intermediate space which is scaled according to
// kernelUnitLength and is axially aligned with our user
// space.
AffineTransform srcAt
= AffineTransform.getScaleInstance(scaleX, scaleY);
// This is the affine transform between our intermediate
// coordinate space (where the convolution takes place) and
// the real device space, or null (if we don't need an
// intermediate space).
// The shear/rotation simply divides out the
// common scale factor in the matrix.
AffineTransform resAt = new AffineTransform(sx/scaleX, shy/scaleX,
shx/scaleY, sy/scaleY,
tx, ty);
RenderedImage ri;
ri = getSource().createRendering(new RenderContext(srcAt, r, rh));
if (ri == null)
return null;
// org.apache.batik.test.gvt.ImageDisplay.printImage
// ("Padded Image", ri,
// new Rectangle(ri.getMinX()+22,ri.getMinY()+38,5,5));
CachableRed cr = convertSourceCS(ri);
Shape devShape = srcAt.createTransformedShape(aoi);
Rectangle2D devRect = devShape.getBounds2D();
r = devRect;
r = new Rectangle2D.Double(Math.floor(r.getX()-kx),
Math.floor(r.getY()-ky),
Math.ceil (r.getX()+r.getWidth())-
Math.floor(r.getX())+(kw-1),
Math.ceil (r.getY()+r.getHeight())-
Math.floor(r.getY())+(kh-1));
if (!r.getBounds().equals(cr.getBounds())) {
if (edgeMode == PadMode.WRAP)
throw new IllegalArgumentException
("edgeMode=\"wrap\" is not supported by ConvolveMatrix.");
cr = new PadRed(cr, r.getBounds(), edgeMode, rh);
}
// org.apache.batik.test.gvt.ImageDisplay.printImage
// ("Padded Image", cr,
// new Rectangle(cr.getMinX()+23,cr.getMinY()+39,5,5));
if (bias != 0.0)
throw new IllegalArgumentException
("Only bias equal to zero is supported in ConvolveMatrix.");
BufferedImageOp op = new ConvolveOp(kernel,
ConvolveOp.EDGE_NO_OP,
rh);
ColorModel cm = cr.getColorModel();
// OK this is a bit of a cheat. We Pull the DataBuffer out of
// The read-only raster that getData gives us. And use it to
// build a WritableRaster. This avoids a copy of the data.
Raster rr = cr.getData();
WritableRaster wr = GraphicsUtil.makeRasterWritable(rr, 0, 0);
// Here we update the translate to account for the phase shift
// (if any) introduced by setting targetX, targetY in SVG.
int phaseShiftX = target.x - kernel.getXOrigin();
int phaseShiftY = target.y - kernel.getYOrigin();
int destX = (int)(r.getX() + phaseShiftX);
int destY = (int)(r.getY() + phaseShiftY);
BufferedImage destBI;
if (!preserveAlpha) {
// Force the data to be premultiplied since often the JDK
// code doesn't properly premultiply the values...
cm = GraphicsUtil.coerceData(wr, cm, true);
BufferedImage srcBI;
srcBI = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
// Easy case just apply the op...
destBI = op.filter(srcBI, null);
} else {
BufferedImage srcBI;
srcBI = new BufferedImage(cm, wr, cm.isAlphaPremultiplied(), null);
// Construct a linear sRGB cm without alpha...
cm = new DirectColorModel(ColorSpace.getInstance
(ColorSpace.CS_LINEAR_RGB), 24,
0x00FF0000, 0x0000FF00,
0x000000FF, 0x0, false,
DataBuffer.TYPE_INT);
// Create an image with that color model
BufferedImage tmpSrcBI = new BufferedImage
(cm, cm.createCompatibleWritableRaster(wr.getWidth(),
wr.getHeight()),
cm.isAlphaPremultiplied(), null);
// Copy the color data (no alpha) to that image
// (dividing out alpha if needed).
GraphicsUtil.copyData(srcBI, tmpSrcBI);
// org.apache.batik.test.gvt.ImageDisplay.showImage
// ("tmpSrcBI: ", tmpSrcBI);
// Get a linear sRGB Premult ColorModel
ColorModel dstCM = GraphicsUtil.Linear_sRGB_Unpre;
// Construct out output image around that ColorModel
destBI = new BufferedImage
(dstCM, dstCM.createCompatibleWritableRaster(wr.getWidth(),
wr.getHeight()),
dstCM.isAlphaPremultiplied(), null);
// Construct another image on the same data buffer but without
// an alpha channel.
// Create the Raster (note we are using 'cm' again).
WritableRaster dstWR =
Raster.createWritableRaster
(cm.createCompatibleSampleModel(wr.getWidth(), wr.getHeight()),
destBI.getRaster().getDataBuffer(),
new Point(0,0));
// Create the BufferedImage.
BufferedImage tmpDstBI = new BufferedImage
(cm, dstWR, cm.isAlphaPremultiplied(), null);
// Filter between the two image without alpha.
tmpDstBI = op.filter(tmpSrcBI, tmpDstBI);
// org.apache.batik.test.gvt.ImageDisplay.showImage
// ("tmpDstBI: ", tmpDstBI);
// Copy the alpha channel into the result (note the color
// channels are still unpremult.
Rectangle srcRect = wr.getBounds();
Rectangle dstRect = new Rectangle(srcRect.x-phaseShiftX,
srcRect.y-phaseShiftY,
srcRect.width, srcRect.height);
GraphicsUtil.copyBand(wr, srcRect, wr.getNumBands()-1,
destBI.getRaster(), dstRect,
destBI.getRaster().getNumBands()-1);
}
// Wrap it as a CachableRed
cr = new BufferedImageCachableRed(destBI, destX, destY);
// org.apache.batik.test.gvt.ImageDisplay.printImage
// ("Cropped Image", cr,
// new Rectangle(cr.getMinX()+22,cr.getMinY()+38,5,5));
// org.apache.batik.test.gvt.ImageDisplay.printImage
// ("Cropped sRGB", GraphicsUtil.convertTosRGB(cr),
// new Rectangle(cr.getMinX()+22,cr.getMinY()+38,5,5));
// Make sure to crop junk from edges.
cr = new PadRed(cr, devRect.getBounds(), PadMode.ZERO_PAD, rh);
// If we need to scale/rotate/translate the result do so now...
if (!resAt.isIdentity())
cr = new AffineRed(cr, resAt, null);
// return the result.
return cr;
}
}