loci.formats.in.MetamorphReader.java Source code

Java tutorial

Introduction

Here is the source code for loci.formats.in.MetamorphReader.java

Source

/*
 * #%L
 * OME Bio-Formats package for reading and converting biological file formats.
 * %%
 * Copyright (C) 2005 - 2017 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.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.Location;
import loci.common.Constants;
import loci.common.RandomAccessInputStream;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.PhotoInterp;
import loci.formats.tiff.TiffIFDEntry;
import loci.formats.tiff.TiffParser;
import loci.formats.tiff.TiffRational;
import ome.xml.model.enums.NamingConvention;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;
import ome.xml.model.primitives.Timestamp;

import ome.units.quantity.Frequency;
import ome.units.quantity.Length;
import ome.units.quantity.Temperature;
import ome.units.quantity.Time;
import ome.units.UNITS;

import org.apache.commons.lang.ArrayUtils;

/**
 * Reader is the file format reader for Metamorph STK files.
 *
 * @author Eric Kjellman egkjellman at wisc.edu
 * @author Melissa Linkert melissa at glencoesoftware.com
 * @author Curtis Rueden ctrueden at wisc.edu
 * @author Sebastien Huart Sebastien dot Huart at curie.fr
 */
public class MetamorphReader extends BaseTiffReader {

    // -- Constants --

    /** Logger for this class. */
    private static final Logger LOGGER = LoggerFactory.getLogger(MetamorphReader.class);

    public static final String SHORT_DATE_FORMAT = "yyyyMMdd HH:mm:ss";
    public static final String LONG_DATE_FORMAT = "dd/MM/yyyy HH:mm:ss";

    public static final String[] ND_SUFFIX = { "nd" };
    public static final String[] STK_SUFFIX = { "stk", "tif", "tiff" };

    public static final Pattern WELL_COORDS = Pattern.compile("scan\\s+([a-z])(\\d+)", Pattern.CASE_INSENSITIVE);

    // IFD tag numbers of important fields
    private static final int METAMORPH_ID = 33628;
    private static final int UIC1TAG = METAMORPH_ID;
    private static final int UIC2TAG = 33629;
    private static final int UIC3TAG = 33630;
    private static final int UIC4TAG = 33631;

    // -- Fields --

    /** The TIFF's name */
    private String imageName;

    /** The TIFF's creation date */
    private String imageCreationDate;

    /** The TIFF's emWavelength */
    private long[] emWavelength;

    private boolean isHCS;
    private String[] stageLabels;
    private double[] wave;

    private String binning;
    private double zoom, stepSize;
    private Double exposureTime;
    private List<String> waveNames;
    private List<String> stageNames;
    private long[] internalStamps;
    private double[] zDistances;
    private Length[] stageX, stageY;
    private double zStart;
    private Double sizeX = null, sizeY = null;
    private double tempZ;
    private boolean validZ;

    private Double gain;

    private int mmPlanes; //number of metamorph planes

    private MetamorphReader[][] stkReaders;

    /** List of STK files in the dataset. */
    private String[][] stks;

    private String ndFilename;
    private boolean canLookForND = true;

    private boolean[] firstSeriesChannels;

    private boolean bizarreMultichannelAcquisition = false;

    private int openFiles = 0;

    private boolean hasStagePositions = false;
    private boolean hasChipOffsets = false;
    private boolean hasAbsoluteZ = false;
    private boolean hasAbsoluteZValid = false;

    // -- Constructor --

    /** Constructs a new Metamorph reader. */
    public MetamorphReader() {
        super("Metamorph STK", new String[] { "stk", "nd", "tif", "tiff" });
        domains = new String[] { FormatTools.LM_DOMAIN, FormatTools.HCS_DOMAIN };
        hasCompanionFiles = true;
        suffixSufficient = false;
        datasetDescription = "One or more .stk or .tif/.tiff files plus an " + "optional .nd file";
    }

    // -- IFormatReader API methods --

    /* @see loci.formats.IFormatReader#getDomains() */
    @Override
    public String[] getDomains() {
        FormatTools.assertId(currentId, true, 1);
        return isHCS ? new String[] { FormatTools.HCS_DOMAIN } : new String[] { FormatTools.LM_DOMAIN };
    }

    /* @see loci.formats.IFormatReader#isThisType(String, boolean) */
    @Override
    public boolean isThisType(String name, boolean open) {
        Location location = new Location(name);
        if (!location.exists()) {
            return false;
        }
        if (checkSuffix(name, "nd"))
            return true;
        if (open) {
            location = location.getAbsoluteFile();
            Location parent = location.getParentFile();

            String baseName = location.getName();

            while (baseName.indexOf('_') >= 0) {
                baseName = baseName.substring(0, baseName.lastIndexOf("_"));
                if (checkSuffix(name, suffixes) && (new Location(parent, baseName + ".nd").exists()
                        || new Location(parent, baseName + ".ND").exists())) {
                    return true;
                }
                if (checkSuffix(name, suffixes) && (new Location(parent, baseName + ".htd").exists()
                        || new Location(parent, baseName + ".HTD").exists())) {
                    return false;
                }
            }
        }
        return super.isThisType(name, open);
    }

    /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
    @Override
    public boolean isThisType(RandomAccessInputStream stream) throws IOException {
        TiffParser tp = new TiffParser(stream);
        IFD ifd = tp.getFirstIFD();
        if (ifd == null)
            return false;
        String software = ifd.getIFDTextValue(IFD.SOFTWARE);
        boolean validSoftware = software != null && software.trim().toLowerCase().startsWith("metamorph");
        return validSoftware || (ifd.containsKey(UIC1TAG) && ifd.containsKey(UIC3TAG) && ifd.containsKey(UIC4TAG));
    }

    /* @see loci.formats.IFormatReader#isSingleFile(String) */
    @Override
    public boolean isSingleFile(String id) throws FormatException, IOException {
        return !checkSuffix(id, ND_SUFFIX);
    }

    /* @see loci.formats.IFormatReader#fileGroupOption(String) */
    @Override
    public int fileGroupOption(String id) throws FormatException, IOException {
        if (checkSuffix(id, ND_SUFFIX))
            return FormatTools.MUST_GROUP;

        Location l = new Location(id).getAbsoluteFile();
        String[] files = l.getParentFile().list();

        for (String file : files) {
            if (checkSuffix(file, ND_SUFFIX) && l.getName().startsWith(file.substring(0, file.lastIndexOf(".")))) {
                return FormatTools.MUST_GROUP;
            }
        }

        return FormatTools.CANNOT_GROUP;
    }

    /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
    @Override
    public String[] getSeriesUsedFiles(boolean noPixels) {
        FormatTools.assertId(currentId, true, 1);
        if (!noPixels && stks == null)
            return new String[] { currentId };
        else if (stks == null)
            return ArrayUtils.EMPTY_STRING_ARRAY;

        final List<String> v = new ArrayList<String>();
        if (ndFilename != null)
            v.add(ndFilename);
        if (!noPixels) {
            for (String stk : stks[getSeries()]) {
                if (stk != null && new Location(stk).exists()) {
                    v.add(stk);
                }
            }
        }
        return v.toArray(new String[v.size()]);
    }

    /**
     * @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.assertId(currentId, true, 1);
        if (stks == null) {
            return super.openBytes(no, buf, x, y, w, h);
        }

        int[] coords = FormatTools.getZCTCoords(this, no % getSizeZ());
        int ndx = no / getSizeZ();
        if (bizarreMultichannelAcquisition) {
            int[] pos = getZCTCoords(no);
            ndx = getIndex(pos[0], 0, pos[2]) / getSizeZ();
        }
        if (stks[getSeries()].length == 1)
            ndx = 0;
        String file = stks[getSeries()][ndx];
        if (file == null)
            return buf;

        // the original file is a .nd file, so we need to construct a new reader
        // for the constituent STK files
        stkReaders[getSeries()][ndx].setMetadataOptions(new DynamicMetadataOptions(MetadataLevel.MINIMUM));
        int plane = stks[getSeries()].length == 1 ? no : coords[0];
        try {
            if (!file.equals(stkReaders[getSeries()][ndx].getCurrentFile())) {
                openFiles++;
            }
            stkReaders[getSeries()][ndx].setId(file);

            if (bizarreMultichannelAcquisition) {
                int realX = getZCTCoords(no)[1] == 0 ? x : x + getSizeX();
                stkReaders[getSeries()][ndx].openBytes(plane, buf, realX, y, w, h);
            } else {
                stkReaders[getSeries()][ndx].openBytes(plane, buf, x, y, w, h);
            }
        } finally {
            int count = stkReaders[getSeries()][ndx].getImageCount();
            if (plane == count - 1 || openFiles > 128) {
                stkReaders[getSeries()][ndx].close();
                openFiles--;
            }
        }

        return buf;
    }

    /* @see loci.formats.IFormatReader#close(boolean) */
    @Override
    public void close(boolean fileOnly) throws IOException {
        super.close(fileOnly);
        if (stkReaders != null) {
            for (MetamorphReader[] s : stkReaders) {
                if (s != null) {
                    for (MetamorphReader reader : s) {
                        if (reader != null)
                            reader.close(fileOnly);
                    }
                }
            }
        }
        if (!fileOnly) {
            imageName = imageCreationDate = null;
            emWavelength = null;
            stks = null;
            mmPlanes = 0;
            ndFilename = null;
            wave = null;
            binning = null;
            zoom = stepSize = 0;
            exposureTime = null;
            waveNames = stageNames = null;
            internalStamps = null;
            zDistances = null;
            stageX = stageY = null;
            firstSeriesChannels = null;
            sizeX = sizeY = null;
            tempZ = 0d;
            validZ = false;
            stkReaders = null;
            gain = null;
            bizarreMultichannelAcquisition = false;
            openFiles = 0;
            hasStagePositions = false;
            hasChipOffsets = false;
            hasAbsoluteZ = false;
            hasAbsoluteZValid = false;
            stageLabels = null;
            isHCS = false;
        }
    }

    // -- Internal FormatReader API methods --

    /* @see loci.formats.FormatReader#initFile(String) */
    @Override
    protected void initFile(String id) throws FormatException, IOException {
        if (checkSuffix(id, ND_SUFFIX)) {
            LOGGER.info("Initializing " + id);
            // find an associated STK file
            String stkFile = id.substring(0, id.lastIndexOf("."));
            if (stkFile.indexOf(File.separatorChar) != -1) {
                stkFile = stkFile.substring(stkFile.lastIndexOf(File.separator) + 1);
            }
            Location parent = new Location(id).getAbsoluteFile().getParentFile();
            LOGGER.info("Looking for STK file in {}", parent.getAbsolutePath());
            String[] dirList = parent.list(true);
            Arrays.sort(dirList);
            for (String f : dirList) {
                int underscore = f.indexOf('_');
                if (underscore < 0)
                    underscore = f.indexOf('.');
                if (underscore < 0)
                    underscore = f.length();
                String prefix = f.substring(0, underscore);

                if ((f.equals(stkFile) || stkFile.startsWith(prefix)) && checkSuffix(f, STK_SUFFIX)) {
                    stkFile = new Location(parent.getAbsolutePath(), f).getAbsolutePath();
                    break;
                }
            }

            if (!checkSuffix(stkFile, STK_SUFFIX)) {
                throw new FormatException("STK file not found in " + parent.getAbsolutePath() + ".");
            }

            super.initFile(stkFile);
        } else
            super.initFile(id);

        Location ndfile = null;

        if (checkSuffix(id, ND_SUFFIX))
            ndfile = new Location(id);
        else if (canLookForND && isGroupFiles()) {
            // an STK file was passed to initFile
            // let's check the parent directory for an .nd file
            Location stk = new Location(id).getAbsoluteFile();
            String stkName = stk.getName();
            String stkPrefix = stkName;
            if (stkPrefix.indexOf('_') >= 0) {
                stkPrefix = stkPrefix.substring(0, stkPrefix.indexOf('_') + 1);
            }
            Location parent = stk.getParentFile();
            String[] list = parent.list(true);
            int matchingChars = 0;
            for (String f : list) {
                if (checkSuffix(f, ND_SUFFIX)) {
                    String prefix = f.substring(0, f.lastIndexOf("."));
                    if (prefix.indexOf('_') >= 0) {
                        prefix = prefix.substring(0, prefix.indexOf('_') + 1);
                    }
                    if (stkName.startsWith(prefix) || prefix.equals(stkPrefix)) {
                        int charCount = 0;
                        for (int i = 0; i < f.length(); i++) {
                            if (i >= stkName.length()) {
                                break;
                            }
                            if (f.charAt(i) == stkName.charAt(i)) {
                                charCount++;
                            } else {
                                break;
                            }
                        }

                        if (charCount > matchingChars
                                || (charCount == matchingChars && f.charAt(charCount) == '.')) {
                            ndfile = new Location(parent, f).getAbsoluteFile();
                            matchingChars = charCount;
                        }
                    }
                }
            }
        }

        String creationTime = null;

        if (ndfile != null && ndfile.exists()
                && (fileGroupOption(id) == FormatTools.MUST_GROUP || isGroupFiles())) {
            // parse key/value pairs from .nd file

            int zc = getSizeZ(), cc = getSizeC(), tc = getSizeT();
            int nstages = 0;
            String z = null, c = null, t = null;
            final List<Boolean> hasZ = new ArrayList<Boolean>();
            waveNames = new ArrayList<String>();
            stageNames = new ArrayList<String>();
            boolean useWaveNames = true;

            ndFilename = ndfile.getAbsolutePath();
            String[] lines = DataTools.readFile(ndFilename).split("\n");

            boolean globalDoZ = true;
            boolean doTimelapse = false;

            StringBuilder currentValue = new StringBuilder();
            String key = "";

            for (String line : lines) {
                int comma = line.indexOf(',');
                if (comma <= 0) {
                    currentValue.append("\n");
                    currentValue.append(line);
                    continue;
                }

                String value = currentValue.toString();
                addGlobalMeta(key, value);
                if (key.equals("NZSteps"))
                    z = value;
                else if (key.equals("DoTimelapse")) {
                    doTimelapse = Boolean.parseBoolean(value);
                } else if (key.equals("NWavelengths"))
                    c = value;
                else if (key.equals("NTimePoints"))
                    t = value;
                else if (key.startsWith("WaveDoZ")) {
                    hasZ.add(Boolean.parseBoolean(value));
                } else if (key.startsWith("WaveName")) {
                    String waveName = value.substring(1, value.length() - 1);
                    if (waveName.equals("Both lasers") || waveName.startsWith("DUAL")) {
                        bizarreMultichannelAcquisition = true;
                    }
                    waveNames.add(waveName);
                } else if (key.startsWith("Stage")) {
                    stageNames.add(value);
                } else if (key.startsWith("StartTime")) {
                    creationTime = value;
                } else if (key.equals("ZStepSize")) {
                    value = value.replace(',', '.');
                    stepSize = Double.parseDouble(value);
                } else if (key.equals("NStagePositions")) {
                    nstages = Integer.parseInt(value);
                } else if (key.equals("WaveInFileName")) {
                    useWaveNames = Boolean.parseBoolean(value);
                } else if (key.equals("DoZSeries")) {
                    globalDoZ = Boolean.parseBoolean(value);
                }

                key = line.substring(1, comma - 1).trim();
                currentValue.delete(0, currentValue.length());
                currentValue.append(line.substring(comma + 1).trim());
            }

            if (!globalDoZ) {
                for (int i = 0; i < hasZ.size(); i++) {
                    hasZ.set(i, false);
                }
            }

            // figure out how many files we need

            if (z != null)
                zc = Integer.parseInt(z);
            if (c != null)
                cc = Integer.parseInt(c);
            if (t != null)
                tc = Integer.parseInt(t);
            else if (!doTimelapse) {
                tc = 1;
            }

            if (cc == 0)
                cc = 1;
            if (cc == 1 && bizarreMultichannelAcquisition) {
                cc = 2;
            }
            if (tc == 0) {
                tc = 1;
            }

            int numFiles = cc * tc;
            if (nstages > 0)
                numFiles *= nstages;

            // determine series count
            int stagesCount = nstages == 0 ? 1 : nstages;
            int seriesCount = stagesCount;
            firstSeriesChannels = new boolean[cc];
            Arrays.fill(firstSeriesChannels, true);
            boolean differentZs = false;
            for (int i = 0; i < cc; i++) {
                boolean hasZ1 = i < hasZ.size() && hasZ.get(i);
                boolean hasZ2 = i != 0 && (i - 1 < hasZ.size()) && hasZ.get(i - 1);
                if (i > 0 && hasZ1 != hasZ2 && globalDoZ) {
                    if (!differentZs)
                        seriesCount *= 2;
                    differentZs = true;
                }
            }

            int channelsInFirstSeries = cc;
            if (differentZs) {
                channelsInFirstSeries = 0;
                for (int i = 0; i < cc; i++) {
                    if ((!hasZ.get(0) && i == 0) || (hasZ.get(0) && hasZ.get(i))) {
                        channelsInFirstSeries++;
                    } else
                        firstSeriesChannels[i] = false;
                }
            }

            stks = new String[seriesCount][];
            if (seriesCount == 1)
                stks[0] = new String[numFiles];
            else if (differentZs) {
                for (int i = 0; i < stagesCount; i++) {
                    stks[i * 2] = new String[channelsInFirstSeries * tc];
                    stks[i * 2 + 1] = new String[(cc - channelsInFirstSeries) * tc];
                }
            } else {
                for (int i = 0; i < stks.length; i++) {
                    stks[i] = new String[numFiles / stks.length];
                }
            }

            String prefix = ndfile.getPath();
            prefix = prefix.substring(prefix.lastIndexOf(File.separator) + 1, prefix.lastIndexOf("."));

            // build list of STK files

            boolean anyZ = hasZ.contains(Boolean.TRUE);
            int[] pt = new int[seriesCount];
            for (int i = 0; i < tc; i++) {
                for (int s = 0; s < stagesCount; s++) {
                    for (int j = 0; j < cc; j++) {
                        boolean validZ = j >= hasZ.size() || hasZ.get(j);
                        int seriesNdx = s * (seriesCount / stagesCount);

                        if ((seriesCount != 1 && (!validZ || (hasZ.size() > 0 && !hasZ.get(0))))
                                || (nstages == 0 && ((!validZ && cc > 1) || seriesCount > 1))) {
                            if (anyZ && j > 0 && seriesNdx < seriesCount - 1 && (!validZ || !hasZ.get(0))) {
                                seriesNdx++;
                            }
                        }
                        if (seriesNdx >= stks.length || seriesNdx >= pt.length
                                || pt[seriesNdx] >= stks[seriesNdx].length) {
                            continue;
                        }
                        stks[seriesNdx][pt[seriesNdx]] = prefix;
                        if (j < waveNames.size() && waveNames.get(j) != null) {
                            stks[seriesNdx][pt[seriesNdx]] += "_w" + (j + 1);
                            if (useWaveNames) {
                                String waveName = waveNames.get(j);
                                // If there are underscores in the wavelength name, translate
                                // them to hyphens. (See #558)
                                waveName = waveName.replace('_', '-');
                                // If there are slashes (forward or backward) in the wavelength
                                // name, translate them to hyphens. (See #5922)
                                waveName = waveName.replace('/', '-');
                                waveName = waveName.replace('\\', '-');
                                waveName = waveName.replace('(', '-');
                                waveName = waveName.replace(')', '-');
                                stks[seriesNdx][pt[seriesNdx]] += waveName;
                            }
                        }
                        if (nstages > 0) {
                            stks[seriesNdx][pt[seriesNdx]] += "_s" + (s + 1);
                        }
                        if (tc > 1 || doTimelapse) {
                            stks[seriesNdx][pt[seriesNdx]] += "_t" + (i + 1) + ".STK";
                        } else
                            stks[seriesNdx][pt[seriesNdx]] += ".STK";
                        pt[seriesNdx]++;
                    }
                }
            }

            ndfile = ndfile.getAbsoluteFile();

            // check that each STK file exists

            for (int s = 0; s < stks.length; s++) {
                for (int f = 0; f < stks[s].length; f++) {
                    Location l = new Location(ndfile.getParent(), stks[s][f]);
                    stks[s][f] = getRealSTKFile(l);
                }
            }

            String file = locateFirstValidFile();
            if (file == null) {
                throw new FormatException("Unable to locate at least one valid STK file!");
            }

            RandomAccessInputStream s = new RandomAccessInputStream(file, 16);
            TiffParser tp = new TiffParser(s);
            IFD ifd = tp.getFirstIFD();
            CoreMetadata ms0 = core.get(0);
            s.close();
            ms0.sizeX = (int) ifd.getImageWidth();
            ms0.sizeY = (int) ifd.getImageLength();

            if (bizarreMultichannelAcquisition) {
                ms0.sizeX /= 2;
            }

            ms0.sizeZ = hasZ.size() > 0 && !hasZ.get(0) ? 1 : zc;
            ms0.sizeC = cc;
            ms0.sizeT = tc;
            ms0.imageCount = getSizeZ() * getSizeC() * getSizeT();
            if (isRGB()) {
                ms0.sizeC *= 3;
            }
            ms0.dimensionOrder = "XYZCT";

            if (stks != null && stks.length > 1) {
                // Note that core can't be replaced with newCore until the end of this block.
                ArrayList<CoreMetadata> newCore = new ArrayList<CoreMetadata>();
                for (int i = 0; i < stks.length; i++) {
                    CoreMetadata ms = new CoreMetadata();
                    newCore.add(ms);
                    ms.sizeX = getSizeX();
                    ms.sizeY = getSizeY();
                    ms.sizeZ = getSizeZ();
                    ms.sizeC = getSizeC();
                    ms.sizeT = getSizeT();
                    ms.pixelType = getPixelType();
                    ms.imageCount = getImageCount();
                    ms.dimensionOrder = getDimensionOrder();
                    ms.rgb = isRGB();
                    ms.littleEndian = isLittleEndian();
                    ms.interleaved = isInterleaved();
                    ms.orderCertain = true;
                }
                if (stks.length > nstages) {
                    for (int j = 0; j < stagesCount; j++) {
                        int idx = j * 2 + 1;
                        CoreMetadata midx = newCore.get(idx);
                        CoreMetadata pmidx = newCore.get(j * 2);
                        pmidx.sizeC = stks[j * 2].length / getSizeT();
                        midx.sizeC = stks[idx].length / midx.sizeT;
                        midx.sizeZ = hasZ.size() > 1 && hasZ.get(1) && core.get(0).sizeZ == 1 ? zc : 1;
                        pmidx.imageCount = pmidx.sizeC * pmidx.sizeT * pmidx.sizeZ;
                        midx.imageCount = midx.sizeC * midx.sizeT * midx.sizeZ;
                    }
                }
                core = newCore;
            }
        }

        if (stks == null) {
            stkReaders = new MetamorphReader[1][1];
            stkReaders[0][0] = new MetamorphReader();
            stkReaders[0][0].setCanLookForND(false);
        } else {
            stkReaders = new MetamorphReader[stks.length][];
            for (int i = 0; i < stks.length; i++) {
                stkReaders[i] = new MetamorphReader[stks[i].length];
                for (int j = 0; j < stkReaders[i].length; j++) {
                    stkReaders[i][j] = new MetamorphReader();
                    stkReaders[i][j].setCanLookForND(false);
                    if (j > 0) {
                        stkReaders[i][j].setMetadataOptions(new DynamicMetadataOptions(MetadataLevel.MINIMUM));
                    }
                }
            }
        }

        // check stage labels for plate data

        int rows = 0;
        int cols = 0;
        Map<String, Integer> rowMap = null;
        Map<String, Integer> colMap = null;
        isHCS = true;
        if (null == stageLabels) {
            isHCS = false;
        } else {
            Set<Map.Entry<Integer, Integer>> uniqueWells = new HashSet<Map.Entry<Integer, Integer>>();
            rowMap = new HashMap<String, Integer>();
            colMap = new HashMap<String, Integer>();
            for (String label : stageLabels) {
                if (null == label) {
                    isHCS = false;
                    break;
                }
                Map.Entry<Integer, Integer> wellCoords = getWellCoords(label);
                if (null == wellCoords) {
                    isHCS = false;
                    break;
                }
                uniqueWells.add(wellCoords);
                rowMap.put(label, wellCoords.getKey());
                colMap.put(label, wellCoords.getValue());
            }
            if (uniqueWells.size() != stageLabels.length) {
                isHCS = false;
            } else {
                rows = Collections.max(rowMap.values());
                cols = Collections.max(colMap.values());
                CoreMetadata c = core.get(0);
                core.clear();
                c.sizeZ = 1;
                c.sizeT = 1;
                c.imageCount = 1;
                for (int s = 0; s < uniqueWells.size(); s++) {
                    CoreMetadata toAdd = new CoreMetadata(c);
                    if (s > 0) {
                        toAdd.seriesMetadata.clear();
                    }
                    core.add(toAdd);
                }
                seriesToIFD = true;
            }
        }

        List<String> timestamps = null;
        MetamorphHandler handler = null;

        MetadataStore store = makeFilterMetadata();
        MetadataTools.populatePixels(store, this, true);

        if (isHCS) {
            store.setPlateID(MetadataTools.createLSID("Plate", 0), 0);
            store.setPlateRows(new PositiveInteger(rows), 0);
            store.setPlateColumns(new PositiveInteger(cols), 0);
            store.setPlateRowNamingConvention(NamingConvention.LETTER, 0);
            store.setPlateColumnNamingConvention(NamingConvention.NUMBER, 0);
        }

        int nextObjective = 0;
        String instrumentID = MetadataTools.createLSID("Instrument", 0);
        String detectorID = MetadataTools.createLSID("Detector", 0, 0);

        store.setInstrumentID(instrumentID, 0);
        store.setDetectorID(detectorID, 0, 0);
        store.setDetectorType(getDetectorType("Other"), 0, 0);

        for (int i = 0; i < getSeriesCount(); i++) {
            setSeries(i);
            // do not reparse the same XML for every well
            if (i == 0 || !isHCS) {
                handler = new MetamorphHandler(getSeriesMetadata());
            }

            if (isHCS) {
                String label = stageLabels[i];
                String wellID = MetadataTools.createLSID("Well", 0, i);
                store.setWellID(wellID, 0, i);
                store.setWellColumn(new NonNegativeInteger(colMap.get(label)), 0, i);
                store.setWellRow(new NonNegativeInteger(rowMap.get(label)), 0, i);
                store.setWellSampleID(MetadataTools.createLSID("WellSample", 0, i, 0), 0, i, 0);
                store.setWellSampleImageRef(MetadataTools.createLSID("Image", i), 0, i, 0);
                store.setWellSampleIndex(new NonNegativeInteger(i), 0, i, 0);
            }

            store.setImageInstrumentRef(instrumentID, i);

            String comment = getFirstComment(i);
            if (i == 0 || !isHCS) {
                if (comment != null && comment.startsWith("<MetaData>")) {
                    try {
                        XMLTools.parseXML(XMLTools.sanitizeXML(comment), handler);
                    } catch (IOException e) {
                    }
                }
            }

            if (creationTime != null) {
                String date = DateTools.formatDate(creationTime, SHORT_DATE_FORMAT, ".");
                if (date != null) {
                    store.setImageAcquisitionDate(new Timestamp(date), 0);
                }
            }

            store.setImageName(makeImageName(i).trim(), i);

            if (getMetadataOptions().getMetadataLevel() == MetadataLevel.MINIMUM) {
                continue;
            }
            store.setImageDescription("", i);

            store.setImagingEnvironmentTemperature(new Temperature(handler.getTemperature(), UNITS.CELSIUS), i);

            if (sizeX == null)
                sizeX = handler.getPixelSizeX();
            if (sizeY == null)
                sizeY = handler.getPixelSizeY();

            Length physicalSizeX = FormatTools.getPhysicalSizeX(sizeX);
            Length physicalSizeY = FormatTools.getPhysicalSizeY(sizeY);
            if (physicalSizeX != null) {
                store.setPixelsPhysicalSizeX(physicalSizeX, i);
            }
            if (physicalSizeY != null) {
                store.setPixelsPhysicalSizeY(physicalSizeY, i);
            }
            if (zDistances != null) {
                stepSize = zDistances[0];
            } else {
                List<Double> zPositions = new ArrayList<Double>();
                final List<Double> uniqueZ = new ArrayList<Double>();

                for (IFD ifd : ifds) {
                    MetamorphHandler zPlaneHandler = new MetamorphHandler();

                    String zComment = ifd.getComment();
                    if (zComment != null && zComment.startsWith("<MetaData>")) {
                        try {
                            XMLTools.parseXML(XMLTools.sanitizeXML(zComment), zPlaneHandler);
                        } catch (IOException e) {
                        }
                    }

                    zPositions = zPlaneHandler.getZPositions();
                    for (Double z : zPositions) {
                        if (!uniqueZ.contains(z))
                            uniqueZ.add(z);
                    }
                }
                if (uniqueZ.size() > 1 && uniqueZ.size() == getSizeZ()) {
                    BigDecimal lastZ = BigDecimal.valueOf(uniqueZ.get(uniqueZ.size() - 1));
                    BigDecimal firstZ = BigDecimal.valueOf(uniqueZ.get(0));
                    BigDecimal zRange = (lastZ.subtract(firstZ)).abs();
                    BigDecimal zSize = BigDecimal.valueOf((double) (getSizeZ() - 1));
                    MathContext mc = new MathContext(10, RoundingMode.HALF_UP);
                    stepSize = zRange.divide(zSize, mc).doubleValue();
                }
            }

            Length physicalSizeZ = FormatTools.getPhysicalSizeZ(stepSize);
            if (physicalSizeZ != null) {
                store.setPixelsPhysicalSizeZ(physicalSizeZ, i);
            }

            if (handler.getLensNA() != 0 || handler.getLensRI() != 0) {
                String objectiveID = MetadataTools.createLSID("Objective", 0, nextObjective);
                store.setObjectiveID(objectiveID, 0, nextObjective);
                if (handler.getLensNA() != 0) {
                    store.setObjectiveLensNA(handler.getLensNA(), 0, nextObjective);
                }
                store.setObjectiveSettingsID(objectiveID, i);
                if (handler.getLensRI() != 0) {
                    store.setObjectiveSettingsRefractiveIndex(handler.getLensRI(), i);
                }
                nextObjective++;
            }

            int waveIndex = 0;
            for (int c = 0; c < getEffectiveSizeC(); c++) {
                if (firstSeriesChannels == null || (stageNames != null && stageNames.size() == getSeriesCount())) {
                    waveIndex = c;
                } else if (firstSeriesChannels != null) {
                    int s = i % 2;
                    while (firstSeriesChannels[waveIndex] == (s == 1) && waveIndex < firstSeriesChannels.length) {
                        waveIndex++;
                    }
                }

                if (waveNames != null && waveIndex < waveNames.size()) {
                    store.setChannelName(waveNames.get(waveIndex).trim(), i, c);
                }
                if (handler.getBinning() != null)
                    binning = handler.getBinning();
                if (binning != null) {
                    store.setDetectorSettingsBinning(getBinning(binning), i, c);
                }
                if (handler.getReadOutRate() != 0) {
                    store.setDetectorSettingsReadOutRate(new Frequency(handler.getReadOutRate(), UNITS.HERTZ), i,
                            c);
                }

                if (gain == null) {
                    gain = handler.getGain();
                }

                if (gain != null) {
                    store.setDetectorSettingsGain(gain, i, c);
                }
                store.setDetectorSettingsID(detectorID, i, c);

                if (wave != null && waveIndex < wave.length) {
                    Length wavelength = FormatTools.getWavelength(wave[waveIndex]);

                    if ((int) wave[waveIndex] >= 1) {
                        // link LightSource to Image
                        int laserIndex = i * getEffectiveSizeC() + c;
                        String lightSourceID = MetadataTools.createLSID("LightSource", 0, laserIndex);
                        store.setLaserID(lightSourceID, 0, laserIndex);
                        store.setChannelLightSourceSettingsID(lightSourceID, i, c);
                        store.setLaserType(getLaserType("Other"), 0, laserIndex);
                        store.setLaserLaserMedium(getLaserMedium("Other"), 0, laserIndex);

                        if (wavelength != null) {
                            store.setChannelLightSourceSettingsWavelength(wavelength, i, c);
                        }
                    }
                }
                waveIndex++;
            }

            timestamps = handler.getTimestamps();

            for (int t = 0; t < timestamps.size(); t++) {
                String date = DateTools.convertDate(DateTools.getTime(timestamps.get(t), SHORT_DATE_FORMAT, "."),
                        DateTools.UNIX, SHORT_DATE_FORMAT + ".SSS");
                addSeriesMetaList("timestamp", date);
            }

            long startDate = 0;
            if (timestamps.size() > 0) {
                startDate = DateTools.getTime(timestamps.get(0), SHORT_DATE_FORMAT, ".");
            }

            final Length positionX = handler.getStagePositionX();
            final Length positionY = handler.getStagePositionY();
            final List<Double> exposureTimes = handler.getExposures();
            if (exposureTimes.size() == 0) {
                for (int p = 0; p < getImageCount(); p++) {
                    exposureTimes.add(exposureTime);
                }
            } else if (exposureTimes.size() == 1 && exposureTimes.size() < getEffectiveSizeC()) {
                for (int c = 1; c < getEffectiveSizeC(); c++) {
                    MetamorphHandler channelHandler = new MetamorphHandler();

                    String channelComment = getComment(i, c);
                    if (channelComment != null && channelComment.startsWith("<MetaData>")) {
                        try {
                            XMLTools.parseXML(XMLTools.sanitizeXML(channelComment), channelHandler);
                        } catch (IOException e) {
                        }
                    }

                    final List<Double> channelExpTime = channelHandler.getExposures();
                    exposureTimes.add(channelExpTime.get(0));
                }
            }

            int lastFile = -1;
            IFDList lastIFDs = null;
            IFD lastIFD = null;

            double distance = zStart;
            TiffParser tp = null;
            RandomAccessInputStream stream = null;

            for (int p = 0; p < getImageCount(); p++) {
                int[] coords = getZCTCoords(p);
                Double deltaT = 0d;
                Double expTime = exposureTime;
                Double xmlZPosition = null;

                int fileIndex = getIndex(0, coords[1], coords[2]) / getSizeZ();
                if (fileIndex >= 0) {
                    String file = stks == null ? currentId : stks[i][fileIndex];
                    if (file != null) {
                        if (fileIndex != lastFile) {
                            if (stream != null) {
                                stream.close();
                            }
                            stream = new RandomAccessInputStream(file, 16);
                            tp = new TiffParser(stream);
                            tp.checkHeader();
                            IFDList f = tp.getMainIFDs();
                            if (f.size() > 0) {
                                lastFile = fileIndex;
                                lastIFDs = f;
                            } else {
                                file = null;
                                stks[i][fileIndex] = null;
                            }
                        }
                    }

                    if (file != null) {
                        lastIFD = lastIFDs.get(p % lastIFDs.size());
                        Object commentEntry = lastIFD.get(IFD.IMAGE_DESCRIPTION);
                        if (commentEntry != null) {
                            if (commentEntry instanceof String) {
                                comment = (String) commentEntry;
                            } else if (commentEntry instanceof TiffIFDEntry) {
                                comment = tp.getIFDValue((TiffIFDEntry) commentEntry).toString();
                            }
                        }
                        if (comment != null)
                            comment = comment.trim();
                        if (comment != null && comment.startsWith("<MetaData>")) {
                            String[] lines = comment.split("\n");

                            timestamps = new ArrayList<String>();

                            for (String line : lines) {
                                line = line.trim();
                                if (line.startsWith("<prop")) {
                                    int firstQuote = line.indexOf("\"") + 1;
                                    int lastQuote = line.lastIndexOf("\"");
                                    String key = line.substring(firstQuote, line.indexOf("\"", firstQuote));
                                    String value = line.substring(line.lastIndexOf("\"", lastQuote - 1) + 1,
                                            lastQuote);

                                    if (key.equals("z-position")) {
                                        xmlZPosition = new Double(value);
                                    } else if (key.equals("acquisition-time-local")) {
                                        timestamps.add(value);
                                    }
                                }
                            }
                        }
                    }
                }

                int index = 0;

                if (timestamps.size() > 0) {
                    if (coords[2] < timestamps.size())
                        index = coords[2];
                    String stamp = timestamps.get(index);
                    long ms = DateTools.getTime(stamp, SHORT_DATE_FORMAT, ".");
                    deltaT = (ms - startDate) / 1000.0;
                } else if (internalStamps != null && p < internalStamps.length) {
                    long delta = internalStamps[p] - internalStamps[0];
                    deltaT = delta / 1000.0;
                    if (coords[2] < exposureTimes.size())
                        index = coords[2];
                }

                if (index == 0 && p > 0 && exposureTimes.size() > 0) {
                    index = coords[1] % exposureTimes.size();
                }

                if (index < exposureTimes.size()) {
                    expTime = exposureTimes.get(index);
                }
                if (deltaT != null) {
                    store.setPlaneDeltaT(new Time(deltaT, UNITS.SECOND), i, p);
                }
                if (expTime != null) {
                    store.setPlaneExposureTime(new Time(expTime, UNITS.SECOND), i, p);
                }

                if (stageX != null && p < stageX.length) {
                    store.setPlanePositionX(stageX[p], i, p);
                } else if (positionX != null) {
                    store.setPlanePositionX(positionX, i, p);
                }
                if (stageY != null && p < stageY.length) {
                    store.setPlanePositionY(stageY[p], i, p);
                } else if (positionY != null) {
                    store.setPlanePositionY(positionY, i, p);
                }
                if (zDistances != null && p < zDistances.length) {
                    if (p > 0) {
                        if (zDistances[p] != 0d)
                            distance += zDistances[p];
                        else
                            distance += zDistances[0];
                    }
                    final Length zPos = new Length(distance, UNITS.REFERENCEFRAME);
                    store.setPlanePositionZ(zPos, i, p);
                } else if (xmlZPosition != null) {
                    final Length zPos = new Length(xmlZPosition, UNITS.REFERENCEFRAME);
                    store.setPlanePositionZ(zPos, i, p);
                }
            }

            if (stream != null) {
                stream.close();
            }
        }
        setSeries(0);
    }

    // -- Internal BaseTiffReader API methods --

    /* @see BaseTiffReader#initStandardMetadata() */
    @Override
    protected void initStandardMetadata() throws FormatException, IOException {
        super.initStandardMetadata();

        CoreMetadata ms0 = core.get(0);

        ms0.sizeZ = 1;
        ms0.sizeT = 0;
        int rgbChannels = getSizeC();

        // Now that the base TIFF standard metadata has been parsed, we need to
        // parse out the STK metadata from the UIC4TAG.

        TiffIFDEntry uic1tagEntry = null;
        TiffIFDEntry uic2tagEntry = null;
        TiffIFDEntry uic4tagEntry = null;

        try {
            uic1tagEntry = tiffParser.getFirstIFDEntry(UIC1TAG);
            uic2tagEntry = tiffParser.getFirstIFDEntry(UIC2TAG);
            uic4tagEntry = tiffParser.getFirstIFDEntry(UIC4TAG);
        } catch (IllegalArgumentException exc) {
            LOGGER.debug("Unknown tag", exc);
        }

        try {
            if (uic4tagEntry != null) {
                mmPlanes = uic4tagEntry.getValueCount();
            }
            if (mmPlanes == 0) {
                mmPlanes = ifds.size();
            }
            if (uic2tagEntry != null) {
                parseUIC2Tags(uic2tagEntry.getValueOffset());
            }
            if (uic4tagEntry != null) {
                parseUIC4Tags(uic4tagEntry.getValueOffset());
            }
            if (uic1tagEntry != null) {
                parseUIC1Tags(uic1tagEntry.getValueOffset(), uic1tagEntry.getValueCount());
            }
            in.seek(uic4tagEntry.getValueOffset());
        } catch (NullPointerException exc) {
            LOGGER.debug("", exc);
        } catch (IOException exc) {
            LOGGER.debug("Failed to parse proprietary tags", exc);
        }

        try {
            // copy ifds into a new array of Hashtables that will accommodate the
            // additional image planes
            IFD firstIFD = ifds.get(0);
            long[] uic2 = firstIFD.getIFDLongArray(UIC2TAG);
            if (uic2 == null) {
                throw new FormatException("Invalid Metamorph file. Tag " + UIC2TAG + " not found.");
            }
            ms0.imageCount = uic2.length;

            Object entry = firstIFD.getIFDValue(UIC3TAG);
            TiffRational[] uic3 = entry instanceof TiffRational[] ? (TiffRational[]) entry
                    : new TiffRational[] { (TiffRational) entry };
            wave = new double[uic3.length];
            final List<Double> uniqueWavelengths = new ArrayList<Double>();
            for (int i = 0; i < uic3.length; i++) {
                wave[i] = uic3[i].doubleValue();
                addSeriesMeta("Wavelength [" + intFormatMax(i, mmPlanes) + "]", wave[i]);
                final Double v = wave[i];
                if (!uniqueWavelengths.contains(v))
                    uniqueWavelengths.add(v);
            }

            if (getSizeC() == 1) {
                ms0.sizeC = uniqueWavelengths.size();

                if (getSizeC() < getImageCount() && getSizeC() > (getImageCount() - getSizeC())
                        && (getImageCount() % getSizeC()) != 0) {
                    ms0.sizeC = getImageCount();
                }
            }

            IFDList tempIFDs = new IFDList();

            long[] oldOffsets = firstIFD.getStripOffsets();
            long[] stripByteCounts = firstIFD.getStripByteCounts();
            int rowsPerStrip = (int) firstIFD.getRowsPerStrip()[0];
            int stripsPerImage = getSizeY() / rowsPerStrip;
            if (stripsPerImage * rowsPerStrip != getSizeY())
                stripsPerImage++;

            PhotoInterp check = firstIFD.getPhotometricInterpretation();
            if (check == PhotoInterp.RGB_PALETTE) {
                firstIFD.putIFDValue(IFD.PHOTOMETRIC_INTERPRETATION, PhotoInterp.BLACK_IS_ZERO);
            }

            emWavelength = firstIFD.getIFDLongArray(UIC3TAG);

            // for each image plane, construct an IFD hashtable

            IFD temp;
            for (int i = 0; i < getImageCount(); i++) {
                // copy data from the first IFD
                temp = new IFD(firstIFD);

                // now we need a StripOffsets entry - the original IFD doesn't have this

                long[] newOffsets = new long[stripsPerImage];
                if (stripsPerImage * (i + 1) <= oldOffsets.length) {
                    System.arraycopy(oldOffsets, stripsPerImage * i, newOffsets, 0, stripsPerImage);
                } else {
                    System.arraycopy(oldOffsets, 0, newOffsets, 0, stripsPerImage);
                    long image = (stripByteCounts[0] / rowsPerStrip) * getSizeY();
                    for (int q = 0; q < stripsPerImage; q++) {
                        newOffsets[q] += i * image;
                    }
                }

                temp.putIFDValue(IFD.STRIP_OFFSETS, newOffsets);

                long[] newByteCounts = new long[stripsPerImage];
                if (stripsPerImage * i < stripByteCounts.length) {
                    System.arraycopy(stripByteCounts, stripsPerImage * i, newByteCounts, 0, stripsPerImage);
                } else {
                    Arrays.fill(newByteCounts, stripByteCounts[0]);
                }
                temp.putIFDValue(IFD.STRIP_BYTE_COUNTS, newByteCounts);

                tempIFDs.add(temp);
            }
            ifds = tempIFDs;
        } catch (IllegalArgumentException exc) {
            LOGGER.debug("Unknown tag", exc);
        } catch (NullPointerException exc) {
            LOGGER.debug("", exc);
        } catch (FormatException exc) {
            LOGGER.debug("Failed to build list of IFDs", exc);
        }

        // parse (mangle) TIFF comment
        String descr = ifds.get(0).getComment();
        if (descr != null) {
            String[] lines = descr.split("\n");
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i].trim();

                if (line.startsWith("<") && line.endsWith(">")) {
                    // XML comment; this will have already been parsed so can be ignored
                    break;
                }

                int colon = line.indexOf(':');

                if (colon < 0) {
                    // normal line (not a key/value pair)
                    if (line.length() > 0) {
                        // not a blank line
                        sb.append(line);
                        sb.append("  ");
                    }
                } else {
                    String descrValue = null;
                    if (i == 0) {
                        // first line could be mangled; make a reasonable guess
                        int dot = line.lastIndexOf(".", colon);
                        if (dot >= 0) {
                            descrValue = line.substring(0, dot + 1);
                        }
                        line = line.substring(dot + 1);
                        colon -= dot + 1;
                    }

                    // append value to description
                    if (descrValue != null) {
                        sb.append(descrValue);
                        if (!descrValue.endsWith("."))
                            sb.append(".");
                        sb.append("  ");
                    }

                    // add key/value pair embedded in comment as separate metadata
                    String key = line.substring(0, colon);
                    String value = line.substring(colon + 1).trim();
                    addSeriesMeta(key, value);
                    if (key.equals("Exposure")) {
                        if (value.indexOf('=') != -1) {
                            value = value.substring(value.indexOf('=') + 1).trim();
                        }
                        if (value.indexOf(' ') != -1) {
                            value = value.substring(0, value.indexOf(' '));
                        }
                        try {
                            value = value.replace(',', '.');
                            double exposure = Double.parseDouble(value);
                            exposureTime = exposure / 1000;
                        } catch (NumberFormatException e) {
                        }
                    } else if (key.equals("Bit Depth")) {
                        if (value.indexOf('-') != -1) {
                            value = value.substring(0, value.indexOf('-'));
                        }
                        try {
                            ms0.bitsPerPixel = Integer.parseInt(value);
                        } catch (NumberFormatException e) {
                        }
                    } else if (key.equals("Gain")) {
                        int space = value.indexOf(' ');
                        if (space != -1) {
                            int nextSpace = value.indexOf(" ", space + 1);
                            if (nextSpace < 0) {
                                nextSpace = value.length();
                            }
                            try {
                                gain = new Double(value.substring(space, nextSpace));
                            } catch (NumberFormatException e) {
                            }
                        }
                    }
                }
            }

            // replace comment with trimmed version
            descr = sb.toString().trim();
            if (descr.equals(""))
                metadata.remove("Comment");
            else
                addSeriesMeta("Comment", descr);
        }

        ms0.sizeT = getImageCount() / (getSizeZ() * (getSizeC() / rgbChannels));
        if (getSizeT() * getSizeZ() * (getSizeC() / rgbChannels) != getImageCount()) {
            ms0.sizeT = 1;
            ms0.sizeZ = getImageCount() / (getSizeC() / rgbChannels);
        }

        // if '_t' is present in the file name, swap Z and T sizes
        // this file was probably part of a larger dataset, but the .nd file is
        //  missing

        String filename = currentId.substring(currentId.lastIndexOf(File.separator) + 1);
        if (filename.contains("_t") && getSizeT() > 1) {
            int z = getSizeZ();
            ms0.sizeZ = getSizeT();
            ms0.sizeT = z;
        }
        if (getSizeZ() == 0)
            ms0.sizeZ = 1;
        if (getSizeT() == 0)
            ms0.sizeT = 1;

        if (getSizeZ() * getSizeT() * (isRGB() ? 1 : getSizeC()) != getImageCount()) {
            ms0.sizeZ = getImageCount();
            ms0.sizeT = 1;
            if (!isRGB())
                ms0.sizeC = 1;
        }
    }

    // -- Helper methods --

    /**
     * Check that the given STK file exists.  If it does, then return the
     * absolute path.  If it does not, then apply various formatting rules until
     * an existing file is found.
     *
     * @return the absolute path of an STK file, or null if no STK file is found.
     */
    private String getRealSTKFile(Location l) {
        if (l.exists())
            return l.getAbsolutePath();
        String name = l.getName();
        String parent = l.getParent();

        if (name.indexOf('_') > 0) {
            String prefix = name.substring(0, name.indexOf('_'));
            String suffix = name.substring(name.indexOf('_'));

            String basePrefix = new Location(currentId).getName();
            int end = basePrefix.indexOf('_');
            if (end < 0)
                end = basePrefix.indexOf('.');
            basePrefix = basePrefix.substring(0, end);

            if (!basePrefix.equals(prefix)) {
                name = basePrefix + suffix;
                Location p = new Location(parent, name);
                if (p.exists())
                    return p.getAbsolutePath();
            }
        }

        // '%' can be converted to '-'
        if (name.indexOf('%') != -1) {
            name = name.replaceAll("%", "-");
            l = new Location(parent, name);
            if (!l.exists()) {
                // try replacing extension
                name = name.substring(0, name.lastIndexOf(".")) + ".TIF";
                l = new Location(parent, name);
                if (!l.exists()) {
                    name = name.substring(0, name.lastIndexOf(".")) + ".tif";
                    l = new Location(parent, name);
                    return l.exists() ? l.getAbsolutePath() : null;
                }
            }
        }

        if (!l.exists()) {
            // try replacing extension
            int index = name.lastIndexOf(".");
            if (index < 0)
                index = name.length();
            name = name.substring(0, index) + ".TIF";
            l = new Location(parent, name);
            if (!l.exists()) {
                name = name.substring(0, name.lastIndexOf(".")) + ".tif";
                l = new Location(parent, name);
                if (!l.exists()) {
                    name = name.substring(0, name.lastIndexOf(".")) + ".stk";
                    l = new Location(parent, name);
                    return l.exists() ? l.getAbsolutePath() : null;
                }
            }
        }
        return l.getAbsolutePath();
    }

    /**
     * Returns the TIFF comment from the first IFD of the first STK file in the
     * given series.
     */
    private String getFirstComment(int i) throws IOException {
        return getComment(i, 0);
    }

    private String getComment(int i, int no) throws IOException {
        if (stks != null && stks[i][no] != null) {
            RandomAccessInputStream stream = new RandomAccessInputStream(stks[i][no], 16);
            TiffParser tp = new TiffParser(stream);
            String comment = tp.getComment();
            stream.close();
            return comment;
        }
        return ifds.get(0).getComment();
    }

    /** Create an appropriate name for the given series. */
    private String makeImageName(int i) {
        StringBuilder name = new StringBuilder();
        if (stageNames != null && stageNames.size() > 0) {
            int stagePosition = i / (getSeriesCount() / stageNames.size());
            name.append("Stage");
            name.append(stagePosition + 1);
            name.append(" ");
            name.append(stageNames.get(stagePosition));
        }

        if (firstSeriesChannels != null
                && (stageNames == null || stageNames.size() == 0 || stageNames.size() != getSeriesCount())) {
            if (name.length() > 0) {
                name.append("; ");
            }
            for (int c = 0; c < firstSeriesChannels.length; c++) {
                if (firstSeriesChannels[c] == ((i % 2) == 0) && c < waveNames.size()) {
                    name.append(waveNames.get(c));
                    name.append("/");
                }
            }
            if (name.length() > 0) {
                return name.substring(0, name.length() - 1);
            }
        }
        if (name.length() == 0 && isHCS) {
            return stageLabels[i];
        }
        return name.toString();
    }

    /**
     * Populates metadata fields with some contained in MetaMorph UIC2 Tag.
     * (for each plane: 6 integers:
     * zdistance numerator, zdistance denominator,
     * creation date, creation time, modif date, modif time)
     * @param uic2offset offset to UIC2 (33629) tag entries
     *
     * not a regular tiff tag (6*N entries, N being the tagCount)
     * @throws IOException
     */
    void parseUIC2Tags(long uic2offset) throws IOException {
        long saveLoc = in.getFilePointer();
        in.seek(uic2offset);

        /*number of days since the 1st of January 4713 B.C*/
        String cDate;
        /*milliseconds since 0:00*/
        String cTime;

        /*z step, distance separating previous slice from  current one*/
        String iAsString;

        zDistances = new double[mmPlanes];
        internalStamps = new long[mmPlanes];

        for (int i = 0; i < mmPlanes; i++) {
            iAsString = intFormatMax(i, mmPlanes);
            if (in.getFilePointer() + 8 > in.length())
                break;
            zDistances[i] = readRational(in).doubleValue();
            addSeriesMeta("zDistance[" + iAsString + "]", zDistances[i]);

            if (zDistances[i] != 0.0)
                core.get(0).sizeZ++;

            cDate = decodeDate(in.readInt());
            cTime = decodeTime(in.readInt());

            internalStamps[i] = DateTools.getTime(cDate + " " + cTime, LONG_DATE_FORMAT, ":");

            addSeriesMeta("creationDate[" + iAsString + "]", cDate);
            addSeriesMeta("creationTime[" + iAsString + "]", cTime);
            // modification date and time are skipped as they all seem equal to 0...?
            in.skip(8);
        }
        if (getSizeZ() == 0)
            core.get(0).sizeZ = 1;

        in.seek(saveLoc);
    }

    /**
     * UIC4 metadata parser
     *
     * UIC4 Table contains per-plane blocks of metadata
     * stage X/Y positions,
     * camera chip offsets,
     * stage labels...
     * @param uic4offset offset of UIC4 table (not tiff-compliant)
     * @throws IOException
     */
    private void parseUIC4Tags(long uic4offset) throws IOException {
        long saveLoc = in.getFilePointer();
        in.seek(uic4offset);
        if (in.getFilePointer() + 2 >= in.length())
            return;

        tempZ = 0d;
        validZ = false;

        short id = in.readShort();
        while (id != 0) {
            switch (id) {
            case 28:
                readStagePositions();
                hasStagePositions = true;
                break;
            case 29:
                readRationals(new String[] { "cameraXChipOffset", "cameraYChipOffset" });
                hasChipOffsets = true;
                break;
            case 37:
                readStageLabels();
                break;
            case 40:
                readRationals(new String[] { "UIC4 absoluteZ" });
                hasAbsoluteZ = true;
                break;
            case 41:
                readAbsoluteZValid();
                hasAbsoluteZValid = true;
                break;
            case 46:
                in.skipBytes(mmPlanes * 8); // TODO
                break;
            default:
                in.skipBytes(4);
            }
            id = in.readShort();
        }
        in.seek(saveLoc);

        if (validZ)
            zStart = tempZ;
    }

    private void readStagePositions() throws IOException {
        stageX = new Length[mmPlanes];
        stageY = new Length[mmPlanes];
        String pos;
        for (int i = 0; i < mmPlanes; i++) {
            pos = intFormatMax(i, mmPlanes);
            final Double posX = readRational(in).doubleValue();
            final Double posY = readRational(in).doubleValue();
            stageX[i] = new Length(posX, UNITS.REFERENCEFRAME);
            stageY[i] = new Length(posY, UNITS.REFERENCEFRAME);
            addSeriesMeta("stageX[" + pos + "]", posX);
            addSeriesMeta("stageY[" + pos + "]", posY);
            addGlobalMeta("X position for position #" + (getSeries() + 1), posX);
            addGlobalMeta("Y position for position #" + (getSeries() + 1), posY);
        }
    }

    private void readRationals(String[] labels) throws IOException {
        String pos;
        Set<Double> uniqueZ = new HashSet<Double>();
        for (int i = 0; i < mmPlanes; i++) {
            pos = intFormatMax(i, mmPlanes);
            for (int q = 0; q < labels.length; q++) {
                double v = readRational(in).doubleValue();
                if (labels[q].endsWith("absoluteZ")) {
                    if (i == 0) {
                        tempZ = v;
                    }
                    uniqueZ.add(v);
                }
                addSeriesMeta(labels[q] + "[" + pos + "]", v);
            }
        }
        if (uniqueZ.size() == mmPlanes) {
            core.get(0).sizeZ = mmPlanes;
        }
    }

    void readStageLabels() throws IOException {
        int strlen;
        String iAsString;
        stageLabels = new String[mmPlanes];
        for (int i = 0; i < mmPlanes; i++) {
            iAsString = intFormatMax(i, mmPlanes);
            strlen = in.readInt();
            stageLabels[i] = readCString(in, strlen);
            addSeriesMeta("stageLabel[" + iAsString + "]", stageLabels[i]);
        }
    }

    void readAbsoluteZValid() throws IOException {
        for (int i = 0; i < mmPlanes; i++) {
            int valid = in.readInt();
            addSeriesMeta("absoluteZValid[" + intFormatMax(i, mmPlanes) + "]", valid);
            if (i == 0) {
                validZ = valid == 1;
            }
        }
    }

    /**
     * UIC1 entry parser
     * @throws IOException
     * @param uic1offset offset as found in the tiff tag 33628 (UIC1Tag)
     * @param uic1count number of entries in UIC1 table (not tiff-compliant)
     */
    private void parseUIC1Tags(long uic1offset, int uic1count) throws IOException {
        // Loop through and parse out each field. A field whose
        // code is "0" represents the end of the fields so we'll stop
        // when we reach that; much like a NULL terminated C string.
        long saveLoc = in.getFilePointer();
        in.seek(uic1offset);
        int currentID;
        long valOrOffset;
        // variable declarations, because switch is dumb
        int num;
        String thedate, thetime;
        long lastOffset;

        tempZ = 0d;
        validZ = false;
        for (int i = 0; i < uic1count; i++) {
            if (in.getFilePointer() >= in.length())
                break;
            currentID = in.readInt();
            valOrOffset = in.readInt() & 0xffffffffL;
            lastOffset = in.getFilePointer();

            String key = getKey(currentID);
            Object value = String.valueOf(valOrOffset);

            boolean skipKey = false;
            switch (currentID) {
            case 3:
                value = valOrOffset != 0 ? "on" : "off";
                break;
            case 4:
            case 5:
            case 21:
            case 22:
            case 23:
            case 24:
            case 38:
            case 39:
                value = readRational(in, valOrOffset);
                break;
            case 6:
            case 25:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    num = in.readInt();
                    if (num + in.getFilePointer() >= in.length()) {
                        num = (int) (in.length() - in.getFilePointer() - 1);
                    }
                    if (num >= 0) {
                        value = in.readString(num);
                    }
                }
                break;
            case 7:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    num = in.readInt();
                    if (num >= 0) {
                        imageName = in.readString(num);
                        value = imageName;
                    }
                }
                break;
            case 8:
                if (valOrOffset == 1)
                    value = "inside";
                else if (valOrOffset == 2)
                    value = "outside";
                else
                    value = "off";
                break;
            case 17: // oh how we hate you Julian format...
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    thedate = decodeDate(in.readInt());
                    thetime = decodeTime(in.readInt());
                    imageCreationDate = thedate + " " + thetime;
                    value = imageCreationDate;
                }
                break;
            case 16:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    thedate = decodeDate(in.readInt());
                    thetime = decodeTime(in.readInt());
                    value = thedate + " " + thetime;
                }
                break;
            case 26:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    int standardLUT = in.readInt();
                    switch (standardLUT) {
                    case 0:
                        value = "monochrome";
                        break;
                    case 1:
                        value = "pseudocolor";
                        break;
                    case 2:
                        value = "Red";
                        break;
                    case 3:
                        value = "Green";
                        break;
                    case 4:
                        value = "Blue";
                        break;
                    case 5:
                        value = "user-defined";
                        break;
                    default:
                        value = "monochrome";
                    }
                }
                break;
            case 28:
                if (valOrOffset < in.length()) {
                    if (!hasStagePositions) {
                        in.seek(valOrOffset);
                        readStagePositions();
                    }
                    skipKey = true;
                }
                break;
            case 29:
                if (valOrOffset < in.length()) {
                    if (!hasChipOffsets) {
                        in.seek(valOrOffset);
                        readRationals(new String[] { "cameraXChipOffset", "cameraYChipOffset" });
                    }
                    skipKey = true;
                }
                break;
            case 34:
                value = String.valueOf(in.readInt());
                break;
            case 42:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    value = String.valueOf(in.readInt());
                }
                break;
            case 46:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    int xBin = in.readInt();
                    int yBin = in.readInt();
                    binning = xBin + "x" + yBin;
                    value = binning;
                }
                break;
            case 40:
                if (valOrOffset != 0 && valOrOffset < in.length()) {
                    if (!hasAbsoluteZ) {
                        in.seek(valOrOffset);
                        readRationals(new String[] { "UIC1 absoluteZ" });
                    }
                    skipKey = true;
                }
                break;
            case 41:
                if (valOrOffset != 0 && valOrOffset < in.length()) {
                    if (!hasAbsoluteZValid) {
                        in.seek(valOrOffset);
                        readAbsoluteZValid();
                    }
                    skipKey = true;
                } else if (valOrOffset == 0 && getSizeZ() < mmPlanes) {
                    core.get(0).sizeZ = 1;
                }
                break;
            case 49:
                if (valOrOffset < in.length()) {
                    in.seek(valOrOffset);
                    readPlaneData();
                    skipKey = true;
                }
                break;
            }
            if (!skipKey) {
                addSeriesMeta(key, value);
            }
            in.seek(lastOffset);

            if ("Zoom".equals(key) && value != null) {
                zoom = Double.parseDouble(value.toString());
            }
            if ("XCalibration".equals(key) && value != null) {
                if (value instanceof TiffRational) {
                    sizeX = ((TiffRational) value).doubleValue();
                } else
                    sizeX = new Double(value.toString());
            }
            if ("YCalibration".equals(key) && value != null) {
                if (value instanceof TiffRational) {
                    sizeY = ((TiffRational) value).doubleValue();
                } else
                    sizeY = new Double(value.toString());
            }
        }
        in.seek(saveLoc);

        if (validZ)
            zStart = tempZ;
    }

    // -- Utility methods --

    /** Converts a Julian date value into a human-readable string. */
    public static String decodeDate(int julian) {
        long a, b, c, d, e, alpha, z;
        short day, month, year;

        // code reused from the Metamorph data specification
        z = julian + 1;

        if (z < 2299161L)
            a = z;
        else {
            alpha = (long) ((z - 1867216.25) / 36524.25);
            a = z + 1 + alpha - alpha / 4;
        }

        b = (a > 1721423L ? a + 1524 : a + 1158);
        c = (long) ((b - 122.1) / 365.25);
        d = (long) (365.25 * c);
        e = (long) ((b - d) / 30.6001);

        day = (short) (b - d - (long) (30.6001 * e));
        month = (short) ((e < 13.5) ? e - 1 : e - 13);
        year = (short) ((month > 2.5) ? (c - 4716) : c - 4715);

        return intFormat(day, 2) + "/" + intFormat(month, 2) + "/" + year;
    }

    /** Converts a time value in milliseconds into a human-readable string. */
    public static String decodeTime(int millis) {
        DateTime tm = new DateTime(millis, DateTimeZone.UTC);
        String hours = intFormat(tm.getHourOfDay(), 2);
        String minutes = intFormat(tm.getMinuteOfHour(), 2);
        String seconds = intFormat(tm.getSecondOfMinute(), 2);
        String ms = intFormat(tm.getMillisOfSecond(), 3);

        return hours + ":" + minutes + ":" + seconds + ":" + ms;
    }

    /** Formats an integer value with leading 0s if needed. */
    public static String intFormat(int myint, int digits) {
        return String.format("%0" + digits + "d", myint);
    }

    /**
     * Formats an integer with leading 0 using maximum sequence number.
     *
     * @param myint integer to format
     * @param maxint max of "myint"
     * @return String
     */
    public static String intFormatMax(int myint, int maxint) {
        return intFormat(myint, String.valueOf(maxint).length());
    }

    /**
     * Locates the first valid file in the STK arrays.
     * @return Path to the first valid file.
     */
    private String locateFirstValidFile() {
        for (int q = 0; q < stks.length; q++) {
            for (int f = 0; f < stks.length; f++) {
                if (stks[q][f] != null) {
                    return stks[q][f];
                }
            }
        }
        return null;
    }

    private TiffRational readRational(RandomAccessInputStream s) throws IOException {
        return readRational(s, s.getFilePointer());
    }

    private TiffRational readRational(RandomAccessInputStream s, long offset) throws IOException {
        if (offset >= s.length() - 8) {
            return null;
        }
        s.seek(offset);
        int num = s.readInt();
        int denom = s.readInt();
        return new TiffRational(num, denom);
    }

    private void setCanLookForND(boolean v) {
        FormatTools.assertId(currentId, false, 1);
        canLookForND = v;
    }

    private void readPlaneData() throws IOException {
        in.skipBytes(4);
        int keyLength = in.read();
        String key = in.readString(keyLength);
        in.skipBytes(4);

        int type = in.read();
        int index = 0;

        switch (type) {
        case 1:
            while (getGlobalMeta("Channel #" + index + " " + key) != null) {
                index++;
            }
            addGlobalMeta("Channel #" + index + " " + key, readRational(in).doubleValue());
            break;
        case 2:
            int valueLength = in.read();
            String value = in.readString(valueLength);
            if (valueLength == 0) {
                in.skipBytes(4);
                valueLength = in.read();
                value = in.readString(valueLength);
            }
            while (getGlobalMeta("Channel #" + index + " " + key) != null) {
                index++;
            }
            addGlobalMeta("Channel #" + index + " " + key, value);

            if (key.equals("_IllumSetting_")) {
                if (waveNames == null)
                    waveNames = new ArrayList<String>();
                waveNames.add(value);
            }
            break;
        }
    }

    private String getKey(int id) {
        switch (id) {
        case 0:
            return "AutoScale";
        case 1:
            return "MinScale";
        case 2:
            return "MaxScale";
        case 3:
            return "Spatial Calibration";
        case 4:
            return "XCalibration";
        case 5:
            return "YCalibration";
        case 6:
            return "CalibrationUnits";
        case 7:
            return "Name";
        case 8:
            return "ThreshState";
        case 9:
            return "ThreshStateRed";
        // there is no 10
        case 11:
            return "ThreshStateGreen";
        case 12:
            return "ThreshStateBlue";
        case 13:
            return "ThreshStateLo";
        case 14:
            return "ThreshStateHi";
        case 15:
            return "Zoom";
        case 16:
            return "DateTime";
        case 17:
            return "LastSavedTime";
        case 18:
            return "currentBuffer";
        case 19:
            return "grayFit";
        case 20:
            return "grayPointCount";
        case 21:
            return "grayX";
        case 22:
            return "grayY";
        case 23:
            return "grayMin";
        case 24:
            return "grayMax";
        case 25:
            return "grayUnitName";
        case 26:
            return "StandardLUT";
        case 27:
            return "Wavelength";
        case 28:
            return "StagePosition";
        case 29:
            return "CameraChipOffset";
        case 30:
            return "OverlayMask";
        case 31:
            return "OverlayCompress";
        case 32:
            return "Overlay";
        case 33:
            return "SpecialOverlayMask";
        case 34:
            return "SpecialOverlayCompress";
        case 35:
            return "SpecialOverlay";
        case 36:
            return "ImageProperty";
        case 38:
            return "AutoScaleLoInfo";
        case 39:
            return "AutoScaleHiInfo";
        case 40:
            return "AbsoluteZ";
        case 41:
            return "AbsoluteZValid";
        case 42:
            return "Gamma";
        case 43:
            return "GammaRed";
        case 44:
            return "GammaGreen";
        case 45:
            return "GammaBlue";
        case 46:
            return "CameraBin";
        case 47:
            return "NewLUT";
        case 48:
            return "ImagePropertyEx";
        case 49:
            return "PlaneProperty";
        case 50:
            return "UserLutTable";
        case 51:
            return "RedAutoScaleInfo";
        case 52:
            return "RedAutoScaleLoInfo";
        case 53:
            return "RedAutoScaleHiInfo";
        case 54:
            return "RedMinScaleInfo";
        case 55:
            return "RedMaxScaleInfo";
        case 56:
            return "GreenAutoScaleInfo";
        case 57:
            return "GreenAutoScaleLoInfo";
        case 58:
            return "GreenAutoScaleHiInfo";
        case 59:
            return "GreenMinScaleInfo";
        case 60:
            return "GreenMaxScaleInfo";
        case 61:
            return "BlueAutoScaleInfo";
        case 62:
            return "BlueAutoScaleLoInfo";
        case 63:
            return "BlueAutoScaleHiInfo";
        case 64:
            return "BlueMinScaleInfo";
        case 65:
            return "BlueMaxScaleInfo";
        case 66:
            return "OverlayPlaneColor";
        }
        return null;
    }

    private Map.Entry<Integer, Integer> getWellCoords(String label) {
        Matcher matcher = WELL_COORDS.matcher(label);
        if (!matcher.find())
            return null;
        int nGroups = matcher.groupCount();
        if (nGroups != 2)
            return null;
        return new AbstractMap.SimpleEntry((int) (matcher.group(1).toUpperCase().charAt(0) - 'A'),
                Integer.parseInt(matcher.group(2)) - 1);
    }

    /**
     * Read a null-terminated string. Read at most n characters if the null
     * character is not found.
     */
    private static String readCString(RandomAccessInputStream s, int n) throws IOException {
        int avail = s.available();
        if (n > avail)
            n = avail;
        byte[] b = new byte[n];
        s.readFully(b);
        int i;
        for (i = 0; i < b.length && b[i] != 0; i++) {
        }
        return new String(b, 0, i, Constants.ENCODING);
    }

}