/******************************************************************************* * 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.media.data; import java.awt.Point; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.renderable.ParameterBlock; import java.io.IOException; import java.lang.ref.Reference; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.gui.util.ActionW; import org.weasis.core.api.gui.util.MathUtil; import org.weasis.core.api.image.LutShape; import org.weasis.core.api.image.OpManager; import org.weasis.core.api.image.measure.MeasurementsAdapter; import org.weasis.core.api.image.util.ImageToolkit; import org.weasis.core.api.image.util.Unit; import org.weasis.core.api.util.ThreadUtil; public class ImageElement extends MediaElement { private static final Logger LOGGER = LoggerFactory.getLogger(ImageElement.class); /* * Imageio issue with native library in multi-thread environment (to avoid JVM crash let only one simultaneous * thread) (https://java.net/jira/browse/JAI_IMAGEIO_CORE-126) * * Try multi-thread reading with new native decoders * * public static final ExecutorService IMAGE_LOADER = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime() * .availableProcessors() / 2)); */ // TODO evaluate the difference, keep one thread with sun decoder. (seems to hangs on shutdown) public static final ExecutorService IMAGE_LOADER = ThreadUtil.buildNewSingleThreadExecutor("Image Loader"); //$NON-NLS-1$ private static final SoftHashMap<ImageElement, PlanarImage> mCache = new SoftHashMap<ImageElement, PlanarImage>() { @Override public void removeElement(Reference<? extends PlanarImage> soft) { ImageElement key = reverseLookup.remove(soft); if (key != null) { hash.remove(key); MediaReader reader = key.getMediaReader(); key.setTag(TagW.ImageCache, false); if (reader != null) { // Close the image stream reader.close(); } } } }; protected boolean readable = true; protected double pixelSizeX = 1.0; protected double pixelSizeY = 1.0; protected Unit pixelSpacingUnit = Unit.PIXEL; protected String pixelSizeCalibrationDescription = null; protected String pixelValueUnit = null; protected Double minPixelValue; protected Double maxPixelValue; public ImageElement(MediaReader mediaIO, Object key) { super(mediaIO, key); } protected void findMinMaxValues(RenderedImage img, boolean exclude8bitImage) throws OutOfMemoryError { // This function can be called several times from the inner class Load. // Do not compute min and max it has already be done if (img != null && !isImageAvailable()) { int datatype = img.getSampleModel().getDataType(); if (datatype == DataBuffer.TYPE_BYTE && exclude8bitImage) { this.minPixelValue = 0.0; this.maxPixelValue = 255.0; } else { ParameterBlock pb = new ParameterBlock(); pb.addSource(img); // ImageToolkit.NOCACHE_HINT to ensure this image won't be stored in tile cache RenderedOp dst = JAI.create("extrema", pb, ImageToolkit.NOCACHE_HINT); //$NON-NLS-1$ double[][] extrema = (double[][]) dst.getProperty("extrema"); //$NON-NLS-1$ double min = Double.MAX_VALUE; double max = -Double.MAX_VALUE; int numBands = dst.getSampleModel().getNumBands(); for (int i = 0; i < numBands; i++) { min = Math.min(min, extrema[0][i]); max = Math.max(max, extrema[1][i]); } this.minPixelValue = min; this.maxPixelValue = max; // Handle special case when min and max are equal, ex. black image // + 1 to max enables to display the correct value if (this.minPixelValue.equals(this.maxPixelValue)) { this.maxPixelValue += 1.0; } } } } public boolean isImageAvailable() { return maxPixelValue != null && minPixelValue != null; } protected boolean isGrayImage(RenderedImage source) { // Binary images have indexColorModel if (source.getSampleModel().getNumBands() > 1 || source.getColorModel() instanceof IndexColorModel) { return false; } return true; } public LutShape getDefaultShape(boolean pixelPadding) { return LutShape.LINEAR; } public double getDefaultWindow(boolean pixelPadding) { return getMaxValue(null, pixelPadding) - getMinValue(null, pixelPadding); } public double getDefaultLevel(boolean pixelPadding) { if (isImageAvailable()) { double min = getMinValue(null, pixelPadding); return min + (getMaxValue(null, pixelPadding) - min) / 2.0; } return 0.0f; } public double getMaxValue(TagReadable tagable, boolean pixelPadding) { return maxPixelValue == null ? 0.0 : maxPixelValue; } public double getMinValue(TagReadable tagable, boolean pixelPadding) { return minPixelValue == null ? 0.0 : minPixelValue; } public int getRescaleWidth(int width) { return (int) Math.ceil(width * getRescaleX() - 0.5); } public int getRescaleHeight(int height) { return (int) Math.ceil(height * getRescaleY() - 0.5); } public double getRescaleX() { return pixelSizeX <= pixelSizeY ? 1.0 : pixelSizeX / pixelSizeY; } public double getRescaleY() { return pixelSizeY <= pixelSizeX ? 1.0 : pixelSizeY / pixelSizeX; } public double getPixelSize() { return pixelSizeX <= pixelSizeY ? pixelSizeX : pixelSizeY; } public void setPixelSize(double pixelSize) { if (MathUtil.isEqual(pixelSizeX, pixelSizeY)) { setPixelSize(pixelSize, pixelSize); } else if (pixelSizeX < pixelSizeY) { setPixelSize(pixelSize, (pixelSizeY / pixelSizeX) * pixelSize); } else { setPixelSize((pixelSizeX / pixelSizeY) * pixelSize, pixelSize); } } public void setPixelSize(double pixelSizeX, double pixelSizeY) { /* * Image is always displayed with a 1/1 aspect ratio, otherwise it becomes very difficult (even impossible) to * handle measurement tools. When the ratio is not 1/1, the image is stretched. The smallest ratio keeps the * pixel size and the largest one is downscaled. */ this.pixelSizeX = pixelSizeX <= 0.0 ? 1.0 : pixelSizeX; this.pixelSizeY = pixelSizeY <= 0.0 ? 1.0 : pixelSizeY; } public void setPixelValueUnit(String pixelValueUnit) { this.pixelValueUnit = pixelValueUnit; } public Unit getPixelSpacingUnit() { return pixelSpacingUnit; } public void setPixelSpacingUnit(Unit pixelSpacingUnit) { this.pixelSpacingUnit = pixelSpacingUnit; } public String getPixelValueUnit() { return pixelValueUnit; } public String getPixelSizeCalibrationDescription() { return pixelSizeCalibrationDescription; } public MeasurementsAdapter getMeasurementAdapter(Unit displayUnit, Point offset) { Unit unit = displayUnit; if (unit == null || pixelSpacingUnit == null || pixelSpacingUnit.equals(Unit.PIXEL)) { unit = Unit.PIXEL; } double unitRatio; if (unit.equals(Unit.PIXEL)) { unitRatio = 1.0; } else { unitRatio = getPixelSize() * unit.getConversionRatio(pixelSpacingUnit.getConvFactor()); } int offsetx = offset == null ? 0 : -offset.x; int offsety = offset == null ? 0 : -offset.y; return new MeasurementsAdapter(unitRatio, offsetx, offsety, false, 0, unit.getAbbreviation()); } public boolean isImageInCache() { return mCache.get(this) != null; } public void removeImageFromCache() { mCache.remove(this); MediaReader reader = this.getMediaReader(); this.setTag(TagW.ImageCache, false); if (reader != null) { // Close the image stream reader.close(); } } public boolean hasSameSize(ImageElement image) { if (image != null) { PlanarImage img = getImage(); PlanarImage img2 = image.getImage(); if (img != null && img2 != null && getRescaleWidth(img.getWidth()) == image.getRescaleWidth(img2.getWidth()) && getRescaleHeight(img.getHeight()) == image.getRescaleHeight(img2.getHeight())) { return true; } } return false; } /** * Loads the original image. Must load and return the original image. * * @throws Exception * * @throws IOException */ protected PlanarImage loadImage() throws Exception { return mediaIO.getImageFragment(this); } public RenderedImage getRenderedImage(final RenderedImage imageSource) { return getRenderedImage(imageSource, null); } /** * @param imageSource * is the RenderedImage upon which transformation is done * @param window * is width from low to high input values around level. If null, getDefaultWindow() value is used * @param level * is center of window values. If null, getDefaultLevel() value is used * @param pixelPadding * indicates if some padding values defined in ImageElement should be applied or not. If null, TRUE is * considered * @return */ public RenderedImage getRenderedImage(final RenderedImage imageSource, Map<String, Object> params) { if (imageSource == null) { return null; } Double window = (params == null) ? null : (Double) params.get(ActionW.WINDOW.cmd()); Double level = (params == null) ? null : (Double) params.get(ActionW.LEVEL.cmd()); Boolean pixelPadding = (params == null) ? null : (Boolean) params.get(ActionW.IMAGE_PIX_PADDING.cmd()); pixelPadding = (pixelPadding == null) ? Boolean.TRUE : pixelPadding; window = (window == null) ? getDefaultWindow(pixelPadding) : window; level = (level == null) ? getDefaultLevel(pixelPadding) : level; return ImageToolkit.getDefaultRenderedImage(this, imageSource, window, level, pixelPadding); } /** * Returns the full size, original image. Returns null if the image is not loaded. * * @return */ public PlanarImage getImage(OpManager manager) { return getImage(manager, true); } @Override public String toString() { return getMediaURI().toString(); } public synchronized PlanarImage getImage(OpManager manager, boolean findMinMax) { try { return getCacheImage(startImageLoading(), manager, findMinMax); } catch (OutOfMemoryError e1) { /* * Appends when loading a big image without tiling, the memory left is not enough for the renderedop (like * Extrema) */ LOGGER.warn("Out of MemoryError: {}", this, e1); //$NON-NLS-1$ mCache.expungeStaleEntries(); System.gc(); System.gc(); try { Thread.sleep(100); } catch (InterruptedException et) { // Do nothing } return getCacheImage(startImageLoading(), manager, findMinMax); } } private PlanarImage getCacheImage(PlanarImage cacheImage, OpManager manager, boolean findMinMax) { if (findMinMax) { try { findMinMaxValues(cacheImage, true); } catch (Exception e) { mCache.remove(this); readable = false; LOGGER.error("Cannot read image: {}", this, e); //$NON-NLS-1$ } } if (manager != null && cacheImage != null) { RenderedImage img = manager.getLastNodeOutputImage(); if (manager.getFirstNodeInputImage() != cacheImage || manager.needProcessing()) { manager.setFirstNode(cacheImage); img = manager.process(); } if (img != null) { return PlanarImage.wrapRenderedImage(img); } } return cacheImage; } public PlanarImage getImage() { return getImage(null); } private PlanarImage startImageLoading() throws OutOfMemoryError { PlanarImage cacheImage; if ((cacheImage = mCache.get(this)) == null && readable && setAsLoading()) { LOGGER.debug("Asking for reading image: {}", this); //$NON-NLS-1$ Load ref = new Load(); Future<PlanarImage> future = IMAGE_LOADER.submit(ref); PlanarImage img = null; try { img = future.get(); } catch (InterruptedException e) { // Re-assert the thread's interrupted status Thread.currentThread().interrupt(); // We don't need the result, so cancel the task too future.cancel(true); } catch (ExecutionException e) { if (e.getCause() instanceof OutOfMemoryError) { setAsLoaded(); throw (OutOfMemoryError) e.getCause(); } else { readable = false; LOGGER.error("Cannot read pixel data!: {}", this, e); //$NON-NLS-1$ } } if (img != null) { readable = true; mCache.put(this, img); cacheImage = img; this.setTag(TagW.ImageCache, true); } setAsLoaded(); } return cacheImage; } public boolean isReadable() { return readable; } @Override public void dispose() { // Let the soft reference mechanism dispose the display image super.dispose(); } class Load implements Callable<PlanarImage> { @Override public PlanarImage call() throws Exception { return loadImage(); } } }