/* * Copyright (C) 2015 Patryk Strach * * This file is part of Virtual Slide Viewer. * * Virtual Slide Viewer 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. * * Virtual Slide Viewer 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 Virtual Slide Viewer. * If not, see <http://www.gnu.org/licenses/>. */ package virtualslideviewer.util; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import virtualslideviewer.core.ImageIndex; import virtualslideviewer.core.Tile; import virtualslideviewer.core.VirtualSlideImage; /** * Helper functions related to images. */ public class ImageUtil { /** * Returns a list of tiles spanning the region. * * @param area The bounds of region. * @param tileSize The size of single tile. * @param imageIndex The image index to which the tiles belong. * * @return List containing tiles from given area. */ public static List<Tile> getTilesInArea(Rectangle area, Dimension tileSize, ImageIndex imageIndex) { ParameterValidator.throwIfNull(area, "area"); ParameterValidator.throwIfNull(tileSize, "tileSize"); ParameterValidator.throwIfNull(imageIndex, "imageIndex"); int firstTileX = (int)Math.floor(area.getX() / tileSize.getWidth()); int firstTileY = (int)Math.floor(area.getY() / tileSize.getHeight()); int lastTileX = (int)Math.ceil(area.getMaxX() / tileSize.getWidth()); int lastTileY = (int)Math.ceil(area.getMaxY() / tileSize.getHeight()); List<Tile> tiles = new ArrayList<>(); for(int y = firstTileY; y < lastTileY; y++) { for(int x = firstTileX; x < lastTileX; x++) { tiles.add(new Tile(x, y, imageIndex)); } } return tiles; } /** * Copies part of image data into part of specified data buffer. * * <p> * The bounds of region to copy is computed as an intersection of source and destination buffers' bounds * relative to the bounds of destination buffer. For example, when source region bounds are (350, 250, 25, 25) * and destination region bounds are (300, 200, 100, 100), then entire source buffer will be copied into a * destination buffer with offset of (50, 50). * * @param src source buffer * @param srcBoundsInImageSpace bounds of source buffer in image space * @param dst destination buffer * @param dstBoundsInImageSpace bounds of destination buffer in image space * @param imageChannelCount number of channels in image */ public static void copyIntersectingPartOfImage(byte[] src, Rectangle srcBoundsInImageSpace, byte[] dst, Rectangle dstBoundsInImageSpace, int imageChannelCount) { ParameterValidator.throwIfNull(src, "src"); ParameterValidator.throwIfNull(srcBoundsInImageSpace, "srcBoundsInImageSpace"); ParameterValidator.throwIfNull(dst, "dst"); ParameterValidator.throwIfNull(dstBoundsInImageSpace, "dstBoundsInImageSpace"); Rectangle intersectionInImageSpace = srcBoundsInImageSpace.intersection(dstBoundsInImageSpace); Point srcOffset = new Point(intersectionInImageSpace.x - srcBoundsInImageSpace.x, intersectionInImageSpace.y - srcBoundsInImageSpace.y); Point dstOffset = new Point(intersectionInImageSpace.x - dstBoundsInImageSpace.x, intersectionInImageSpace.y - dstBoundsInImageSpace.y); copyPartOfImage(src, srcOffset, srcBoundsInImageSpace.width, dst, dstOffset, dstBoundsInImageSpace.width, imageChannelCount, intersectionInImageSpace.getSize()); } /** * Copies part of image data using explicitly specified region to copy. * * @param src source buffer with pixels * @param srcOffset offset of a pixel in source buffer from which the copy will start * @param srcWidth width of source buffer in pixels * @param dst destination buffer with pixels * @param dstOffset offset of a pixel in destination buffer * @param dstWidth width of destination buffer in pixels * @param imageChannelCount number of channels in the image * @param areaToCopy area to copy */ private static void copyPartOfImage(byte[] src, Point srcOffset, int srcWidth, byte[] dst, Point dstOffset, int dstWidth, int imageChannelCount, Dimension areaToCopy) { int srcOffsetInBytes = (srcOffset.y * srcWidth + srcOffset.x) * imageChannelCount; int dstOffsetInBytes = (dstOffset.y * dstWidth + dstOffset.x) * imageChannelCount; copyRectangleOfData(src, srcOffsetInBytes, srcWidth * imageChannelCount, dst, dstOffsetInBytes, dstWidth * imageChannelCount, new Dimension(areaToCopy.width * imageChannelCount, areaToCopy.height)); } private static void copyRectangleOfData(byte[] src, int srcOffset, int srcWidth, byte[] dst, int dstOffset, int dstWidth, Dimension areaToCopy) { for(int y = 0; y < areaToCopy.height ;++y) { System.arraycopy(src, srcOffset + y * srcWidth, dst, dstOffset + y * dstWidth, areaToCopy.width); } } /** * Computes scale required to apply to an image with size passed in <code>originalSize</code> parameter to fit it * into a rectangle with a size equal to <code>destinationSize</code>. * * @param originalSize Size of image to compute scale for. * @param destinationSize The size of rectangle which the image should be fit into. * * @return The scale. */ public static double getScaleToFit(Dimension originalSize, Dimension destinationSize) { ParameterValidator.throwIfNull(originalSize, "originalSize"); ParameterValidator.throwIfNull(destinationSize, "destinationSize"); double xScale = destinationSize.getWidth() / originalSize.getWidth(); double yScale = destinationSize.getHeight() / originalSize.getHeight(); return Math.min(xScale, yScale); } /** * Scales the image using high quality filtering to fit into given dimensions while preserving the aspect ratio of image. * * @param imageToScale The image to scale. * @param destinationSize The size of area to which the image should be fit. * * @return Scaled image. */ public static BufferedImage scaleToFitPreservingAspectRatio(BufferedImage imageToScale, Dimension destinationSize) { ParameterValidator.throwIfNull(imageToScale, "imageToScale"); ParameterValidator.throwIfNull(destinationSize, "destinationSize"); Dimension imageSize = new Dimension(imageToScale.getWidth(null), imageToScale.getHeight(null)); double scaleToFit = getScaleToFit(imageSize, destinationSize); int newWidth = (int)(imageSize.width * scaleToFit); int newHeight = (int)(imageSize.height * scaleToFit); // getScaledInstance() makes the scaled image a lot brighter than the original one. // return imageToScale.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); return scaleImage(imageToScale, new Dimension(newWidth, newHeight), RenderingHints.VALUE_INTERPOLATION_BICUBIC); } /** * Scale the image. * * @param imageToScale The image to scale. * @param destinationSize New size of the image. * @param filtering The filtering method determining the speed and quality of the scaling. * Available values are: {@link java.awt.RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, * {@link java.awt.RenderingHints#VALUE_INTERPOLATION_BILINEAR} and * {@link java.awt.RenderingHints#VALUE_INTERPOLATION_BICUBIC} * * @return The scaled image. */ public static BufferedImage scaleImage(BufferedImage imageToScale, Dimension destinationSize, Object filtering) { ParameterValidator.throwIfNull(imageToScale, "imageToScale"); ParameterValidator.throwIfNull(destinationSize, "destinationSize"); ParameterValidator.throwIfNull(filtering, "filtering"); BufferedImage resizedImage = new BufferedImage(destinationSize.width, destinationSize.height, imageToScale.getType()); Graphics2D drawingSurface = resizedImage.createGraphics(); drawingSurface.setRenderingHint(RenderingHints.KEY_INTERPOLATION, filtering); drawingSurface.drawImage(imageToScale, 0, 0, destinationSize.width, destinationSize.height, null); drawingSurface.dispose(); return resizedImage; } /** * Computes a position of an object with specified size in such a way that it is centered if placed inside a panel with given size. * * @param objectSize A size of an object which should be centered. * @param areaSize A size of an area in which the object should be centered. */ public static Point getCenteredPosition(Dimension objectSize, Dimension areaSize) { ParameterValidator.throwIfNull(objectSize, "objectSize"); ParameterValidator.throwIfNull(areaSize, "areaSize"); int x = (areaSize.width - objectSize.width) / 2; int y = (areaSize.height - objectSize.height) / 2; return new Point(x, y); } /** * Loads data using specified loader into a buffered image. * * The data is injected directly into specified image without any allocations nor copying. * * @param image Image to load data into. * @param loader Loader which will be used to load data. It should store loaded data into given buffer. * If the data is RGB it should be loaded in RGBRGB... pattern. */ public static void loadDataIntoBufferedImage(BufferedImage image, Consumer<byte[]> loader) { ParameterValidator.throwIfNull(image, "image"); ParameterValidator.throwIfNull(loader, "loader"); byte[] imageDataBuffer = ((DataBufferByte)image.getRaster().getDataBuffer()).getData(); loader.accept(imageDataBuffer); // BufferedImage stores its pixels in BGR order, while we use RGB. if(image.getType() == BufferedImage.TYPE_3BYTE_BGR) { PixelDataUtil.swapRgbColorComponents(imageDataBuffer); } } /** * Gets the biggest resolution index of image whose size is at most the specified byte count. * * When there is no resolution smaller than specified value, the first resolution is returned (index == 0). */ public static int getResolutionIndexWithSizeNotBiggerThan(VirtualSlideImage image, long byteCount) { ParameterValidator.throwIfNull(image, "image"); for(int i = 1; i < image.getResolutionCount() ;++i) { long width = image.getImageSize(i).width; long height = image.getImageSize(i).height; long channelCount = image.isRGB() ? 3 : 1; if(width * height * channelCount > byteCount) { return i - 1; } } return 0; } }