/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.model.ImageEditor;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.LCROIShape;
import com.lightcrafts.jai.operator.LCMSColorConvertDescriptor;
import com.lightcrafts.jai.opimage.BlendOpImage;
import com.lightcrafts.jai.opimage.RGBColorSelectionMaskOpImage;
import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.jai.utils.Transform;
import com.lightcrafts.mediax.jai.*;
import com.lightcrafts.model.*;
import com.lightcrafts.utils.ColorScience;
import com.lightcrafts.utils.LCMS_ColorSpace;
import com.lightcrafts.utils.LCMS;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.Raster;
import java.awt.image.renderable.ParameterBlock;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public abstract class BlendedOperation extends GenericOperationImpl implements Cloneable {
private double opacity = 1.0;
private String blendingMode = "Normal";
private LCROIShape mask = null;
private PlanarImage colorSelectionMask = null;
private RGBColorSelection lastColorSelection = null;
protected boolean colorInputOnly = false;
protected AffineTransform lastTransform = null;
public BlendedOperation clone(Rendering rendering) {
try {
BlendedOperation object = (BlendedOperation) this.clone();
object.rendering = rendering;
// object.mask = null;
object.operation = null;
object.lastTransform = null;
return object;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
private boolean validRegion(Region region) {
return region != null
&& region.getOuterShape().getBounds().height > 0
&& region.getOuterShape().getBounds().width > 0;
}
public boolean hasMask() {
return mask != null;
}
public void setRegion(Region region) {
if (validRegion(region)) {
mask = new LCROIShape(region, rendering.getInputTransform());
} else if (region == null || region.getContours().size() == 0)
mask = null;
lastTransform = rendering.getInputTransform();
super.setRegion(region);
}
public void setRegionInverted(boolean inverted) {
super.setRegionInverted(inverted);
}
// Define the List of LayerModes statically so they can be vended
// by ImageEditorEngine:
static List<LayerModeImpl> blendingModes;
static {
blendingModes = new ArrayList<LayerModeImpl>();
for (BlendOpImage.BlendingMode b : BlendOpImage.BlendingMode.values())
blendingModes.add(new LayerModeImpl(b.getName()));
}
BlendedOperation(Rendering rendering, OperationType type) {
super(rendering, type);
}
// Support for color picker in color based selections
private Point2D clickPoint = null;
private Color selectedColor = null;
static float arctan2(float y, float x) {
final float coeff_1 = (float) Math.PI / 4;
final float coeff_2 = 3 * coeff_1;
final float abs_y = Math.abs(y) + 1e-10f; // kludge to prevent 0/0 condition
float angle;
if (x >= 0) {
float r = (x - abs_y) / (x + abs_y);
angle = coeff_1 - coeff_1 * r;
} else {
float r = (x + abs_y) / (abs_y - x);
angle = coeff_2 - coeff_1 * r;
}
return y < 0 ? -angle : angle;
}
public static float hue(float r, float g, float b) {
float x = r - (g+b) / 2;
float y = ((g-b) * (float) Math.sqrt(3) / 2);
return arctan2(y, x) + (float) Math.PI;
}
public void setColorSelection(RGBColorSelection selection) {
super.setColorSelection(selection);
}
public PlanarImage getColorSelectionMask() {
return colorSelectionMask;
}
public RGBColorSelection getColorSelectionAt(Point2D p) {
System.out.println("setColorSelection(): " + p);
this.clickPoint = p;
settingsChanged();
if (selectedColor != null) {
float r = selectedColor.getRed() / (float) 0xff;
float g = selectedColor.getGreen() / (float) 0xff;
float b = selectedColor.getBlue() / (float) 0xff;
selectedColor = null;
float feather = 0.1f;
float luminosity = (float) (Math.log1p(0xff * ColorScience.Wr * r +
0xff * ColorScience.Wg * g +
0xff * ColorScience.Wb * b) / (8 * Math.log(2)));
float minLuminosity = Math.max(luminosity-feather, 0);
float minLuminosityFeather = Math.min(minLuminosity, feather);
float maxLuminosity = Math.min(luminosity+feather, 1);
float maxLuminosityFeather = Math.min(1-maxLuminosity, feather);
return new RGBColorSelection(r, g, b, 0.4f,
minLuminosity, minLuminosityFeather,
maxLuminosity, maxLuminosityFeather,
false, true, true);
}
return new RGBColorSelection();
}
abstract class BlendedTransform extends Transform {
PlanarImage back;
SoftReference<PlanarImage> softFront = new SoftReference<PlanarImage>(null);
SoftReference<PlanarImage> softBlender = new SoftReference<PlanarImage>(null);
SoftReference<PlanarImage> softResult = new SoftReference<PlanarImage>(null);
int[] pointToPixel(Point2D p) {
if (p != null) {
Point2D pp = rendering.getTransform().transform(p, null);
int x = (int) pp.getX();
int y = (int) pp.getY();
if (rendering.getScaleFactor() > 1) {
x /= rendering.getScaleFactor();
y /= rendering.getScaleFactor();
}
if (back.getBounds().contains(x, y)) {
int tx = back.XToTileX(x);
int ty = back.YToTileY(y);
Raster tile = back.getTile(tx, ty);
int[] pixel = null;
int averagePixels = 3;
if (averagePixels > 1) {
Rectangle tileBounds = tile.getBounds();
Rectangle sampleRect = new Rectangle(x - averagePixels / 2,
y - averagePixels / 2,
averagePixels,
averagePixels);
Rectangle intersection = tileBounds.intersection(sampleRect);
pixel = new int[]{0, 0, 0};
int currentPixel[] = new int[3];
for (int i = intersection.x; i < intersection.x + intersection.width; i++)
for (int j = intersection.y; j < intersection.y + intersection.height; j++) {
currentPixel = tile.getPixel(i, j, currentPixel);
for (int k = 0; k < 3; k++)
pixel[k] = (pixel[k] + currentPixel[k]) / 2;
}
} else
pixel = tile.getPixel(x, y, pixel);
return pixel;
}
}
return null;
}
abstract public PlanarImage setFront();
public void dispose() {
back.removeSinks();
back.dispose();
back = null;
mask = null;
cachedImage = null;
}
private RenderedOp createBlender(PlanarImage front) {
// Overlay result on the original image
ParameterBlock pb = new ParameterBlock();
pb.addSource(front);
pb.addSource(back);
pb.add(blendingMode);
pb.add(new Double(invertedRegion ? -opacity : opacity));
pb.add(mask);
pb.add(colorSelectionMask);
// we don't know what front might generate, specify the output format to be the same as the input
RenderingHints formatHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(back));
RenderedOp blender = JAI.create("Blend", pb, formatHints);
// blender.setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
return blender;
}
BlendedTransform(PlanarImage source) {
back = source;
}
public void setSource(Object source) {
if (source != back) {
back.removeSinks();
back.dispose();
back = (PlanarImage) source;
changed = true;
}
}
private CachedImage cachedImage = null;
public PlanarImage render() {
/*
we have to update before any render, this is necessary otherwise
we would not propagate changes from one resolution to the other
NB: make sure that operations cache their state efficiently
*/
if (colorInputOnly && back.getColorModel().getNumComponents() != 3)
return back;
PlanarImage newRendering = update();
if (cachedImage == null || newRendering != cachedImage.getRendering())
cachedImage = new CachedImage(newRendering, scale);
return cachedImage;
}
public class CachedImage extends PlanarImage {
private final TileCache cache = JAIContext.fileCache;
private SoftReference<PlanarImage> softRendering = new SoftReference<PlanarImage>(null);
final float scale;
public CachedImage(PlanarImage rendering, float scale) {
super(new ImageLayout(rendering), null, null);
setProperty(JAIContext.PERSISTENT_CACHE_TAG, Boolean.TRUE);
softRendering = new SoftReference<PlanarImage>(rendering);
this.scale = scale;
// System.out.println("new CachedImage " + BlendedOperation.this.getClass());
}
public PlanarImage getRendering() {
return softRendering != null ? softRendering.get() : null;
}
public Raster getTile(int tileX, int tileY) {
Raster tile = cache.getTile(this, tileX, tileY);
if (tile == null) {
PlanarImage rendering = softRendering.get();
if (rendering == null) {
System.out.println("rendering null..." + BlendedOperation.this.getClass());
rendering = update();
softRendering = new SoftReference<PlanarImage>(rendering);
}
tile = rendering.getTile(tileX, tileY);
cache.add(this, tileX, tileY, tile);
}
return tile;
}
}
/*
Note: ever change sources or parameters in JAI pipelines, it is the slowest thing of all,
just rebuild everything from scratch
*/
public PlanarImage update() {
if (clickPoint != null) {
int pixel[] = pointToPixel(clickPoint);
if (pixel != null) {
int r = pixel[0] / 256;
int g = pixel[1] / 256;
int b = pixel[2] / 256;
selectedColor = new Color(r, g, b);
}
clickPoint = null;
}
if (!colorInputOnly || back.getColorModel().getNumComponents() == 3) {
boolean newFront = false;
PlanarImage front = softFront.get();
if (front == null || changed) {
front = setFront();
softFront = new SoftReference<PlanarImage>(front);
newFront = true;
}
PlanarImage result = softResult.get();
if (newFront || result == null) {
RGBColorSelection colorSelection = getColorSelection();
if (opacity != 1 || blendingMode != "Normal" || validRegion(getRegion())
|| (colorSelection != null && !colorSelection.isAllSelected())) {
RenderedOp blender = (RenderedOp) softBlender.get();
if (validRegion(getRegion())
&& (!rendering.getInputTransform().equals(lastTransform)
|| (blender != null && blender.getParameters().get(2) != mask))) {
mask = new LCROIShape(getRegion(), rendering.getInputTransform());
blender = null;
} else if (getRegion() == null)
mask = null;
if (colorSelection != null && !colorSelection.isAllSelected()
&& (!colorSelection.equals(lastColorSelection)
|| !rendering.getInputTransform().equals(lastTransform)
|| (blender != null && blender.getParameters().get(3) != colorSelectionMask))) {
PlanarImage labImage = Functions.toColorSpace(back, new LCMS_ColorSpace(new LCMS.LABProfile()),
LCMSColorConvertDescriptor.RELATIVE_COLORIMETRIC, null);
ParameterBlock pb = new ParameterBlock();
pb.addSource(labImage);
pb.add(new int[]{1, 2});
RenderedOp abImage = JAI.create("bandselect", pb, null);
pb = new ParameterBlock();
pb.addSource(back);
pb.add(new double[][]{{ColorScience.Wr, ColorScience.Wg, ColorScience.Wb, 0}});
PlanarImage monochrome = JAI.create("BandCombine", pb, null);
RenderingHints layoutHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, Functions.getImageLayout(labImage));
// layoutHints.add(JAIContext.noCacheHint);
pb = new ParameterBlock();
pb.addSource(monochrome);
pb.addSource(abImage);
PlanarImage maskImage = JAI.create("BandMerge", pb, layoutHints);
colorSelectionMask = new RGBColorSelectionMaskOpImage(maskImage, getColorSelection(), null);
ParameterBlock maskPB;
// KernelJAI morph = new KernelJAI(3, 3, new float[]{1, 1, 1, 1, 1, 1, 1, 1, 1});
//
// maskPB = new ParameterBlock();
// maskPB.addSource(colorSelectionMask);
// maskPB.add(morph);
// colorSelectionMask = JAI.create("Erode", maskPB, null);
//
// maskPB = new ParameterBlock();
// maskPB.addSource(colorSelectionMask);
// maskPB.add(morph);
// colorSelectionMask = JAI.create("Dilate", maskPB, null);
RenderingHints extenderHints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
BorderExtender.createInstance(BorderExtender.BORDER_COPY));
KernelJAI kernel = Functions.getGaussKernel(0.5 * scale);
maskPB = new ParameterBlock();
maskPB.addSource(colorSelectionMask);
maskPB.add(kernel);
colorSelectionMask = JAI.create("Convolve", maskPB, extenderHints);
lastColorSelection = colorSelection;
} else if (colorSelection == null || colorSelection.isAllSelected())
colorSelectionMask = null;
lastTransform = rendering.getInputTransform();
softResult = softBlender = new SoftReference<PlanarImage>(blender = createBlender(front));
return blender;
} else {
softResult = softFront;
return front;
}
} else
return result;
} else
return back;
}
}
abstract protected BlendedTransform createBlendedOp(PlanarImage source);
protected Transform createOp(PlanarImage source) {
return createBlendedOp(source);
}
public void setLayerConfig(LayerConfig layer) {
if (blendingMode != layer.getMode().getName() || opacity != layer.getOpacity()) {
blendingMode = layer.getMode().getName();
opacity = layer.getOpacity();
settingsChanged();
}
}
public LayerConfig getDefaultLayerConfig() {
return new LayerConfig(new LayerModeImpl("Normal"), 1.);
}
}