//
// LegacyZVIReader.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program 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 Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.in;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
import loci.formats.*;
/**
* LegacyZVIReader is the legacy file format reader for Zeiss ZVI files.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/LegacyZVIReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/LegacyZVIReader.java">SVN</a></dd></dl>
*
* @author Curtis Rueden ctrueden at wisc.edu
* @author Melissa Linkert linkert at wisc.edu
* @author Michel Boudinot Michel dot boudinot at iaf.cnrs-gif.fr
*/
public class LegacyZVIReader extends FormatReader {
// -- Constants --
/** First few bytes of every ZVI file. */
private static final byte[] ZVI_SIG = {
-48, -49, 17, -32, -95, -79, 26, -31
};
/** Block identifying start of useful header information. */
private static final byte[] ZVI_MAGIC_BLOCK_1 = {65, 0, 16}; // 41 00 10
/** Block identifying second part of useful header information. */
private static final byte[] ZVI_MAGIC_BLOCK_2 = {65, 0, -128}; // 41 00 80
/** Block identifying third part of useful header information. */
private static final byte[] ZVI_MAGIC_BLOCK_3 = {32, 0, 16}; // 20 00 10
/** Memory buffer size in bytes, for reading from disk. */
private static final int BUFFER_SIZE = 8192;
/** String apologizing for the fact that this ZVI support still sucks. */
private static final String WHINING = "Sorry, " +
"ZVI support is still preliminary. It will be improved as time permits.";
// -- Fields --
/** List of image blocks. */
private Vector blockList;
/** Bytes per pixel. */
private int bytesPerPixel;
/** Counters of image elements */
private int numZ = 0, numC = 0, numT = 0;
private int cFlag = 0, zFlag = 0, tFlag = 0;
// -- Constructor --
/** Constructs a new legacy ZVI reader. */
public LegacyZVIReader() { super("Legacy ZVI", "zvi"); }
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) {
if (block == null) return false;
int len = block.length < ZVI_SIG.length ? block.length : ZVI_SIG.length;
for (int i=0; i<len; i++) {
if (block[i] != ZVI_SIG[i]) return false;
}
return true;
}
/* @see loci.formats.IFormatReader#openBytes(int, byte[]) */
public byte[] openBytes(int no, byte[] buf)
throws FormatException, IOException
{
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
FormatTools.checkBufferSize(this, buf.length);
ZVIBlock zviBlock = (ZVIBlock) blockList.elementAt(no);
zviBlock.readBytes(in, buf);
return buf;
}
/* @see loci.formats.IFormatReader#openImage(int) */
public BufferedImage openImage(int no) throws FormatException, IOException {
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
if (debug) debug("Reading image #" + no + "...");
ZVIBlock zviBlock = (ZVIBlock) blockList.elementAt(no);
return zviBlock.readImage(in);
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
if (debug) debug("LegacyZVIReader.initFile(" + id + ")");
super.initFile(id);
in = new RandomAccessStream(id);
in.order(true);
// Highly questionable decoding strategy:
//
// Note that all byte ordering is little endian, includeing 4-byte header
// fields. Other examples: 16-bit data is LSB MSB, and 3-channel data is
// BGR instead of RGB.
//
// 1) Find image header byte sequence:
// A) Find 41 00 10. (ZVI_MAGIC_BLOCK_1)
// B) Skip 19 bytes of stuff.
// C) Read 41 00 80. (ZVI_MAGIC_BLOCK_2)
// D) Read 11 bytes of 00
// E) Read potential header information:
// - Z-slice (4 bytes)
// - channel (4 bytes)
// - timestep (4 bytes)
// F) Read 108 bytes of 00.
//
// 2) If byte sequence is not as expected at any point (e.g.,
// stuff that is supposed to be 00 isn't), start over at 1A.
//
// 3) Find 20 00 10. (ZVI_MAGIC_BLOCK_3)
//
// 4) Read more header information:
// - width (4 bytes)
// - height (4 bytes)
// - ? (4 bytes; always 1)
// - bytesPerPixel (4 bytes)
// - pixelType (this is what the AxioVision software calls it)
// - 1=24-bit (3 color components, 8-bit each)
// - 3=8-bit (1 color component, 8-bit)
// - 4=16-bit (1 color component, 16-bit)
// - bitDepth (4 bytes--usually, but not always, bytesPerPixel * 8)
//
// 5) Read image data (width * height * bytesPerPixel)
//
// 6) Repeat the entire process until no more headers are identified.
long pos = 0;
blockList = new Vector();
Set zSet = new HashSet(); // to hold Z plan index collection.
Set cSet = new HashSet(); // to hold C channel index collection
Set tSet = new HashSet(); // to hold T time index collection.
numZ = 0;
numC = 0;
numT = 0;
int numI = 0;
while (true) {
// search for start of next image header
status("Searching for next image");
long header = findBlock(in, ZVI_MAGIC_BLOCK_1, pos);
if (header < 0) {
// no more potential headers found; we're done
break;
}
pos = header + ZVI_MAGIC_BLOCK_1.length;
if (debug) debug("Found potential image block: " + header);
// these byte don't matter
in.skipBytes(19);
pos += 19;
// these bytes should match ZVI_MAGIC_BLOCK_2
byte[] b = new byte[ZVI_MAGIC_BLOCK_2.length];
in.readFully(b);
boolean ok = true;
for (int i=0; i<b.length; i++) {
if (b[i] != ZVI_MAGIC_BLOCK_2[i]) {
ok = false;
break;
}
pos++;
}
if (!ok) continue;
// these bytes should be 00
b = new byte[11];
in.readFully(b);
for (int i=0; i<b.length; i++) {
if (b[i] != 0) {
ok = false;
break;
}
pos++;
}
if (!ok) continue;
// read potential header information
int theZ = in.readInt();
int theC = in.readInt();
int theT = in.readInt();
pos += 12;
// these byte should be 00
b = new byte[108];
in.readFully(b);
for (int i=0; i<b.length; i++) {
if (b[i] != 0) {
ok = false;
break;
}
pos++;
}
if (!ok) continue;
// everything checks out; looks like an image header to me
//+ (mb) decoding strategy modification
// Some zvi images have the following structure:
// ZVI_SIG Decoding:
// ZVI_MAGIC_BLOCK_1
// ZVI_MAGIC_BLOCK_2 <== Start of header information
// - Z-slice (4 bytes) -> theZ = 0
// - channel (4 bytes) -> theC = 0
// - timestep (4 bytes) -> theT = 0
// ZVI_MAGIC_BLOCK_2 <== Start of header information
// - Z-slice (4 bytes) -> theZ actual value
// - channel (4 bytes) -> theC actual value
// - timestep (4 bytes) -> theT actual value
// ZVI_MAGIC_BLOCK_3 <== End of header information
// ...
//
// Two consecutive Start of header information ZVI_MAGIC_BLOCK_2
// make test 3) of original decoding strategy fail. The first
// null values are taken as theZ, theC and theT values, the
// following actual values are ignored.
// Parsing the rest of the file appears to be ok.
//
// New decoding strategy looks for the last header information
// ZVI_MAGIC_BLOCK_2 / ZVI_MAGIC_BLOCK_3 to get proper image
// slice theZ, theC and theT values.
// these bytes don't matter
in.skipBytes(89);
pos += 89;
byte[] magic3 = new byte[ZVI_MAGIC_BLOCK_3.length];
in.readFully(magic3);
for (int i=0; i<magic3.length; i++) {
if (magic3[i] != ZVI_MAGIC_BLOCK_3[i]) {
ok = false;
break;
}
}
if (!ok) continue;
pos += ZVI_MAGIC_BLOCK_3.length;
status("Reading image header");
// read more header information
core.sizeX[0] = in.readInt();
core.sizeY[0] = in.readInt();
// don't know what this is for
int alwaysOne = in.readInt();
bytesPerPixel = in.readInt();
// not clear what this value signifies
int pixType = in.readInt();
// doesn't always equal bytesPerPixel * 8
int bitDepth = in.readInt();
pos += 24;
String type = "";
switch (pixType) {
case 1:
type = "8 bit rgb tuple, 24 bpp";
core.pixelType[0] = FormatTools.UINT8;
break;
case 2:
type = "8 bit rgb quad, 32 bpp";
core.pixelType[0] = FormatTools.UINT8;
break;
case 3:
type = "8 bit grayscale";
core.pixelType[0] = FormatTools.UINT8;
break;
case 4:
type = "16 bit signed int, 16 bpp";
core.pixelType[0] = FormatTools.UINT16;
break;
case 5:
type = "32 bit int, 32 bpp";
core.pixelType[0] = FormatTools.UINT32;
break;
case 6:
type = "32 bit float, 32 bpp";
core.pixelType[0] = FormatTools.FLOAT;
break;
case 7:
type = "64 bit float, 64 bpp";
core.pixelType[0] = FormatTools.DOUBLE;
break;
case 8:
type = "16 bit unsigned short triple, 48 bpp";
core.pixelType[0] = FormatTools.UINT16;
break;
case 9:
type = "32 bit int triple, 96 bpp";
core.pixelType[0] = FormatTools.UINT32;
break;
default:
type = "undefined pixel type (" + pixType + ")";
}
addMeta("Width", new Integer(core.sizeX[0]));
addMeta("Height", new Integer(core.sizeY[0]));
addMeta("PixelType", type);
addMeta("BPP", new Integer(bytesPerPixel));
ZVIBlock zviBlock = new ZVIBlock(theZ, theC, theT, core.sizeX[0],
core.sizeY[0], alwaysOne, bytesPerPixel, pixType, bitDepth, pos);
if (debug) debug(zviBlock.toString());
// perform some checks on the header info
// populate Z, C and T index collections
zSet.add(new Integer(theZ));
cSet.add(new Integer(theC));
tSet.add(new Integer(theT));
numI++;
// sorry not a very clever way to find dimension order
if ((numI == 2) && (cSet.size() == 2)) cFlag = 1;
if ((numI == 2) && (zSet.size() == 2)) zFlag = 1;
if ((numI == 2) && (tSet.size() == 2)) tFlag = 1;
if ((numI % 3 == 0) && (zSet.size() > 1) && (cFlag == 1)) {
core.currentOrder[0] = "XYCZT";
}
if ((numI % 3 == 0) && (tSet.size() > 1) && (cFlag == 1)) {
core.currentOrder[0] = "XYCTZ";
}
if ((numI % 3 == 0) && (cSet.size() > 1) && (zFlag == 1)) {
core.currentOrder[0] = "XYZCT";
}
if ((numI % 3 == 0) && (tSet.size() > 1) && (zFlag == 1)) {
core.currentOrder[0] = "XYZTC";
}
if ((numI % 3 == 0) && (cSet.size() > 1) && (tFlag == 1)) {
core.currentOrder[0] = "XYTCZ";
}
if ((numI % 3 == 0) && (zSet.size() > 1) && (tFlag == 1)) {
core.currentOrder[0] = "XYTZC";
}
if (core.currentOrder[0] == null) core.currentOrder[0] = "XYZCT";
// save this image block's position
blockList.add(zviBlock);
pos += core.sizeX[0] * core.sizeY[0] * bytesPerPixel;
core.imageCount[0] = blockList.size();
core.sizeX[0] = openImage(0).getWidth();
core.sizeY[0] = openImage(0).getHeight();
core.sizeZ[0] = zSet.size();
core.sizeC[0] = cSet.size();
core.sizeT[0] = tSet.size();
core.rgb[0] = bytesPerPixel == 3 || bytesPerPixel > 4;
core.interleaved[0] = false;
core.littleEndian[0] = true;
core.indexed[0] = false;
core.falseColor[0] = false;
// Populate metadata store
MetadataStore store = getMetadataStore();
store.setImage(currentId, null, null, null);
FormatTools.populatePixels(store, this);
for (int i=0; i<core.sizeC[0]; i++) {
store.setLogicalChannel(i, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null);
}
}
status("Verifying image count");
if (blockList.isEmpty()) {
throw new FormatException("No image data found." + WHINING);
}
// number of Z, C and T index
numZ = zSet.size();
numC = cSet.size();
numT = tSet.size();
if (numZ * numC * numT != blockList.size()) {
LogTools.println("Warning: image counts do not match. " + WHINING);
}
}
// -- Utility methods --
/**
* Finds the first occurence of the given byte block within the file,
* starting from the given file position.
*/
private static long findBlock(RandomAccessStream in, byte[] block, long start)
throws IOException
{
long filePos = start;
long fileSize = in.length();
byte[] buf = new byte[BUFFER_SIZE];
long spot = -1;
int step = 0;
boolean found = false;
in.seek(start);
while (true) {
int len = (int) (fileSize - filePos);
if (len < 0) break;
if (len > buf.length) len = buf.length;
in.readFully(buf, 0, len);
for(int i=0; i<len; i++) {
if (buf[i] == block[step]) {
if (step == 0) {
// could be a match; flag this spot
spot = filePos + i;
}
step++;
if (step == block.length) {
// found complete match; done searching
found = true;
break;
}
}
else {
// no match; reset step indicator
spot = -1;
step = 0;
}
}
if (found) break; // found a match; we're done
if (len < buf.length) break; // EOF reached; we're done
filePos += len;
}
// set file pointer to byte immediately following pattern
if (spot >= 0) in.seek(spot + block.length);
return spot;
}
// -- Helper classes --
/** Contains information collected from a ZVI image header. */
private class ZVIBlock {
private int theZ, theC, theT;
private int width, height;
private int alwaysOne;
private int bytesPerPixel;
private int pixelType;
private int bitDepth;
private long imagePos;
private int numPixels;
private int imageSize;
private int numChannels;
private int bytesPerChannel;
public ZVIBlock(int theZ, int theC, int theT, int width, int height,
int alwaysOne, int bytesPerPixel, int pixelType, int bitDepth,
long imagePos)
{
this.theZ = theZ;
this.theC = theC;
this.theT = theT;
this.width = width;
this.height = height;
this.alwaysOne = alwaysOne;
this.bytesPerPixel = bytesPerPixel;
this.pixelType = pixelType;
this.bitDepth = bitDepth;
this.imagePos = imagePos;
numPixels = width * height;
imageSize = numPixels * bytesPerPixel;
numChannels = pixelType == 1 ? 3 : 1; // a total shot in the dark
if (bytesPerPixel % numChannels != 0) {
LogTools.println("Warning: incompatible bytesPerPixel (" +
bytesPerPixel + ") and numChannels (" + numChannels +
"). Assuming grayscale data. " + WHINING);
numChannels = 1;
}
bytesPerChannel = bytesPerPixel / numChannels;
}
/** Reads in this block's image bytes from the given file. */
public byte[] readBytes(RandomAccessStream raf, byte[] buf)
throws IOException, FormatException
{
long fileSize = raf.length();
if (imagePos + imageSize > fileSize) {
throw new FormatException("File is not big enough to contain the " +
"pixels (width=" + width + "; height=" + height +
"; bytesPerPixel=" + bytesPerPixel + "; imagePos=" + imagePos +
"; fileSize=" + fileSize + "). " + WHINING);
}
if (buf.length < imageSize) throw new FormatException("Buffer too small");
// read image
raf.seek(imagePos);
raf.readFully(buf);
return buf;
}
/** Reads in this block's image data from the given file. */
public BufferedImage readImage(RandomAccessStream raf)
throws IOException, FormatException
{
byte[] imageBytes = readBytes(raf, new byte[imageSize]);
// convert image bytes into BufferedImage
if (bytesPerPixel > 4) {
numChannels = bytesPerPixel / 2;
bytesPerPixel /= numChannels;
bytesPerChannel = bytesPerPixel;
}
int index = 0;
short[][] samples = new short[numChannels][numPixels * bytesPerPixel];
for (int i=0; i<numPixels; i++) {
for (int c=numChannels-1; c>=0; c--) {
byte[] b = new byte[bytesPerChannel];
System.arraycopy(imageBytes, index, b, 0, bytesPerChannel);
index += bytesPerChannel;
// our zvi images are 16 bit per pixel (BitsPerPixel) but
// with an Acquisition Bit Depth of 12
samples[c][i] = (short) (DataTools.bytesToShort(b, true) * 8);
}
}
return ImageTools.makeImage(samples, width, height);
}
public String toString() {
return "Image header block:\n" +
" theZ = " + theZ + "\n" + " theC = " + theC + "\n" +
" theT = " + theT + "\n" + " width = " + width + "\n" +
" height = " + height + "\n" + " alwaysOne = " + alwaysOne + "\n" +
" bytesPerPixel = " + bytesPerPixel + "\n" +
" pixelType = " + pixelType + "\n" + " bitDepth = " + bitDepth;
}
}
}