util.ImgJaiTool.java Source code

Java tutorial

Introduction

Here is the source code for util.ImgJaiTool.java

Source

/**
* Copyright (c) 2001-2012 "Redbasin Networks, INC" [http://redbasin.org]
*
* This file is part of Redbasin OpenDocShare community project.
*
* Redbasin OpenDocShare 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/

package util;

import com.sun.media.jai.codec.ImageEncodeParam;
import com.sun.media.jai.codec.JPEGEncodeParam;
import com.sun.media.jai.codec.SeekableStream;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import javax.media.jai.*;
import javax.media.jai.operator.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Java Advanced Imaging API
 * 
 * This may be replaced by ImageMagick implementation
 */
public class ImgJaiTool implements ImgUtil {

    protected boolean applyAutoLevels = false;
    protected float subsampleReduceThreshold = 0.5F;
    protected int trimBorder = 5;
    protected int maxAutoLevelsShift = 15;

    protected double autoLevelsPixelCountThreshold = 0.006D; // gimp says .006

    private static final Float ZERO = 0F;

    public static Log logger = LogFactory.getLog(ImgJaiTool.class);

    static {
        // http://archives.java.sun.com/cgi-bin/wa?A2=ind0202&L=jai-interest&P=3228
        // setting tile cache to 160MB
        JAI.getDefaultInstance().setTileCache(JAI.createTileCache(1024 * 1024 * 160));
    }

    @Override
    public ImgMetadata makeImg(byte[] bytes) throws ImgCreateException {
        return new ImageDataJaiImpl(bytes);
    }

    /**
     * 
     * get a new scale factor given target maxwidth and maxheight
     * 
     * @param image
     * @param width
     *            maxwidth for target
     * @param height
     *            maxheight for target
     * @return
     */
    private float scaleRatio(RenderedOp image, int width, int height) {
        int curWidth = image.getWidth();
        int curHeight = image.getHeight();
        float ratio = (float) width / (float) height;
        float curRatio = (float) curWidth / (float) curHeight;
        float newRatio = 1.0f;

        if (ratio < curRatio) { // width too big
            newRatio = ((float) width + 0.5F) / (float) curWidth;
            // + 0.5 to prevent rounding errors
        } else { // height too big
            newRatio = ((float) height + 0.5F) / (float) curHeight;
            // + 0.5 to prevent rounding errors
        }
        return newRatio;
    }

    /**
     * A helper method that adds the tile cache hint with JAI.KEY_IMAGE_LAYOUT
     * to improve scale performance. This reduces the tile cache used to scale
     * the image.
     * 
     * warning... seems to slow things down in some cases.
     * 
     * http://archives.java.sun.com/cgi-bin/wa?A2=ind0202&L=jai-interest&P=3228
     * 
     * @param hints
     *            existing list of RenderingHints
     * @param image
     *            image to scale
     * @param factX
     *            scaling factor x
     * @param factY
     *            scaling factor y
     */
    protected static RenderingHints addScaleTileCacheHint(RenderingHints hints, RenderedOp image, float factX,
            float factY) {
        int tW = (int) (image.getTileWidth() * factX);
        int tH = (int) (image.getTileHeight() * factY);
        if (tW <= 0) {
            tW = 1;
        }
        if (tH <= 0) {
            tH = 1;
        }
        ImageLayout il = null;
        if (hints == null) {
            il = new ImageLayout().setTileWidth(tW).setTileHeight(tH);
            hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, il);
        } else {
            il = (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT);
            if (il == null) {
                il = new ImageLayout();
            }
            il.setTileWidth(tW).setTileHeight(tH);
            hints.put(JAI.KEY_IMAGE_LAYOUT, il);
        }
        return hints;
    }

    private static RenderingHints borderHint = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
            BorderExtender.createInstance(BorderExtender.BORDER_COPY));

    /**
     * trim some number of pixels off all corners
     * 
     * @param image
     * @param pixels #
     *            of pixels in the border to trim
     * @return
     */
    private RenderedOp trimBorder(RenderedOp image, int pixels) {
        Float pixf = new Float(pixels);
        return CropDescriptor.create(image, pixf, pixf, (float) image.getWidth() - 2 * pixels,
                (float) image.getHeight() - 2 * pixels, null);
    }

    /**
     * 
     * algorithm from:
     * http://archives.java.sun.com/cgi-bin/wa?A2=ind0207&L=jai-interest&F=&S=&P=31835jai
     * 
     */
    @Override
    public ImgMetadata scaleImg(ImgMetadata imagedata, int width, int height) {
        RenderedOp image = ((ImageDataJaiImpl) imagedata).getImage();
        return new ImageDataJaiImpl(scaleImage(image, width, height));
    }

    protected RenderedOp scaleImage(RenderedOp image, int width, int height) {

        int iwidth = image.getWidth();
        int iheight = image.getHeight();

        if (width == iwidth && height == iheight) {
            return image;
        }

        if (trimBorder > 0 && iwidth - width > 2 * trimBorder && iheight - height > 2 * trimBorder) {
            image = trimBorder(image, trimBorder);
        }

        float newRatio = scaleRatio(image, width, height);

        if (newRatio == 1.0F) {
            return image;
        }

        // use buffered Image here because scale operations don't work well with
        // cropped images which are not buffered.
        BufferedImage bi = image.getAsBufferedImage();
        RenderedOp img;

        if (newRatio < subsampleReduceThreshold) {
            Double scale = new Double(newRatio);
            img = SubsampleAverageDescriptor.create(bi, scale, scale, borderHint);
        } else {
            Interpolation interpolation = Interpolation.getInstance(Interpolation.INTERP_BICUBIC_2);
            img = ScaleDescriptor.create(bi, newRatio, newRatio, ZERO, ZERO, interpolation, borderHint);
        }
        if (img.getWidth() > width || img.getHeight() > height) {
            // off-by-one error can occur here
            return CropDescriptor.create(img, ZERO, ZERO, (float) Math.min(img.getWidth(), width),
                    (float) Math.min(img.getHeight(), height), null);
        }
        return img;
    }

    @Override
    public ImgMetadata cropImg(ImgMetadata imagedata, int width, int height) {
        RenderedOp image = ((ImageDataJaiImpl) imagedata).getImage();
        return new ImageDataJaiImpl(cropImage(image, width, height));
    }

    public RenderedOp cropImage(RenderedOp image, int width, int height) {
        int scaleWidth = image.getWidth();
        int scaleHeight = image.getHeight();

        if (scaleWidth == width && scaleHeight == height) {
            return image;
        }

        if (trimBorder > 0 && scaleWidth - width > 2 * trimBorder && scaleHeight - height > 2 * trimBorder) {
            image = trimBorder(image, trimBorder);
        }

        float ratio = (float) width / (float) height;
        float curRatio = (float) scaleWidth / (float) scaleHeight;
        if (ratio < curRatio) { // width bigger
            if (scaleHeight < height) {
                scaleWidth = scaleWidth * height / scaleHeight;
            }
            scaleHeight = height;
        } else { // height bigger
            if (scaleWidth < width) {
                scaleHeight = scaleHeight * width / scaleWidth;
            }
            scaleWidth = width;
        }

        RenderedOp intermediate = scaleImage(image, scaleWidth, scaleHeight);

        if (ratio == curRatio || (intermediate.getWidth() <= width && intermediate.getHeight() <= height)) {
            // XXX: maybe if the image is too small we should impose a white border?
            return intermediate;
        }

        return CropDescriptor.create(intermediate,
                (ratio < curRatio) ? (intermediate.getWidth() - width) / 2 : ZERO, ZERO, (float) width,
                (float) height, null);

    }

    @Override
    public ImgMetadata cropImg(ImgMetadata image, int xoffset, int yoffset, int cropwidth, int cropheight) {
        ImageDataJaiImpl idj = (ImageDataJaiImpl) image;

        if (cropwidth == 0) {
            cropwidth = image.getWidth() - xoffset;
        }
        if (cropheight == 0) {
            cropheight = image.getHeight() - yoffset;
        }
        RenderedOp ro = CropDescriptor.create(idj.getImage(), (float) xoffset, (float) yoffset, (float) cropwidth,
                (float) cropheight, null);
        return new ImageDataJaiImpl(ro);
    }

    public final static double[][] strip_alpha_matrix = { { 1.0D, 0.0D, 0.0D, 0.0D, 0.0D },
            { 0.0D, 1.0D, 0.0D, 0.0D, 0.0D }, { 0.0D, 0.0D, 1.0D, 0.0D, 0.0D } };

    protected static RenderedOp removeAlphaTransparency(RenderedOp image) {
        ColorModel cm = image.getColorModel();
        if (cm.hasAlpha() || cm.getNumColorComponents() == 4) {
            if (cm instanceof IndexColorModel) {
                // band combine doesn't work on IndexColorModel
                // http://java.sun.com/products/java-media/jai/jai-bugs-1_0_2.html
                IndexColorModel icm = (IndexColorModel) cm;
                byte[][] data = new byte[3][icm.getMapSize()];
                icm.getReds(data[0]);
                icm.getGreens(data[1]);
                icm.getBlues(data[2]);
                LookupTableJAI lut = new LookupTableJAI(data);
                image = JAI.create("lookup", image, lut);
            } else {
                image = BandCombineDescriptor.create(image, strip_alpha_matrix, null);
            }
        }
        return image;
    }

    /**
     * 
     * This function replicates the "Auto Levels" functionality from 'GIMP'
     * 
     * @param image
     * @param numStandardDeviations
     * @return
     */
    protected RenderedOp auto_levels(RenderedOp image) {
        final int binCount = 256;
        final int[] nbins = { binCount, binCount, binCount }; // The number of bins.
        final double[] low = { 0.0D, 0.0D, 0.0D }; // The low value.
        final double[] high = { 256.0D, 256.0D, 256.0D }; // The high value.

        // get a histogram for analysis
        image = HistogramDescriptor.create(image, null, 1, 1, nbins, low, high, null);
        Histogram hist = (Histogram) image.getProperty("histogram");

        int[] totals = hist.getTotals();
        int[][] bins = hist.getBins();
        double[] scale = new double[bins.length];
        double[] offset = new double[bins.length];

        for (int channel = 0; channel < bins.length; channel++) {
            int newlow = 0;
            int newhigh = 255;
            double count = 0;
            double percentage, next_percentage;
            // algorithm to find newhigh and newlow is from the gimp
            int i;
            for (i = 0; i <= maxAutoLevelsShift; i++) {
                count += bins[channel][i];
                percentage = count / totals[channel];
                next_percentage = (count + bins[channel][i + 1]) / totals[channel];
                if (Math.abs(percentage - autoLevelsPixelCountThreshold) < Math
                        .abs(next_percentage - autoLevelsPixelCountThreshold)) {
                    break;
                }
            }
            newlow = i;
            count = 0;
            for (i = 255; i >= 255 - maxAutoLevelsShift; i--) {
                count += bins[channel][i];
                percentage = count / totals[channel];
                next_percentage = (count + bins[channel][i - 1]) / totals[channel];
                if (Math.abs(percentage - autoLevelsPixelCountThreshold) < Math
                        .abs(next_percentage - autoLevelsPixelCountThreshold)) {

                    break;
                }
            }
            newhigh = i; // gimp says i-1, but this would be a bit too agressive

            // http://java.sun.com/products/java-media/jai/forDevelopers/jai1_0_1guide-unc/Image-enhance.doc.html#76502
            scale[channel] = 255.0D / (newhigh - newlow);
            offset[channel] = (255.0D * newlow) / (newlow - newhigh);

            logger.debug("channel " + channel + " [" + newlow + "," + newhigh + "]");

        }
        return RescaleDescriptor.create(image, scale, offset, null);
    }

    public class ImageDataJaiImpl implements ImgMetadata {
        private RenderedOp imageForProcessing = null;
        private RenderedOp imageRaw = null;

        public ImageDataJaiImpl(RenderedOp image) {
            this.imageForProcessing = image;
        }

        public ImageDataJaiImpl(byte[] bytes) throws ImgCreateException {
            try {
                imageRaw = JAI.create("stream",
                        SeekableStream.wrapInputStream(new ByteArrayInputStream(bytes), true));
                imageRaw.getWidth();
                imageRaw.getHeight();
            } catch (Exception ex) {
                throw new ImgCreateException(ex);
            }
        }

        @Override
        public void writeTo(OutputStream os, String type, int quality) {
            if (quality == 0) {
                quality = 75;
            }
            if (type == null) {
                type = "jpeg";
            }
            ImageEncodeParam encodeParam = null;
            if (type.equalsIgnoreCase("jpeg")) {
                encodeParam = new JPEGEncodeParam();
                quality = Math.max(0, Math.min(quality, 100));
                ((JPEGEncodeParam) encodeParam).setQuality((float) quality / 100.0f);
            }

            BufferedImage bufferedImage = getImage().getAsBufferedImage();
            JAI.create("encode", bufferedImage, os, type, encodeParam);
        }

        @Override
        public int getHeight() {
            if (imageRaw != null) {
                return imageRaw.getHeight();
            }
            return getImage().getHeight();
        }

        @Override
        public int getWidth() {
            if (imageRaw != null) {
                return imageRaw.getWidth();
            }
            return getImage().getWidth();
        }

        public RenderedOp getImage() {
            if (imageForProcessing == null) {
                if (imageRaw == null) {
                    return null;
                }
                ColorModel cm = imageRaw.getColorModel();
                imageForProcessing = removeAlphaTransparency(imageRaw);
                if (applyAutoLevels && !(cm instanceof IndexColorModel)) {
                    // don't apply auto_levels if original image used an index
                    // color model
                    imageForProcessing = auto_levels(imageForProcessing);
                }
            }
            return imageForProcessing;
        }
    } //inner class ImageDataJaiImpl

    public float getSubsampleReduceThreshold() {
        return subsampleReduceThreshold;
    }

    public void setSubsampleReduceThreshold(float subsampleReduceThreshold) {
        this.subsampleReduceThreshold = subsampleReduceThreshold;
    }

    public int getTrimBorder() {
        return trimBorder;
    }

    public void setTrimBorder(int trimBorder) {
        this.trimBorder = trimBorder;
    }

    public boolean getApplyAutoLevels() {
        return applyAutoLevels;
    }

    public void setApplyAutoLevels(boolean applyAutoLevels) {
        this.applyAutoLevels = applyAutoLevels;
    }

    public double getAutoLevelsPixelCountThreshold() {
        return autoLevelsPixelCountThreshold;
    }

    public void setAutoLevelsPixelCountThreshold(double autoLevelsPixelCountThreshold) {
        this.autoLevelsPixelCountThreshold = autoLevelsPixelCountThreshold;
    }

}