Java tutorial
/** * 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; } }