/*
* #%L
* OME Bio-Formats package for reading and converting biological file formats.
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* 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, see
* <http://www.gnu.org/licenses/gpl-2.0.html>.
* #L%
*/
package loci.formats.in;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import loci.common.ByteArrayHandle;
import loci.common.Constants;
import loci.common.DataTools;
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.Color;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.PositiveInteger;
import ome.units.quantity.ElectricPotential;
import ome.units.quantity.Frequency;
import ome.units.quantity.Length;
import ome.units.quantity.Temperature;
import ome.units.quantity.Time;
import ome.units.UNITS;
/**
* 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.
*/
public class NativeND2Reader extends FormatReader {
// -- Constants --
public static final long ND2_MAGIC_BYTES_1 = 0xdacebe0aL;
public static final long ND2_MAGIC_BYTES_2 = 0x6a502020L;
private static final int BUFFER_SIZE = 32 * 1024;
// -- 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 positionCount = 0;
private int fieldIndex;
private long xOffset, yOffset, zOffset;
private long pfsOffset, pfsStateOffset;
private ArrayList<Length> posX;
private ArrayList<Length> posY;
private ArrayList<Length> posZ;
private ArrayList<Double> exposureTime = new ArrayList<Double>();
private Map<String, Integer> channelColors;
private boolean split = false;
private int lastChannel = 0;
private int[] colors;
private Boolean useZ = null;
private int nXFields;
private ND2Handler backupHandler;
private double trueSizeX = 0;
private double trueSizeY = 0;
private Double trueSizeZ = null;
private ArrayList<String> textChannelNames = new ArrayList<String>();
private ArrayList<Double> textEmissionWavelengths = new ArrayList<Double>();
private boolean textData = false;
private Double refractiveIndex = null;
// -- 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) */
@Override
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() */
@Override
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 redMax = color & 0xff;
int greenMax = (color & 0xff00) >> 8;
int blueMax = (color & 0xff0000) >> 16;
for (int i=0; i<256; i++) {
double scale = i / 255.0;
lut[0][i] = (byte) (redMax * scale);
lut[1][i] = (byte) (greenMax * scale);
lut[2][i] = (byte) (blueMax * scale);
}
return lut;
}
/* @see loci.formats.IFormatReader#get16BitLookupTable() */
@Override
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 redMax = color & 0xff;
int greenMax = (color & 0xff00) >> 8;
int blueMax = (color & 0xff0000) >> 16;
for (int i=0; i<65536; i++) {
// *Max values are from 0-255; we want LUT values from 0-65535
double scale = i / 255.0;
lut[0][i] = (short) (redMax * scale);
lut[1][i] = (short) (greenMax * scale);
lut[2][i] = (short) (blueMax * scale);
}
return lut;
}
/**
* @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
*/
@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);
lastChannel = split ? no % getSizeC() : 0;
int planeIndex = split ? no / getSizeC() : no;
in.seek(offsets[getSeries()][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[getSeries()][planeIndex + 1];
CodecOptions options = new CodecOptions();
options.littleEndian = isLittleEndian();
options.interleaved = isInterleaved();
options.maxBytes = (int) maxFP;
int scanlinePad = getScanlinePad();
if (isJPEG || isLossless) {
if (codec == null) codec = createCodec(isJPEG);
byte[] t = null;
try {
t = codec.decompress(in, options);
}
catch (IOException e) {
LOGGER.debug("Failed to decompress; plane may be corrupt", e);
return buf;
}
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++) {
if (p + destLength <= t.length) {
System.arraycopy(t, p, pix, row * destLength, destLength);
int skip = pixel * (getSizeX() - w - x) + scanlinePad * bpp;
p += destLength + skip;
}
else {
break;
}
}
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);
byte[] pix = new byte[destLength * h];
for (int row=0; row<h; row++) {
in.skipBytes(x * pixel);
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) */
@Override
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;
pfsOffset = pfsStateOffset = 0;
posX = posY = posZ = null;
channelColors = null;
split = false;
nXFields = 0;
backupHandler = null;
trueSizeX = 0;
trueSizeY = 0;
trueSizeZ = null;
textChannelNames.clear();
textEmissionWavelengths.clear();
useZ = null;
textData = false;
refractiveIndex = null;
exposureTime.clear();
positionCount = 0;
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
// using a 32KB buffer instead of the default 1MB gives
// better performance with the seek/skip pattern used here
in = new RandomAccessInputStream(id, BUFFER_SIZE);
channelColors = new HashMap<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[]>();
// order matters when working with the text blocks, which is
// why two ArrayLists are used instead of a HashMap
ArrayList<String> textStrings = new ArrayList<String>();
ArrayList<Boolean> validDimensions = new ArrayList<Boolean>();
ByteArrayHandle xml = new ByteArrayHandle();
StringBuffer name = new StringBuffer();
int extraZDataCount = 0;
boolean foundMetadata = false;
boolean useLastText = false;
int blockCount = 0;
// search for blocks
byte[] sigBytes = {-38, -50, -66, 10}; // 0xDACEBE0A
byte[] buf = new byte[BUFFER_SIZE];
while (in.getFilePointer() < in.length() - 1 && in.getFilePointer() >= 0)
{
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) {
// likely faster than System.arraycopy
buf[0] = buf[buf.length - 4];
buf[1] = buf[buf.length - 3];
buf[2] = buf[buf.length - 2];
buf[3] = buf[buf.length - 1];
}
else if (in.getFilePointer() - n + foundIndex < in.length()) {
in.seek(in.getFilePointer() - n + foundIndex);
}
}
if (in.getFilePointer() > in.length() - 24 || foundIndex == -1) {
break;
}
Long helper = in.getFilePointer(); // Remember starting position
int nameLength = in.readInt(); // Length of the block name
long dataLength = in.readLong(); // Length of the data
String nameAttri = in.readString(nameLength).trim(); // Read the name
Long stop = helper + (dataLength + nameLength); // Where this block ends
boolean seq = false; // Only 1 MetadataSeq is needed
// Send to iteration
// (we are interested only in xxxxLV - LV = light variant)
if (nameAttri.contains("MetadataLV") ||
nameAttri.contains("CalibrationLV") ||
(nameAttri.contains("MetadataSeqLV") && !seq))
{
// prevent position count from being doubled
if (nameAttri.equals("ImageMetadataLV!")) {
positionCount = 0;
}
iterateIn(in, stop);
}
// Only 1 MetadataSeq is needed
// (others should be same, and time is saved elsewhere)
if (nameAttri.contains("MetadataSeqLV")) {
seq = true;
}
in.seek(helper + 12); // Return to starting position
int len = (int) (nameLength + dataLength);
long fp = in.getFilePointer();
String blockType = in.readString(12);
int percent = (int) (100 * fp / in.length());
LOGGER.info("Parsing block '{}' {}%", blockType, percent);
blockCount++;
int skip = len - 12 - nameLength * 2;
if (skip <= 0) skip += nameLength * 2;
// Image calibration for newer nd2 files
if (blockType.endsWith("Calibra")) {
long veryStart = in.getFilePointer();
in.skipBytes(12); // ImageCalibra|tionLV
long endFP = in.getFilePointer() + len - 24;
while (in.read() == 0);
while (in.getFilePointer() < endFP) {
int nameLen = in.read();
if (nameLen == 0) {
in.seek(in.getFilePointer() - 3);
nameLen = in.read();
}
if (nameLen < 0) {
break;
}
// Get data
String attributeName =
DataTools.stripString(in.readString(nameLen * 2));
double valueOrLength = in.readDouble();
if (attributeName.equals("dCalibration")) {
if (valueOrLength > 0) {
addGlobalMeta(attributeName, valueOrLength);
if (trueSizeX == 0) {
trueSizeX = valueOrLength;
}
else if (trueSizeY == 0) {
trueSizeY = valueOrLength;
}
}
break; // Done with calibration
}
}
in.seek(veryStart); // For old nd2 files
}
if (blockType.startsWith("ImageDataSeq")) {
if (foundMetadata) {
imageOffsets.clear();
imageNames.clear();
imageLengths.clear();
customDataOffsets.clear();
customDataLengths.clear();
foundMetadata = false;
extraZDataCount = 0;
useLastText = true;
}
imageOffsets.add(fp);
imageLengths.add(new int[] {nameLength, (int) dataLength, getSizeX() * getSizeY()});
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("ImageText")) {
foundMetadata = true;
in.skipBytes(6);
while (in.read() == 0);
long startFP = in.getFilePointer();
in.seek(startFP - 1);
String textString = DataTools.stripString(in.readString((int) dataLength));
textStrings.add(textString);
validDimensions.add(blockCount > 2);
if (!textString.startsWith("<")) {
skip = 0;
}
}
else if (blockType.startsWith("Image") ||
blockType.startsWith("CustomDataVa"))
{
if (blockType.equals("ImageAttribu")) {
foundMetadata = true;
in.skipBytes(6);
long endFP = in.getFilePointer() + len - 18;
while (in.read() == 0);
boolean canBeLossless = true;
while (in.getFilePointer() < endFP) {
int nameLen = in.read();
if (nameLen == 0) {
in.seek(in.getFilePointer() - 3);
nameLen = in.read();
}
if (nameLen < 0) {
break;
}
long start = in.getFilePointer();
String attributeName =
DataTools.stripString(in.readString(nameLen * 2));
if (attributeName.startsWith("xml ") ||
attributeName.startsWith("ml version") ||
attributeName.startsWith("l version") ||
attributeName.startsWith("version"))
{
if (attributeName.startsWith("xml ")) {
in.seek(start - 2);
}
else if (attributeName.startsWith("ml version")) {
in.seek(start - 3);
}
else if (attributeName.startsWith("l version")) {
in.seek(start - 4);
}
else {
in.seek(start - 6);
}
attributeName = in.readCString();
String xmlString = XMLTools.sanitizeXML(attributeName.trim());
xmlString =
xmlString.substring(0, xmlString.lastIndexOf(">") + 1);
if (xmlString.startsWith("<?xml")) {
xmlString = xmlString.substring(xmlString.indexOf(">") + 1);
}
if (!xmlString.endsWith("</variant>")) {
xmlString += "</variant>";
}
if (getDimensionOrder() == null) {
core.get(0).dimensionOrder = "";
}
try {
ND2Handler handler =
new ND2Handler(core, imageOffsets.size());
XMLTools.parseXML(xmlString, handler);
xmlString = null;
core = handler.getCoreMetadataList();
if (backupHandler == null) {
backupHandler = handler;
}
}
catch (IOException e) {
LOGGER.debug("Could not parse XML", e);
}
in.seek(in.getFilePointer() - 8);
break;
}
int valueOrLength = in.readInt();
addGlobalMeta(attributeName, valueOrLength);
if (attributeName.equals("uiWidth")) {
core.get(0).sizeX = valueOrLength;
}
else if (attributeName.equals("uiHeight")) {
core.get(0).sizeY = valueOrLength;
}
else if (attributeName.equals("uiComp")) {
core.get(0).sizeC = valueOrLength;
}
else if (attributeName.equals("uiBpcInMemory")) {
core.get(0).pixelType = FormatTools.pixelTypeFromBytes(
valueOrLength / 8, false, false);
}
else if (attributeName.equals("uiBpcSignificant")) {
core.get(0).bitsPerPixel = valueOrLength;
}
else if (attributeName.equals("dCompressionParam")) {
isLossless = valueOrLength >= 0;
}
else if (attributeName.equals("eCompression")) {
canBeLossless = valueOrLength <= 0;
}
else if (attributeName.equals("SLxImageAttributes")) {
int toSkip = valueOrLength - 5;
if ((toSkip % 2) == 1) {
toSkip++;
}
in.skipBytes(toSkip);
}
else if (attributeName.endsWith("Desc")) {
in.seek(in.getFilePointer() - 2);
}
else if (attributeName.equals("SLxExperiment")) {
in.skipBytes(8);
}
else if (attributeName.equals("wsCameraName")) {
in.seek(in.getFilePointer() - 4);
byte[] b = new byte[2];
in.read(b);
StringBuilder value = new StringBuilder();
while (b[0] != 0) {
value.append(b[0]);
in.read(b);
}
addGlobalMeta(attributeName, value.toString());
}
else if (attributeName.equals("uLoopPars")) {
int v2 = in.readInt();
int v3 = in.readInt();
addGlobalMeta(attributeName,
valueOrLength + ", " + v2 + ", " + v3);
}
else if (attributeName.equals("pPeriod")) {
in.skipBytes(22);
}
else if (attributeName.equals("dPeriod") ||
attributeName.equals("dDuration"))
{
in.skipBytes(4);
}
else if (attributeName.equals("bDurationPref")) {
in.seek(in.getFilePointer() - 3);
}
in.skipBytes(1);
}
if (in.getFilePointer() > endFP) {
in.seek(endFP);
}
isLossless = isLossless && canBeLossless;
}
else {
int length = len - 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)
{
int nDoubles = len / 8;
int nInts = len / 4;
long doubleOffset = fp + 8 * (nDoubles - imageOffsets.size());
long intOffset = fp + 4 * (nInts - imageOffsets.size());
if (blockType.startsWith("CustomData|A")) {
customDataOffsets.add(fp);
customDataLengths.add(new int[] {nameLength, (int) dataLength});
}
else if (blockType.startsWith("CustomData|Z")) {
zOffset = doubleOffset;
extraZDataCount++;
}
else if (blockType.startsWith("CustomData|X")) {
xOffset = doubleOffset;
}
else if (blockType.startsWith("CustomData|Y")) {
yOffset = doubleOffset;
}
else if (blockType.startsWith("CustomData|P")) {
if (pfsOffset == 0) {
pfsOffset = intOffset;
}
else if (pfsStateOffset == 0) {
pfsStateOffset = intOffset;
}
}
}
if (skip > 0 && skip + in.getFilePointer() <= in.length()) {
in.skipBytes(skip);
}
}
// parse text blocks
int nChannelNames = textChannelNames.size();
for (int i=0; i<textStrings.size(); i++) {
parseText(textStrings.get(i), imageOffsets.size(),
validDimensions.get(i));
}
if (textChannelNames.size() > nChannelNames) {
int diff = textChannelNames.size() - nChannelNames;
while (textChannelNames.size() > diff) {
textChannelNames.remove(0);
}
}
// parse XML blocks
String xmlString =
new String(xml.getBytes(), 0, (int) xml.length(), Constants.ENCODING);
xml = null;
xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ND2>" +
xmlString + "</ND2>";
xmlString = XMLTools.sanitizeXML(xmlString);
core.get(0).dimensionOrder = "";
ND2Handler handler =
new ND2Handler(core, getSizeX() == 0, imageOffsets.size());
XMLTools.parseXML(xmlString, handler);
xmlString = null;
if (handler.getChannelColors().size() > 0) {
channelColors = handler.getChannelColors();
}
if (!isLossless) {
isLossless = handler.isLossless();
}
fieldIndex = handler.getFieldIndex();
core = handler.getCoreMetadataList();
final Map<String, Object> globalMetadata = handler.getMetadata();
nXFields = handler.getXFields();
if (nXFields > 6) {
nXFields = 0;
}
for (String key : globalMetadata.keySet()) {
addGlobalMeta(key, globalMetadata.get(key));
if (key.equals("ChannelCount")) {
for (int i=0; i<getSeriesCount(); i++) {
CoreMetadata ms = core.get(i);
if (ms.sizeC == 0) {
ms.sizeC =
Integer.parseInt(globalMetadata.get(key).toString());
if (ms.sizeC > 1) {
ms.rgb = true;
}
}
}
}
else if (key.equals("uiBpcInMemory")) {
int bpc = Integer.parseInt(globalMetadata.get(key).toString());
core.get(0).pixelType = FormatTools.pixelTypeFromBytes(
bpc / 8, false, false);
}
}
int planeCount = core.size() * getSizeZ() * getSizeT();
if (!textData && planeCount < imageOffsets.size() && planeCount > 0 &&
(imageOffsets.size() % (planeCount / core.size())) == 0)
{
int seriesCount = imageOffsets.size() / (planeCount / core.size());
core = new ArrayList<CoreMetadata>();
for (int i=0; i<seriesCount; i++) {
core.add(handler.getCoreMetadataList().get(0));
}
}
int numSeries = core.size();
// rearrange image data offsets
if (numSeries == 0) numSeries = 1;
else if (numSeries == 1 && positionCount > 1) {
for (int i=1; i<positionCount; i++) {
core.add(core.get(0));
}
numSeries = core.size();
}
if (getSizeZ() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core.get(i).sizeZ = 1;
}
if (getSizeT() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core.get(i).sizeT = imageOffsets.size() / getSeriesCount();
}
}
}
if (getSizeT() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core.get(i).sizeT = 1;
}
}
if (getSizeC() == 0) {
for (int i=0; i<getSeriesCount(); i++) {
core.get(i).sizeC = 1;
}
}
if (getSizeZ() * getSizeT() == imageOffsets.size() && core.size() > 1) {
CoreMetadata ms0 = core.get(0);
core = new ArrayList<CoreMetadata>();
core.add(ms0);
}
if (positionCount != getSeriesCount() && (getSizeZ() == imageOffsets.size() || (extraZDataCount > 1 && getSizeZ() == 1 && (extraZDataCount == getSizeC())) || (handler.getXPositions().size() == 0 && (xOffset == 0 && getSizeZ() != getSeriesCount()))) && getSeriesCount() > 1) {
CoreMetadata ms0 = core.get(0);
if (getSeriesCount() > ms0.sizeZ) {
ms0.sizeZ = getSeriesCount();
}
core = new ArrayList<CoreMetadata>();
core.add(ms0);
}
// 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();
long availableBytes = secondOffset - firstOffset;
// make sure that we have the compression setting correct
// it's not always easy to tell from the metadata
isLossless = true;
long fp = in.getFilePointer();
int[] firstLengths = imageLengths.get(0);
in.seek(firstOffset + firstLengths[0] + 8);
if (codec == null) codec = createCodec(false);
try {
CodecOptions options = new CodecOptions();
options.littleEndian = isLittleEndian();
options.interleaved = true;
options.maxBytes = (int) secondOffset;
byte[] t = codec.decompress(in, options);
if (t.length == 2 * getSizeX() * getSizeY() &&
getPixelType() == FormatTools.INT8)
{
core.get(0).pixelType = FormatTools.UINT16;
}
availableBytes = t.length;
}
catch (IOException e) {
isLossless = false;
}
boolean allEqual = true;
for (int i=1; i<imageOffsets.size(); i++) {
if (imageLengths.get(i)[1] != imageLengths.get(0)[1]) {
allEqual = false;
break;
}
}
if (!allEqual && !isLossless && imageOffsets.size() > 1) {
int plane = (getSizeX() + getScanlinePad()) * getSizeY();
boolean fixByteCounts = false;
if (plane > 0) {
for (int i=0; i<imageOffsets.size(); i++) {
int check = imageLengths.get(i)[2];
int length = imageLengths.get(i)[1] - 8;
if ((length % plane != 0 && length % (getSizeX() * getSizeY()) != 0) || (check > 0 && plane != check)) {
if (i == 0) {
fixByteCounts = true;
}
imageOffsets.remove(i);
imageLengths.remove(i);
i--;
}
}
}
if (fixByteCounts) {
firstOffset = imageOffsets.get(0);
secondOffset = imageOffsets.size() > 1 ?
imageOffsets.get(1) : in.length();
availableBytes = secondOffset - firstOffset;
if (isLossless) {
firstLengths = imageLengths.get(0);
in.seek(firstOffset + firstLengths[0] + 8);
CodecOptions options = new CodecOptions();
options.littleEndian = isLittleEndian();
options.interleaved = true;
options.maxBytes = (int) secondOffset;
byte[] t = codec.decompress(in, options);
availableBytes = t.length;
}
}
}
in.seek(fp);
long planeSize = getSizeX() * getSizeY() * getSizeC() *
FormatTools.getBytesPerPixel(getPixelType());
if (availableBytes < planeSize) {
LOGGER.debug("Correcting SizeC: was {}", getSizeC());
LOGGER.debug("plane size = {}", planeSize);
LOGGER.debug("available bytes = {}", availableBytes);
core.get(0).sizeC = (int) (availableBytes / (planeSize / getSizeC()));
if (getSizeC() == 0) {
core.get(0).sizeC = 1;
}
planeSize = getSizeX() * getSizeY() * getSizeC() *
FormatTools.getBytesPerPixel(getPixelType());
}
if (planeSize > 0 && availableBytes % planeSize != 0) {
// an extra 4K block of zeros may have been appended
if ((availableBytes - 4096) % planeSize == 0) {
availableBytes -= 4096;
}
}
if (planeSize > 0 &&
availableBytes > DataTools.safeMultiply64(planeSize, 3))
{
if (availableBytes < DataTools.safeMultiply64(planeSize, 6)) {
core.get(0).sizeC = 3;
core.get(0).rgb = true;
if (getPixelType() == FormatTools.INT8) {
core.get(0).pixelType = availableBytes > planeSize * 5 ?
FormatTools.UINT16 : FormatTools.UINT8;
}
}
}
else if (((planeSize > 0 &&
availableBytes >= DataTools.safeMultiply64(planeSize, 2)) ||
getSizeC() > 3) && getPixelType() == FormatTools.INT8)
{
core.get(0).pixelType = FormatTools.UINT16;
planeSize *= 2;
if (getSizeC() > 3 && availableBytes % planeSize != 0 &&
planeSize > availableBytes)
{
core.get(0).sizeC = 3;
core.get(0).rgb = true;
}
}
else if (getSizeC() == 2 && getPixelType() == FormatTools.INT8 &&
availableBytes >= planeSize * 2)
{
core.get(0).pixelType = FormatTools.UINT16;
}
else if (getPixelType() == FormatTools.INT8) {
core.get(0).pixelType = FormatTools.UINT8;
}
if (getSizeX() == 0) {
core.get(0).sizeX = (int) Math.sqrt(availableBytes /
(getSizeC() * FormatTools.getBytesPerPixel(getPixelType())));
core.get(0).sizeY = getSizeX();
}
int rowSize = getSizeX() * FormatTools.getBytesPerPixel(getPixelType()) *
getSizeC();
long sizeY = availableBytes / rowSize;
if (sizeY < getSizeY()) {
core.get(0).sizeY = (int) sizeY;
}
if (getSizeT() == imageOffsets.size() && getSeriesCount() > 1) {
CoreMetadata firstCore = core.get(0);
core = new ArrayList<CoreMetadata>();
core.add(firstCore);
}
// calculate the image count
for (int i=0; i<getSeriesCount(); i++) {
CoreMetadata ms = core.get(i);
ms.imageCount = getSizeZ() * getSizeT() * getSizeC();
if (imageOffsets.size() / getSeriesCount() < ms.imageCount) {
ms.imageCount /= getSizeC();
}
if (ms.imageCount > imageOffsets.size() / getSeriesCount()) {
int diff = imageOffsets.size() - ms.imageCount;
if (diff >= 0 && diff < ms.sizeZ && diff < ms.sizeT) {
CoreMetadata ms0 = core.get(0);
core = new ArrayList<CoreMetadata>();
core.add(ms0);
numSeries = 1;
break;
}
else if (imageOffsets.size() % ms.sizeT == 0) {
ms.imageCount = imageOffsets.size() / getSeriesCount();
ms.sizeZ = ms.imageCount / ms.sizeT;
ms.dimensionOrder = "CZT";
}
else {
ms.imageCount = imageOffsets.size() / getSeriesCount();
ms.sizeZ = 1;
ms.sizeT = ms.imageCount;
}
}
}
if (numSeries * getImageCount() == 1 && imageOffsets.size() > 1) {
for (int i=0; i<getSeriesCount(); i++) {
core.get(i).imageCount = imageOffsets.size() / getSeriesCount();
core.get(i).sizeZ = getImageCount();
core.get(i).sizeT = 1;
}
}
if (getSizeZ() * getSizeT() * (split ? 1 : getSizeC()) <
imageOffsets.size() / getSeriesCount())
{
split = getSizeC() > 1;
int count = imageOffsets.size() / getSeriesCount();
if (!split && count >= getSizeC()) {
count /= getSizeC();
}
int diff = count - getSizeZ() * getSizeT();
if (diff == getSizeZ()) {
core.get(0).sizeT++;
}
else if (getSizeT() > getSizeZ()) {
core.get(0).sizeZ = 1;
core.get(0).sizeT = count;
}
else {
core.get(0).sizeT = 1;
core.get(0).sizeZ = count;
}
if (useZ != null && !useZ) {
CoreMetadata original = core.get(0);
int nSeries = imageOffsets.size() / (getSizeZ() * getSizeT());
for (int i=1; i<nSeries; i++) {
core.add(original);
}
numSeries = core.size();
}
if (getSizeZ() * getSizeT() * (split ? 1 : getSizeC()) <
imageOffsets.size() / getSeriesCount() && getSizeC() > 4)
{
core.get(0).sizeZ = 1;
core.get(0).sizeT = imageOffsets.size() / getSeriesCount();
}
core.get(0).imageCount = getSizeZ() * getSizeT() * getSizeC();
}
if (getDimensionOrder().equals("T")) {
fieldIndex = 0;
}
else if (getDimensionOrder().equals("ZT") && fieldIndex == 2) {
fieldIndex--;
}
if (getSizeC() > 1 && getDimensionOrder().indexOf("C") == -1) {
core.get(0).dimensionOrder = "C" + getDimensionOrder();
fieldIndex++;
}
core.get(0).dimensionOrder = "XY" + getDimensionOrder();
if (getDimensionOrder().indexOf("Z") == -1) core.get(0).dimensionOrder += "Z";
if (getDimensionOrder().indexOf("C") == -1) core.get(0).dimensionOrder += "C";
if (getDimensionOrder().indexOf("T") == -1) core.get(0).dimensionOrder += "T";
if (getSizeZ() == 0) {
core.get(0).sizeZ = 1;
}
if (getSizeT() == 0) {
core.get(0).sizeT = 1;
}
if (getSizeC() == 0) {
core.get(0).sizeC = 1;
}
core.get(0).imageCount = getSizeZ() * getSizeT();
if (!isRGB()) {
core.get(0).imageCount *= getSizeC();
}
posX = handler.getXPositions();
posY = handler.getYPositions();
posZ = handler.getZPositions();
int uniqueX = 0, uniqueY = 0, uniqueZ = 0;
if (posX.size() == 0 && xOffset != 0) {
in.seek(xOffset);
for (int i=0; i<imageOffsets.size(); i++) {
final Double number = Double.valueOf(in.readDouble());
final Length x = new Length(number, UNITS.REFERENCEFRAME);
if (!posX.contains(x)) {
uniqueX++;
}
posX.add(x);
}
}
if (posY.size() == 0 && yOffset != 0) {
in.seek(yOffset);
for (int i=0; i<imageOffsets.size(); i++) {
final Double number = Double.valueOf(in.readDouble());
final Length y = new Length(number, UNITS.REFERENCEFRAME);
if (!posY.contains(y)) {
uniqueY++;
}
posY.add(y);
}
}
if (posZ.size() == 0 && zOffset != 0) {
in.seek(zOffset);
for (int i=0; i<imageOffsets.size(); i++) {
final Double number = Double.valueOf(in.readDouble());
final Length z = new Length(number, UNITS.REFERENCEFRAME);
if (!posZ.contains(z)) {
boolean unique = true;
for (int q=0; q<posZ.size(); q++) {
// account for potential stage drift
final double z1 = z.value(UNITS.REFERENCEFRAME).doubleValue();
final double z2 = posZ.get(q).value(UNITS.REFERENCEFRAME).doubleValue();
if (Math.abs(z1 - z2) <= 0.05) {
unique = false;
break;
}
}
if (unique) {
uniqueZ++;
}
}
posZ.add(z);
}
}
if (pfsOffset != 0) {
in.seek(pfsOffset);
for (int i=0; i<imageOffsets.size(); i++) {
addGlobalMetaList("PFS Offset", in.readInt());
}
}
if (pfsStateOffset != 0) {
in.seek(pfsStateOffset);
for (int i=0; i<imageOffsets.size(); i++) {
addGlobalMetaList("PFS Status", in.readInt());
}
}
if (core.size() == 1 && ((uniqueX == getSizeT() &&
uniqueY == getSizeT()) || uniqueZ == getSizeT()))
{
int count = getSizeT();
core.get(0).imageCount /= count;
core.get(0).sizeT = 1;
for (int i=1; i<count; i++) {
core.add(core.get(0));
}
numSeries = core.size();
}
// reset the series count if we're confident that the image count
// covers all of the offsets; this prevents too much memory being used
// when the offsets array is allocated
if (getImageCount() == imageOffsets.size() && numSeries > 1 &&
getSizeC() == 1)
{
CoreMetadata first = core.get(0);
core.clear();
core.add(first);
numSeries = 1;
}
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.size();
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;
boolean oneIndexed = false;
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.get(q).sizeC = sizeC;
}
}
String imageName = imageNames.get(i);
int ndx = Integer.parseInt(imageName.replaceAll("\\D", ""));
if (ndx == 1 && i == 0) {
oneIndexed = true;
}
if (oneIndexed) {
ndx--;
}
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].length > 0 && 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 ArrayList<CoreMetadata>();
for (int i=0; i<offsets.length; i++) {
CoreMetadata ms = new CoreMetadata();
core.add(ms);
ms.sizeX = x;
ms.sizeY = y;
ms.sizeC = c == 0 ? 1 : c;
ms.pixelType = pixelType;
ms.bitsPerPixel = bitsPerPixel;
ms.rgb = rgb;
ms.sizeZ = 1;
ms.dimensionOrder = order;
int invalid = 0;
for (int q=0; q<offsets[i].length; q++) {
if (offsets[i][q] == 0) invalid++;
}
ms.imageCount = offsets[i].length - invalid;
ms.sizeT = ms.imageCount / (rgb ? 1 : ms.sizeC);
if (ms.sizeT == 0) ms.sizeT = 1;
}
}
else {
for (int i=0; i<getSeriesCount(); i++) {
CoreMetadata ms = core.get(i);
ms.sizeX = getSizeX();
ms.sizeY = getSizeY();
ms.sizeC = getSizeC() == 0 ? 1 : getSizeC();
ms.sizeZ = getSizeZ() == 0 ? 1 : getSizeZ();
ms.sizeT = getSizeT() == 0 ? 1 : getSizeT();
ms.imageCount = getImageCount();
ms.pixelType = getPixelType();
ms.bitsPerPixel = getBitsPerPixel();
ms.dimensionOrder = getDimensionOrder();
}
}
boolean hasColor = false;
for (String ch : channelColors.keySet()) {
Integer color = channelColors.get(ch);
// look for a color that is neither black nor white
if (color != 0xffffff && color != 0) {
hasColor = true;
break;
}
}
split = getSizeC() > 1;
for (int i=0; i<getSeriesCount(); i++) {
CoreMetadata ms = core.get(i);
ms.rgb = false;
ms.littleEndian = true;
ms.interleaved = false;
ms.indexed = channelColors.size() > 0 && hasColor;
ms.falseColor = true;
ms.metadataComplete = true;
ms.imageCount = ms.sizeZ * ms.sizeT * ms.sizeC;
}
// read first CustomData block
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
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(time);
addSeriesMetaList("timestamp", time);
}
}
setSeries(0);
}
}
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(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);
}
}
s = null;
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.get(0).dimensionOrder = "";
if (len - offset < offset) {
offset = 0;
}
String xml = sb.substring(offset, len);
sb = null;
handler = new ND2Handler(core, vs.size());
try {
xml = XMLTools.sanitizeXML(xml);
XMLTools.parseXML(xml, handler);
}
catch (IOException e) { }
xml = null;
isLossless = handler.isLossless();
fieldIndex = handler.getFieldIndex();
zs = handler.getZSections();
ts = handler.getTimepoints();
numSeries = handler.getSeriesCount();
core = handler.getCoreMetadataList();
final Map<String, Object> globalMetadata = handler.getMetadata();
for (final Map.Entry<String, Object> entry : globalMetadata.entrySet()) {
addGlobalMeta(entry.getKey(), entry.getValue());
}
}
LOGGER.info("Populating metadata");
core.get(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.get(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++) {
CoreMetadata ms = core.get(i);
ms.sizeC = c;
ms.rgb = numBands > 1;
ms.pixelType = type;
}
if (getDimensionOrder() == null) core.get(0).dimensionOrder = "";
if (getSizeC() > 1) {
core.get(0).dimensionOrder = getDimensionOrder().replaceAll("C", "");
core.get(0).dimensionOrder = "C" + getDimensionOrder();
fieldIndex++;
}
if (getDimensionOrder().indexOf("Z") == -1) core.get(0).dimensionOrder += "Z";
if (getDimensionOrder().indexOf("C") == -1) core.get(0).dimensionOrder += "C";
if (getDimensionOrder().indexOf("T") == -1) core.get(0).dimensionOrder += "T";
core.get(0).dimensionOrder = "XY" + getDimensionOrder();
if (getImageCount() == 0) {
core.get(0).imageCount = vs.size();
core.get(0).sizeZ = (int) Math.max(zs.size(), 1);
core.get(0).sizeT = (int) Math.max(ts.size(), 1);
int channels = isRGB() ? 1 : getSizeC();
if (channels * getSizeZ() * getSizeT() != getImageCount()) {
core.get(0).sizeZ = 1;
core.get(0).sizeT = getImageCount() / channels;
core.get(0).imageCount = getSizeZ() * getSizeT() * channels;
}
}
if (getSizeZ() == 0) core.get(0).sizeZ = 1;
if (getSizeT() == 0) core.get(0).sizeT = 1;
for (int i=0; i<getSeriesCount(); i++) {
CoreMetadata ms = core.get(i);
ms.sizeZ = getSizeZ();
ms.sizeT = getSizeT();
ms.imageCount = getSizeZ() * getSizeT() * (isRGB() ? 1 : getSizeC());
ms.dimensionOrder = getDimensionOrder();
ms.sizeX = x;
ms.sizeY = y;
ms.interleaved = false;
ms.littleEndian = false;
ms.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 --
/**
* Function for iterating through ND2 metaAttributes
* @param in stream of bytes from file
* @param stop position where to stop
*/
private void iterateIn(RandomAccessInputStream in, Long stop) {
Object value; // We don't know if attribute will be int, double, string....
try {
Integer currentColor = null;
while (in.getFilePointer() < stop) {
long startOffset = in.getFilePointer();
int type = in.read(); // @See switch
int letters = in.read(); // Letters in the Attribute name
// Attribute name
String name = DataTools.stripString(in.readString(letters*2));
int numberOfItems; // Number of items in level (see level)
Long off; // Offset to index (see level)
switch (type) {
case (1): // bool
value = in.readBoolean();
break;
case (2): // int32
value = in.readInt();
break;
case (3): // unsigned int32
value = in.readInt();
break;
case (4): // int64
value = in.readLong();
break;
case (5): // unsigned int64
value = in.readLong();
break;
case (6): // double
value = in.readDouble();
break;
case (7): // VoidPointer
value = in.readLong();
break;
case (8): // String
char currentChar = 0;
StringBuilder resultString = new StringBuilder();
while((currentChar = in.readChar()) != 0) {
resultString.append(currentChar);
}
value = resultString.toString();
break;
case (9): // ByteArray
long length = in.readLong();
if (length + in.getFilePointer() > stop) {
in.seek(stop);
continue;
}
byte[] data = new byte[(int) length];
in.read(data);
value = new String(data, Constants.ENCODING);
break;
case (10): // deprecated
// Its like LEVEL but offset is pointing absolutely not relatively
numberOfItems = in.readInt(); // number of level atributes
// offset to index (absolute number - current position -
// numberOfItems(4B) - type (1B) - nameLength (1B) - name
off = in.readLong() - in.getFilePointer() - 4 - 2 - letters * 2;
value = "LEVEL";
// Iterate
iterateIn(in, off + in.getFilePointer());
// Last 4 bytes in level is some kind of point table
if (off < 0) {
break;
}
in.seek(off + in.getFilePointer() + numberOfItems * 8);
value = in.readLong();
break;
case (11): // level
numberOfItems = in.readInt(); // number of level attributes
off = in.readLong(); // offset to index
value = "LEVEL";
// Iterate
long endOffset = off + startOffset;
iterateIn(in, endOffset);
// Last 4 bytes in level is some kind of point table
/* ***** Index is pointer. NumberofItemes*8B ***** */
in.seek(endOffset + numberOfItems * 8);
break;
default: // Shall not happen :-)
continue;
}
name = name.trim();
if (name.equals("bUseZ")) {
useZ = new Boolean(value.toString());
}
else if (name.equals("sDescription")) {
if (currentColor != null) {
textChannelNames.add(value.toString());
channelColors.put(value.toString(), currentColor);
}
}
else if (name.equals("uiColor")) {
currentColor = (Integer) value;
}
else if (name.equals("dExposureTime")) {
exposureTime.add((Double) value);
}
else if (name.equals("EmWavelength")) {
Double wave = Double.parseDouble(value.toString());
textEmissionWavelengths.add(wave);
}
else if (name.equals("dZStep")) {
trueSizeZ = new Double(value.toString());
}
else if (name.equals("dPosX")) {
positionCount++;
}
if (type != 11 && type != 10) { // if not level add global meta
addGlobalMeta(name, value);
}
}
}
catch (Exception e) {
LOGGER.debug("", e);
}
}
private void populateMetadataStore(ND2Handler handler) throws FormatException
{
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this, true);
String filename = new Location(getCurrentFile()).getName();
if (handler != null) {
ArrayList<String> posNames = handler.getPositionNames();
int nameWidth = String.valueOf(getSeriesCount()).length();
for (int i=0; i<getSeriesCount(); i++) {
String seriesSuffix = String.format("(series %0" + nameWidth + "d)", i + 1);
String suffix = (i < posNames.size() && !posNames.get(i).equals("")) ?
posNames.get(i) : seriesSuffix;
String name = filename + " " + suffix;
store.setImageName(name.trim(), i);
}
}
colors = new int[getEffectiveSizeC()];
ArrayList<String> channelNames = null;
if (handler != null) {
channelNames = handler.getChannelNames();
if (channelNames.size() < getEffectiveSizeC() && backupHandler != null) {
channelNames = backupHandler.getChannelNames();
}
else if (channelNames.size() < getEffectiveSizeC()) {
channelNames = textChannelNames;
}
for (int c=0; c<getEffectiveSizeC(); c++) {
if (c < channelNames.size()) {
String channelName = channelNames.get(c);
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);
// set the channel color
for (int c=0; c<getEffectiveSizeC(); c++) {
int red = colors[c] & 0xff;
int green = (colors[c] & 0xff00) >> 8;
int blue = (colors[c] & 0xff0000) >> 16;
// do not set the channel color if the recorded color is black
// doing so can prevent the image from displaying correctly
if (red != 0 || green != 0 || blue != 0) {
// always set the alpha to 255, otherwise the colors may not appear
store.setChannelColor(new Color(red, green, blue, 255), i, c);
}
}
}
// 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 (trueSizeX > 0) {
store.setPixelsPhysicalSizeX(FormatTools.getPhysicalSizeX(trueSizeX), i);
}
else {
Length size = FormatTools.getPhysicalSizeX(sizeX);
if (size != null) {
store.setPixelsPhysicalSizeX(size, i);
}
}
if (trueSizeY > 0) {
store.setPixelsPhysicalSizeY(FormatTools.getPhysicalSizeY(trueSizeY), i);
}
else if (trueSizeX > 0) {
// if the X size is set, assume X and Y are equal
store.setPixelsPhysicalSizeY(FormatTools.getPhysicalSizeY(trueSizeX), i);
}
else {
Length size = FormatTools.getPhysicalSizeY(sizeY);
if (size == null) {
// if the X size is set, assume X and Y are equal
size = FormatTools.getPhysicalSizeY(sizeX);
}
if (size != null) {
store.setPixelsPhysicalSizeY(size, i);
}
}
if (trueSizeZ != null && trueSizeZ > 0) {
store.setPixelsPhysicalSizeZ(FormatTools.getPhysicalSizeZ(trueSizeZ), i);
}
else {
Length size = FormatTools.getPhysicalSizeZ(sizeZ);
if (size != null) {
store.setPixelsPhysicalSizeZ(size, i);
}
}
}
}
// populate PlaneTiming and StagePosition data
if (handler != null && handler.getExposureTimes().size() > 0) {
exposureTime = handler.getExposureTimes();
}
int zcPlanes = getImageCount() / ((split ? getSizeC() : 1) * getSizeT());
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 = getIndex(coords[0], split ? 0 : coords[1], 0);
stampIndex += (coords[2] * getSeriesCount() + i) * zcPlanes;
if (tsT.size() == getImageCount()) stampIndex = n;
else if (tsT.size() == getSizeZ()) {
stampIndex = coords[0];
}
if (stampIndex < tsT.size()) {
double stamp = tsT.get(stampIndex).doubleValue();
store.setPlaneDeltaT(new Time(stamp, UNITS.S), i, n);
}
int index = i * getSizeC() + coords[1];
if (exposureTime.size() == getSizeC()) {
index = coords[1];
}
if (exposureTime != null && index < exposureTime.size() && exposureTime.get(index) != null) {
store.setPlaneExposureTime(new Time(exposureTime.get(index), UNITS.S), 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";
for (int n=0; n<getImageCount(); n++) {
int[] coords = getZCTCoords(n);
int index = coords[0];
index += (coords[2] * getSeriesCount() + i) * zcPlanes;
if (posX != null) {
if (index >= posX.size()) index = i;
if (index < posX.size()) {
String key = "X position ";
store.setPlanePositionX(posX.get(index), i, n);
addSeriesMetaList(key, posX.get(index));
addGlobalMetaList(key + pos, posX.get(index));
}
}
if (posY != null) {
if (index < posY.size()) {
String key = "Y position ";
store.setPlanePositionY(posY.get(index), i, n);
addSeriesMetaList(key, posY.get(index));
addGlobalMetaList(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";
addSeriesMetaList(key, posZ.get(index));
addGlobalMetaList(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<Double> exWave = handler.getExcitationWavelengths();
ArrayList<Double> emWave = handler.getEmissionWavelengths();
ArrayList<Integer> power = handler.getPowers();
ArrayList<Hashtable<String, String>> rois = handler.getROIs();
if (backupHandler != null) {
if (emWave.size() == 0) {
emWave = backupHandler.getEmissionWavelengths();
}
if (exWave.size() == 0) {
exWave = backupHandler.getExcitationWavelengths();
}
}
for (int i=0; i<getSeriesCount(); i++) {
for (int c=0; c<getEffectiveSizeC(); c++) {
int index = c;
Double pinholeSize = handler.getPinholeSize();
if (pinholeSize != null) {
store.setChannelPinholeSize(new Length(pinholeSize, UNITS.MICROM), i, c);
}
if (index < channelNames.size()) {
String channelName = channelNames.get(index);
store.setChannelName(channelName, i, c);
}
else if (channelNames.size() >= getEffectiveSizeC()) {
store.setChannelName(channelNames.get(c), i, c);
}
if (index < modality.size()) {
store.setChannelAcquisitionMode(
getAcquisitionMode(modality.get(index)), i, c);
}
if (index < emWave.size() || index < textEmissionWavelengths.size()) {
Double value = index < emWave.size() ? emWave.get(index) :
textEmissionWavelengths.get(index);
Length emission = FormatTools.getEmissionWavelength(value);
if (emission != null) {
store.setChannelEmissionWavelength(emission, i, c);
}
}
else if (emWave.size() > 0 || textEmissionWavelengths.size() > 0) {
store.setChannelColor(new Color(255, 255, 255, 255), i, c);
}
if (index < exWave.size()) {
Length excitation =
FormatTools.getExcitationWavelength(exWave.get(index));
if (excitation != null) {
store.setChannelExcitationWavelength(excitation, 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(
new Frequency(speed.get(index), UNITS.HZ), 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(
new Temperature(temp, UNITS.DEGREEC), i);
}
}
// populate DetectorSettings
Double voltage = handler.getVoltage();
if (voltage != null) {
store.setDetectorSettingsVoltage(
new ElectricPotential(voltage, UNITS.V), 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);
if (refractiveIndex == null) {
refractiveIndex = handler.getRefractiveIndex();
}
for (int i=0; i<getSeriesCount(); i++) {
store.setObjectiveSettingsID(objectiveID, i);
if (refractiveIndex != null) {
store.setObjectiveSettingsRefractiveIndex(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();
}
/** Remove control and invalid characters from the given string. */
public static String sanitizeControl(String s) {
final char[] c = s.toCharArray();
for (int i=0; i<s.length(); i++) {
if (Character.isISOControl(c[i]) ||
!Character.isDefined(c[i]))
{
c[i] = ' ';
}
}
return new String(c);
}
private int getScanlinePad() {
int scanlinePad = isJPEG ? 0 : getSizeX() % 2;
if (scanlinePad == 1) {
if (split && !isLossless && ((nXFields % 2) != 0 ||
(nXFields == 0 && (getSizeC() >= 4 || getSizeC() == 2))))
{
scanlinePad = 0;
}
}
return scanlinePad;
}
private void parseText(String textString, int offsetCount, boolean useDimensions) {
try {
ND2Handler handler = new ND2Handler(core, offsetCount);
String xmlString = XMLTools.sanitizeXML(textString);
int start = xmlString.indexOf("<");
int end = xmlString.lastIndexOf(">");
if (start >= 0 && end >= 0 && end >= start) {
xmlString = xmlString.substring(start, end + 1);
}
XMLTools.parseXML(xmlString, handler);
xmlString = null;
textString = null;
core = handler.getCoreMetadataList();
if (backupHandler == null ||
backupHandler.getChannelNames().size() == 0)
{
backupHandler = handler;
}
final Map<String, Object> globalMetadata = handler.getMetadata();
for (final Map.Entry<String, Object> entry : globalMetadata.entrySet()) {
addGlobalMeta(entry.getKey(), entry.getValue());
}
}
catch (IOException e) {
LOGGER.debug("Could not parse XML", e);
String[] lines = textString.split("\n");
ND2Handler handler = new ND2Handler(core, offsetCount);
for (String line : lines) {
int separator = line.indexOf(":");
if (separator >= 0) {
String key = line.substring(0, separator).trim();
String value = line.substring(separator + 1).trim();
if (useDimensions) {
handler.parseKeyAndValue(key, value, null);
}
if (handler.isDimensions(key)) {
textData = true;
}
}
}
if (useDimensions) {
core = handler.getCoreMetadataList();
}
// only accept the Z and T sizes from the text annotations
// if both values were set
if (core.get(0).sizeZ == 0 && getSizeT() != offsetCount) {
core.get(0).sizeT = 0;
}
textString = sanitizeControl(textString);
lines = textString.split(" ");
for (int i=0; i<lines.length; i++) {
String key = lines[i++];
while (!key.endsWith(":") && key.indexOf("_") < 0 && i < lines.length) {
key += " " + lines[i++];
if (i >= lines.length) {
break;
}
}
if (i >= lines.length) {
break;
}
String value = lines[i++];
while (i < lines.length && lines[i].trim().length() > 0) {
value += " " + lines[i++];
if (i >= lines.length) {
break;
}
}
key = key.trim();
key = key.substring(0, key.length() - 1);
value = value.trim();
if (key.startsWith("- Step")) {
int end = key.indexOf(" ", 8);
if (end < 0) {
end = key.length();
}
if (end >= 8) {
value = key.substring(8, end);
key = key.substring(0, 8);
}
if (trueSizeZ == null) {
try {
trueSizeZ = Double.parseDouble(DataTools.sanitizeDouble(value));
}
catch (NumberFormatException nfe) {
LOGGER.trace("Could not parse step", nfe);
}
}
}
else if (key.equals("Name")) {
textChannelNames.add(value);
}
else if (key.startsWith("Line:")) {
if (value.endsWith("Active")) {
int first = key.lastIndexOf(":") + 1;
int last = key.lastIndexOf(";");
try {
textEmissionWavelengths.add(
new Double(key.substring(first, last)) + 20);
}
catch (NumberFormatException nfe) {
LOGGER.trace("Could not parse emission wavelength", nfe);
}
}
}
else if (key.equals("Refractive Index")) {
try {
refractiveIndex = Double.parseDouble(DataTools.sanitizeDouble(value));
}
catch (NumberFormatException nfe) {
LOGGER.trace("Could not parse refractive index", nfe);
}
}
if (metadata.containsKey(key)) {
Object oldValue = metadata.get(key);
metadata.put(key + " #1", oldValue);
metadata.put(key + " #2", value);
metadata.remove(key);
}
else if (metadata.containsKey(key + " #1")) {
int index = 1;
while (metadata.containsKey(key + " #" + index)) {
index++;
}
metadata.put(key + " #" + index, value);
}
else {
metadata.put(key, value);
}
}
}
}
}