org.photovault.image.PhotovaultImage.java Source code

Java tutorial

Introduction

Here is the source code for org.photovault.image.PhotovaultImage.java

Source

/*
  Copyright (c) 2007 Harri Kaimio
     
  This file is part of Photovault.
     
  Photovault is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
     
  Photovault is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  General Public License for more details.
     
  You should have received a copy of the GNU General Public License
  along with Photovault; if not, write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

package org.photovault.image;

import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import java.io.File;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
import javax.media.jai.Histogram;
import javax.media.jai.IHSColorSpace;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationBilinear;
import javax.media.jai.JAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.OpImage;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderableOp;
import javax.media.jai.RenderedOp;
import javax.media.jai.operator.CropDescriptor;
import javax.media.jai.operator.HistogramDescriptor;
import javax.media.jai.operator.LookupDescriptor;
import javax.media.jai.operator.OverlayDescriptor;
import javax.media.jai.operator.ScaleDescriptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 PhotovaultImage is a facade fro Photovault imaging pipeline. It is abstract 
 class, different image providers must derive their own classes from it.
 */
public abstract class PhotovaultImage {

    static private Log log = LogFactory.getLog(PhotovaultImage.class.getName());

    /** Creates a new instance of PhotovaultImage */
    public PhotovaultImage() {
    }

    protected File f = null;

    /**
     * Get aperture (f-stop) used when shooting the image
     * 
     * @return F-stop number reported by dcraw
     */
    public abstract double getAperture();

    /**
     * Get the camera mode used to shoot the image
     * 
     * @return Camera model reported by dcraw
     */
    public abstract String getCamera();

    /**
     * Get the name of company that made the camera
     * @return
     */
    public abstract String getCameraMaker();

    /**
     Get the original image
     @deprecated Use getRenderedImage instead
     */
    public abstract RenderableOp getCorrectedImage(int minWidth, int minHeight, boolean isLowQualityAllowed);

    /**
     Get the original image
     @deprecated Use getRenderedImage instead. TODO: This should be changed to 
     private, but some test cases depend on this.
     */
    public RenderableOp getCorrectedImage() {
        return getCorrectedImage(Integer.MAX_VALUE, Integer.MAX_VALUE, false);
    }

    /**
     Get the sample model of the corrected image. This must be overriden by 
     derived classes
     */
    public abstract SampleModel getCorrectedImageSampleModel();

    /**
     Get the color model of the corrected image. This must be overriden by 
     derived classes
     */
    public abstract ColorModel getCorrectedImageColorModel();

    RenderableOp previousCorrectedImage = null;
    RenderableOp cropped = null;
    RenderableOp saturated = null;

    RenderableOp colorCorrected = null;

    /*
    Operation that applies rotation to original image
     */
    RenderableOp rotatedImage = null;

    /*
     Operation that crops rotatedImage
     */
    RenderableOp croppedImage = null;

    /*
     Operation that transforms croppedImage so that it begins in origo
     */
    RenderableOp xformCroppedImage = null;

    /**
     The multiplyChan operation used to adjust saturation
     */
    RenderableOp saturatedIhsImage = null;

    RenderableOp originalImage = null;

    /**
     Last rendering created.
     */
    RenderedImage lastRendering = null;

    protected void buildPipeline(RenderableOp original) {
        originalImage = original;
        cropped = getCropped(original);
        cropped.setProperty("org.photovault.opname", "cropped_image");

        colorCorrected = getColorCorrected(cropped);
        colorCorrected.setProperty("org.photovault.opname", "color_corrected_rgb_image");
        //        RenderableOp scaled = getScaled( cropped, maxWidth, maxHeight );
        saturated = getSaturated(colorCorrected);

        previousCorrectedImage = original;
    }

    /**
     Objects that should be notified about new renderings.
     */
    Set<ImageRenderingListener> renderingListeners = new HashSet<ImageRenderingListener>();

    /**
     Request that a given {@link ImageRenderingListener} should be notified of
     changes to this image.
     */
    public void addRenderingListener(ImageRenderingListener l) {
        renderingListeners.add(l);
    }

    /**
     Request that a given {@link ImageRenderingListener} should not anymore be 
     notified of changes to this image.
     */
    public void removeRenderingListener(ImageRenderingListener l) {
        renderingListeners.remove(l);
    }

    protected void fireNewRenderingEvent() {
        for (ImageRenderingListener l : renderingListeners) {
            l.newRenderingCreated(this);
        }
    }

    /**
     Get the image, adjusted according to current parameters and scaled to a 
     specified resolution.
     @param maxWidth Maximum width of the image in pixels. Image aspect ratio is
     preserved so actual width can be smaller than this. If maxWidth < 0, use
     actual width of original image instead.
     @param maxHeight Maximum height of the image in pixels. Image aspect ratio is
     preserved so actual height can be smaller than this. If maxHeight < 0, use
     actual height of original image instead.
     @param isLowQualityAllowed Specifies whether image quality can be traded off 
     for speed/memory consumpltion optimizations.
     @return The image as RenderedImage
     */

    public RenderedImage getRenderedImage(int maxWidth, int maxHeight, boolean isLowQualityAllowed) {
        /*
         Calculate the resolution we need for the original image based on crop
         & rotation information
         */

        // First, calculate the size of whole inage after rotation
        double rotRad = rot * Math.PI / 180.0;
        double rotSin = Math.abs(Math.sin(rotRad));
        double rotCos = Math.abs(Math.cos(rotRad));
        double rotW = rotCos * getWidth() + rotSin * getHeight();
        double rotH = rotSin * getWidth() + rotCos * getHeight();
        // Size if full image was cropped
        double cropW = rotW * (cropMaxX - cropMinX);
        double cropH = rotH * (cropMaxY - cropMinY);
        if (maxWidth < 0) {
            maxWidth = (int) cropW;
        }
        if (maxHeight < 0) {
            maxHeight = (int) cropH;
        }
        double scaleW = maxWidth / cropW;
        double scaleH = maxHeight / cropH;
        // We are fitting cropped area to max{width x height} so we must use the 
        // scale from dimension needing smallest scale.
        double scale = Math.min(scaleW, scaleH);
        double needW = getWidth() * scale;
        double needH = getHeight() * scale;

        RenderableOp original = getCorrectedImage((int) needW, (int) needH, isLowQualityAllowed);
        if (previousCorrectedImage != original) {
            buildPipeline(original);
        }

        int renderingWidth = maxWidth;
        int renderingHeight = (int) (renderingWidth * cropH / cropW);
        if (renderingHeight > maxHeight) {
            renderingHeight = maxHeight;
            renderingWidth = (int) (renderingHeight * cropW / cropH);
        }
        RenderingHints hints = new RenderingHints(null);
        hints.put(JAI.KEY_INTERPOLATION, new InterpolationBilinear());

        RenderedImage rendered = null;
        if (lastRendering != null && lastRendering instanceof PlanarImage) {
            ((PlanarImage) lastRendering).dispose();
            System.gc();
        }
        if (saturated != null) {
            rendered = saturated.createScaledRendering(renderingWidth, renderingHeight, hints);
            lastRendering = rendered;
        } else {
            /*
             The image color model does not support saturation (e.g. b/w image),
             show the previous step instead.
             */
            rendered = colorCorrected.createScaledRendering(renderingWidth, renderingHeight, hints);
            lastRendering = rendered;
        }
        fireNewRenderingEvent();
        return rendered;
    }

    /**
     Get the image, adjusted according to current parameters and scaled with a 
     specified factor.
     @param scale Scaling compared to original image file.
     @param isLowQualityAllowed Specifies whether image quality can be traded off 
     for speed/memory consumpltion optimizations.
     @return The image as RenderedImage
     */

    public RenderedImage getRenderedImage(double scale, boolean isLowQualityAllowed) {
        /*
         Calculate the resolution we need for the original image based on crop
         & rotation information
         */

        // First, calculate the size of whole inage after rotation
        double rotRad = rot * Math.PI / 180.0;
        double rotSin = Math.abs(Math.sin(rotRad));
        double rotCos = Math.abs(Math.cos(rotRad));
        double rotW = rotCos * getWidth() + rotSin * getHeight();
        double rotH = rotSin * getWidth() + rotCos * getHeight();
        // Size if full image was cropped
        double cropW = rotW * (cropMaxX - cropMinX);
        double cropH = rotH * (cropMaxY - cropMinY);

        RenderableOp original = getCorrectedImage((int) (getWidth() * scale), (int) (getHeight() * scale),
                isLowQualityAllowed);
        if (previousCorrectedImage != original) {
            buildPipeline(original);
        }
        RenderingHints hints = new RenderingHints(null);
        hints.put(JAI.KEY_INTERPOLATION, new InterpolationBilinear());
        if (lastRendering != null && lastRendering instanceof PlanarImage) {
            ((PlanarImage) lastRendering).dispose();
            System.gc();
        }
        RenderedImage rendered = null;
        if (saturated != null) {
            rendered = saturated.createScaledRendering((int) (scale * cropW), (int) (scale * cropH), hints);
            lastRendering = rendered;
        } else {
            rendered = colorCorrected.createScaledRendering((int) (scale * cropW), (int) (scale * cropH), hints);
            lastRendering = rendered;
        }
        fireNewRenderingEvent();
        return rendered;
    }

    public static final String HISTOGRAM_RGB_CHANNELS = "rgb";
    public static final String HISTOGRAM_IHS_CHANNELS = "ihs";

    /**
     Get a histogram of a specific phase of imaging pipeline
     @param histType What histogram to retrieve. Possible values in PhotovaultImage
     are
     <ul>
     <li>HISTOGRAM_RGB_CHANNELS - sRGB color space histogam calculated after 
     cropping but before color correction is applied</li>
     <li>HISTOGRAM_IHS_CHANNELS - IHS color space histogram calculated after RGB 
     color corrections but before saturation correction</li>
     </ul>
     Derived classes may add additional histograms
     @return The histogram from last rendering. TODO: Currently returns 
     <code>null</code> if no rendering has been made.
     */

    public Histogram getHistogram(String histType) {
        Histogram ret = null;
        RenderedImage src = null;
        if (histType.equals(HISTOGRAM_RGB_CHANNELS)) {
            // Calculate histogram from the image into which color correction was applied
            src = findNamedRendering(lastRendering, "cropped_image");
        } else if (histType.equals(HISTOGRAM_IHS_CHANNELS)) {
            src = findNamedRendering(lastRendering, "color_corrected_ihs_image");
            // src = lastSaturatedRendering.getSources().get(0).getSources().get(0);
        }
        if (src != null) {
            int[] componentSizes = src.getColorModel().getComponentSize();
            int numBins[] = new int[componentSizes.length];
            double lowValue[] = new double[componentSizes.length];
            double highValue[] = new double[componentSizes.length];
            for (int n = 0; n < componentSizes.length; n++) {
                numBins[n] = 1 << componentSizes[n];
                lowValue[n] = 0.0;
                highValue[n] = (double) numBins[n] - 1.0;
            }
            RenderedOp histOp = HistogramDescriptor.create(src, null, Integer.valueOf(1), Integer.valueOf(1),
                    numBins, lowValue, highValue, null);
            ret = (Histogram) histOp.getProperty("histogram");
        }
        return ret;
    }

    public void dispose() {
        if (lastRendering != null && lastRendering instanceof PlanarImage) {
            ((PlanarImage) lastRendering).dispose();
        }
        System.gc();
    }

    /**
     Get width of the original image
     @return width in pixels
     */
    public abstract int getWidth();

    /**
     Get height of the original image
     @return height in pixels
     */
    public abstract int getHeight();

    /**
     Amount of rotation that is applied to the image.
     */
    double rot = 0.0;

    /**
     Set the rotation applied to original image
     @param r Rotation in degrees. The image is rotated clockwise
     */
    public void setRotation(double r) {
        rot = r;
        applyRotCrop();
    }

    /**
     Get current rotation
     @return Rotation in degrees, clockwise.
     */
    public double getRotation() {
        return rot;
    }

    double saturation = 1.0;

    /**
     Set saturation for the image
     @param s New saturation
     */
    public void setSaturation(double s) {
        saturation = s;
        ColorCurve satCurve = new ColorCurve();
        satCurve.addPoint(0.0, 0.0);
        if (s < 1.0) {
            satCurve.addPoint(1.0, s);
        } else {
            satCurve.addPoint(1.0 / s, 1.0);
        }
        ChannelMapOperationFactory cmf = new ChannelMapOperationFactory(channelMap);
        cmf.setChannelCurve("saturation", satCurve);
        channelMap = cmf.create();
        // Check that this image has a color model that supports saturation change
        if (saturatedIhsImage != null) {
            saturatedIhsImage.setParameter(createSaturationMappingLUT(), 0);
        }
    }

    /**
     Get current saturation multiplier for the image
     @return saturation
     */
    public double getSaturation() {
        return saturation;
    }

    /**
     Mapping of color channels
     */
    ChannelMapOperation channelMap = null;

    /**
     *     Set color channel mapping
     * 
     * @param cm New mapping of color channels
     */
    public void setColorAdjustment(ChannelMapOperation cm) {
        this.channelMap = cm;
        applyColorMapping();
    }

    /**
     Get current color channel mapping
     */
    public ChannelMapOperation getColorAdjustment() {
        return channelMap;
    }

    /**
     Set the lookup tables in JAI pipeline to match color adjustment
     */
    protected void applyColorMapping() {
        if (channelMap == null || colorCorrected == null) {
            return;
        }
        LookupTableJAI jailut = createColorMappingLUT();
        colorCorrected.setParameter(jailut, 0);
        if (saturatedIhsImage != null) {
            saturatedIhsImage.setParameter(createSaturationMappingLUT(), 0);
        }

    }

    /**
     Create a color mapping LUT based on current color channel mapping.
     @return the created LUT. If not channel mapping is specified, returns an
     identity mapping.
     */
    private LookupTableJAI createColorMappingLUT() {
        ColorModel colorModel = this.getCorrectedImageColorModel();
        ColorSpace colorSpace = colorModel.getColorSpace();

        int[] componentSizes = colorModel.getComponentSize();
        ColorCurve valueCurve = null;
        ColorCurve[] componentCurves = new ColorCurve[componentSizes.length];
        boolean[] applyValueCurve = new boolean[componentSizes.length];
        for (int n = 0; n < componentSizes.length; n++) {
            applyValueCurve[n] = false;
        }

        if (channelMap != null) {
            valueCurve = channelMap.getChannelCurve("value");
            switch (colorSpace.getType()) {
            case ColorSpace.TYPE_GRAY:
                // Gray scale image - just apply value curve to 1st channel
                componentCurves[0] = valueCurve;
                break;
            case ColorSpace.TYPE_RGB:
                componentCurves[0] = channelMap.getChannelCurve("red");
                componentCurves[1] = channelMap.getChannelCurve("green");
                componentCurves[2] = channelMap.getChannelCurve("blue");
                applyValueCurve[0] = true;
                applyValueCurve[1] = true;
                applyValueCurve[2] = true;
                break;
            default:
                // Unsupported color format. Just return identity transform
                break;
            }
        }
        if (valueCurve == null) {
            // No color mapping found, use identity mapping
            valueCurve = new ColorCurve();
        }

        LookupTableJAI jailut = null;
        if (componentSizes[0] == 8) {
            byte[][] lut = new byte[componentSizes.length][256];
            double dx = 1.0 / 256.0;
            for (int band = 0; band < colorModel.getNumComponents(); band++) {
                for (int n = 0; n < lut[band].length; n++) {
                    double x = dx * n;
                    double val = x;
                    if (band < componentCurves.length && componentCurves[band] != null) {
                        val = componentCurves[band].getValue(val);
                    }
                    if (band < applyValueCurve.length && applyValueCurve[band]) {
                        val = valueCurve.getValue(val);
                    }
                    val = Math.max(0.0, Math.min(val, 1.0));
                    lut[band][n] = (byte) ((lut[band].length - 1) * val);
                }
            }
            jailut = new LookupTableJAI(lut);
        } else if (componentSizes[0] == 16) {
            short[][] lut = new short[componentSizes.length][0x10000];
            double dx = 1.0 / 65536.0;
            for (int band = 0; band < colorModel.getNumComponents(); band++) {
                for (int n = 0; n < lut[band].length; n++) {
                    double x = dx * n;
                    double val = x;
                    if (band < componentCurves.length && componentCurves[band] != null) {
                        val = componentCurves[band].getValue(val);
                    }
                    if (band < applyValueCurve.length && applyValueCurve[band]) {
                        val = valueCurve.getValue(val);
                    }
                    val = Math.max(0.0, Math.min(val, 1.0));
                    lut[band][n] = (short) ((lut[band].length - 1) * val);
                }
            }
            jailut = new LookupTableJAI(lut, true);
        } else {
            log.error("Unsupported data type with with = " + componentSizes[0]);
        }
        return jailut;
    }

    /**
     Create lookup table for saturation correction. The operation is done 
     in IHS color space, so the lookup table will contain identity mapping
     for intensity & hue channels and map based "saturation" curve from channelMap
     for saturation. If "saturation curve is nonexistent, use identity mapping for 
     saturation as well.
         
     */
    private LookupTableJAI createSaturationMappingLUT() {
        ColorModel colorModel = this.getCorrectedImageColorModel();

        int[] componentSizes = colorModel.getComponentSize();
        ColorCurve satCurve = null;
        if (channelMap != null) {
            satCurve = channelMap.getChannelCurve("saturation");
        }
        if (satCurve == null) {
            satCurve = new ColorCurve();
        }

        LookupTableJAI jailut = null;
        if (componentSizes[0] == 8) {
            byte[][] lut = new byte[componentSizes.length][256];
            double dx = 1.0 / 256.0;
            for (int band = 0; band < componentSizes.length; band++) {
                // Saturation
                for (int n = 0; n < lut[band].length; n++) {
                    double x = dx * n;
                    double val = x;
                    if (band == 2) {
                        val = satCurve.getValue(val);
                    }
                    val = Math.max(0.0, Math.min(val, 1.0));
                    lut[band][n] = (byte) ((lut[band].length - 1) * val);
                }
            }

            jailut = new LookupTableJAI(lut);
        } else if (componentSizes[0] == 16) {
            short[][] lut = new short[componentSizes.length][0x10000];
            double dx = 1.0 / 65536.0;
            for (int band = 0; band < componentSizes.length; band++) {
                for (int n = 0; n < lut[band].length; n++) {
                    double x = dx * n;
                    double val = x;
                    if (band == 2) {
                        val = satCurve.getValue(val);
                    }
                    val = Math.max(0.0, Math.min(val, 1.0));
                    lut[band][n] = (short) ((lut[band].length - 1) * val);
                }
            }
            jailut = new LookupTableJAI(lut, true);
        } else {
            log.error("Unsupported data type with with = " + componentSizes[0]);
        }
        return jailut;
    }

    double cropMinX = 0.0;
    double cropMinY = 0.0;
    double cropMaxX = 1.0;
    double cropMaxY = 1.0;

    /**
     Set new crop bounds for the image. Crop bounds are applied after rotation,
     so that top left corner is (0, 0) and bottom right corner (1, 1)
     @param c New crop bounds
     */
    public void setCropBounds(Rectangle2D c) {
        cropMinX = Math.min(1.0, Math.max(0.0, c.getMinX()));
        cropMinY = Math.min(1.0, Math.max(0.0, c.getMinY()));
        cropMaxX = Math.min(1.0, Math.max(0.0, c.getMaxX()));
        cropMaxY = Math.min(1.0, Math.max(0.0, c.getMaxY()));

        if (cropMaxX - cropMinX <= 0.0) {
            double tmp = cropMaxX;
            cropMaxX = cropMinX;
            cropMinX = tmp;
        }
        if (cropMaxY - cropMinY <= 0.0) {
            double tmp = cropMaxY;
            cropMaxY = cropMinY;
            cropMinY = tmp;
        }
        applyRotCrop();
    }

    /**
     Get the current crop bounds
     @return Crop bounds
     */
    public Rectangle2D getCropBounds() {
        return new Rectangle2D.Double(cropMinX, cropMinY, cropMaxX - cropMinX, cropMaxY - cropMinY);
    }

    protected RenderableOp getCropped(RenderableOp uncroppedImage) {
        float origWidth = uncroppedImage.getWidth();
        float origHeight = uncroppedImage.getHeight();

        AffineTransform xform = org.photovault.image.ImageXform.getRotateXform(rot, origWidth, origHeight);

        ParameterBlockJAI rotParams = new ParameterBlockJAI("affine");
        rotParams.addSource(uncroppedImage);
        rotParams.setParameter("transform", xform);
        rotParams.setParameter("interpolation", Interpolation.getInstance(Interpolation.INTERP_NEAREST));
        RenderingHints hints = new RenderingHints(null);
        /* 
         In practice, JAI optimizes the pipeline so that this trasnformation is
         concatenated with scaling from MultiresolutionImage to desired resolution.
         This transformation obeys KYE_INTERPOLATION hint so we must set it here,
         otherwise nearest neighborough interpolation will be used regardless of
         the settings on parameter block.
         */
        hints.put(JAI.KEY_INTERPOLATION, new InterpolationBilinear());
        rotatedImage = JAI.createRenderable("affine", rotParams, hints);
        rotatedImage.setProperty("org.photovault.opname", "rotated_image");
        /*
         Due to rounding errors in JAI pipeline transformations we have a danger 
         that the crop border goes outside image area. To avoind this, create a
         slightly larger background and overlay the actual image in front of it.         
         */
        RenderableOp background = ScaleDescriptor.createRenderable(rotatedImage, 1.01f, 1.01f, 0.0f, 0.0f,
                Interpolation.getInstance(Interpolation.INTERP_BILINEAR), hints);
        RenderableOp overlayed = OverlayDescriptor.createRenderable(background, rotatedImage, hints);

        ParameterBlockJAI cropParams = new ParameterBlockJAI("crop");
        cropParams.addSource(overlayed);
        float cropWidth = (float) (cropMaxX - cropMinX);
        cropWidth = (cropWidth > 0.000001) ? cropWidth : 0.000001f;
        float cropHeight = (float) (cropMaxY - cropMinY);
        cropHeight = (cropHeight > 0.000001) ? cropHeight : 0.000001f;

        float cropX = (float) (rotatedImage.getMinX() + cropMinX * rotatedImage.getWidth());
        float cropY = (float) (rotatedImage.getMinY() + cropMinY * rotatedImage.getHeight());
        float cropW = cropWidth * rotatedImage.getWidth();
        float cropH = cropHeight * rotatedImage.getHeight();
        Rectangle2D.Float cropRect = new Rectangle2D.Float(cropX, cropY, cropW, cropH);
        Rectangle2D.Float srcRect = new Rectangle2D.Float(rotatedImage.getMinX(), rotatedImage.getMinY(),
                rotatedImage.getWidth(), rotatedImage.getHeight());
        if (!srcRect.contains(cropRect)) {
            // Crop rectangle must fit inside source image
            Rectangle2D.intersect(srcRect, cropRect, cropRect);
            if (!srcRect.contains(cropRect)) {
                // Ups, we have a problem... this can happen due to rounding errors
                // (for example, if cropY is just above zero). 
                // Make the crop rectangle slightly smaller so that the rounding 
                // error is avoided
                final float correction = 5E-3f;
                if (srcRect.getMinX() > cropRect.getMinX()) {
                    cropX += correction;
                }
                if (srcRect.getMinY() > cropRect.getMinY()) {
                    cropY += correction;
                }
                if (srcRect.getMaxX() < cropRect.getMaxX()) {
                    cropW -= correction;
                }
                if (srcRect.getMaxY() < cropRect.getMaxY()) {
                    cropH -= correction;
                }
                cropRect = new Rectangle2D.Float(cropX, cropY, cropW, cropH);
                if (!srcRect.contains(cropRect)) {
                    // Hmm, now I am out of ideas...
                    cropX = rotatedImage.getMinX();
                    cropY = rotatedImage.getMinY();
                    cropW = rotatedImage.getWidth();
                    cropH = rotatedImage.getHeight();
                }
            }
        }

        cropParams.setParameter("x", cropX);
        cropParams.setParameter("y", cropY);
        cropParams.setParameter("width", cropW);
        cropParams.setParameter("height", cropH);
        CropDescriptor cdesc = new CropDescriptor();
        StringBuffer msg = new StringBuffer();
        if (!cdesc.validateArguments("renderable", cropParams, msg)) {
            log.error("Error settings up crop operation: " + msg.toString());
        }

        croppedImage = JAI.createRenderable("crop", cropParams, hints);
        // Translate the image so that it begins in origo
        rotatedImage.setProperty("org.photovault.opname", "cropped_image");
        ParameterBlockJAI pbXlate = new ParameterBlockJAI("translate");
        pbXlate.addSource(croppedImage);
        pbXlate.setParameter("xTrans", (float) (-croppedImage.getMinX()));
        pbXlate.setParameter("yTrans", (float) (-croppedImage.getMinY()));
        xformCroppedImage = JAI.createRenderable("translate", pbXlate);
        rotatedImage.setProperty("org.photovault.opname", "xlated_image");
        return xformCroppedImage;
    }

    /**
     Apply the current rotation & cropping settings to JAi image pipeline.
     */
    protected void applyRotCrop() {
        if (originalImage == null) {
            return;
        }

        float origWidth = originalImage.getWidth();
        float origHeight = originalImage.getHeight();

        AffineTransform xform = org.photovault.image.ImageXform.getRotateXform(rot, origWidth, origHeight);

        ParameterBlockJAI rotParams = (ParameterBlockJAI) rotatedImage.getParameterBlock();
        rotParams.setParameter("transform", xform);
        rotParams.setParameter("interpolation", Interpolation.getInstance(Interpolation.INTERP_NEAREST));
        rotatedImage.setParameterBlock(rotParams);

        ParameterBlockJAI cropParams = (ParameterBlockJAI) croppedImage.getParameterBlock();
        float cropWidth = (float) (cropMaxX - cropMinX);
        cropWidth = (cropWidth > 0.000001) ? cropWidth : 0.000001f;
        float cropHeight = (float) (cropMaxY - cropMinY);
        cropHeight = (cropHeight > 0.000001) ? cropHeight : 0.000001f;
        float cropX = (float) (rotatedImage.getMinX() + cropMinX * rotatedImage.getWidth());
        float cropY = (float) (rotatedImage.getMinY() + cropMinY * rotatedImage.getHeight());
        float cropW = cropWidth * rotatedImage.getWidth();
        float cropH = cropHeight * rotatedImage.getHeight();
        cropParams.setParameter("x", cropX);
        cropParams.setParameter("y", cropY);
        cropParams.setParameter("width", cropW);
        cropParams.setParameter("height", cropH);
        croppedImage.setParameterBlock(cropParams);

        // Translate the image so that it begins in origo
        ParameterBlockJAI pbXlate = (ParameterBlockJAI) xformCroppedImage.getParameterBlock();
        pbXlate.addSource(croppedImage);
        pbXlate.setParameter("xTrans", (float) (-croppedImage.getMinX()));
        pbXlate.setParameter("yTrans", (float) (-croppedImage.getMinY()));
        xformCroppedImage.setParameterBlock(pbXlate);
    }

    protected PlanarImage getScaled(RenderableOp unscaledImage, int maxWidth, int maxHeight) {
        AffineTransform thumbScale = org.photovault.image.ImageXform.getFittingXform(maxWidth, maxHeight, 0,
                unscaledImage.getWidth(), unscaledImage.getHeight());
        ParameterBlockJAI scaleParams = new ParameterBlockJAI("affine");
        scaleParams.addSource(unscaledImage);
        scaleParams.setParameter("transform", thumbScale);
        scaleParams.setParameter("interpolation", Interpolation.getInstance(Interpolation.INTERP_NEAREST));

        RenderedOp scaledImage = JAI.create("affine", scaleParams);
        scaledImage.setProperty("org.photovault.opname", "scaled_image");

        return scaledImage;
    }

    protected PlanarImage getScaled(PlanarImage unscaledImage, double scale) {
        AffineTransform thumbScale = org.photovault.image.ImageXform.getScaleXform(scale, 0,
                unscaledImage.getWidth(), unscaledImage.getHeight());
        ParameterBlockJAI scaleParams = new ParameterBlockJAI("affine");
        scaleParams.addSource(unscaledImage);
        scaleParams.setParameter("transform", thumbScale);
        scaleParams.setParameter("interpolation", Interpolation.getInstance(Interpolation.INTERP_BILINEAR));

        RenderedOp scaledImage = JAI.create("affine", scaleParams);
        scaledImage.setProperty("org.photovault.opname", "scaled_image");
        return scaledImage;
    }

    /**
     Get the JAI graph node for color correction
     @param src Node to use as input for color correction.
     */

    protected RenderableOp getColorCorrected(RenderableOp src) {
        // Initialize lookup table based on original image color model
        LookupTableJAI jailut = createColorMappingLUT();
        return LookupDescriptor.createRenderable(src, jailut, null);
    }

    /**
     Add saturation mapping into from of the image processing pipeline.
     @param src The image to which saturation correction is applied
     @return Saturation change operator.
     */
    protected RenderableOp getSaturated(RenderableOp src) {
        IHSColorSpace ihs = IHSColorSpace.getInstance();
        ColorModel srcCm = getCorrectedImageColorModel();
        int[] componentSizes = srcCm.getComponentSize();
        if (componentSizes.length != 3) {
            // This is not an RGB image
            // TODO: handle also images with alpha channel
            return null;
        }
        ColorModel ihsColorModel = new ComponentColorModel(ihs, componentSizes, false, false, Transparency.OPAQUE,
                srcCm.getTransferType());

        // Create a ParameterBlock for the conversion.
        ParameterBlock pb = new ParameterBlock();
        pb.addSource(src);
        pb.add(ihsColorModel);
        // Do the conversion.
        RenderableOp ihsImage = JAI.createRenderable("colorconvert", pb);
        ihsImage.setProperty("org.photovault.opname", "color_corrected_ihs_image");

        //        saturatedIhsImage =
        //                MultiplyConstDescriptor.createRenderable( ihsImage, new double[] {1.0, 1.0, saturation}, null );
        LookupTableJAI jailut = createSaturationMappingLUT();
        saturatedIhsImage = LookupDescriptor.createRenderable(ihsImage, jailut, null);
        saturatedIhsImage.setProperty("org.photovault.opname", "saturated_ihs_image");
        pb = new ParameterBlock();
        pb.addSource(saturatedIhsImage);
        ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
        ColorModel srgbColorModel = new ComponentColorModel(sRGB, componentSizes, false, false, Transparency.OPAQUE,
                srcCm.getTransferType());
        pb.add(srgbColorModel); // RGB color model!        
        RenderableOp saturatedImage = JAI.createRenderable("colorconvert", pb);
        saturatedImage.setProperty("org.photovault.opname", "saturated_image");

        return saturatedImage;
    }

    /**
     Find the last cached rendering of a certain phase of image processing pipeline.
     The name is stored in JAI property "org.photovault.opname".
     <p>
     This function makes a depth-first search to the sources of given node and 
     returns the first node that has the name.
     @param op The "sink" node of the image processing graph that is searched
     @param The name
     @return First node found with given name or <code>NULL</codel> if no such 
     node is found.
     */
    RenderedImage findNamedRendering(RenderedImage op, String name) {
        if (op != null) {
            Object imgName = op.getProperty("org.photovault.opname");
            if (imgName.equals(name)) {
                return op;
            }
            Vector sources = op.getSources();
            if (sources == null) {
                // Apparently we arrived at the leaf node without finding the 
                // correct rendering.
                return null;
            }
            for (Object o : sources) {
                if (o != null && o instanceof RenderedImage) {
                    RenderedImage candidate = findNamedRendering((RenderedImage) o, name);
                    if (candidate != null) {
                        return candidate;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Get the film speed setting used when shooting the image
     * 
     * @return Film speed (in ISO) as reported by dcraw
     */
    public abstract int getFilmSpeed();

    /**
     * Get the focal length from image file meta data.
     * 
     * @return Focal length used when taking the picture (in millimetres)
     */
    public abstract double getFocalLength();

    public abstract RenderedImage getImage();

    public File getImageFile() {
        return f;
    }

    /**
     * Get the shutter speed used when shooting the image
     * 
     * @return Exposure time (in seconds) as reported by dcraw
     */
    public abstract double getShutterSpeed();

    /**
     * Get the shooting time of the image
     * 
     * @return Shooting time as reported by dcraw or <CODE>null</CODE> if
     * not available
     */
    public abstract Date getTimestamp();

    private void debugPrintGraph(RenderedImage img, String prefix) {
        Object opName = img.getProperty("org.photovault.opname");
        if (opName != null && opName instanceof String) {
            System.out.println(prefix + opName + " (" + img.getClass().getName() + ")");
        } else {
            System.out.println(prefix + "unnamed image" + " (" + img.getClass().getName() + ")");
        }

        System.out.println(prefix + "  " + img.toString());
        if (img instanceof RenderedOp) {
            RenderedOp op = (RenderedOp) img;
            System.out.println(prefix + "  operation name" + op.getOperationName());
            ParameterBlock pb = op.getParameterBlock();
            if (pb instanceof ParameterBlockJAI) {
                ParameterBlockJAI pbj = (ParameterBlockJAI) pb;
                String[] paramNames = pbj.getParameterListDescriptor().getParamNames();
                Vector params = pbj.getParameters();
                for (int n = 0; n < paramNames.length; n++) {
                    System.out.println(prefix + "  " + paramNames[n] + params.get(n));
                }
            }
        }
        Vector<RenderedImage> sources = img.getSources();
        if (sources == null) {
            return;
        }
        for (RenderedImage parent : sources) {
            debugPrintGraph(parent, prefix + "    ");
        }
    }
}