/*
* #%L
* BSD implementations of Bio-Formats readers and writers
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package loci.formats.in;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import javax.xml.parsers.ParserConfigurationException;
import loci.common.DataTools;
import loci.common.RandomAccessInputStream;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.TiffParser;
import ome.xml.model.primitives.PositiveInteger;
import org.xml.sax.SAXException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* @author Lee Kamentsky
*
* Reader for the Amris ImageStream / FlowSight file format
*
* The cif file format is tiff-like, but doesn't adhere to
* the TIFF standard, so we use the TiffParser where we can,
* but do not use the TiffReader hierarchy.
*/
public class FlowSightReader extends FormatReader {
final private static int CHANNEL_COUNT_TAG = 33000;
final private static int ACQUISITION_TIME_TAG = 33004;
final private static int CHANNEL_NAMES_TAG = 33007;
final private static int CHANNEL_DESCS_TAG = 33008;
final private static int METADATA_XML_TAG = 33027;
final private static int GREYSCALE_COMPRESSION = 30817;
final private static int BITMASK_COMPRESSION = 30818;
/**
* The tags that must be present for this reader
* to function properly
*/
final private static int [] MINIMAL_TAGS = {
METADATA_XML_TAG
};
private transient TiffParser tiffParser;
private long [] ifdOffsets;
private String [] channelNames;
private String [] channelDescs;
public FlowSightReader() {
super("FlowSight", "cif");
}
/* (non-Javadoc)
* @see loci.formats.FormatReader#isThisType(loci.common.RandomAccessInputStream)
*/
@Override
public boolean isThisType(RandomAccessInputStream stream)
throws IOException {
TiffParser tiffParser = new TiffParser(stream);
if (! tiffParser.isValidHeader()) return false;
IFD ifd = tiffParser.getFirstIFD();
if (ifd == null) return false;
tiffParser.fillInIFD(ifd);
for (int tag: MINIMAL_TAGS) {
try {
if (ifd.getIFDStringValue(tag) == null) return false;
} catch (FormatException e) {
return false;
}
}
return true;
}
/* (non-Javadoc)
* @see loci.formats.FormatReader#initFile(java.lang.String)
*/
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
tiffParser = new TiffParser(in);
tiffParser.setDoCaching(false);
tiffParser.setUse64BitOffsets(false);
final Boolean littleEndian = tiffParser.checkHeader();
if (littleEndian == null) {
throw new FormatException("Invalid FlowSight file");
}
final boolean little = littleEndian.booleanValue();
in.order(little);
LOGGER.info("Reading IFDs");
ifdOffsets = tiffParser.getIFDOffsets();
if (ifdOffsets.length < 2) {
throw new FormatException("No IFDs found");
}
LOGGER.info("Populating metadata");
/*
* The first IFD contains file-scope metadata
*/
final IFD ifd0 = tiffParser.getFirstIFD();
tiffParser.fillInIFD(ifd0);
int channelCount = ifd0.getIFDIntValue(CHANNEL_COUNT_TAG, 1);
final String channelNamesString = ifd0.getIFDStringValue(CHANNEL_NAMES_TAG);
if (channelNamesString != null) {
channelNames = channelNamesString.split("\\|");
if (channelNames.length != channelCount) {
throw new FormatException(String.format(
"Channel count (%d) does not match number of channel names (%d) in string \"%s\"",
channelCount, channelNames.length, channelNamesString));
}
LOGGER.debug("Found {} channels: {}",
channelCount, channelNamesString.replace('|', ','));
}
final String channelDescsString = ifd0.getIFDStringValue(CHANNEL_DESCS_TAG);
if (channelDescsString != null) {
channelDescs = channelDescsString.split("\\|");
if (channelDescs.length != channelCount) {
throw new FormatException(String.format(
"Channel count (%d) does not match number of channel descriptions (%d) in string \"%s\"",
channelCount, channelDescs.length, channelDescsString));
}
}
String xml = ifd0.getIFDTextValue(METADATA_XML_TAG);
xml = XMLTools.sanitizeXML(xml);
try {
Element xmlRoot = XMLTools.parseDOM(xml).getDocumentElement();
NodeList imagingNodes = xmlRoot.getElementsByTagName("Imaging");
if (imagingNodes.getLength() > 0) {
Element imagingNode = (Element) imagingNodes.item(0);
NodeList children = imagingNode.getChildNodes();
for (int child=0; child<children.getLength(); child++) {
Node childNode = children.item(child);
String name = childNode.getNodeName();
if (name.startsWith("ChannelInUseIndicators")) {
channelCount = 0;
String text = childNode.getTextContent();
String[] tokens = text.split(" ");
for (String token : tokens) {
if (token.equals("1")) {
channelCount++;
}
}
}
}
}
}
catch (ParserConfigurationException e) {
LOGGER.debug("Could not parse XML", e);
}
catch (SAXException e) {
LOGGER.debug("Could not parse XML", e);
}
/*
* Scan the remaining IFDs
*
* Unfortunately, each image can have a different width and height
* and the images and masks have a different bit depth, so in the
* OME scheme of things, we get one series per plane.
*/
for (int idxOff=1; idxOff<ifdOffsets.length;idxOff++) {
// TODO: Record the channel names
final long offset = ifdOffsets[idxOff];
final boolean first=(idxOff == 1);
final IFD ifd = tiffParser.getIFD(offset);
tiffParser.fillInIFD(ifd);
CoreMetadata ms = first ? core.get(0) : new CoreMetadata();
ms.rgb = false;
ms.interleaved = false;
ms.littleEndian = ifd0.isLittleEndian();
ms.sizeX = (int) ifd.getImageWidth() / channelCount;
ms.sizeY = (int) ifd.getImageLength();
ms.sizeZ = 1;
ms.sizeC = channelCount;
ms.sizeT = 1;
ms.indexed = false;
ms.dimensionOrder = "XYCZT";
ms.bitsPerPixel = ifd.getIFDIntValue(IFD.BITS_PER_SAMPLE);
ms.pixelType = (ms.bitsPerPixel == 8) ? FormatTools.UINT8 : FormatTools.UINT16;
ms.imageCount = channelCount;
ms.resolutionCount = 1;
ms.thumbnail = false;
ms.metadataComplete = true;
if (! first) {
core.add(ms);
}
}
/*
* Run through the metadata store, setting the channel names
* for all the series.
*/
final MetadataStore store = getMetadataStore();
MetadataTools.populatePixels(store, this);
if (channelNames != null && channelDescs != null) {
String [] maskDescs = new String [channelCount];
for (int i=0; i<channelCount; i++) {
maskDescs[i] = channelDescs[i] + " Mask";
}
for (int series=0; series < ifdOffsets.length-1; series++) {
final boolean isMask = core.get(series).pixelType == FormatTools.UINT8;
String[] descs = isMask ? maskDescs : channelDescs;
for (int channel=0; channel < channelCount; channel++) {
store.setChannelName(descs[channel], series, channel);
String cid = MetadataTools.createLSID("Channel", series, channel) + ":";
store.setChannelID(cid + channelNames[channel], series, channel);
}
}
}
}
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
tiffParser = null;
ifdOffsets = null;
channelNames = null;
channelDescs = null;
}
@Override
public void reopenFile() throws IOException {
super.reopenFile();
tiffParser = new TiffParser(in);
tiffParser.setDoCaching(false);
tiffParser.setUse64BitOffsets(false);
}
@Override
public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
final int idx = getSeries() + 1;
final IFD ifd = tiffParser.getIFD(ifdOffsets[idx]);
final int imageWidth = (int) (ifd.getImageWidth());
final int imageHeight = (int) (ifd.getImageLength());
final int wOff = x + no * imageWidth / getSizeC();
if (x+w > imageWidth / getSizeC()) {
throw new FormatException("Requested tile dimensions extend beyond those of the image.");
}
final int compression = ifd.getIFDIntValue(IFD.COMPRESSION);
byte [] tempBuffer; /* NB - these images are very small */
switch(compression) {
case GREYSCALE_COMPRESSION:
tempBuffer = openGreyscaleBytes(ifd, imageWidth, imageHeight);
break;
case BITMASK_COMPRESSION:
tempBuffer = openBitmaskBytes(ifd, imageWidth, imageHeight);
break;
default:
throw new FormatException(String.format("Unknown compression code: %d", compression));
}
final int bytesPerSample = ifd.getIFDIntValue(IFD.BITS_PER_SAMPLE) / 8;
for (int yy=y; yy<y+h; yy++) {
final int srcOff = bytesPerSample * (wOff + yy * imageWidth);
final int destOff = bytesPerSample * (yy-y) * w;
System.arraycopy(tempBuffer, srcOff, buf, destOff, w * bytesPerSample);
}
return buf;
}
/**
* Decode the whole IFD plane using bitmask compression
*
* @param ifd - the IFD to decode
* @param imageWidth the width of the IFD plane
* @param imageHeight the height of the IFD plane
* @return a byte array of length imageWidth * imageHeight
* containing the uncompressed data
* @throws FormatException
*/
private byte[] openBitmaskBytes(IFD ifd, int imageWidth, int imageHeight) throws FormatException {
final byte [] uncompressed = new byte[imageWidth * imageHeight];
final long [] stripByteCounts = ifd.getIFDLongArray(IFD.STRIP_BYTE_COUNTS);
final long [] stripOffsets = ifd.getIFDLongArray(IFD.STRIP_OFFSETS);
int off = 0;
for (int i=0; i<stripByteCounts.length; i++) {
try {
in.seek(stripOffsets[i]);
for (int j=0; j<stripByteCounts[i]; j+=2) {
byte value = in.readByte();
int runLength = (in.readByte() & 0xFF)+1;
if (off + runLength > uncompressed.length) {
throw new FormatException("Unexpected buffer overrun encountered when decompressing bitmask data");
}
Arrays.fill(uncompressed, off, off+runLength, value);
off += runLength;
}
} catch (IOException e) {
LOGGER.error("Caught exception while reading bitmask IFD data", e);
throw new FormatException(String.format("Error in FlowSight file format: %s", e.getMessage()));
}
}
if (off != uncompressed.length) throw new FormatException("Buffer shortfall encountered when decompressing bitmask data");
return uncompressed;
}
/**
* Decode the whole IFD plane using greyscale compression
*
* @param ifd
* @param imageWidth
* @param imageHeight
* @return a byte array
* @throws FormatException
*/
private byte[] openGreyscaleBytes(final IFD ifd, final int imageWidth, final int imageHeight) throws FormatException {
final FormatException [] formatException = new FormatException[1];
final long [] stripByteCounts = ifd.getIFDLongArray(IFD.STRIP_BYTE_COUNTS);
final long [] stripOffsets = ifd.getIFDLongArray(IFD.STRIP_OFFSETS);
Iterator<Short> diffs = new Iterator<Short> () {
int index = -1;
int offset = 0;
int count = 0;
byte currentByte;
int nibbleIdx = 2;
short value = 0;
short shift = 0;
boolean bHasNext = (formatException[0] != null);
boolean loaded = bHasNext;
@Override
public boolean hasNext() {
if (loaded) return bHasNext;
shift = 0;
value = 0;
while (! loaded) {
byte nibble;
try {
nibble = getNextNibble();
value += ((short) (nibble & 0x7) ) << shift;
shift += 3;
if ((nibble & 0x8) == 0) {
loaded = true;
bHasNext = true;
if ((nibble & 0x4) != 0) {
/*
* The number is negative
* and the bits at 1 << shift and above
* should all be "1". This does it.
*/
value |= - (1 << shift);
}
}
} catch (IOException e) {
LOGGER.error("IOException during read of greyscale image", e);
formatException[0] = new FormatException(
String.format("Error in FlowSight format: %s", e.getMessage()));
loaded = true;
bHasNext = false;
} catch (FormatException e) {
LOGGER.error("Format exception during read of greyscale image", e);
formatException[0] = e;
loaded = true;
bHasNext = false;
}
}
return bHasNext;
}
private byte getNextNibble() throws IOException, FormatException {
if (nibbleIdx >= 2) {
if (! getNextByte()) {
return (byte)0xff;
}
nibbleIdx = 0;
}
if (nibbleIdx++ == 0) {
return (byte)(currentByte & 0x0f);
} else {
return (byte)(currentByte >> 4);
}
}
private boolean getNextByte() throws IOException, FormatException {
while (offset == count) {
index++;
if (index == stripByteCounts.length) {
loaded = true;
bHasNext = false;
return false;
}
in.seek(stripOffsets[index]);
offset = 0;
count = (int)stripByteCounts[index];
}
currentByte = in.readByte();
offset++;
return true;
}
@Override
public Short next() {
if (! hasNext()) throw new IndexOutOfBoundsException("Tried to read past end of IFD data");
loaded = false;
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
byte [] buffer = new byte [imageWidth * imageHeight * 2];
short [] lastRow = new short[imageWidth];
short [] thisRow = new short[imageWidth];
int index = 0;
for (int y=0; y<imageHeight; y++) {
for (int x = 0; x<imageWidth; x++) {
if (x != 0) {
thisRow[x] = (short)(diffs.next() + lastRow[x] + thisRow[x-1] - lastRow[x-1]);
} else {
thisRow[x] = (short)(diffs.next() + lastRow[x]);
}
DataTools.unpackBytes(thisRow[x], buffer, index, 2, in.isLittleEndian());
index += 2;
}
final short [] temp = lastRow;
lastRow = thisRow;
thisRow = temp;
}
return buffer;
}
}