/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.core.api.image.util; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.awt.image.renderable.ParameterBlock; import java.io.File; import java.util.Hashtable; import javax.media.jai.BorderExtenderConstant; import javax.media.jai.Interpolation; import javax.media.jai.JAI; import javax.media.jai.LookupTableJAI; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import org.weasis.core.api.gui.util.MathUtil; import org.weasis.core.api.media.data.ImageElement; /** * An image manipulation toolkit. * */ public class ImageToolkit { public static final RenderingHints NOCACHE_HINT = new RenderingHints(JAI.KEY_TILE_CACHE, null); private ImageToolkit() { } /** * Load an image. * * NOTE: Encapsulate a mechanism to close properly the image (see closeLoadImageStream()) */ public static RenderedOp loadImage(File file) { return JAI.create("LoadImage", file); //$NON-NLS-1$ } public static RenderedImage getImageOp(RenderedImage image, String opName) { if (image instanceof RenderedOp) { RenderedOp op = (RenderedOp) image; if (op.getOperationName().equalsIgnoreCase(opName)) { return image; } while (op.getNumSources() > 0) { try { PlanarImage img = op.getSourceImage(0); if (image instanceof RenderedOp) { RenderedOp op2 = (RenderedOp) img; if (op2.getOperationName().equalsIgnoreCase(opName)) { return img; } } } catch (Exception ex) { return null; } } } return null; } public static BufferedImage convertRenderedImage(RenderedImage img) { if (img instanceof BufferedImage) { return (BufferedImage) img; } ColorModel cm = img.getColorModel(); int width = img.getWidth(); int height = img.getHeight(); WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); Hashtable<String, Object> properties = new Hashtable<>(); String[] keys = img.getPropertyNames(); if (keys != null) { for (int i = 0; i < keys.length; i++) { properties.put(keys[i], img.getProperty(keys[i])); } } BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties); img.copyData(raster); return result; } /** * Convert index color mapped image content to a full 24-bit 16-million color RGB image. * * @param image * the source image to convert. * @return a full RGB color image as RenderedOp. */ public static RenderedImage convertIndexColorToRGBColor(RenderedImage image) { RenderedImage result = image; // If the source image is color mapped, convert it to 3-band RGB. // Note that GIF and PNG files fall into this category. if (image.getColorModel() instanceof IndexColorModel) { // Retrieve the IndexColorModel IndexColorModel icm = (IndexColorModel) image.getColorModel(); // Cache the number of elements in each band of the colormap. int mapSize = icm.getMapSize(); // Allocate an array for the lookup table data. byte[][] lutData = new byte[3][mapSize]; // Load the lookup table data from the IndexColorModel. icm.getReds(lutData[0]); icm.getGreens(lutData[1]); icm.getBlues(lutData[2]); // Create the lookup table object. LookupTableJAI lut = new LookupTableJAI(lutData); // Replace the original image with the 3-band RGB image. result = JAI.create("lookup", image, lut); //$NON-NLS-1$ } return result; } /** * Scale an image up/down to the desired width and height. The aspect ratio of the image will not be maintained. * * @param image * the source image to scale * @param scaleWidth * the new width to scale to * @param scaleHeight * the new height to scale to * @return a scaled image as RenderedOp * @see #scaleImage(RenderedOp, int, int, boolean, double) */ public static RenderedOp scaleImage(RenderedOp image, int scaleWidth, int scaleHeight) { return scaleImage(image, scaleWidth, scaleHeight, false, 0); } /** * Scale an image up/down to the desired width and height, while maintaining the image's aspect ratio (if * requested). * * @param image * the source image to scale * @param scaleWidth * the new width to scale to * @param scaleHeight * the new height to scale to * @param keepAspect * true if the aspect ratio should be maintained. * @param color * the color to fill borders when maintaining aspect ratio (0 = black, 255 = white). * @return a scaled image as RenderedOp * @see #scaleImage(RenderedOp, int, int) */ public static RenderedOp scaleImage(RenderedOp image, int scaleWidth, int scaleHeight, boolean keepAspect, double color) { float xScale = (float) scaleWidth / (float) image.getWidth(); float yScale = (float) scaleHeight / (float) image.getHeight(); boolean resize = false; if (keepAspect) { resize = Math.abs(xScale - yScale) < .0000001; xScale = Math.min(xScale, yScale); yScale = xScale; } ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add(xScale); // x scale factor params.add(yScale); // y scale factor params.add(0.0F); // x translate params.add(0.0F); // y translate params.add(Interpolation.getInstance(Interpolation.INTERP_BICUBIC)); RenderedOp result = JAI.create("scale", params, ImageToolkit.NOCACHE_HINT); //$NON-NLS-1$ if (resize) { result = resizeImage(result, scaleWidth, scaleHeight, color); } return result; } /** * Resize an image to the new dimensions - no scaling is performed on the image, but the canvas size is changed. Any * empty areas are filled with white. * * @param image * the source image to resize * @param toWidth * the new width to resize to * @param toHeight * the new height to resize to * @param color * the color to fill borders when resizing up. * @return the resized image as RenderedOp */ public static RenderedOp resizeImage(RenderedOp image, int toWidth, int toHeight, double color) { int width = image.getWidth(); int height = image.getHeight(); RenderedOp resImage = image; if (width > toWidth || height > toHeight) { resImage = cropImage(resImage, Math.min(width, toWidth), Math.min(height, toHeight)); } if (width < toWidth || height < toHeight) { int w = Math.max((toWidth - width) / 2, 0); int h = Math.max((toHeight - height) / 2, 0); resImage = borderImage(resImage, w, w, h, h, color); } return resImage; } /** * Crop down an image to smaller dimensions. Used by resizeImage() when an image dimension is smaller. * * @param image * the source image to crop * @param toWidth * the new width to crop to * @param toHeight * the new height to crop to * @return a cropped image as RenderedOp * @see #resizeImage(RenderedOp, int, int) */ public static RenderedOp cropImage(RenderedOp image, int toWidth, int toHeight) { int width = image.getWidth(); int height = image.getHeight(); int xOffset = (width - toWidth) / 2; int yOffset = (height - toHeight) / 2; ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add((float) xOffset); // x origin params.add((float) yOffset); // y origin params.add((float) toWidth); // width params.add((float) toHeight); // height return JAI.create("crop", params, null); //$NON-NLS-1$ } /** * Add a colored border edge around an image, with the margin defined as left, right, top and bottom. Used by * resizeImage() when an image dimension is larger. * * @param image * the source image to add borders to * @param left * the left edge border width * @param right * the right edge border width * @param top * the top edge border height * @param btm * the bottom edge border height * @param color * the color to use for the border (0 = black, 255 = white) * @return a bordered image as RenderedOp * @see #resizeImage(RenderedOp, int, int) */ public static RenderedOp borderImage(RenderedOp image, int left, int right, int top, int btm, double color) { ParameterBlock params = new ParameterBlock(); params.addSource(image); params.add(left); // left pad params.add(right); // right pad params.add(top); // top pad params.add(btm); // bottom pad double[] fill = { color }; params.add(new BorderExtenderConstant(fill));// type params.add(color); // fill color return JAI.create("border", params, null); //$NON-NLS-1$ } /** * Apply window/level to the image source. Note: this method cannot be used with a DicomImageElement as image * parameter. * * @param image * @param source * @param window * @param level * @param pixelPadding * @return */ public static RenderedImage getDefaultRenderedImage(ImageElement image, RenderedImage source, double window, double level, boolean pixelPadding) { if (image == null || source == null) { return null; } RenderedImage result = null; SampleModel sampleModel = source.getSampleModel(); if (sampleModel == null) { return null; } int datatype = sampleModel.getDataType(); if (datatype == DataBuffer.TYPE_BYTE && MathUtil.isEqual(window, 255.0) && (MathUtil.isEqual(level, 127.5) || MathUtil.isEqual(level, 127.0))) { return source; } // Get pixel values of Min and Max (values must not be rescaled rescaled, works only for ImageElement not for // DicomImageElement class) int minValue = (int) image.getMinValue(null, pixelPadding); int maxValue = (int) image.getMaxValue(null, pixelPadding); int tableLength = maxValue - minValue + 1; double low = level - window / 2.0; double high = level + window / 2.0; // use a lookup table for rescaling double range = high - low; if (range < 1.0) { range = 1.0; } double slope = 255.0 / range; double yInt = 255.0 - slope * high; if (datatype >= DataBuffer.TYPE_BYTE && datatype < DataBuffer.TYPE_INT) { byte[][] lut = new byte[1][tableLength]; for (int i = 0; i < tableLength; i++) { int value = (int) (slope * (i + minValue) + yInt); if (value > 255) { value = 255; } if (value < 0) { value = 0; } lut[0][i] = (byte) value; } LookupTableJAI lookup = new LookupTableJAI(lut, minValue); ParameterBlock pb = new ParameterBlock(); pb.addSource(source); pb.add(lookup); result = JAI.create("lookup", pb, null); // hints); //$NON-NLS-1$ } else if (datatype == DataBuffer.TYPE_INT || datatype == DataBuffer.TYPE_FLOAT || datatype == DataBuffer.TYPE_DOUBLE) { ParameterBlock pb = new ParameterBlock(); pb.addSource(source); pb.add(new double[] { slope }); pb.add(new double[] { yInt }); result = JAI.create("rescale", pb, null); //$NON-NLS-1$ // produce a byte image pb = new ParameterBlock(); pb.addSource(result); pb.add(DataBuffer.TYPE_BYTE); result = JAI.create("format", pb, null); //$NON-NLS-1$ } return result; } public static RenderedImage getDefaultRenderedImage(ImageElement image, RenderedImage source, boolean pixelPadding) { return getDefaultRenderedImage(image, source, image.getDefaultWindow(pixelPadding), image.getDefaultLevel(pixelPadding), true); } }