/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.model.ImageEditor;
import com.lightcrafts.model.CropBounds;
import com.lightcrafts.model.Operation;
import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.opimage.CachedImage;
import com.lightcrafts.jai.opimage.UnlicensedOpImage;
import com.lightcrafts.mediax.jai.*;
import com.lightcrafts.license.LicenseChecker;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.*;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.*;
import java.util.LinkedList;
import java.util.Vector;
public class Rendering implements Cloneable {
private float scaleFactor = 1;
private CropBounds cropBounds = new CropBounds();
private AffineTransform inputTransform = new AffineTransform();
private AffineTransform transform = new AffineTransform();
private final PlanarImage sourceImage;
private PlanarImage xformedSourceImage;
private ImageEditorEngine engine;
private LinkedList<OperationImpl> pipeline = new LinkedList<OperationImpl>();
private ImagePyramid pyramid;
public boolean cheapScale = false;
private boolean licenseExpired = LicenseChecker.hasExpiredTrialLicense();
public Rendering clone() /* throws CloneNotSupportedException */ {
try {
Rendering object = (Rendering) super.clone();
object.engine = null;
RenderedOp downSampler = createDownScaleOp(sourceImage, MIP_SCALE_RATIO);
downSampler.removeSources();
object.inputTransform = buildTransform(true);
object.transform = buildTransform(false);
object.xformedSourceImage = null;
object.pipeline = new LinkedList<OperationImpl>();
for ( OperationImpl op : pipeline )
object.pipeline.add(((BlendedOperation) op).clone(object));
return object;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
void dispose() {
if (pipeline != null) {
while (!pipeline.isEmpty())
pipeline.removeLast().dispose();
pipeline = null;
}
}
void addOperation(int position, Operation op) {
pipeline.add(position, (OperationImpl) op);
}
Operation removeOperation(int position) {
return pipeline.remove(position);
}
public int indexOf(Operation op) {
return pipeline.indexOf(op);
}
public Operation getOperation(int index) {
return pipeline.get(index);
}
protected RenderedOp createDownScaleOp(RenderedImage src, int ratio) {
KernelJAI kernel = Functions.getLanczos2Kernel(ratio);
int ko = kernel.getXOrigin();
float kdata[] = kernel.getHorizontalKernelData();
float qsFilterArray[] = new float[kdata.length - ko];
System.arraycopy(kdata, ko, qsFilterArray, 0, qsFilterArray.length);
ParameterBlock params = new ParameterBlock();
params.addSource(src);
params.add(ratio);
params.add(ratio);
params.add(qsFilterArray);
params.add(Interpolation.getInstance(Interpolation.INTERP_NEAREST));
return JAI.create("FilteredSubsample", params,
new RenderingHints(JAI.KEY_BORDER_EXTENDER,
BorderExtender.createInstance(BorderExtender.BORDER_COPY)));
}
private static final int MIP_SCALE_RATIO = 2;
class ImagePyramid {
private RenderedImage currentImage;
private int currentLevel = 0;
Vector<RenderedImage> renderings = new Vector<RenderedImage>();
ImagePyramid(RenderedImage image) {
currentImage = image;
renderings.addElement(currentImage);
}
public RenderedImage getUpImage() {
if (currentLevel > 0) {
currentLevel--;
currentImage = renderings.get(currentLevel);
}
return currentImage;
}
public RenderedImage getDownImage() {
currentLevel++;
if (renderings.size() <= currentLevel) {
RenderedOp smaller = createDownScaleOp(currentImage, MIP_SCALE_RATIO);
smaller.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
renderings.addElement(smaller);
return currentImage = smaller;
} else {
return currentImage = renderings.get(currentLevel);
}
}
public RenderedImage getImage(int level) {
if (level < 0)
return null;
while (currentLevel < level)
getDownImage();
while (currentLevel > level)
getUpImage();
return currentImage;
}
}
public Rendering(PlanarImage sourceImage, ImageEditorEngine engine) {
this.sourceImage = sourceImage;
this.engine = engine;
RenderedOp downSampler = createDownScaleOp(sourceImage, MIP_SCALE_RATIO);
downSampler.removeSources();
pyramid = new ImagePyramid(sourceImage);
xformedSourceImage = null;
inputTransform = buildTransform(true);
transform = buildTransform(false);
}
public Rendering(PlanarImage sourceImage) {
this(sourceImage, null);
}
ImageEditorEngine getEngine() {
return engine;
}
public void update(OperationImpl op, boolean isLive) {
if (engine != null)
engine.update(op, isLive);
}
public AffineTransform getInputTransform() {
return new AffineTransform(inputTransform);
}
public AffineTransform getTransform() {
return new AffineTransform(transform);
}
public void setCropBounds(CropBounds cropBounds) {
if (!cropBounds.equals(this.cropBounds)) {
this.cropBounds = cropBounds;
inputTransform = buildTransform(true);
transform = buildTransform(false);
if (xformedSourceImage != null) {
xformedSourceImage.dispose();
xformedSourceImage = null;
}
}
}
public CropBounds getCropBounds() {
return cropBounds;
}
public void setScaleFactor(float scaleFactor) {
if (scaleFactor != this.scaleFactor) {
this.scaleFactor = scaleFactor;
inputTransform = buildTransform(true);
transform = buildTransform(false);
if (xformedSourceImage != null) {
xformedSourceImage.dispose();
xformedSourceImage = null;
}
}
}
public float getScaleFactor() {
return scaleFactor;
}
public void setCropAndScale(CropBounds cropBounds, float scaleFactor) {
if (!cropBounds.equals(this.cropBounds) || scaleFactor != this.scaleFactor) {
this.cropBounds = cropBounds;
this.scaleFactor = scaleFactor;
inputTransform = buildTransform(true);
transform = buildTransform(false);
if (xformedSourceImage != null) {
xformedSourceImage.dispose();
xformedSourceImage = null;
}
}
}
public PlanarImage getXformedSourceImage() {
if (xformedSourceImage == null)
xformedSourceImage = transformSourceImage();
return xformedSourceImage;
}
public PlanarImage getRendering(boolean inactive, int stopBefore) {
PlanarImage processedImage = getXformedSourceImage();
if (pipeline != null) {
int index = 0;
for (OperationImpl operation : pipeline) {
if (index == stopBefore)
break;
if (operation.isActive() && !(inactive && operation.isDeactivatable())) {
PlanarImage result = operation.render(processedImage, scaleFactor < 1 ? scaleFactor : 1);
if (result != null)
processedImage = result;
}
index++;
}
} else
System.out.println("Rendering.renderPipeline: null pipeline?");
return licenseExpired ? new UnlicensedOpImage(processedImage, null) : processedImage;
}
public void prefetch(Rectangle area) {
PlanarImage processedImage = getXformedSourceImage();
if (pipeline != null) {
int index = 0;
for (OperationImpl operation : pipeline) {
if (operation.isActive() /* && !(inactive && operation.isDeactivatable()) */) {
PlanarImage result = operation.render(processedImage, scaleFactor < 1 ? scaleFactor : 1);
if (result != null) {
Point[] indices = result.getTileIndices(area);
if (indices != null) {
CachedImage cachedResult = new CachedImage(new ImageLayout(result), JAIContext.fileCache);
result.prefetchTiles(indices);
for (Point tile : indices) {
Raster newTile = result.getTile(tile.x, tile.y);
WritableRaster cachedTile = cachedResult.getWritableTile(tile.x, tile.y);
Functions.copyData(cachedTile, newTile);
}
result = cachedResult;
}
System.out.println("Rendered layer " + index);
processedImage = result;
}
}
index++;
}
} else
System.out.println("Rendering.renderPipeline: null pipeline?");
}
public PlanarImage getRendering() {
return getRendering(false, -1);
}
public PlanarImage getRendering(boolean inactive) {
return getRendering(inactive, -1);
}
public PlanarImage getRendering(int stopBefore) {
return getRendering(false, stopBefore);
}
public Dimension getRenderingSize() {
if (cropBounds.isAngleOnly()) {
// NOTE: we must clone PlanarImage.getBounds() since it returns a reference to an object
Rectangle sourceBounds = new Rectangle(sourceImage.getBounds());
if (cropBounds.getAngle() != 0) {
Point2D center = new Point2D.Double(sourceBounds.getCenterX(), sourceBounds.getCenterY());
sourceBounds = AffineTransform.getRotateInstance(-cropBounds.getAngle(),
center.getX(),
center.getY()).createTransformedShape(sourceBounds).getBounds();
}
return new Dimension(sourceBounds.width,
sourceBounds.height);
} else
return new Dimension((int) cropBounds.getWidth(),
(int) cropBounds.getHeight());
}
private AffineTransform buildTransform(boolean isInputTransform) {
// NOTE: we must clone PlanarImage.getBounds() since it returns a reference to an object
Rectangle sourceBounds = new Rectangle(sourceImage.getBounds());
AffineTransform transform = new AffineTransform();
// Scale
if (scaleFactor < 1 || !isInputTransform) {
float scaleX = (float) Math.floor(scaleFactor * sourceBounds.width) / (float) sourceBounds.width;
float scaleY = (float) Math.floor(scaleFactor * sourceBounds.height) / (float) sourceBounds.height;
transform.preConcatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
}
// Rotate
if (cropBounds.getAngle() != 0) {
Rectangle2D bounds = transform.createTransformedShape(sourceBounds).getBounds2D();
Point2D center = new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
transform.preConcatenate(AffineTransform.getRotateInstance(-cropBounds.getAngle(),
center.getX(),
center.getY()));
bounds = transform.createTransformedShape(sourceBounds).getBounds2D();
transform.preConcatenate(AffineTransform.getTranslateInstance(-bounds.getMinX(), -bounds.getMinY()));
}
// Crop
if (!cropBounds.isAngleOnly()) {
CropBounds actualCropBounds = CropBounds.transform(transform, cropBounds);
Point2D cropUpperLeft = actualCropBounds.getUpperLeft();
transform.preConcatenate(AffineTransform.getTranslateInstance(-cropUpperLeft.getX(),
-cropUpperLeft.getY()));
}
return transform;
}
private PlanarImage transformSourceImage() {
PlanarImage image = sourceImage;
PlanarImage xformedSourceImage = image;
AffineTransform completeInputTransform = inputTransform;
if (!completeInputTransform.isIdentity()) {
AffineTransform transform = completeInputTransform;
Point2D zero = transform.transform(new Point2D.Double(0, 0), null);
Point2D one = transform.transform(new Point2D.Double(1, 1), null);
double dx = one.getX() - zero.getX();
double dy = one.getY() - zero.getY();
double scale = Math.sqrt(dx*dx + dy*dy)/Math.sqrt(2.0);
if (!cheapScale && scale <= 0.5) {
int level = 0;
while(scale <= 1/(double) MIP_SCALE_RATIO) {
scale *= MIP_SCALE_RATIO;
level++;
transform = new AffineTransform(transform);
transform.concatenate(AffineTransform.getScaleInstance(MIP_SCALE_RATIO, MIP_SCALE_RATIO));
}
image = (PlanarImage) pyramid.getImage(level);
}
if (!transform.isIdentity()) {
RenderingHints extenderHints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
BorderExtender.createInstance(BorderExtender.BORDER_COPY));
ParameterBlock params = new ParameterBlock();
params.addSource(image);
params.add(transform);
params.add(Interpolation.getInstance(cheapScale ? Interpolation.INTERP_BILINEAR : Interpolation.INTERP_BICUBIC));
// params.add(Interpolation.getInstance(Interpolation.INTERP_BILINEAR));
xformedSourceImage = JAI.create("Affine", params, extenderHints);
} else
xformedSourceImage = image;
}
if (!cropBounds.isAngleOnly()) {
CropBounds actualCropBounds = CropBounds.transform(completeInputTransform, cropBounds);
Rectangle bounds = new Rectangle(xformedSourceImage.getMinX(), xformedSourceImage.getMinY(),
xformedSourceImage.getWidth(), xformedSourceImage.getHeight());
Rectangle finalBounds = bounds.intersection(new Rectangle(0, 0,
(int) actualCropBounds.getWidth(),
(int) actualCropBounds.getHeight()));
if (finalBounds.width > 0 && finalBounds.height > 0)
xformedSourceImage = Functions.crop(xformedSourceImage,
finalBounds.x, finalBounds.y,
finalBounds.width, finalBounds.height, null);
}
// We explicitly cache this
xformedSourceImage = Functions.toUShortLinear(xformedSourceImage, null);
if (xformedSourceImage instanceof RenderedOp)
xformedSourceImage.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
return xformedSourceImage;
}
}