<commit_before>//
// ZeissLSMReader.java
//

/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package loci.formats.in;

import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;

import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceFactory;
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.meta.MetadataStore;
import loci.formats.services.MDBService;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.PhotoInterp;
import loci.formats.tiff.TiffCompression;
import loci.formats.tiff.TiffConstants;
import loci.formats.tiff.TiffParser;

/**
 * ZeissLSMReader is the file format reader for Zeiss LSM files.
 *
 * <dl><dt><b>Source code:</b></dt>
 * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/components/bio-formats/src/loci/formats/in/ZeissLSMReader.java">Trac</a>,
 * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/components/bio-formats/src/loci/formats/in/ZeissLSMReader.java">SVN</a></dd></dl>
 *
 * @author <NAME> egkjellman at wisc.edu
 * @author <NAME> <EMAIL>issa at glencoesoftware.com
 * @author <NAME> ctrueden at wisc.edu
 */
public class ZeissLSMReader extends FormatReader {

  // -- Constants --

  public static final String[] MDB_SUFFIX = {"mdb"};

  /** Tag identifying a Zeiss LSM file. */
  private static final int ZEISS_ID = 34412;

  /** Data types. */
  private static final int TYPE_SUBBLOCK = 0;
  private static final int TYPE_ASCII = 2;
  private static final int TYPE_LONG = 4;
  private static final int TYPE_RATIONAL = 5;

  /** Subblock types. */
  private static final int SUBBLOCK_RECORDING = 0x10000000;
  private static final int SUBBLOCK_LASER = 0x50000000;
  private static final int SUBBLOCK_TRACK = 0x40000000;
  private static final int SUBBLOCK_DETECTION_CHANNEL = 0x70000000;
  private static final int SUBBLOCK_ILLUMINATION_CHANNEL = 0x90000000;
  private static final int SUBBLOCK_BEAM_SPLITTER = 0xb0000000;
  private static final int SUBBLOCK_DATA_CHANNEL = 0xd0000000;
  private static final int SUBBLOCK_TIMER = 0x12000000;
  private static final int SUBBLOCK_MARKER = 0x14000000;
  private static final int SUBBLOCK_END = (int) 0xffffffff;

  /** Data types. */
  private static final int RECORDING_NAME = 0x10000001;
  private static final int RECORDING_DESCRIPTION = 0x10000002;
  private static final int RECORDING_OBJECTIVE = 0x10000004;
  private static final int RECORDING_ZOOM = 0x10000016;
  private static final int RECORDING_SAMPLE_0TIME = 0x10000036;
  private static final int RECORDING_CAMERA_BINNING = 0x10000052;

  private static final int TRACK_ACQUIRE = 0x40000006;
  private static final int TRACK_TIME_BETWEEN_STACKS = 0x4000000b;

  private static final int LASER_NAME = 0x50000001;
  private static final int LASER_ACQUIRE = 0x50000002;
  private static final int LASER_POWER = 0x50000003;

  private static final int CHANNEL_DETECTOR_GAIN = 0x70000003;
  private static final int CHANNEL_PINHOLE_DIAMETER = 0x70000009;
  private static final int CHANNEL_AMPLIFIER_GAIN = 0x70000005;
  private static final int CHANNEL_FILTER_SET = 0x7000000f;
  private static final int CHANNEL_FILTER = 0x70000010;
  private static final int CHANNEL_ACQUIRE = 0x7000000b;
  private static final int CHANNEL_NAME = 0x70000014;

  private static final int ILLUM_CHANNEL_ATTENUATION = 0x90000002;
  private static final int ILLUM_CHANNEL_WAVELENGTH = 0x90000003;
  private static final int ILLUM_CHANNEL_ACQUIRE = 0x90000004;

  private static final int START_TIME = 0x10000036;
  private static final int DATA_CHANNEL_NAME = 0xd0000001;
  private static final int DATA_CHANNEL_ACQUIRE = 0xd0000017;

  private static final int BEAM_SPLITTER_FILTER = 0xb0000002;
  private static final int BEAM_SPLITTER_FILTER_SET = 0xb0000003;

  /** Drawing element types. */
  private static final int TEXT = 13;
  private static final int LINE = 14;
  private static final int SCALE_BAR = 15;
  private static final int OPEN_ARROW = 16;
  private static final int CLOSED_ARROW = 17;
  private static final int RECTANGLE = 18;
  private static final int ELLIPSE = 19;
  private static final int CLOSED_POLYLINE = 20;
  private static final int OPEN_POLYLINE = 21;
  private static final int CLOSED_BEZIER = 22;
  private static final int OPEN_BEZIER = 23;
  private static final int CIRCLE = 24;
  private static final int PALETTE = 25;
  private static final int POLYLINE_ARROW = 26;
  private static final int BEZIER_WITH_ARROW = 27;
  private static final int ANGLE = 28;
  private static final int CIRCLE_3POINT = 29;

  // -- Static fields --

  private static Hashtable<Integer, String> metadataKeys = createKeys();

  // -- Fields --

  private double pixelSizeX, pixelSizeY, pixelSizeZ;
  private byte[][] lut = null;
  private Vector<Double> timestamps;
  private int validChannels;

  private String[] lsmFilenames;
  private Vector<IFDList> ifdsList;
  private TiffParser tiffParser;

  private int nextLaser = 0, nextDetector = 0;
  private int nextFilter = 0, nextDichroicChannel = 0, nextDichroic = 0;
  private int nextDataChannel = 0, nextIllumChannel = 0, nextDetectChannel = 0;
  private boolean splitPlanes = false;
  private double zoom;
  private Vector<String> imageNames;
  private String binning;
  private Vector<Double> xCoordinates, yCoordinates;

  private int totalROIs = 0;

  private int prevPlane = -1;
  private byte[] prevBuf = null;

  // -- Constructor --

  /** Constructs a new Zeiss LSM reader. */
  public ZeissLSMReader() {
    super("Zeiss Laser-Scanning Microscopy", new String[] {"lsm", "mdb"});
    domains = new String[] {FormatTools.LM_DOMAIN};
    hasCompanionFiles = true;
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#isSingleFile(String) */
  public boolean isSingleFile(String id) throws FormatException, IOException {
    if (checkSuffix(id, MDB_SUFFIX)) return false;
    return isGroupFiles() ? getMDBFile(id) != null : true;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      pixelSizeX = pixelSizeY = pixelSizeZ = 0;
      lut = null;
      timestamps = null;
      validChannels = 0;
      lsmFilenames = null;
      ifdsList = null;
      tiffParser = null;
      nextLaser = nextDetector = 0;
      nextFilter = nextDichroicChannel = nextDichroic = 0;
      nextDataChannel = nextIllumChannel = nextDetectChannel = 0;
      splitPlanes = false;
      zoom = 0;
      imageNames = null;
      binning = null;
      totalROIs = 0;
      prevPlane = -1;
      prevBuf = null;
      xCoordinates = null;
      yCoordinates = null;
    }
  }

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 4;
    if (!FormatTools.validStream(stream, blockLen, false)) return false;
    TiffParser parser = new TiffParser(stream);
    return parser.isValidHeader() || stream.readShort() == 0x5374;
  }

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

  /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
  public String[] getSeriesUsedFiles(boolean noPixels) {
    FormatTools.assertId(currentId, true, 1);
    if (noPixels) {
      if (checkSuffix(currentId, MDB_SUFFIX)) return new String[] {currentId};
      return null;
    }
    if (lsmFilenames == null) return new String[] {currentId};
    if (lsmFilenames.length == 1 && currentId.equals(lsmFilenames[0])) {
      return lsmFilenames;
    }
    return new String[] {currentId, lsmFilenames[getSeries()]};
  }

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  public byte[][] get8BitLookupTable() throws FormatException, IOException {
    FormatTools.assertId(currentId, true, 1);
    if (lut == null || lut[getSeries()]  == null ||
      getPixelType() != FormatTools.UINT8)
    {
      return null;
    }
    byte[][] b = new byte[3][256];
    for (int i=2; i>=3-validChannels; i--) {
      for (int j=0; j<256; j++) {
        b[i][j] = (byte) j;
      }
    }
    return b;
  }

  /* @see loci.formats.IFormatReader#get16BitLookupTable() */
  public short[][] get16BitLookupTable() throws FormatException, IOException {
    FormatTools.assertId(currentId, true, 1);
    if (lut == null || lut[getSeries()] == null ||
      getPixelType() != FormatTools.UINT16)
    {
      return null;
    }
    short[][] s = new short[3][65536];
    for (int i=2; i>=3-validChannels; i--) {
      for (int j=0; j<s[i].length; j++) {
        s[i][j] = (short) j;
      }
    }
    return s;
  }

  /* @see loci.formats.IFormatReader#setSeries(int) */
  public void setSeries(int series) {
    if (series != getSeries()) {
      prevBuf = null;
    }
    super.setSeries(series);
  }

  /**
   * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
   */
  public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);

    in = new RandomAccessInputStream(lsmFilenames[getSeries()]);
    in.order(!isLittleEndian());

    tiffParser = new TiffParser(in);

    IFDList ifds = ifdsList.get(getSeries());

    if (splitPlanes && getSizeC() > 1 && ifds.size() == getSizeZ() * getSizeT())
    {
      int bpp = FormatTools.getBytesPerPixel(getPixelType());
      int plane = no / getSizeC();
      int c = no % getSizeC();

      if (prevPlane != plane || prevBuf == null) {
        prevBuf = new byte[buf.length * getSizeC()];
        tiffParser.getSamples(ifds.get(plane), prevBuf, x, y, w, h);
        prevPlane = plane;
      }
      ImageTools.splitChannels(prevBuf, buf, c, getSizeC(), bpp, false, false);
    }
    else tiffParser.getSamples(ifds.get(no), buf, x, y, w, h);
    in.close();
    return buf;
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);

    if (!checkSuffix(id, MDB_SUFFIX) && isGroupFiles()) {
      String mdb = getMDBFile(id);
      if (mdb != null) {
        setId(mdb);
        return;
      }
      lsmFilenames = new String[] {id};
    }
    else if (checkSuffix(id, MDB_SUFFIX)) {
      lsmFilenames = parseMDB(id);
    }
    else lsmFilenames = new String[] {id};

    if (lsmFilenames.length == 0) {
      throw new FormatException("LSM files were not found.");
    }

    timestamps = new Vector<Double>();
    imageNames = new Vector<String>();
    xCoordinates = new Vector<Double>();
    yCoordinates = new Vector<Double>();

    core = new CoreMetadata[lsmFilenames.length];
    ifdsList = new Vector<IFDList>();
    ifdsList.setSize(core.length);
    for (int i=0; i<core.length; i++) {
      core[i] = new CoreMetadata();
      RandomAccessInputStream s = new RandomAccessInputStream(lsmFilenames[i]);
      core[i].littleEndian = s.read() == TiffConstants.LITTLE;
      s.order(isLittleEndian());
      s.seek(0);
      // calling tp.getNonThumbnailIFDs() would give us the same IFDList, but
      // assuming that every other IFD is a thumbnail reduces the parsing time
      TiffParser tp = new TiffParser(s);
      long[] ifdOffsets = tp.getIFDOffsets();
      IFDList ifds = new IFDList();
      for (int offset=0; offset<ifdOffsets.length;) {
        ifds.add(tp.getIFD(ifdOffsets[offset]));
        if (ifds.get(0).containsKey(ZEISS_ID)) offset += 2;
        else offset++;
      }
      ifdsList.set(i, ifds);
      s.close();
    }

    MetadataStore store = makeFilterMetadata();

    for (int series=0; series<ifdsList.size(); series++) {
      IFDList ifds = ifdsList.get(series);
      for (IFD ifd : ifds) {
        // check that predictor is set to 1 if anything other
        // than LZW compression is used
        if (ifd.getCompression() != TiffCompression.LZW) {
          ifd.putIFDValue(IFD.PREDICTOR, 1);
        }
      }

      // fix the offsets for > 4 GB files
      for (int i=1; i<ifds.size(); i++) {
        long[] stripOffsets = ifds.get(i).getStripOffsets();
        long[] previousStripOffsets = ifds.get(i - 1).getStripOffsets();

        if (stripOffsets == null || previousStripOffsets == null) {
          throw new FormatException(
            "Strip offsets are missing; this is an invalid file.");
        }

        boolean neededAdjustment = false;
        for (int j=0; j<stripOffsets.length; j++) {
          if (j >= previousStripOffsets.length) break;
          if (stripOffsets[j] < previousStripOffsets[j]) {
            stripOffsets[j] = (previousStripOffsets[j] & ~0xffffffffL) |
              (stripOffsets[j] & 0xffffffffL);
            if (stripOffsets[j] < previousStripOffsets[j]) {
              stripOffsets[j] += 0x100000000L;
            }
            neededAdjustment = true;
          }
          if (neededAdjustment) {
            ifds.get(i).putIFDValue(IFD.STRIP_OFFSETS, stripOffsets);
          }
        }
      }

      initMetadata(series);
    }
    MetadataTools.populatePixels(store, this, true);
    for (int series=0; series<ifdsList.size(); series++) {
      store.setImageName(imageNames.get(series), series);
      store.setPixelsBinDataBigEndian(!isLittleEndian(), series, 0);
    }
    setSeries(0);
  }

  // -- Helper methods --

  private String getMDBFile(String id) throws FormatException, IOException {
    Location parentFile = new Location(id).getAbsoluteFile().getParentFile();
    String[] fileList = parentFile.list();
    for (int i=0; i<fileList.length; i++) {
      if (fileList[i].startsWith(".")) continue;
      if (checkSuffix(fileList[i], MDB_SUFFIX)) {
        Location file =
          new Location(parentFile, fileList[i]).getAbsoluteFile();
        if (file.isDirectory()) continue;
        // make sure that the .mdb references this .lsm
        String[] lsms = parseMDB(file.getAbsolutePath());
        for (String lsm : lsms) {
          if (id.endsWith(lsm) || lsm.endsWith(id)) {
            return file.getAbsolutePath();
          }
        }
      }
    }
    return null;
  }

  protected void initMetadata(int series) throws FormatException, IOException {
    setSeries(series);
    IFDList ifds = ifdsList.get(series);
    IFD ifd = ifds.get(0);

    in = new RandomAccessInputStream(lsmFilenames[series]);
    in.order(isLittleEndian());

    tiffParser = new TiffParser(in);

    PhotoInterp photo = ifd.getPhotometricInterpretation();
    int samples = ifd.getSamplesPerPixel();

    core[series].sizeX = (int) ifd.getImageWidth();
    core[series].sizeY = (int) ifd.getImageLength();
    core[series].rgb = samples > 1 || photo == PhotoInterp.RGB;
    core[series].interleaved = false;
    core[series].sizeC = isRGB() ? samples : 1;
    core[series].pixelType = ifd.getPixelType();
    core[series].imageCount = ifds.size();
    core[series].sizeZ = getImageCount();
    core[series].sizeT = 1;

    LOGGER.info("Reading LSM metadata for series #{}", series);

    MetadataStore store = makeFilterMetadata();

    String imageName = lsmFilenames[series];
    if (imageName.indexOf(".") != -1) {
      imageName = imageName.substring(0, imageName.lastIndexOf("."));
    }
    if (imageName.indexOf(File.separator) != -1) {
      imageName =
        imageName.substring(imageName.lastIndexOf(File.separator) + 1);
    }

    // link Instrument and Image
    store.setImageID(MetadataTools.createLSID("Image", series), series);
    String instrumentID = MetadataTools.createLSID("Instrument", series);
    store.setInstrumentID(instrumentID, series);
    store.setImageInstrumentRef(instrumentID, series);

    // get TIF_CZ_LSMINFO structure
    short[] s = ifd.getIFDShortArray(ZEISS_ID);
    if (s == null) {
      LOGGER.warn("Invalid Zeiss LSM file. Tag {} not found.", ZEISS_ID);
      TiffReader reader = new TiffReader();
      reader.setId(lsmFilenames[series]);
      core[series] = reader.getCoreMetadata()[0];
      reader.close();
      imageNames.add(imageName);
      return;
    }
    byte[] cz = new byte[s.length];
    for (int i=0; i<s.length; i++) {
      cz[i] = (byte) s[i];
    }

    RandomAccessInputStream ras = new RandomAccessInputStream(cz);
    ras.order(isLittleEndian());

    ras.seek(16);

    core[series].sizeZ = ras.readInt();
    ras.skipBytes(4);
    core[series].sizeT = ras.readInt();

    int dataType = ras.readInt();
    switch (dataType) {
      case 2:
        addSeriesMeta("DataType", "12 bit unsigned integer");
        break;
      case 5:
        addSeriesMeta("DataType", "32 bit float");
        break;
      case 0:
        addSeriesMeta("DataType", "varying data types");
        break;
      default:
        addSeriesMeta("DataType", "8 bit unsigned integer");
    }

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      ras.seek(0);
      addSeriesMeta("MagicNumber ", ras.readInt());
      addSeriesMeta("StructureSize", ras.readInt());
      addSeriesMeta("DimensionX", ras.readInt());
      addSeriesMeta("DimensionY", ras.readInt());

      ras.seek(32);
      addSeriesMeta("ThumbnailX", ras.readInt());
      addSeriesMeta("ThumbnailY", ras.readInt());

      // pixel sizes are stored in meters, we need them in microns
      pixelSizeX = ras.readDouble() * 1000000;
      pixelSizeY = ras.readDouble() * 1000000;
      pixelSizeZ = ras.readDouble() * 1000000;

      addSeriesMeta("VoxelSizeX", new Double(pixelSizeX));
      addSeriesMeta("VoxelSizeY", new Double(pixelSizeY));
      addSeriesMeta("VoxelSizeZ", new Double(pixelSizeZ));

      addSeriesMeta("OriginX", ras.readDouble());
      addSeriesMeta("OriginY", ras.readDouble());
      addSeriesMeta("OriginZ", ras.readDouble());
    }
    else ras.seek(56);

    int scanType = ras.readShort();
    switch (scanType) {
      case 0:
        addSeriesMeta("ScanType", "x-y-z scan");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 1:
        addSeriesMeta("ScanType", "z scan (x-z plane)");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 2:
        addSeriesMeta("ScanType", "line scan");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 3:
        addSeriesMeta("ScanType", "time series x-y");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 4:
        addSeriesMeta("ScanType", "time series x-z");
        core[series].dimensionOrder = "XYZTC";
        break;
      case 5:
        addSeriesMeta("ScanType", "time series 'Mean of ROIs'");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 6:
        addSeriesMeta("ScanType", "time series x-y-z");
        core[series].dimensionOrder = "XYZTC";
        break;
      case 7:
        addSeriesMeta("ScanType", "spline scan");
        core[series].dimensionOrder = "XYCTZ";
        break;
      case 8:
        addSeriesMeta("ScanType", "spline scan x-z");
        core[series].dimensionOrder = "XYCZT";
        break;
      case 9:
        addSeriesMeta("ScanType", "time series spline plane x-z");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 10:
        addSeriesMeta("ScanType", "point mode");
        core[series].dimensionOrder = "XYZCT";
        break;
      default:
        addSeriesMeta("ScanType", "x-y-z scan");
        core[series].dimensionOrder = "XYZCT";
    }

    core[series].indexed =
      lut != null && lut[series] != null && getSizeC() == 1;
    if (isIndexed()) {
      core[series].sizeC = 1;
      core[series].rgb = false;
    }
    if (getSizeC() == 0) core[series].sizeC = 1;

    if (isRGB()) {
      // shuffle C to front of order string
      core[series].dimensionOrder = getDimensionOrder().replaceAll("C", "");
      core[series].dimensionOrder = getDimensionOrder().replaceAll("XY", "XYC");
    }

    if (getEffectiveSizeC() == 0) {
      core[series].imageCount = getSizeZ() * getSizeT();
    }
    else {
      core[series].imageCount = getSizeZ() * getSizeT() * getEffectiveSizeC();
    }

    if (getImageCount() != ifds.size()) {
      int diff = getImageCount() - ifds.size();
      core[series].imageCount = ifds.size();
      if (diff % getSizeZ() == 0) {
        core[series].sizeT -= (diff / getSizeZ());
      }
      else if (diff % getSizeT() == 0) {
        core[series].sizeZ -= (diff / getSizeT());
      }
      else if (getSizeZ() > 1) {
        core[series].sizeZ = ifds.size();
        core[series].sizeT = 1;
      }
      else if (getSizeT() > 1) {
        core[series].sizeT = ifds.size();
        core[series].sizeZ = 1;
      }
    }

    if (getSizeZ() == 0) core[series].sizeZ = getImageCount();
    if (getSizeT() == 0) core[series].sizeT = getImageCount() / getSizeZ();

    MetadataTools.setDefaultCreationDate(store, getCurrentFile(), series);
    if (getSizeC() > 1) {
      if (!splitPlanes) splitPlanes = isRGB();
      core[series].rgb = false;
      if (splitPlanes) core[series].imageCount *= getSizeC();
    }

    for (int c=0; c<getEffectiveSizeC(); c++) {
      String lsid = MetadataTools.createLSID("Channel", series, c);
      store.setChannelID(lsid, series, c);
    }

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      int spectralScan = ras.readShort();
      if (spectralScan != 1) {
        addSeriesMeta("SpectralScan", "no spectral scan");
      }
      else addSeriesMeta("SpectralScan", "acquired with spectral scan");

      int type = ras.readInt();
      switch (type) {
        case 1:
          addSeriesMeta("DataType2", "calculated data");
          break;
        case 2:
          addSeriesMeta("DataType2", "animation");
          break;
        default:
          addSeriesMeta("DataType2", "original scan data");
      }

      long[] overlayOffsets = new long[9];
      String[] overlayKeys = new String[] {"VectorOverlay", "InputLut",
        "OutputLut", "ROI", "BleachROI", "MeanOfRoisOverlay",
        "TopoIsolineOverlay", "TopoProfileOverlay", "LinescanOverlay"};

      overlayOffsets[0] = ras.readInt();
      overlayOffsets[1] = ras.readInt();
      overlayOffsets[2] = ras.readInt();

      long channelColorsOffset = ras.readInt();

      addSeriesMeta("TimeInterval", ras.readDouble());
      ras.skipBytes(4);
      long scanInformationOffset = ras.readInt();
      ras.skipBytes(4);
      long timeStampOffset = ras.readInt();
      long eventListOffset = ras.readInt();
      overlayOffsets[3] = ras.readInt();
      overlayOffsets[4] = ras.readInt();
      ras.skipBytes(4);

      addSeriesMeta("DisplayAspectX", ras.readDouble());
      addSeriesMeta("DisplayAspectY", ras.readDouble());
      addSeriesMeta("DisplayAspectZ", ras.readDouble());
      addSeriesMeta("DisplayAspectTime", ras.readDouble());

      overlayOffsets[5] = ras.readInt();
      overlayOffsets[6] = ras.readInt();
      overlayOffsets[7] = ras.readInt();
      overlayOffsets[8] = ras.readInt();

      if (getMetadataOptions().getMetadataLevel() != MetadataLevel.NO_OVERLAYS)
      {
        for (int i=0; i<overlayOffsets.length; i++) {
          parseOverlays(series, overlayOffsets[i], overlayKeys[i], store);
        }
      }

      totalROIs = 0;

      addSeriesMeta("ToolbarFlags", ras.readInt());

      int wavelengthOffset = ras.readInt();
      ras.skipBytes(56);
      int dimensionP = ras.readInt();
      int dimensionM = ras.readInt();

      // NB: the Zeiss LSM 5.5 specification indicates that there should be
      //     15 32-bit integers here; however, there are actually 16 32-bit
      //     integers before the tile position offset.
      ras.skipBytes(64);

      int tilePositionOffset = ras.readInt();

      // read referenced structures

      addSeriesMeta("DimensionZ", getSizeZ());
      addSeriesMeta("DimensionChannels", getSizeC());
      addSeriesMeta("DimensionM", dimensionM);
      addSeriesMeta("DimensionP", dimensionP);

      if (tilePositionOffset != 0) {
        in.seek(tilePositionOffset);
        int nTiles = in.readInt();
        for (int i=0; i<nTiles; i++) {
          xCoordinates.add(in.readDouble());
          yCoordinates.add(in.readDouble());
        }
      }

      if (channelColorsOffset != 0) {
        in.seek(channelColorsOffset + 16);
        int namesOffset = in.readInt();

        // read the name of each channel

        if (namesOffset > 0) {
          in.skipBytes(namesOffset - 16);

          for (int i=0; i<getSizeC(); i++) {
            if (in.getFilePointer() >= in.length() - 1) break;
            // we want to read until we find a null char
            String name = in.readCString();
            if (name.length() <= 128) {
              addSeriesMeta("ChannelName" + i, name);
            }
          }
        }
      }

      if (timeStampOffset != 0) {
        in.seek(timeStampOffset + 8);
        for (int i=0; i<getSizeT(); i++) {
          double stamp = in.readDouble();
          addSeriesMeta("TimeStamp" + i, stamp);
          timestamps.add(new Double(stamp));
        }
      }

      if (eventListOffset != 0) {
        in.seek(eventListOffset + 4);
        int numEvents = in.readInt();
        in.seek(in.getFilePointer() - 4);
        in.order(!in.isLittleEndian());
        int tmpEvents = in.readInt();
        if (numEvents < 0) numEvents = tmpEvents;
        else numEvents = (int) Math.min(numEvents, tmpEvents);
        in.order(!in.isLittleEndian());

        if (numEvents > 65535) numEvents = 0;

        for (int i=0; i<numEvents; i++) {
          if (in.getFilePointer() + 16 <= in.length()) {
            int size = in.readInt();
            double eventTime = in.readDouble();
            int eventType = in.readInt();
            addSeriesMeta("Event" + i + " Time", eventTime);
            addSeriesMeta("Event" + i + " Type", eventType);
            long fp = in.getFilePointer();
            int len = size - 16;
            if (len > 65536) len = 65536;
            if (len < 0) len = 0;
            addSeriesMeta("Event" + i + " Description", in.readString(len));
            in.seek(fp + size - 16);
            if (in.getFilePointer() < 0) break;
          }
        }
      }

      if (scanInformationOffset != 0) {
        in.seek(scanInformationOffset);

        nextLaser = nextDetector = 0;
        nextFilter = nextDichroicChannel = nextDichroic = 0;
        nextDataChannel = nextDetectChannel = nextIllumChannel = 0;

        Vector<SubBlock> blocks = new Vector<SubBlock>();

        while (in.getFilePointer() < in.length() - 12) {
          if (in.getFilePointer() < 0) break;
          int entry = in.readInt();
          int blockType = in.readInt();
          int dataSize = in.readInt();

          if (blockType == TYPE_SUBBLOCK) {
            SubBlock block = null;
            switch (entry) {
              case SUBBLOCK_RECORDING:
                block = new Recording();
                break;
              case SUBBLOCK_LASER:
                block = new Laser();
                break;
              case SUBBLOCK_TRACK:
                block = new Track();
                break;
              case SUBBLOCK_DETECTION_CHANNEL:
                block = new DetectionChannel();
                break;
              case SUBBLOCK_ILLUMINATION_CHANNEL:
                block = new IlluminationChannel();
                break;
              case SUBBLOCK_BEAM_SPLITTER:
                block = new BeamSplitter();
                break;
              case SUBBLOCK_DATA_CHANNEL:
                block = new DataChannel();
                break;
              case SUBBLOCK_TIMER:
                block = new Timer();
                break;
              case SUBBLOCK_MARKER:
                block = new Marker();
                break;
            }
            if (block != null) {
              blocks.add(block);
            }
          }
          else if (dataSize + in.getFilePointer() <= in.length()) {
            in.skipBytes(dataSize);
          }
          else break;
        }

        Vector<SubBlock> nonAcquiredBlocks = new Vector<SubBlock>();

        SubBlock[] metadataBlocks = blocks.toArray(new SubBlock[0]);
        for (SubBlock block : metadataBlocks) {
          block.addToHashtable();
          if (!block.acquire) {
            nonAcquiredBlocks.add(block);
            blocks.remove(block);
          }
        }

        for (int i=0; i<blocks.size(); i++) {
          SubBlock block = blocks.get(i);
          // every valid IlluminationChannel must be immediately followed by
          // a valid DataChannel or IlluminationChannel
          if ((block instanceof IlluminationChannel) && i < blocks.size() - 1) {
            SubBlock nextBlock = blocks.get(i + 1);
            if (!(nextBlock instanceof DataChannel) &&
              !(nextBlock instanceof IlluminationChannel))
            {
              ((IlluminationChannel) block).wavelength = null;
            }
          }
          // every valid DetectionChannel must be immediately preceded by
          // a valid Track or DetectionChannel
          else if ((block instanceof DetectionChannel) && i > 0) {
            SubBlock prevBlock = blocks.get(i - 1);
            if (!(prevBlock instanceof Track) &&
              !(prevBlock instanceof DetectionChannel))
            {
              block.acquire = false;
              nonAcquiredBlocks.add(block);
            }
          }
          if (block.acquire) populateMetadataStore(block, store, series);
        }

        for (SubBlock block : nonAcquiredBlocks) {
          populateMetadataStore(block, store, series);
        }
      }
    }

    imageNames.add(imageName);

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      Double pixX = new Double(pixelSizeX);
      Double pixY = new Double(pixelSizeY);
      Double pixZ = new Double(pixelSizeZ);

      store.setPixelsPhysicalSizeX(pixX, series);
      store.setPixelsPhysicalSizeY(pixY, series);
      store.setPixelsPhysicalSizeZ(pixZ, series);

      double firstStamp = 0;
      if (timestamps.size() > 0) {
        firstStamp = timestamps.get(0).doubleValue();
      }

      for (int i=0; i<getImageCount(); i++) {
        int[] zct = FormatTools.getZCTCoords(this, i);

        if (zct[2] < timestamps.size()) {
          double thisStamp = timestamps.get(zct[2]).doubleValue();
          store.setPlaneDeltaT(thisStamp - firstStamp, series, i);
          int index = zct[2] + 1;
          double nextStamp = index < timestamps.size() ?
            timestamps.get(index).doubleValue() : thisStamp;
          if (i == getSizeT() - 1 && zct[2] > 0) {
            thisStamp = timestamps.get(zct[2] - 1).doubleValue();
          }
          store.setPlaneExposureTime(nextStamp - thisStamp, series, i);
        }
        int stage = i / (getImageCount() / xCoordinates.size());
        store.setPlanePositionX(xCoordinates.get(stage), series, i);
        store.setPlanePositionY(yCoordinates.get(stage), series, i);
      }
    }
    ras.close();
    in.close();
  }<commit_msg>Prevent ArithmeticException if stage coordinates are not present.<commit_after>//
// ZeissLSMReader.java
//

/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package loci.formats.in;

import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;

import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveInteger;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceFactory;
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.meta.MetadataStore;
import loci.formats.services.MDBService;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.PhotoInterp;
import loci.formats.tiff.TiffCompression;
import loci.formats.tiff.TiffConstants;
import loci.formats.tiff.TiffParser;

/**
 * ZeissLSMReader is the file format reader for Zeiss LSM files.
 *
 * <dl><dt><b>Source code:</b></dt>
 * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/components/bio-formats/src/loci/formats/in/ZeissLSMReader.java">Trac</a>,
 * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/components/bio-formats/src/loci/formats/in/ZeissLSMReader.java">SVN</a></dd></dl>
 *
 * @author <NAME> egkjellman at wisc.edu
 * @author <NAME> melissa at glencoesoftware.com
 * @author <NAME> ctrueden at wisc.edu
 */
public class ZeissLSMReader extends FormatReader {

  // -- Constants --

  public static final String[] MDB_SUFFIX = {"mdb"};

  /** Tag identifying a Zeiss LSM file. */
  private static final int ZEISS_ID = 34412;

  /** Data types. */
  private static final int TYPE_SUBBLOCK = 0;
  private static final int TYPE_ASCII = 2;
  private static final int TYPE_LONG = 4;
  private static final int TYPE_RATIONAL = 5;

  /** Subblock types. */
  private static final int SUBBLOCK_RECORDING = 0x10000000;
  private static final int SUBBLOCK_LASER = 0x50000000;
  private static final int SUBBLOCK_TRACK = 0x40000000;
  private static final int SUBBLOCK_DETECTION_CHANNEL = 0x70000000;
  private static final int SUBBLOCK_ILLUMINATION_CHANNEL = 0x90000000;
  private static final int SUBBLOCK_BEAM_SPLITTER = 0xb0000000;
  private static final int SUBBLOCK_DATA_CHANNEL = 0xd0000000;
  private static final int SUBBLOCK_TIMER = 0x12000000;
  private static final int SUBBLOCK_MARKER = 0x14000000;
  private static final int SUBBLOCK_END = (int) 0xffffffff;

  /** Data types. */
  private static final int RECORDING_NAME = 0x10000001;
  private static final int RECORDING_DESCRIPTION = 0x10000002;
  private static final int RECORDING_OBJECTIVE = 0x10000004;
  private static final int RECORDING_ZOOM = 0x10000016;
  private static final int RECORDING_SAMPLE_0TIME = 0x10000036;
  private static final int RECORDING_CAMERA_BINNING = 0x10000052;

  private static final int TRACK_ACQUIRE = 0x40000006;
  private static final int TRACK_TIME_BETWEEN_STACKS = 0x4000000b;

  private static final int LASER_NAME = 0x50000001;
  private static final int LASER_ACQUIRE = 0x50000002;
  private static final int LASER_POWER = 0x50000003;

  private static final int CHANNEL_DETECTOR_GAIN = 0x70000003;
  private static final int CHANNEL_PINHOLE_DIAMETER = 0x70000009;
  private static final int CHANNEL_AMPLIFIER_GAIN = 0x70000005;
  private static final int CHANNEL_FILTER_SET = 0x7000000f;
  private static final int CHANNEL_FILTER = 0x70000010;
  private static final int CHANNEL_ACQUIRE = 0x7000000b;
  private static final int CHANNEL_NAME = 0x70000014;

  private static final int ILLUM_CHANNEL_ATTENUATION = 0x90000002;
  private static final int ILLUM_CHANNEL_WAVELENGTH = 0x90000003;
  private static final int ILLUM_CHANNEL_ACQUIRE = 0x90000004;

  private static final int START_TIME = 0x10000036;
  private static final int DATA_CHANNEL_NAME = 0xd0000001;
  private static final int DATA_CHANNEL_ACQUIRE = 0xd0000017;

  private static final int BEAM_SPLITTER_FILTER = 0xb0000002;
  private static final int BEAM_SPLITTER_FILTER_SET = 0xb0000003;

  /** Drawing element types. */
  private static final int TEXT = 13;
  private static final int LINE = 14;
  private static final int SCALE_BAR = 15;
  private static final int OPEN_ARROW = 16;
  private static final int CLOSED_ARROW = 17;
  private static final int RECTANGLE = 18;
  private static final int ELLIPSE = 19;
  private static final int CLOSED_POLYLINE = 20;
  private static final int OPEN_POLYLINE = 21;
  private static final int CLOSED_BEZIER = 22;
  private static final int OPEN_BEZIER = 23;
  private static final int CIRCLE = 24;
  private static final int PALETTE = 25;
  private static final int POLYLINE_ARROW = 26;
  private static final int BEZIER_WITH_ARROW = 27;
  private static final int ANGLE = 28;
  private static final int CIRCLE_3POINT = 29;

  // -- Static fields --

  private static Hashtable<Integer, String> metadataKeys = createKeys();

  // -- Fields --

  private double pixelSizeX, pixelSizeY, pixelSizeZ;
  private byte[][] lut = null;
  private Vector<Double> timestamps;
  private int validChannels;

  private String[] lsmFilenames;
  private Vector<IFDList> ifdsList;
  private TiffParser tiffParser;

  private int nextLaser = 0, nextDetector = 0;
  private int nextFilter = 0, nextDichroicChannel = 0, nextDichroic = 0;
  private int nextDataChannel = 0, nextIllumChannel = 0, nextDetectChannel = 0;
  private boolean splitPlanes = false;
  private double zoom;
  private Vector<String> imageNames;
  private String binning;
  private Vector<Double> xCoordinates, yCoordinates;

  private int totalROIs = 0;

  private int prevPlane = -1;
  private byte[] prevBuf = null;

  // -- Constructor --

  /** Constructs a new Zeiss LSM reader. */
  public ZeissLSMReader() {
    super("Zeiss Laser-Scanning Microscopy", new String[] {"lsm", "mdb"});
    domains = new String[] {FormatTools.LM_DOMAIN};
    hasCompanionFiles = true;
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#isSingleFile(String) */
  public boolean isSingleFile(String id) throws FormatException, IOException {
    if (checkSuffix(id, MDB_SUFFIX)) return false;
    return isGroupFiles() ? getMDBFile(id) != null : true;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      pixelSizeX = pixelSizeY = pixelSizeZ = 0;
      lut = null;
      timestamps = null;
      validChannels = 0;
      lsmFilenames = null;
      ifdsList = null;
      tiffParser = null;
      nextLaser = nextDetector = 0;
      nextFilter = nextDichroicChannel = nextDichroic = 0;
      nextDataChannel = nextIllumChannel = nextDetectChannel = 0;
      splitPlanes = false;
      zoom = 0;
      imageNames = null;
      binning = null;
      totalROIs = 0;
      prevPlane = -1;
      prevBuf = null;
      xCoordinates = null;
      yCoordinates = null;
    }
  }

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 4;
    if (!FormatTools.validStream(stream, blockLen, false)) return false;
    TiffParser parser = new TiffParser(stream);
    return parser.isValidHeader() || stream.readShort() == 0x5374;
  }

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

  /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
  public String[] getSeriesUsedFiles(boolean noPixels) {
    FormatTools.assertId(currentId, true, 1);
    if (noPixels) {
      if (checkSuffix(currentId, MDB_SUFFIX)) return new String[] {currentId};
      return null;
    }
    if (lsmFilenames == null) return new String[] {currentId};
    if (lsmFilenames.length == 1 && currentId.equals(lsmFilenames[0])) {
      return lsmFilenames;
    }
    return new String[] {currentId, lsmFilenames[getSeries()]};
  }

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  public byte[][] get8BitLookupTable() throws FormatException, IOException {
    FormatTools.assertId(currentId, true, 1);
    if (lut == null || lut[getSeries()]  == null ||
      getPixelType() != FormatTools.UINT8)
    {
      return null;
    }
    byte[][] b = new byte[3][256];
    for (int i=2; i>=3-validChannels; i--) {
      for (int j=0; j<256; j++) {
        b[i][j] = (byte) j;
      }
    }
    return b;
  }

  /* @see loci.formats.IFormatReader#get16BitLookupTable() */
  public short[][] get16BitLookupTable() throws FormatException, IOException {
    FormatTools.assertId(currentId, true, 1);
    if (lut == null || lut[getSeries()] == null ||
      getPixelType() != FormatTools.UINT16)
    {
      return null;
    }
    short[][] s = new short[3][65536];
    for (int i=2; i>=3-validChannels; i--) {
      for (int j=0; j<s[i].length; j++) {
        s[i][j] = (short) j;
      }
    }
    return s;
  }

  /* @see loci.formats.IFormatReader#setSeries(int) */
  public void setSeries(int series) {
    if (series != getSeries()) {
      prevBuf = null;
    }
    super.setSeries(series);
  }

  /**
   * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
   */
  public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
    throws FormatException, IOException
  {
    FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);

    in = new RandomAccessInputStream(lsmFilenames[getSeries()]);
    in.order(!isLittleEndian());

    tiffParser = new TiffParser(in);

    IFDList ifds = ifdsList.get(getSeries());

    if (splitPlanes && getSizeC() > 1 && ifds.size() == getSizeZ() * getSizeT())
    {
      int bpp = FormatTools.getBytesPerPixel(getPixelType());
      int plane = no / getSizeC();
      int c = no % getSizeC();

      if (prevPlane != plane || prevBuf == null) {
        prevBuf = new byte[buf.length * getSizeC()];
        tiffParser.getSamples(ifds.get(plane), prevBuf, x, y, w, h);
        prevPlane = plane;
      }
      ImageTools.splitChannels(prevBuf, buf, c, getSizeC(), bpp, false, false);
    }
    else tiffParser.getSamples(ifds.get(no), buf, x, y, w, h);
    in.close();
    return buf;
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);

    if (!checkSuffix(id, MDB_SUFFIX) && isGroupFiles()) {
      String mdb = getMDBFile(id);
      if (mdb != null) {
        setId(mdb);
        return;
      }
      lsmFilenames = new String[] {id};
    }
    else if (checkSuffix(id, MDB_SUFFIX)) {
      lsmFilenames = parseMDB(id);
    }
    else lsmFilenames = new String[] {id};

    if (lsmFilenames.length == 0) {
      throw new FormatException("LSM files were not found.");
    }

    timestamps = new Vector<Double>();
    imageNames = new Vector<String>();
    xCoordinates = new Vector<Double>();
    yCoordinates = new Vector<Double>();

    core = new CoreMetadata[lsmFilenames.length];
    ifdsList = new Vector<IFDList>();
    ifdsList.setSize(core.length);
    for (int i=0; i<core.length; i++) {
      core[i] = new CoreMetadata();
      RandomAccessInputStream s = new RandomAccessInputStream(lsmFilenames[i]);
      core[i].littleEndian = s.read() == TiffConstants.LITTLE;
      s.order(isLittleEndian());
      s.seek(0);
      // calling tp.getNonThumbnailIFDs() would give us the same IFDList, but
      // assuming that every other IFD is a thumbnail reduces the parsing time
      TiffParser tp = new TiffParser(s);
      long[] ifdOffsets = tp.getIFDOffsets();
      IFDList ifds = new IFDList();
      for (int offset=0; offset<ifdOffsets.length;) {
        ifds.add(tp.getIFD(ifdOffsets[offset]));
        if (ifds.get(0).containsKey(ZEISS_ID)) offset += 2;
        else offset++;
      }
      ifdsList.set(i, ifds);
      s.close();
    }

    MetadataStore store = makeFilterMetadata();

    for (int series=0; series<ifdsList.size(); series++) {
      IFDList ifds = ifdsList.get(series);
      for (IFD ifd : ifds) {
        // check that predictor is set to 1 if anything other
        // than LZW compression is used
        if (ifd.getCompression() != TiffCompression.LZW) {
          ifd.putIFDValue(IFD.PREDICTOR, 1);
        }
      }

      // fix the offsets for > 4 GB files
      for (int i=1; i<ifds.size(); i++) {
        long[] stripOffsets = ifds.get(i).getStripOffsets();
        long[] previousStripOffsets = ifds.get(i - 1).getStripOffsets();

        if (stripOffsets == null || previousStripOffsets == null) {
          throw new FormatException(
            "Strip offsets are missing; this is an invalid file.");
        }

        boolean neededAdjustment = false;
        for (int j=0; j<stripOffsets.length; j++) {
          if (j >= previousStripOffsets.length) break;
          if (stripOffsets[j] < previousStripOffsets[j]) {
            stripOffsets[j] = (previousStripOffsets[j] & ~0xffffffffL) |
              (stripOffsets[j] & 0xffffffffL);
            if (stripOffsets[j] < previousStripOffsets[j]) {
              stripOffsets[j] += 0x100000000L;
            }
            neededAdjustment = true;
          }
          if (neededAdjustment) {
            ifds.get(i).putIFDValue(IFD.STRIP_OFFSETS, stripOffsets);
          }
        }
      }

      initMetadata(series);
    }
    MetadataTools.populatePixels(store, this, true);
    for (int series=0; series<ifdsList.size(); series++) {
      store.setImageName(imageNames.get(series), series);
      store.setPixelsBinDataBigEndian(!isLittleEndian(), series, 0);
    }
    setSeries(0);
  }

  // -- Helper methods --

  private String getMDBFile(String id) throws FormatException, IOException {
    Location parentFile = new Location(id).getAbsoluteFile().getParentFile();
    String[] fileList = parentFile.list();
    for (int i=0; i<fileList.length; i++) {
      if (fileList[i].startsWith(".")) continue;
      if (checkSuffix(fileList[i], MDB_SUFFIX)) {
        Location file =
          new Location(parentFile, fileList[i]).getAbsoluteFile();
        if (file.isDirectory()) continue;
        // make sure that the .mdb references this .lsm
        String[] lsms = parseMDB(file.getAbsolutePath());
        for (String lsm : lsms) {
          if (id.endsWith(lsm) || lsm.endsWith(id)) {
            return file.getAbsolutePath();
          }
        }
      }
    }
    return null;
  }

  protected void initMetadata(int series) throws FormatException, IOException {
    setSeries(series);
    IFDList ifds = ifdsList.get(series);
    IFD ifd = ifds.get(0);

    in = new RandomAccessInputStream(lsmFilenames[series]);
    in.order(isLittleEndian());

    tiffParser = new TiffParser(in);

    PhotoInterp photo = ifd.getPhotometricInterpretation();
    int samples = ifd.getSamplesPerPixel();

    core[series].sizeX = (int) ifd.getImageWidth();
    core[series].sizeY = (int) ifd.getImageLength();
    core[series].rgb = samples > 1 || photo == PhotoInterp.RGB;
    core[series].interleaved = false;
    core[series].sizeC = isRGB() ? samples : 1;
    core[series].pixelType = ifd.getPixelType();
    core[series].imageCount = ifds.size();
    core[series].sizeZ = getImageCount();
    core[series].sizeT = 1;

    LOGGER.info("Reading LSM metadata for series #{}", series);

    MetadataStore store = makeFilterMetadata();

    String imageName = lsmFilenames[series];
    if (imageName.indexOf(".") != -1) {
      imageName = imageName.substring(0, imageName.lastIndexOf("."));
    }
    if (imageName.indexOf(File.separator) != -1) {
      imageName =
        imageName.substring(imageName.lastIndexOf(File.separator) + 1);
    }

    // link Instrument and Image
    store.setImageID(MetadataTools.createLSID("Image", series), series);
    String instrumentID = MetadataTools.createLSID("Instrument", series);
    store.setInstrumentID(instrumentID, series);
    store.setImageInstrumentRef(instrumentID, series);

    // get TIF_CZ_LSMINFO structure
    short[] s = ifd.getIFDShortArray(ZEISS_ID);
    if (s == null) {
      LOGGER.warn("Invalid Zeiss LSM file. Tag {} not found.", ZEISS_ID);
      TiffReader reader = new TiffReader();
      reader.setId(lsmFilenames[series]);
      core[series] = reader.getCoreMetadata()[0];
      reader.close();
      imageNames.add(imageName);
      return;
    }
    byte[] cz = new byte[s.length];
    for (int i=0; i<s.length; i++) {
      cz[i] = (byte) s[i];
    }

    RandomAccessInputStream ras = new RandomAccessInputStream(cz);
    ras.order(isLittleEndian());

    ras.seek(16);

    core[series].sizeZ = ras.readInt();
    ras.skipBytes(4);
    core[series].sizeT = ras.readInt();

    int dataType = ras.readInt();
    switch (dataType) {
      case 2:
        addSeriesMeta("DataType", "12 bit unsigned integer");
        break;
      case 5:
        addSeriesMeta("DataType", "32 bit float");
        break;
      case 0:
        addSeriesMeta("DataType", "varying data types");
        break;
      default:
        addSeriesMeta("DataType", "8 bit unsigned integer");
    }

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      ras.seek(0);
      addSeriesMeta("MagicNumber ", ras.readInt());
      addSeriesMeta("StructureSize", ras.readInt());
      addSeriesMeta("DimensionX", ras.readInt());
      addSeriesMeta("DimensionY", ras.readInt());

      ras.seek(32);
      addSeriesMeta("ThumbnailX", ras.readInt());
      addSeriesMeta("ThumbnailY", ras.readInt());

      // pixel sizes are stored in meters, we need them in microns
      pixelSizeX = ras.readDouble() * 1000000;
      pixelSizeY = ras.readDouble() * 1000000;
      pixelSizeZ = ras.readDouble() * 1000000;

      addSeriesMeta("VoxelSizeX", new Double(pixelSizeX));
      addSeriesMeta("VoxelSizeY", new Double(pixelSizeY));
      addSeriesMeta("VoxelSizeZ", new Double(pixelSizeZ));

      addSeriesMeta("OriginX", ras.readDouble());
      addSeriesMeta("OriginY", ras.readDouble());
      addSeriesMeta("OriginZ", ras.readDouble());
    }
    else ras.seek(56);

    int scanType = ras.readShort();
    switch (scanType) {
      case 0:
        addSeriesMeta("ScanType", "x-y-z scan");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 1:
        addSeriesMeta("ScanType", "z scan (x-z plane)");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 2:
        addSeriesMeta("ScanType", "line scan");
        core[series].dimensionOrder = "XYZCT";
        break;
      case 3:
        addSeriesMeta("ScanType", "time series x-y");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 4:
        addSeriesMeta("ScanType", "time series x-z");
        core[series].dimensionOrder = "XYZTC";
        break;
      case 5:
        addSeriesMeta("ScanType", "time series 'Mean of ROIs'");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 6:
        addSeriesMeta("ScanType", "time series x-y-z");
        core[series].dimensionOrder = "XYZTC";
        break;
      case 7:
        addSeriesMeta("ScanType", "spline scan");
        core[series].dimensionOrder = "XYCTZ";
        break;
      case 8:
        addSeriesMeta("ScanType", "spline scan x-z");
        core[series].dimensionOrder = "XYCZT";
        break;
      case 9:
        addSeriesMeta("ScanType", "time series spline plane x-z");
        core[series].dimensionOrder = "XYTCZ";
        break;
      case 10:
        addSeriesMeta("ScanType", "point mode");
        core[series].dimensionOrder = "XYZCT";
        break;
      default:
        addSeriesMeta("ScanType", "x-y-z scan");
        core[series].dimensionOrder = "XYZCT";
    }

    core[series].indexed =
      lut != null && lut[series] != null && getSizeC() == 1;
    if (isIndexed()) {
      core[series].sizeC = 1;
      core[series].rgb = false;
    }
    if (getSizeC() == 0) core[series].sizeC = 1;

    if (isRGB()) {
      // shuffle C to front of order string
      core[series].dimensionOrder = getDimensionOrder().replaceAll("C", "");
      core[series].dimensionOrder = getDimensionOrder().replaceAll("XY", "XYC");
    }

    if (getEffectiveSizeC() == 0) {
      core[series].imageCount = getSizeZ() * getSizeT();
    }
    else {
      core[series].imageCount = getSizeZ() * getSizeT() * getEffectiveSizeC();
    }

    if (getImageCount() != ifds.size()) {
      int diff = getImageCount() - ifds.size();
      core[series].imageCount = ifds.size();
      if (diff % getSizeZ() == 0) {
        core[series].sizeT -= (diff / getSizeZ());
      }
      else if (diff % getSizeT() == 0) {
        core[series].sizeZ -= (diff / getSizeT());
      }
      else if (getSizeZ() > 1) {
        core[series].sizeZ = ifds.size();
        core[series].sizeT = 1;
      }
      else if (getSizeT() > 1) {
        core[series].sizeT = ifds.size();
        core[series].sizeZ = 1;
      }
    }

    if (getSizeZ() == 0) core[series].sizeZ = getImageCount();
    if (getSizeT() == 0) core[series].sizeT = getImageCount() / getSizeZ();

    MetadataTools.setDefaultCreationDate(store, getCurrentFile(), series);
    if (getSizeC() > 1) {
      if (!splitPlanes) splitPlanes = isRGB();
      core[series].rgb = false;
      if (splitPlanes) core[series].imageCount *= getSizeC();
    }

    for (int c=0; c<getEffectiveSizeC(); c++) {
      String lsid = MetadataTools.createLSID("Channel", series, c);
      store.setChannelID(lsid, series, c);
    }

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      int spectralScan = ras.readShort();
      if (spectralScan != 1) {
        addSeriesMeta("SpectralScan", "no spectral scan");
      }
      else addSeriesMeta("SpectralScan", "acquired with spectral scan");

      int type = ras.readInt();
      switch (type) {
        case 1:
          addSeriesMeta("DataType2", "calculated data");
          break;
        case 2:
          addSeriesMeta("DataType2", "animation");
          break;
        default:
          addSeriesMeta("DataType2", "original scan data");
      }

      long[] overlayOffsets = new long[9];
      String[] overlayKeys = new String[] {"VectorOverlay", "InputLut",
        "OutputLut", "ROI", "BleachROI", "MeanOfRoisOverlay",
        "TopoIsolineOverlay", "TopoProfileOverlay", "LinescanOverlay"};

      overlayOffsets[0] = ras.readInt();
      overlayOffsets[1] = ras.readInt();
      overlayOffsets[2] = ras.readInt();

      long channelColorsOffset = ras.readInt();

      addSeriesMeta("TimeInterval", ras.readDouble());
      ras.skipBytes(4);
      long scanInformationOffset = ras.readInt();
      ras.skipBytes(4);
      long timeStampOffset = ras.readInt();
      long eventListOffset = ras.readInt();
      overlayOffsets[3] = ras.readInt();
      overlayOffsets[4] = ras.readInt();
      ras.skipBytes(4);

      addSeriesMeta("DisplayAspectX", ras.readDouble());
      addSeriesMeta("DisplayAspectY", ras.readDouble());
      addSeriesMeta("DisplayAspectZ", ras.readDouble());
      addSeriesMeta("DisplayAspectTime", ras.readDouble());

      overlayOffsets[5] = ras.readInt();
      overlayOffsets[6] = ras.readInt();
      overlayOffsets[7] = ras.readInt();
      overlayOffsets[8] = ras.readInt();

      if (getMetadataOptions().getMetadataLevel() != MetadataLevel.NO_OVERLAYS)
      {
        for (int i=0; i<overlayOffsets.length; i++) {
          parseOverlays(series, overlayOffsets[i], overlayKeys[i], store);
        }
      }

      totalROIs = 0;

      addSeriesMeta("ToolbarFlags", ras.readInt());

      int wavelengthOffset = ras.readInt();
      ras.skipBytes(56);
      int dimensionP = ras.readInt();
      int dimensionM = ras.readInt();

      // NB: the Zeiss LSM 5.5 specification indicates that there should be
      //     15 32-bit integers here; however, there are actually 16 32-bit
      //     integers before the tile position offset.
      ras.skipBytes(64);

      int tilePositionOffset = ras.readInt();

      // read referenced structures

      addSeriesMeta("DimensionZ", getSizeZ());
      addSeriesMeta("DimensionChannels", getSizeC());
      addSeriesMeta("DimensionM", dimensionM);
      addSeriesMeta("DimensionP", dimensionP);

      if (tilePositionOffset != 0) {
        in.seek(tilePositionOffset);
        int nTiles = in.readInt();
        for (int i=0; i<nTiles; i++) {
          xCoordinates.add(in.readDouble());
          yCoordinates.add(in.readDouble());
        }
      }

      if (channelColorsOffset != 0) {
        in.seek(channelColorsOffset + 16);
        int namesOffset = in.readInt();

        // read the name of each channel

        if (namesOffset > 0) {
          in.skipBytes(namesOffset - 16);

          for (int i=0; i<getSizeC(); i++) {
            if (in.getFilePointer() >= in.length() - 1) break;
            // we want to read until we find a null char
            String name = in.readCString();
            if (name.length() <= 128) {
              addSeriesMeta("ChannelName" + i, name);
            }
          }
        }
      }

      if (timeStampOffset != 0) {
        in.seek(timeStampOffset + 8);
        for (int i=0; i<getSizeT(); i++) {
          double stamp = in.readDouble();
          addSeriesMeta("TimeStamp" + i, stamp);
          timestamps.add(new Double(stamp));
        }
      }

      if (eventListOffset != 0) {
        in.seek(eventListOffset + 4);
        int numEvents = in.readInt();
        in.seek(in.getFilePointer() - 4);
        in.order(!in.isLittleEndian());
        int tmpEvents = in.readInt();
        if (numEvents < 0) numEvents = tmpEvents;
        else numEvents = (int) Math.min(numEvents, tmpEvents);
        in.order(!in.isLittleEndian());

        if (numEvents > 65535) numEvents = 0;

        for (int i=0; i<numEvents; i++) {
          if (in.getFilePointer() + 16 <= in.length()) {
            int size = in.readInt();
            double eventTime = in.readDouble();
            int eventType = in.readInt();
            addSeriesMeta("Event" + i + " Time", eventTime);
            addSeriesMeta("Event" + i + " Type", eventType);
            long fp = in.getFilePointer();
            int len = size - 16;
            if (len > 65536) len = 65536;
            if (len < 0) len = 0;
            addSeriesMeta("Event" + i + " Description", in.readString(len));
            in.seek(fp + size - 16);
            if (in.getFilePointer() < 0) break;
          }
        }
      }

      if (scanInformationOffset != 0) {
        in.seek(scanInformationOffset);

        nextLaser = nextDetector = 0;
        nextFilter = nextDichroicChannel = nextDichroic = 0;
        nextDataChannel = nextDetectChannel = nextIllumChannel = 0;

        Vector<SubBlock> blocks = new Vector<SubBlock>();

        while (in.getFilePointer() < in.length() - 12) {
          if (in.getFilePointer() < 0) break;
          int entry = in.readInt();
          int blockType = in.readInt();
          int dataSize = in.readInt();

          if (blockType == TYPE_SUBBLOCK) {
            SubBlock block = null;
            switch (entry) {
              case SUBBLOCK_RECORDING:
                block = new Recording();
                break;
              case SUBBLOCK_LASER:
                block = new Laser();
                break;
              case SUBBLOCK_TRACK:
                block = new Track();
                break;
              case SUBBLOCK_DETECTION_CHANNEL:
                block = new DetectionChannel();
                break;
              case SUBBLOCK_ILLUMINATION_CHANNEL:
                block = new IlluminationChannel();
                break;
              case SUBBLOCK_BEAM_SPLITTER:
                block = new BeamSplitter();
                break;
              case SUBBLOCK_DATA_CHANNEL:
                block = new DataChannel();
                break;
              case SUBBLOCK_TIMER:
                block = new Timer();
                break;
              case SUBBLOCK_MARKER:
                block = new Marker();
                break;
            }
            if (block != null) {
              blocks.add(block);
            }
          }
          else if (dataSize + in.getFilePointer() <= in.length()) {
            in.skipBytes(dataSize);
          }
          else break;
        }

        Vector<SubBlock> nonAcquiredBlocks = new Vector<SubBlock>();

        SubBlock[] metadataBlocks = blocks.toArray(new SubBlock[0]);
        for (SubBlock block : metadataBlocks) {
          block.addToHashtable();
          if (!block.acquire) {
            nonAcquiredBlocks.add(block);
            blocks.remove(block);
          }
        }

        for (int i=0; i<blocks.size(); i++) {
          SubBlock block = blocks.get(i);
          // every valid IlluminationChannel must be immediately followed by
          // a valid DataChannel or IlluminationChannel
          if ((block instanceof IlluminationChannel) && i < blocks.size() - 1) {
            SubBlock nextBlock = blocks.get(i + 1);
            if (!(nextBlock instanceof DataChannel) &&
              !(nextBlock instanceof IlluminationChannel))
            {
              ((IlluminationChannel) block).wavelength = null;
            }
          }
          // every valid DetectionChannel must be immediately preceded by
          // a valid Track or DetectionChannel
          else if ((block instanceof DetectionChannel) && i > 0) {
            SubBlock prevBlock = blocks.get(i - 1);
            if (!(prevBlock instanceof Track) &&
              !(prevBlock instanceof DetectionChannel))
            {
              block.acquire = false;
              nonAcquiredBlocks.add(block);
            }
          }
          if (block.acquire) populateMetadataStore(block, store, series);
        }

        for (SubBlock block : nonAcquiredBlocks) {
          populateMetadataStore(block, store, series);
        }
      }
    }

    imageNames.add(imageName);

    if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
      Double pixX = new Double(pixelSizeX);
      Double pixY = new Double(pixelSizeY);
      Double pixZ = new Double(pixelSizeZ);

      store.setPixelsPhysicalSizeX(pixX, series);
      store.setPixelsPhysicalSizeY(pixY, series);
      store.setPixelsPhysicalSizeZ(pixZ, series);

      double firstStamp = 0;
      if (timestamps.size() > 0) {
        firstStamp = timestamps.get(0).doubleValue();
      }

      for (int i=0; i<getImageCount(); i++) {
        int[] zct = FormatTools.getZCTCoords(this, i);

        if (zct[2] < timestamps.size()) {
          double thisStamp = timestamps.get(zct[2]).doubleValue();
          store.setPlaneDeltaT(thisStamp - firstStamp, series, i);
          int index = zct[2] + 1;
          double nextStamp = index < timestamps.size() ?
            timestamps.get(index).doubleValue() : thisStamp;
          if (i == getSizeT() - 1 && zct[2] > 0) {
            thisStamp = timestamps.get(zct[2] - 1).doubleValue();
          }
          store.setPlaneExposureTime(nextStamp - thisStamp, series, i);
        }
        if (xCoordinates.size() > 0) {
          int stage = i / (getImageCount() / xCoordinates.size());
          store.setPlanePositionX(xCoordinates.get(stage), series, i);
          store.setPlanePositionY(yCoordinates.get(stage), series, i);
        }
      }
    }
    ras.close();
    in.close();
  }
{'fraction_non_alphanumeric': 0.12204282042204743, 'fraction_numerical': 0.019686311933383615, 'mean_word_length': 2.7134202103337905, 'pattern_counts': {'":': 0, '<': 124, '<?xml version=': 0, '>': 95, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 22, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '936201', 'n_tokens_mistral': 21471, 'n_tokens_neox': 20005, 'n_words': 4886}