/*
* 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.bioformats;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.List;
import virtualslideviewer.UncheckedInterruptedException;
import virtualslideviewer.core.ImageIndex;
import virtualslideviewer.core.Tile;
import virtualslideviewer.core.VirtualSlideImage;
import virtualslideviewer.util.*;
import loci.common.DataTools;
import loci.formats.*;
public class BioformatsVirtualSlideImage extends VirtualSlideImage
{
private final ReaderPool mReaderPool;
private final ByteArrayPool mCacheBuffersPool = new ByteArrayPool();
private final int mSeriesIndex;
private String mName;
private final int mColorChannelCount;
private final int mBitsPerPixel;
private final boolean mLittleEndian;
private final int mPixelType;
private final boolean mInterleaved;
private final int mResolutionCount;
private final List<Dimension> mTileSize = new ArrayList<>();
private final List<Dimension> mImageSize = new ArrayList<>();
private final List<Dimension> mPadding = new ArrayList<>();
private final int mChannelCount;
private final int mZPlanesCount;
private final int mTimePointsCount;
/**
* @param metadata Metadata of the virtual slide.
* @param seriesIndex The index of series.
* @param nameSuffix The suffix to append to image name.
*/
public BioformatsVirtualSlideImage(String name, int seriesIndex, int resolutionCount, ReaderPool readerPool)
{
ParameterValidator.throwIfNull(name, "name");
ParameterValidator.throwIfNull(readerPool, "readerPool");
mReaderPool = readerPool;
mSeriesIndex = seriesIndex;
mName = name;
IFormatReader reader = mReaderPool.borrow();
{
reader.setSeries(mSeriesIndex);
mColorChannelCount = reader.getRGBChannelCount();
mBitsPerPixel = reader.getBitsPerPixel();
mLittleEndian = reader.isLittleEndian();
mPixelType = reader.getPixelType();
mInterleaved = reader.isInterleaved();
mChannelCount = reader.getEffectiveSizeC();
mZPlanesCount = reader.getSizeZ();
mTimePointsCount = reader.getSizeT();
mResolutionCount = resolutionCount;
for(int i = 0; i < mResolutionCount; i++)
{
configureReader(reader, i);
mTileSize.add(new Dimension(reader.getOptimalTileWidth(), reader.getOptimalTileHeight()));
mImageSize.add(new Dimension(reader.getSizeX(), reader.getSizeY()));
mPadding.add(new Dimension(0, 0));
}
}
mReaderPool.putBack(reader);
}
@Override
public void getTileData(byte[] dst, Tile tile)
{
validateGetTileDataArguments(dst, tile);
Rectangle tileBounds = tile.getBounds(this);
// TODO refactor the ifs to a strategy object?
if(mBitsPerPixel != 8)
{
byte[] rawDataTempBuffer = mCacheBuffersPool.borrow(getRawDataBufferMinimumSize(tile));
{
readRawTileData(rawDataTempBuffer, tileBounds, tile.getImageIndex());
if(isRGB() && !mInterleaved)
{
byte[] tempByteBufffer = mCacheBuffersPool.borrow(getOutputBufferMinimumSize(tile));
{
convertToBytes(tempByteBufffer, rawDataTempBuffer, tileBounds.getSize());
PixelDataUtil.convertPlanarToInterleaved(tempByteBufffer, dst, tileBounds.getSize(), mColorChannelCount);
}
mCacheBuffersPool.putBack(tempByteBufffer);
}
else
{
convertToBytes(dst, rawDataTempBuffer, tileBounds.getSize());
}
}
mCacheBuffersPool.putBack(rawDataTempBuffer);
}
else
{
if(isRGB() && !mInterleaved)
{
byte[] tempBuffer = mCacheBuffersPool.borrow(getOutputBufferMinimumSize(tile));
{
readRawTileData(tempBuffer, tileBounds, tile.getImageIndex());
PixelDataUtil.convertPlanarToInterleaved(tempBuffer, dst, tileBounds.getSize(), mColorChannelCount);
}
mCacheBuffersPool.putBack(tempBuffer);
}
else
{
readRawTileData(dst, tileBounds, tile.getImageIndex());
}
}
}
private void validateGetTileDataArguments(byte[] dst, Tile tile)
{
ParameterValidator.throwIfNull(dst, "dst");
ParameterValidator.throwIfNull(tile, "tile");
if(!tile.isValid(this))
throw new IllegalArgumentException(tile + " does not exist.");
if(dst.length < getOutputBufferMinimumSize(tile))
{
throw new IllegalArgumentException("Passed buffer is not big enough to store entire tile's data. " +
"It should have the capacity of at least " + getOutputBufferMinimumSize(tile) + " bytes.");
}
}
private int getOutputBufferMinimumSize(Tile tile)
{
Dimension tileBounds = tile.getBounds(this).getSize();
return tileBounds.width * tileBounds.height * mColorChannelCount;
}
private int getRawDataBufferMinimumSize(Tile tile)
{
// Output buffer always has 8-bit per color component, while raw data buffer can have more.
return getOutputBufferMinimumSize(tile) * FormatTools.getBytesPerPixel(mPixelType);
}
private void readRawTileData(byte[] dst, Rectangle tileBounds, ImageIndex imageIndex)
{
IFormatReader reader = mReaderPool.borrow();
try
{
while(true)
{
try
{
configureReader(reader, imageIndex.getResolutionIndex());
int readerImageIndex = reader.getIndex(imageIndex.getZPlane(), imageIndex.getChannel(), imageIndex.getTimePoint());
reader.openBytes(readerImageIndex, dst, tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
break;
}
catch(ClosedByInterruptException e)
{
throw new UncheckedInterruptedException(e.getMessage());
}
catch(ClosedChannelException e)
{
// Bioformats reader is unusable after an exception has been thrown (such as is the case during canceling)...
// Get a new reader and discard the old one by not returning it into the pool.
reader.close();
reader = mReaderPool.borrow();
}
}
}
catch(FormatException | IOException e)
{
throw new RuntimeException(e);
}
finally
{
mReaderPool.putBack(reader);
}
}
protected void configureReader(IFormatReader reader, int resIndex)
{
reader.setSeries(mSeriesIndex);
reader.setResolution(mResolutionCount - 1 - resIndex);
}
/**
* Converts pixels from native type to 8-bits per color component.
*
* Based on loci.formats.ImageTools.autoscale().
*/
private void convertToBytes(byte[] dst, byte[] src, Dimension tileSize)
{
long[] minmax = FormatTools.defaultMinMax(mPixelType);
int min = (int)minmax[0];
int max = (int)minmax[1];
int bytesPerPixel = FormatTools.getBytesPerPixel(mPixelType);
int srcSize = tileSize.width * tileSize.height * mColorChannelCount * bytesPerPixel;
for(int i = 0; i < srcSize; i += bytesPerPixel)
{
int s = DataTools.bytesToInt(src, i, bytesPerPixel, mLittleEndian);
float diff = max - min;
float dist = (s - min) / diff;
dst[i / bytesPerPixel] = (byte)(dist * 255);
}
}
@Override
public Dimension getTileSize(int resIndex)
{
return new Dimension(mTileSize.get(resIndex));
}
@Override
public Dimension getImageSize(int resIndex)
{
return new Dimension(mImageSize.get(resIndex).width - mPadding.get(resIndex).width,
mImageSize.get(resIndex).height - mPadding.get(resIndex).height);
}
@Override
public int getResolutionCount()
{
return mResolutionCount;
}
@Override
public int getChannelCount()
{
return mChannelCount;
}
@Override
public int getZPlaneCount()
{
return mZPlanesCount;
}
@Override
public int getTimePointCount()
{
return mTimePointsCount;
}
@Override
public boolean isRGB()
{
return mColorChannelCount == 3;
}
@Override
public String getName()
{
return mName;
}
@Override
public void setName(String newName)
{
ParameterValidator.throwIfNull(newName, "newName");
if(newName.trim().isEmpty())
throw new IllegalArgumentException("Image name cannot be empty.");
String oldName = mName;
mName = newName;
super.firePropertyChange("name", oldName, newName);
}
@Override
public String getID()
{
return Integer.toString(mSeriesIndex);
}
public int getSeriesIndex()
{
return mSeriesIndex;
}
public void setPadding(int resIndex, Dimension padding)
{
ParameterValidator.throwIfNull(padding, "padding");
if(resIndex < 0 || resIndex >= mResolutionCount)
throw new IllegalArgumentException("Invalid resolution index.");
mPadding.set(resIndex, new Dimension(padding));
}
@Override
public void close()
{
mReaderPool.close();
}
}