//
// NativeND2Reader.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program 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 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 General Public License for more details.
You should have received a copy of the GNU 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.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import loci.common.ByteArrayHandle;
import loci.common.Location;
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.ImageTools;
import loci.formats.MetadataTools;
import loci.formats.codec.Codec;
import loci.formats.codec.CodecOptions;
import loci.formats.codec.JPEG2000Codec;
import loci.formats.codec.ZlibCodec;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;
/**
* NativeND2Reader is the file format reader for Nikon ND2 files.
* The JAI ImageIO library is required to use this reader; it is available from
* http://jai-imageio.dev.java.net. Note that JAI ImageIO is bundled with a
* version of the JJ2000 library, so it is important that either:
* (1) the JJ2000 jar file is *not* in the classpath; or
* (2) the JAI jar file precedes JJ2000 in the classpath.
*
* Thanks to Tom Caswell for additions to the ND2 metadata parsing logic.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/NativeND2Reader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/NativeND2Reader.java;hb=HEAD">Gitweb</a></dd></dl>
*/
public class NativeND2Reader extends FormatReader {
// -- Constants --
public static final long ND2_MAGIC_BYTES_1 = 0xdacebe0aL;
public static final long ND2_MAGIC_BYTES_2 = 0x6a502020L;
// -- Fields --
/** Array of image offsets. */
private long[][] offsets;
/** Whether or not the pixel data is compressed using JPEG 2000. */
private boolean isJPEG;
/** Codec to use when decompressing pixel data. */
private Codec codec;
/** Whether or not the pixel data is losslessly compressed. */
private boolean isLossless;
private ArrayList<Double> tsT = new ArrayList<Double>();
private int fieldIndex;
private long xOffset, yOffset, zOffset;
private ArrayList<Double> posX;
private ArrayList<Double> posY;
private ArrayList<Double> posZ;
private Hashtable<String, Integer> channelColors;
private boolean split = false;
private int lastChannel = 0;
private int[] colors;
private int nXFields;
// -- Constructor --
/** Constructs a new ND2 reader. */
public NativeND2Reader() {
super("Nikon ND2", new String[] {"nd2", "jp2"});
suffixSufficient = false;
domains = new String[] {FormatTools.LM_DOMAIN};
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
final int blockLen = 8;
if (!FormatTools.validStream(stream, blockLen, false)) return false;
long magic1 = stream.readInt() & 0xffffffffL;
long magic2 = stream.readInt() & 0xffffffffL;
return magic1 == ND2_MAGIC_BYTES_1 || magic2 == ND2_MAGIC_BYTES_2;
}
/* @see loci.formats.IFormatReader#get8BitLookupTable() */
public byte[][] get8BitLookupTable() {
if (FormatTools.getBytesPerPixel(getPixelType()) != 1 ||
!isIndexed() || lastChannel < 0 || lastChannel >= colors.length)
{
return null;
}
int color = colors[lastChannel];
if (color == 0) return null;
byte[][] lut = new byte[3][256];
int index = -1;
if (color > 0 && color < 256) index = 0;
else if (color >= 256 && color < 65280) index = 1;
else if (color > 65280 && color <= 16711680) index = 2;
for (int i=0; i<256; i++) {
if (index == -1) {
lut[0][i] = (byte) i;
lut[1][i] = (byte) i;
lut[2][i] = (byte) i;
}
else {
lut[index][i] = (byte) i;
}
}
return lut;
}
/* @see loci.formats.IFormatReader#get16BitLookupTable() */
public short[][] get16BitLookupTable() {
if (FormatTools.getBytesPerPixel(getPixelType()) != 2 ||
!isIndexed() || lastChannel < 0 || lastChannel >= colors.length)
{
return null;
}
int color = colors[lastChannel];
if (color == 0) return null;
short[][] lut = new short[3][65536];
int index = -1;
if (color > 0 && color < 256) index = 0;
else if (color >= 256 && color <= 65280) index = 1;
else if (color > 65280 && color <= 16711680) index = 2;
for (int i=0; i<65536; i++) {
if (index == -1) {
lut[0][i] = (short) i;
lut[1][i] = (short) i;
lut[2][i] = (short) i;
}
else {
lut[index][i] = (short) i;
}
}
return lut;
}
/**
* @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
*/
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);
lastChannel = split ? no % getSizeC() : 0;
int planeIndex = split ? no / getSizeC() : no;
in.seek(offsets[series][planeIndex]);
int bpp = FormatTools.getBytesPerPixel(getPixelType());
int pixel = bpp * getRGBChannelCount();
if (split) pixel *= getSizeC();
int totalPlanes = split ? getImageCount() / getSizeC() : getImageCount();
long maxFP = planeIndex == totalPlanes - 1 ?
in.length() : offsets[series][planeIndex + 1];
CodecOptions options = new CodecOptions();
options.littleEndian = isLittleEndian();
options.interleaved = isInterleaved();
options.maxBytes = (int) maxFP;
int scanlinePad = isJPEG ? 0 : getSizeX() % 2;
if (scanlinePad == 1) {
if (split && !isLossless && (nXFields % 2) != 0) {
scanlinePad = 0;
}
}
if (isJPEG || isLossless) {
if (codec == null) codec = createCodec(isJPEG);
byte[] t = codec.decompress(in, options);
if ((getSizeX() + scanlinePad) * getSizeY() * pixel > t.length) {
// one padding pixel per row total, instead of one padding pixel
// per channel per row
int rowLength = getSizeX() * pixel + scanlinePad * bpp;
int destLength = w * pixel;
int p = rowLength * y + x * pixel;
byte[] pix = new byte[destLength * h];
for (int row=0; row<h; row++) {
System.arraycopy(t, p, pix, row * destLength, destLength);
int skip = pixel * (getSizeX() - w - x) + scanlinePad * bpp;
p += destLength + skip;
}
if (split) {
pix = ImageTools.splitChannels(pix, lastChannel, getEffectiveSizeC(),
bpp, false, true);
}
System.arraycopy(pix, 0, buf, 0, pix.length);
}
else {
copyPixels(x, y, w, h, bpp, scanlinePad, t, buf, split);
}
t = null;
}
else if (split && (getSizeC() <= 4 || scanlinePad == 0) && nXFields <= 1) {
byte[] pix = new byte[(getSizeX() + scanlinePad) * getSizeY() * pixel];
in.read(pix);
copyPixels(x, y, w, h, bpp, scanlinePad, pix, buf, split);
pix = null;
}
else if (split) {
// one padding pixel per row total, instead of one padding pixel
// per channel per row
int rowLength = getSizeX() * pixel + scanlinePad * bpp;
int destLength = w * pixel;
in.skipBytes(rowLength * y + x * pixel);
byte[] pix = new byte[destLength * h];
for (int row=0; row<h; row++) {
in.read(pix, row * destLength, destLength);
in.skipBytes(pixel * (getSizeX() - w - x) + scanlinePad * bpp);
}
pix = ImageTools.splitChannels(pix, lastChannel, getEffectiveSizeC(),
bpp, false, true);
System.arraycopy(pix, 0, buf, 0, pix.length);
}
else {
// plane is not compressed
readPlane(in, x, y, w, h, scanlinePad, buf);
}
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
offsets = null;
isJPEG = isLossless = false;
codec = null;
tsT.clear();
fieldIndex = 0;
xOffset = yOffset = zOffset = 0;
posX = posY = posZ = null;
channelColors = null;
split = false;
nXFields = 0;
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
channelColors = new Hashtable<String, Integer>();
if (in.read() == -38 && in.read() == -50) {
// newer version of ND2 - doesn't use JPEG2000
LOGGER.info("Searching for blocks");
isJPEG = false;
in.seek(0);
in.order(true);
// assemble offsets to each block
ArrayList<String> imageNames = new ArrayList<String>();
ArrayList<Long> imageOffsets = new ArrayList<Long>();
ArrayList<int[]> imageLengths = new ArrayList<int[]>();
ArrayList<Long> customDataOffsets = new ArrayList<Long>();
ArrayList<int[]> customDataLengths = new ArrayList<int[]>();
ByteArrayHandle xml = new ByteArrayHandle();
StringBuffer name = new StringBuffer();
// search for blocks
byte[] sigBytes = {-38, -50, -66, 10}; // 0xDACEBE0A
while (in.getFilePointer() < in.length() - 1 && in.getFilePointer() >= 0)
{
byte[] buf = new byte[1024];
int foundIndex = -1;
in.read(buf, 0, sigBytes.length);
while (foundIndex == -1 && in.getFilePointer() < in.length()) {
int n = in.read(buf, sigBytes.length, buf.length - sigBytes.length);
for (int i=0; i<buf.length-sigBytes.length; i++) {
for (int j=0; j<sigBytes.length; j++) {
if (buf[i + j] != sigBytes[j]) break;
if (j == sigBytes.length - 1) foundIndex = i;
}
if (foundIndex != -1) break;
}
if (foundIndex == -1) {
System.arraycopy(buf, buf.length - sigBytes.length - 1,
buf, 0, sigBytes.length);
}
else in.seek(in.getFilePointer() - n + foundIndex);
}
if (in.getFilePointer() >= in.length() || foundIndex == -1) {
break;
}
if (in.getFilePointer() > in.length() - 24) break;
int lenOne = in.readInt();
int lenTwo = in.readInt();
int len = lenOne + lenTwo;
in.skipBytes(4);
long fp = in.getFilePointer();
String blockType = in.readString(12);
int percent = (int) (100 * fp / in.length());
LOGGER.info("Parsing block '{}' {}%", blockType, percent);
int skip = len - 12 - lenOne * 2;
if (skip <= 0) skip += lenOne * 2;
if (blockType.startsWith("ImageDataSeq")) {
imageOffsets.add(new Long(fp));
imageLengths.add(new int[] {lenOne, lenTwo});
char b = (char) in.readByte();
while (b != '!') {
name.append(b);
b = (char) in.readByte();
}
imageNames.add(name.toString());
name = name.delete(0, name.length());
}
else if (blockType.startsWith("Image") ||
blockType.startsWith("CustomDataVa"))
{
int length = lenOne + lenTwo - 12;
byte[] b = new byte[length];
in.read(b);
// strip out invalid characters
int off = 0;
for (int j=0; j<length; j++) {
char c = (char) b[j];
if ((off == 0 && c == '!') || c == 0) off = j + 1;
if (Character.isISOControl(c) || !Character.isDefined(c)) {
b[j] = (byte) ' ';
}
}
if (length - off >= 5 && b[off] == '<' && b[off + 1] == '?' &&
b[off + 2] == 'x' && b[off + 3] == 'm' && b[off + 4] == 'l')
{
boolean endBracketFound = false;
while (!endBracketFound) {
if (b[off++] == '>') {
endBracketFound = true;
}
}
xml.write(b, off, b.length - off);
}
skip = 0;
}
else if (getMetadataOptions().getMetadataLevel() !=
MetadataLevel.MINIMUM)
{
if (blockType.startsWith("CustomData|A")) {
customDataOffsets.add(new Long(fp));
customDataLengths.add(new int[] {lenOne, lenTwo});
}
else if (blockType.startsWith("CustomData|Z")) {
int nDoubles = (lenOne + lenTwo) / 8;
zOffset = fp + 8 * (nDoubles - imageOffsets.size());
}
else if (blockType.startsWith("CustomData|X")) {
int nDoubles = (lenOne + lenTwo) / 8;
xOffset = fp + 8 * (nDoubles - imageOffsets.size());
}
else if (blockType.startsWith("CustomData|Y")) {
int nDoubles = (lenOne + lenTwo) / 8;
yOffset = fp + 8 * (nDoubles - imageOffsets.size());
}
}
in.skipBytes(skip);
}
// parse XML blocks
String xmlString = new String(xml.getBytes(), 0, (int) xml.length());
xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ND2>" +
xmlString + "</ND2>";
xmlString = XMLTools.sanitizeXML(xmlString);
core[0].dimensionOrder = "";
ND2Handler handler = new ND2Handler(core);
XMLTools.parseXML(xmlString, handler);
channelColors = handler.getChannelColors();
isLossless = handler.isLossless();
fieldIndex = handler.getFieldIndex();
core = handler.getCoreMetadata();
Hashtable<String, Object> globalMetadata = handler.getMetadata();
nXFields = 0;
for (String key : globalMetadata.keySet()) {
addGlobalMeta(key, globalMetadata.get(key));
if (key.equals("iXFields")) {
nXFields = Integer.parseInt(globalMetadata.get(key).toString());
}
else if (key.equals("ChannelCount")) {
for (int i=0; i<getSeriesCount(); i++) {
if (core[i].sizeC == 0) {
core[i].sizeC =
Integer.parseInt(globalMetadata.get(key).toString());
if (core[i].sizeC > 1) {
core[i].rgb = true;
}
}
}
}
}
int numSeries = handler.getSeriesCount();
// rearrange image data offsets
if (numSeries == 0) numSeries = 1;
if (getSizeZ() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeZ = 1;
}
}
if (getSizeT() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeT = 1;
}
}
if (getSizeC() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeC = 1;
}
}
// make sure the channel count is reasonable
// sometimes the XML will indicate that there are multiple channels,
// when in fact there is only one channel
long firstOffset = imageOffsets.get(0);
long secondOffset =
imageOffsets.size() > 1 ? imageOffsets.get(1) : in.length();
int planeSize = getSizeX() * getSizeY() * getSizeC() *
FormatTools.getBytesPerPixel(getPixelType());
if (isLossless) planeSize /= 4;
long availableBytes = secondOffset - firstOffset;
if (availableBytes < planeSize) {
LOGGER.debug("Correcting SizeC: was {}", getSizeC());
LOGGER.debug("plane size = {}", planeSize);
LOGGER.debug("available bytes = {}", availableBytes);
core[0].sizeC = (int) (availableBytes / (planeSize / getSizeC()));
if (getSizeC() == 0) {
core[0].sizeC = 1;
}
}
if (getSizeT() == imageOffsets.size() && getSeriesCount() > 1) {
CoreMetadata firstCore = core[0];
core = new CoreMetadata[] {firstCore};
}
// calculate the image count
for (int i=0; i<getSeriesCount(); i++) {
core[i].imageCount = getSizeZ() * getSizeT() * getSizeC();
if (imageOffsets.size() / getSeriesCount() < core[i].imageCount) {
core[i].imageCount /= getSizeC();
}
if (core[i].imageCount > imageOffsets.size() / getSeriesCount()) {
if (core[i].imageCount == imageOffsets.size()) {
CoreMetadata originalCore = core[0];
core = new CoreMetadata[] {originalCore};
numSeries = 1;
break;
}
else if (imageOffsets.size() % core[i].sizeT == 0) {
core[i].imageCount = imageOffsets.size() / getSeriesCount();
core[i].sizeZ = core[i].imageCount / core[i].sizeT;
core[i].dimensionOrder = "CZT";
}
else {
core[i].imageCount = imageOffsets.size() / getSeriesCount();
core[i].sizeZ = 1;
core[i].sizeT = core[i].imageCount;
}
}
}
if (numSeries * getImageCount() == 1 && imageOffsets.size() > 1) {
for (int i=0; i<getSeriesCount(); i++) {
core[i].imageCount = imageOffsets.size() / getSeriesCount();
core[i].sizeZ = getImageCount();
core[i].sizeT = 1;
}
}
if (getDimensionOrder().equals("T")) {
fieldIndex = 0;
}
else if (getDimensionOrder().equals("ZT") && fieldIndex == 2) {
fieldIndex--;
}
if (getSizeC() > 1 && getDimensionOrder().indexOf("C") == -1) {
core[0].dimensionOrder = "C" + getDimensionOrder();
fieldIndex++;
}
core[0].dimensionOrder = "XY" + getDimensionOrder();
if (getDimensionOrder().indexOf("Z") == -1) core[0].dimensionOrder += "Z";
if (getDimensionOrder().indexOf("C") == -1) core[0].dimensionOrder += "C";
if (getDimensionOrder().indexOf("T") == -1) core[0].dimensionOrder += "T";
offsets = new long[numSeries][getImageCount()];
int[] lengths = new int[4];
int nextChar = 2;
for (int i=0; i<lengths.length; i++) {
if (i == fieldIndex) lengths[i] = core.length;
else {
char axis = getDimensionOrder().charAt(nextChar++);
if (axis == 'Z') lengths[i] = getSizeZ();
else if (axis == 'C') lengths[i] = 1;
else if (axis == 'T') lengths[i] = getSizeT();
}
}
int[] zctLengths = new int[4];
System.arraycopy(lengths, 0, zctLengths, 0, lengths.length);
zctLengths[fieldIndex] = 1;
for (int i=0; i<imageOffsets.size(); i++) {
long offset = imageOffsets.get(i).longValue();
int[] p = imageLengths.get(i);
int length = p[0] + p[1];
if (getSizeC() == 0) {
int sizeC = length / (getSizeX() * getSizeY() *
FormatTools.getBytesPerPixel(getPixelType()));
for (int q=0; q<getSeriesCount(); q++) {
core[q].sizeC = sizeC;
}
}
String imageName = imageNames.get(i);
int ndx = Integer.parseInt(imageName.replaceAll("\\D", ""));
int[] pos = FormatTools.rasterToPosition(lengths, ndx);
int seriesIndex = pos[fieldIndex];
pos[fieldIndex] = 0;
int plane = FormatTools.positionToRaster(zctLengths, pos);
if (seriesIndex < offsets.length && plane < offsets[seriesIndex].length)
{
offsets[seriesIndex][plane] = offset + p[0] + 8;
}
}
ArrayList<long[]> tmpOffsets = new ArrayList<long[]>();
for (int i=0; i<offsets.length; i++) {
if (offsets[i][0] > 0) tmpOffsets.add(offsets[i]);
}
offsets = new long[tmpOffsets.size()][];
for (int i=0; i<tmpOffsets.size(); i++) {
offsets[i] = tmpOffsets.get(i);
}
if (offsets.length != getSeriesCount()) {
int x = getSizeX();
int y = getSizeY();
int c = getSizeC();
int pixelType = getPixelType();
int bitsPerPixel = getBitsPerPixel();
boolean rgb = isRGB();
String order = getDimensionOrder();
core = new CoreMetadata[offsets.length];
for (int i=0; i<offsets.length; i++) {
core[i] = new CoreMetadata();
core[i].sizeX = x;
core[i].sizeY = y;
core[i].sizeC = c == 0 ? 1 : c;
core[i].pixelType = pixelType;
core[i].bitsPerPixel = bitsPerPixel;
core[i].rgb = rgb;
core[i].sizeZ = 1;
core[i].dimensionOrder = order;
int invalid = 0;
for (int q=0; q<offsets[i].length; q++) {
if (offsets[i][q] == 0) invalid++;
}
core[i].imageCount = offsets[i].length - invalid;
core[i].sizeT = core[i].imageCount / (rgb ? 1 : core[i].sizeC);
if (core[i].sizeT == 0) core[i].sizeT = 1;
}
}
else {
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeX = getSizeX();
core[i].sizeY = getSizeY();
core[i].sizeC = getSizeC() == 0 ? 1 : getSizeC();
core[i].sizeZ = getSizeZ() == 0 ? 1 : getSizeZ();
core[i].sizeT = getSizeT() == 0 ? 1 : getSizeT();
core[i].imageCount = getImageCount();
core[i].pixelType = getPixelType();
core[i].bitsPerPixel = getBitsPerPixel();
core[i].dimensionOrder = getDimensionOrder();
}
}
split = getSizeC() > 1;
for (int i=0; i<getSeriesCount(); i++) {
core[i].rgb = false;
core[i].littleEndian = true;
core[i].interleaved = false;
core[i].indexed = channelColors.size() > 0;
core[i].falseColor = true;
core[i].metadataComplete = true;
core[i].imageCount = core[i].sizeZ * core[i].sizeT * core[i].sizeC;
}
// read first CustomData block
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
posX = handler.getXPositions();
posY = handler.getYPositions();
posZ = handler.getZPositions();
if (customDataOffsets.size() > 0) {
in.seek(customDataOffsets.get(0).longValue());
int[] p = customDataLengths.get(0);
int len = p[0] + p[1];
int timestampBytes = imageOffsets.size() * 8;
in.skipBytes(len - timestampBytes);
// the acqtimecache is a undeliniated stream of doubles
for (int series=0; series<getSeriesCount(); series++) {
setSeries(series);
int count = split ? getImageCount() / getSizeC() : getImageCount();
for (int plane=0; plane<count; plane++) {
// timestamps are stored in ms; we want them in seconds
double time = in.readDouble() / 1000;
tsT.add(new Double(time));
addSeriesMeta("timestamp " + plane, time);
}
}
setSeries(0);
}
if (posX.size() == 0 && xOffset != 0) {
in.seek(xOffset);
for (int i=0; i<imageOffsets.size(); i++) {
posX.add(new Double(in.readDouble()));
}
}
if (posY.size() == 0 && yOffset != 0) {
in.seek(yOffset);
for (int i=0; i<imageOffsets.size(); i++) {
posY.add(new Double(in.readDouble()));
}
}
if (posZ.size() == 0 && zOffset != 0) {
in.seek(zOffset);
for (int i=0; i<imageOffsets.size(); i++) {
posZ.add(new Double(in.readDouble()));
}
}
}
populateMetadataStore(handler);
return;
}
else in.seek(0);
// older version of ND2 - uses JPEG 2000 compression
isJPEG = true;
LOGGER.info("Calculating image offsets");
ArrayList<Long> vs = new ArrayList<Long>();
long pos = in.getFilePointer();
boolean lastBoxFound = false;
int length = 0;
int box = 0;
// assemble offsets to each plane
int x = 0, y = 0, c = 0, type = 0;
while (!lastBoxFound) {
pos = in.getFilePointer();
length = in.readInt();
long nextPos = pos + length;
if (nextPos < 0 || nextPos >= in.length() || length == 0) {
lastBoxFound = true;
}
box = in.readInt();
pos = in.getFilePointer();
length -= 8;
if (box == 0x6a703263) {
vs.add(new Long(pos));
}
else if (box == 0x6a703268) {
in.skipBytes(4);
String s = in.readString(4);
if (s.equals("ihdr")) {
y = in.readInt();
x = in.readInt();
c = in.readShort();
type = in.readInt();
if (type == 0xf070100 || type == 0xf070000) type = FormatTools.UINT16;
else type = FormatTools.UINT8;
}
}
if (!lastBoxFound && box != 0x6a703268) in.skipBytes(length);
}
LOGGER.info("Finding XML metadata");
// read XML metadata from the end of the file
in.seek(vs.get(vs.size() - 1).longValue());
boolean found = false;
long off = -1;
byte[] buf = new byte[8192];
while (!found && in.getFilePointer() < in.length()) {
int read = 0;
if (in.getFilePointer() == vs.get(vs.size() - 1).longValue()) {
read = in.read(buf);
}
else {
System.arraycopy(buf, buf.length - 10, buf, 0, 10);
read = in.read(buf, 10, buf.length - 10);
}
if (read == buf.length) read -= 10;
for (int i=0; i<read+9; i++) {
if (buf[i] == (byte) 0xff && buf[i+1] == (byte) 0xd9) {
found = true;
off = in.getFilePointer() - (read + 10) + i;
i = buf.length;
break;
}
}
}
buf = null;
LOGGER.info("Parsing XML");
ArrayList<Long> zs = new ArrayList<Long>();
ArrayList<Long> ts = new ArrayList<Long>();
int numSeries = 0;
ND2Handler handler = null;
if (off > 0 && off < in.length() - 5 && (in.length() - off - 5) > 14) {
in.seek(off + 4);
StringBuffer sb = new StringBuffer();
// stored XML doesn't have a root node - add one, so that we can parse
// using SAX
sb.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><NIKON>");
String s = null;
int blockLength = 0;
while (in.getFilePointer() < in.length()) {
blockLength = in.readShort();
if (blockLength < 2) break;
blockLength -= 2;
if (blockLength + in.getFilePointer() >= in.length()) {
blockLength = (int) (in.length() - in.getFilePointer());
}
s = in.readString(blockLength);
s = s.replaceAll("<!--.+?>", ""); // remove comments
int openBracket = s.indexOf("<");
if (openBracket == -1) continue;
int closedBracket = s.lastIndexOf(">") + 1;
if (closedBracket < openBracket) continue;
s = s.substring(openBracket, closedBracket).trim();
if (s.indexOf("CalibrationSeq") == -1 && s.indexOf("VCAL") == -1 &&
s.indexOf("jp2cLUNK") == -1)
{
sb.append(s);
}
}
sb.append("</NIKON>");
LOGGER.info("Finished assembling XML string");
// strip out invalid characters
int offset = 0;
int len = sb.length();
for (int i=0; i<len; i++) {
char ch = sb.charAt(i);
if (offset == 0 && ch == '!') offset = i + 1;
if (Character.isISOControl(ch) || !Character.isDefined(ch)) {
sb.setCharAt(i, ' ');
}
}
core[0].dimensionOrder = "";
String xml = sb.toString().substring(offset, len - offset);
handler = new ND2Handler(core);
try {
XMLTools.parseXML(XMLTools.sanitizeXML(xml), handler);
}
catch (IOException e) { }
xml = null;
isLossless = handler.isLossless();
fieldIndex = handler.getFieldIndex();
zs = handler.getZSections();
ts = handler.getTimepoints();
numSeries = handler.getSeriesCount();
core = handler.getCoreMetadata();
Hashtable<String, Object> globalMetadata = handler.getMetadata();
for (String key : globalMetadata.keySet()) {
addGlobalMeta(key, globalMetadata.get(key));
}
}
LOGGER.info("Populating metadata");
core[0].pixelType = FormatTools.UINT8;
offsets = new long[1][2];
offsets[0][0] = vs.get(0).longValue();
if (offsets[0].length > 1 && vs.size() > 1) {
offsets[0][1] = vs.get(1).longValue();
}
in.seek(offsets[0][0]);
if (getSizeC() == 0) core[0].sizeC = 1;
int numBands = c;
c = numBands > 1 ? numBands : getSizeC();
if (numBands == 1 && getImageCount() == 1) c = 1;
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeC = c;
core[i].rgb = numBands > 1;
core[i].pixelType = type;
}
if (getDimensionOrder() == null) core[0].dimensionOrder = "";
if (isRGB() && getDimensionOrder().indexOf("C") == -1) {
core[0].dimensionOrder = "C" + getDimensionOrder();
}
if (getDimensionOrder().indexOf("Z") == -1) core[0].dimensionOrder += "Z";
if (getDimensionOrder().indexOf("C") == -1) core[0].dimensionOrder += "C";
if (getDimensionOrder().indexOf("T") == -1) core[0].dimensionOrder += "T";
core[0].dimensionOrder = "XY" + getDimensionOrder();
if (getImageCount() == 0) {
core[0].imageCount = vs.size();
core[0].sizeZ = (int) Math.max(zs.size(), 1);
core[0].sizeT = (int) Math.max(ts.size(), 1);
int channels = isRGB() ? 1 : getSizeC();
if (channels * getSizeZ() * getSizeT() != getImageCount()) {
core[0].sizeZ = 1;
core[0].sizeT = getImageCount() / channels;
core[0].imageCount = getSizeZ() * getSizeT() * channels;
}
}
if (getSizeZ() == 0) core[0].sizeZ = 1;
if (getSizeT() == 0) core[0].sizeT = 1;
for (int i=0; i<getSeriesCount(); i++) {
core[i].sizeZ = getSizeZ();
core[i].sizeT = getSizeT();
core[i].imageCount = getSizeZ() * getSizeT() * (isRGB() ? 1 : getSizeC());
core[i].dimensionOrder = getDimensionOrder();
core[i].sizeX = x;
core[i].sizeY = y;
core[i].interleaved = false;
core[i].littleEndian = false;
core[i].metadataComplete = true;
}
int nplanes = getSizeZ() * getEffectiveSizeC();
if (numSeries == 0) numSeries = 1;
if (numSeries * nplanes * getSizeT() > vs.size()) {
numSeries = vs.size() / (nplanes * getSizeT());
}
offsets = new long[numSeries][getImageCount()];
for (int i=0; i<getSizeT(); i++) {
for (int j=0; j<numSeries; j++) {
for (int q=0; q<nplanes; q++) {
offsets[j][i*nplanes + q] = vs.remove(0).longValue();
}
}
}
populateMetadataStore(handler);
}
// -- Helper methods --
private void populateMetadataStore(ND2Handler handler) throws FormatException
{
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this, true);
String filename = new Location(getCurrentFile()).getName();
ArrayList<String> posNames = handler.getPositionNames();
for (int i=0; i<getSeriesCount(); i++) {
String suffix =
i < posNames.size() ? posNames.get(i) : "(series " + (i + 1) + ")";
store.setImageName(filename + " " + suffix, i);
MetadataTools.setDefaultCreationDate(store, currentId, i);
}
colors = new int[getEffectiveSizeC()];
ArrayList<String> channelNames = null;
if (handler != null) {
channelNames = handler.getChannelNames();
for (int i=0; i<getSeriesCount(); i++) {
for (int c=0; c<getEffectiveSizeC(); c++) {
int index = i * getSizeC() + c;
if (index < channelNames.size()) {
String channelName = channelNames.get(index);
Integer channelColor = channelColors.get(channelName);
colors[c] = channelColor == null ? 0 : channelColor.intValue();
}
}
}
}
if (getMetadataOptions().getMetadataLevel() == MetadataLevel.MINIMUM) {
return;
}
String instrumentID = MetadataTools.createLSID("Instrument", 0);
store.setInstrumentID(instrumentID, 0);
for (int i=0; i<getSeriesCount(); i++) {
// link Instrument and Image
store.setImageInstrumentRef(instrumentID, i);
}
// populate Dimensions data
if (handler != null) {
for (int i=0; i<getSeriesCount(); i++) {
double sizeX = handler.getPixelSizeX();
double sizeY = handler.getPixelSizeY();
double sizeZ = handler.getPixelSizeZ();
if (sizeX > 0) {
store.setPixelsPhysicalSizeX(new PositiveFloat(sizeX), i);
}
if (sizeY > 0) {
store.setPixelsPhysicalSizeY(new PositiveFloat(sizeY), i);
}
if (sizeZ > 0) {
store.setPixelsPhysicalSizeZ(new PositiveFloat(sizeZ), i);
}
}
}
// populate PlaneTiming and StagePosition data
ArrayList<Double> exposureTime = null;
if (handler != null) handler.getExposureTimes();
for (int i=0; i<getSeriesCount(); i++) {
if (tsT.size() > 0) {
setSeries(i);
for (int n=0; n<getImageCount(); n++) {
int[] coords = getZCTCoords(n);
int stampIndex = coords[2] + i * getSizeT();
if (tsT.size() == getImageCount()) stampIndex = n;
else if (tsT.size() == getSizeZ()) {
stampIndex = coords[0];
}
double stamp = tsT.get(stampIndex).doubleValue();
store.setPlaneDeltaT(stamp, i, n);
int index = i * getSizeC() + coords[1];
if (exposureTime != null && index < exposureTime.size()) {
store.setPlaneExposureTime(exposureTime.get(index), i, n);
}
}
}
if (handler != null) {
if (posX == null) posX = handler.getXPositions();
if (posY == null) posY = handler.getYPositions();
if (posZ == null) posZ = handler.getZPositions();
}
String pos = "for position #" + (i + 1);
for (int n=0; n<getImageCount(); n++) {
int index = i * getImageCount() + n;
if (posX != null) {
if (index >= posX.size()) index = i;
if (index < posX.size()) {
String key = "X position ";
store.setPlanePositionX(posX.get(index), i, n);
addSeriesMeta(key + (i + 1), posX.get(index));
addGlobalMeta(key + pos, posX.get(index));
}
}
if (posY != null) {
if (index < posY.size()) {
String key = "Y position ";
store.setPlanePositionY(posY.get(index), i, n);
addSeriesMeta(key + (i + 1), posY.get(index));
addGlobalMeta(key + pos, posY.get(index));
}
}
if (posZ != null) {
if (index < posZ.size()) {
store.setPlanePositionZ(posZ.get(index), i, n);
String key = "Z position " + pos + ", plane #" + (n + 1);
addSeriesMeta(key, posZ.get(index));
addGlobalMeta(key, posZ.get(index));
}
}
}
}
if (handler == null) {
setSeries(0);
return;
}
String detectorID = MetadataTools.createLSID("Detector", 0, 0);
store.setDetectorID(detectorID, 0, 0);
store.setDetectorModel(handler.getCameraModel(), 0, 0);
store.setDetectorType(getDetectorType("Other"), 0, 0);
ArrayList<String> modality = handler.getModalities();
ArrayList<String> binning = handler.getBinnings();
ArrayList<Double> speed = handler.getSpeeds();
ArrayList<Double> gain = handler.getGains();
ArrayList<Double> temperature = handler.getTemperatures();
ArrayList<Integer> exWave = handler.getExcitationWavelengths();
ArrayList<Integer> emWave = handler.getEmissionWavelengths();
ArrayList<Integer> power = handler.getPowers();
ArrayList<Hashtable<String, String>> rois = handler.getROIs();
for (int i=0; i<getSeriesCount(); i++) {
for (int c=0; c<getEffectiveSizeC(); c++) {
int index = i * getSizeC() + c;
Double pinholeSize = handler.getPinholeSize();
if (pinholeSize != null) {
store.setChannelPinholeSize(pinholeSize, i, c);
}
if (index < channelNames.size()) {
String channelName = channelNames.get(index);
store.setChannelName(channelName, i, c);
}
else if (channelNames.size() == getSizeC()) {
store.setChannelName(channelNames.get(c), i, c);
}
if (index < modality.size()) {
store.setChannelAcquisitionMode(
getAcquisitionMode(modality.get(index)), i, c);
}
if (index < emWave.size()) {
store.setChannelEmissionWavelength(
new PositiveInteger(emWave.get(index)), i, c);
}
if (index < exWave.size()) {
store.setChannelExcitationWavelength(
new PositiveInteger(exWave.get(index)), i, c);
}
if (index < binning.size()) {
store.setDetectorSettingsBinning(
getBinning(binning.get(index)), i, c);
}
if (index < gain.size()) {
store.setDetectorSettingsGain(gain.get(index), i, c);
}
if (index < speed.size()) {
store.setDetectorSettingsReadOutRate(speed.get(index), i, c);
}
store.setDetectorSettingsID(detectorID, i, c);
}
}
for (int i=0; i<getSeriesCount(); i++) {
if (i * getSizeC() < temperature.size()) {
Double temp = temperature.get(i * getSizeC());
store.setImagingEnvironmentTemperature(temp, i);
}
}
// populate DetectorSettings
Double voltage = handler.getVoltage();
if (voltage != null) {
store.setDetectorSettingsVoltage(voltage, 0, 0);
}
// populate Objective
Double na = handler.getNumericalAperture();
if (na != null) {
store.setObjectiveLensNA(na, 0, 0);
}
Double mag = handler.getMagnification();
if (mag != null) {
store.setObjectiveCalibratedMagnification(mag, 0, 0);
}
store.setObjectiveModel(handler.getObjectiveModel(), 0, 0);
String immersion = handler.getImmersion();
if (immersion == null) immersion = "Other";
store.setObjectiveImmersion(getImmersion(immersion), 0, 0);
String correction = handler.getCorrection();
if (correction == null || correction.length() == 0) correction = "Other";
store.setObjectiveCorrection(getCorrection(correction), 0, 0);
// link Objective to Image
String objectiveID = MetadataTools.createLSID("Objective", 0, 0);
store.setObjectiveID(objectiveID, 0, 0);
Double refractiveIndex = handler.getRefractiveIndex();
for (int i=0; i<getSeriesCount(); i++) {
store.setImageObjectiveSettingsID(objectiveID, i);
if (refractiveIndex != null) {
store.setImageObjectiveSettingsRefractiveIndex(refractiveIndex, i);
}
}
setSeries(0);
// populate ROI data
if (getMetadataOptions().getMetadataLevel() == MetadataLevel.NO_OVERLAYS) {
return;
}
handler.populateROIs(store);
}
private Codec createCodec(boolean isJPEG) {
return isJPEG ? new JPEG2000Codec() : new ZlibCodec();
}
private void copyPixels(int x, int y, int w, int h, int bpp, int scanlinePad,
byte[] pix, byte[] buf, boolean split)
throws IOException
{
if (split) {
pix = ImageTools.splitChannels(pix, lastChannel, getEffectiveSizeC(), bpp,
false, true);
}
RandomAccessInputStream s = new RandomAccessInputStream(pix);
readPlane(s, x, y, w, h, scanlinePad, buf);
s.close();
}
}