<commit_before>//
// LIFReader.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.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.ImageTools;
import loci.formats.MetadataTools;
import loci.formats.meta.IMetadata;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import loci.formats.services.OMEXMLService;

import ome.xml.model.enums.DetectorType;
import ome.xml.model.enums.LaserMedium;
import ome.xml.model.enums.LaserType;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PercentFraction;
import ome.xml.model.primitives.PositiveInteger;

import org.xml.sax.SAXException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * LIFReader is the file format reader for Leica LIF files.
 *
 * <dl><dt><b>Source code:</b></dt>
 * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/LIFReader.java">Trac</a>,
 * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/LIFReader.java;hb=HEAD">Gitweb</a></dd></dl>
 *
 * @author <NAME> melissa at glencoesoftware.com
 */
public class LIFReader extends FormatReader {

  // -- Constants --

  public static final byte LIF_MAGIC_BYTE = 0x70;
  public static final byte LIF_MEMORY_BYTE = 0x2a;

  private static final HashMap<String, Integer> CHANNEL_PRIORITIES =
    createChannelPriorities();

  private static HashMap<String, Integer> createChannelPriorities() {
    HashMap<String, Integer> h = new HashMap<String, Integer>();

    h.put("red", new Integer(0));
    h.put("green", new Integer(1));
    h.put("blue", new Integer(2));
    h.put("cyan", new Integer(3));
    h.put("magenta", new Integer(4));
    h.put("yellow", new Integer(5));
    h.put("black", new Integer(6));
    h.put("gray", new Integer(7));
    h.put("", new Integer(8));

    return h;
  }

  private static final byte[][][] BYTE_LUTS = createByteLUTs();

  private static byte[][][] createByteLUTs() {
    byte[][][] lut = new byte[9][3][256];
    for (int i=0; i<256; i++) {
      // red
      lut[0][0][i] = (byte) (i & 0xff);
      // green
      lut[1][1][i] = (byte) (i & 0xff);
      // blue
      lut[2][2][i] = (byte) (i & 0xff);

      // cyan
      lut[3][1][i] = (byte) (i & 0xff);
      lut[3][2][i] = (byte) (i & 0xff);

      // magenta
      lut[4][0][i] = (byte) (i & 0xff);
      lut[4][2][i] = (byte) (i & 0xff);

      // yellow
      lut[5][0][i] = (byte) (i & 0xff);
      lut[5][1][i] = (byte) (i & 0xff);

      // gray
      lut[6][0][i] = (byte) (i & 0xff);
      lut[6][1][i] = (byte) (i & 0xff);
      lut[6][2][i] = (byte) (i & 0xff);
      lut[7][0][i] = (byte) (i & 0xff);
      lut[7][1][i] = (byte) (i & 0xff);
      lut[7][2][i] = (byte) (i & 0xff);
      lut[8][0][i] = (byte) (i & 0xff);
      lut[8][1][i] = (byte) (i & 0xff);
      lut[8][2][i] = (byte) (i & 0xff);
    }
    return lut;
  }

  private static final short[][][] SHORT_LUTS = createShortLUTs();

  private static short[][][] createShortLUTs() {
    short[][][] lut = new short[9][3][65536];
    for (int i=0; i<65536; i++) {
      // red
      lut[0][0][i] = (short) (i & 0xffff);
      // green
      lut[1][1][i] = (short) (i & 0xffff);
      // blue
      lut[2][2][i] = (short) (i & 0xffff);

      // cyan
      lut[3][1][i] = (short) (i & 0xffff);
      lut[3][2][i] = (short) (i & 0xffff);

      // magenta
      lut[4][0][i] = (short) (i & 0xffff);
      lut[4][2][i] = (short) (i & 0xffff);

      // yellow
      lut[5][0][i] = (short) (i & 0xffff);
      lut[5][1][i] = (short) (i & 0xffff);

      // gray
      lut[6][0][i] = (short) (i & 0xffff);
      lut[6][1][i] = (short) (i & 0xffff);
      lut[6][2][i] = (short) (i & 0xffff);
      lut[7][0][i] = (short) (i & 0xffff);
      lut[7][1][i] = (short) (i & 0xffff);
      lut[7][2][i] = (short) (i & 0xffff);
      lut[8][0][i] = (short) (i & 0xffff);
      lut[8][1][i] = (short) (i & 0xffff);
      lut[8][2][i] = (short) (i & 0xffff);
    }
    return lut;
  }

  // -- Fields --

  /** Offsets to memory blocks, paired with their corresponding description. */
  private Vector<Long> offsets;

  private int[][] realChannel;
  private int lastChannel = 0;

  private Vector<String> lutNames = new Vector<String>();
  private Vector<Double> physicalSizeXs = new Vector<Double>();
  private Vector<Double> physicalSizeYs = new Vector<Double>();

  private String[] descriptions, microscopeModels, serialNumber;
  private Double[] pinholes, zooms, zSteps, tSteps, lensNA;
  private Double[][] expTimes, gains;
  private Vector[] detectorOffsets;
  private String[][] channelNames;
  private Vector[] detectorModels, voltages;
  private Integer[][] exWaves;
  private Vector[] activeDetector;

  private String[] immersions, corrections, objectiveModels;
  private Integer[] magnification;
  private Double[] posX, posY, posZ;
  private Double[] refractiveIndex;
  private Vector[] cutIns, cutOuts, filterModels;
  private double[][] timestamps;
  private Vector[] laserWavelength, laserIntensity;
  private ROI[][] imageROIs;
  private boolean alternateCenter = false;
  private String[] imageNames;
  private double[] acquiredDate;

  // -- Constructor --

  /** Constructs a new Leica LIF reader. */
  public LIFReader() {
    super("Leica Image File Format", "lif");
    domains = new String[] {FormatTools.LM_DOMAIN};
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#getOptimalTileHeight() */
  public int getOptimalTileHeight() {
    FormatTools.assertId(currentId, true, 1);
    return getSizeY();
  }

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 1;
    if (!FormatTools.validStream(stream, blockLen, true)) return false;
    return stream.read() == LIF_MAGIC_BYTE;
  }

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  public byte[][] get8BitLookupTable() {
    FormatTools.assertId(currentId, true, 1);
    if (getPixelType() != FormatTools.UINT8 || !isIndexed()) return null;
    return lastChannel < BYTE_LUTS.length ? BYTE_LUTS[lastChannel] : null;
  }

  /* @see loci.formats.IFormatReader#get16BitLookupTable() */
  public short[][] get16BitLookupTable() {
    FormatTools.assertId(currentId, true, 1);
    if (getPixelType() != FormatTools.UINT16 || !isIndexed()) return null;
    return lastChannel < SHORT_LUTS.length ? SHORT_LUTS[lastChannel] : null;
  }

  /**
   * @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);

    if (!isRGB()) {
      int[] pos = getZCTCoords(no);
      lastChannel = realChannel[series][pos[1]];
    }

    if (series >= offsets.size()) {
      // truncated file; imitate LAS AF and return black planes
      Arrays.fill(buf, (byte) 0);
      return buf;
    }

    long offset = offsets.get(series).longValue();
    int bytes = FormatTools.getBytesPerPixel(getPixelType());
    int bpp = bytes * getRGBChannelCount();

    long planeSize = (long) getSizeX() * getSizeY() * bpp;

    long nextOffset = series + 1 < offsets.size() ?
      offsets.get(series + 1).longValue() : in.length();
    int bytesToSkip = (int) (nextOffset - offset - planeSize * getImageCount());
    bytesToSkip /= getSizeY();
    if ((getSizeX() % 4) == 0) bytesToSkip = 0;

    if (offset + (planeSize + bytesToSkip * getSizeY()) * no >= in.length()) {
      // truncated file; imitate LAS AF and return black planes
      Arrays.fill(buf, (byte) 0);
      return buf;
    }

    in.seek(offset + planeSize * no);
    in.skipBytes(bytesToSkip * getSizeY() * no);

    if (bytesToSkip == 0) {
      readPlane(in, x, y, w, h, buf);
    }
    else {
      in.skipBytes(y * (getSizeX() * bpp + bytesToSkip));
      for (int row=0; row<h; row++) {
        in.skipBytes(x * bpp);
        in.read(buf, row * w * bpp, w * bpp);
        in.skipBytes(bpp * (getSizeX() - w - x) + bytesToSkip);
      }
    }

    // color planes are stored in BGR order
    if (getRGBChannelCount() == 3) {
      ImageTools.bgrToRgb(buf, isInterleaved(), bytes, getRGBChannelCount());
    }

    return buf;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      offsets = null;
      realChannel = null;
      lastChannel = 0;
      lutNames.clear();
      physicalSizeXs.clear();
      physicalSizeYs.clear();
      descriptions = microscopeModels = serialNumber = null;
      pinholes = zooms = lensNA = null;
      zSteps = tSteps = null;
      expTimes = gains = null;
      detectorOffsets = null;
      channelNames = null;
      detectorModels = voltages = null;
      exWaves = null;
      activeDetector = null;
      immersions = corrections = null;
      magnification = null;
      objectiveModels = null;
      posX = posY = posZ = null;
      refractiveIndex = null;
      cutIns = cutOuts = filterModels = null;
      timestamps = null;
      laserWavelength = laserIntensity = null;
      imageROIs = null;
      alternateCenter = false;
      imageNames = null;
      acquiredDate = null;
    }
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);
    in = new RandomAccessInputStream(id);
    offsets = new Vector<Long>();

    in.order(true);

    // read the header

    LOGGER.info("Reading header");

    byte checkOne = in.readByte();
    in.skipBytes(2);
    byte checkTwo = in.readByte();
    if (checkOne != LIF_MAGIC_BYTE && checkTwo != LIF_MAGIC_BYTE) {
      throw new FormatException(id + " is not a valid Leica LIF file");
    }

    in.skipBytes(4);

    // read and parse the XML description

    if (in.read() != LIF_MEMORY_BYTE) {
      throw new FormatException("Invalid XML description");
    }

    // number of Unicode characters in the XML block
    int nc = in.readInt();
    String xml = DataTools.stripString(in.readString(nc * 2));

    LOGGER.info("Finding image offsets");

    while (in.getFilePointer() < in.length()) {
      LOGGER.debug("Looking for a block at {}; {} blocks read",
        in.getFilePointer(), offsets.size());
      int check = in.readInt();
      if (check != LIF_MAGIC_BYTE) {
        throw new FormatException("Invalid Memory Block: found magic bytes " +
          check + ", expected " + LIF_MAGIC_BYTE);
      }

      in.skipBytes(4);
      check = in.read();
      if (check != LIF_MEMORY_BYTE) {
        throw new FormatException("Invalid Memory Description: found magic " +
          "byte " + check + ", expected " + LIF_MEMORY_BYTE);
      }

      long blockLength = in.readInt();
      if (in.read() != LIF_MEMORY_BYTE) {
        in.seek(in.getFilePointer() - 5);
        blockLength = in.readLong();
        check = in.read();
        if (check != LIF_MEMORY_BYTE) {
          throw new FormatException("Invalid Memory Description: found magic " +
            "byte " + check + ", expected " + LIF_MEMORY_BYTE);
        }
      }

      int descrLength = in.readInt() * 2;

      if (blockLength > 0) {
        offsets.add(new Long(in.getFilePointer() + descrLength));
      }

      in.seek(in.getFilePointer() + descrLength + blockLength);
    }
    initMetadata(xml);
    xml = null;

    // correct offsets, if necessary
    if (offsets.size() > getSeriesCount()) {
      Long[] storedOffsets = offsets.toArray(new Long[offsets.size()]);
      offsets.clear();
      int index = 0;
      for (int i=0; i<getSeriesCount(); i++) {
        setSeries(i);
        long nBytes = (long) FormatTools.getPlaneSize(this) * getImageCount();
        long start = storedOffsets[index];
        long end = index == storedOffsets.length - 1 ? in.length() :
          storedOffsets[index + 1];
        while (end - start < nBytes && ((end - start) / nBytes) != 1) {
          index++;
          start = storedOffsets[index];
          end = index == storedOffsets.length - 1 ? in.length() :
            storedOffsets[index + 1];
        }
        offsets.add(storedOffsets[index]);
        index++;
      }
      setSeries(0);
    }
  }

  // -- Helper methods --

  /** Parses a string of XML and puts the values in a Hashtable. */
  private void initMetadata(String xml) throws FormatException, IOException {
    IMetadata omexml = null;
    try {
      ServiceFactory factory = new ServiceFactory();
      OMEXMLService service = factory.getInstance(OMEXMLService.class);
      omexml = service.createOMEXMLMetadata();
    }
    catch (DependencyException exc) {
      throw new FormatException("Could not create OME-XML store.", exc);
    }
    catch (ServiceException exc) {
      throw new FormatException("Could not create OME-XML store.", exc);
    }
    MetadataStore store = makeFilterMetadata();
    MetadataLevel level = getMetadataOptions().getMetadataLevel();

    // the XML blocks stored in a LIF file are invalid,
    // because they don't have a root node

    xml = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><LEICA>" + xml +
      "</LEICA>";

    xml = XMLTools.sanitizeXML(xml);

    translateMetadata(getMetadataRoot(xml));

    for (int i=0; i<imageNames.length; i++) {
      setSeries(i);
      addSeriesMeta("Image name", imageNames[i]);
    }
    setSeries(0);

    // set up mapping to rearrange channels
    // for instance, the green channel may be #0, and the red channel may be #1
    realChannel = new int[getSeriesCount()][];
    int nextLut = 0;
    for (int i=0; i<getSeriesCount(); i++) {
      realChannel[i] = new int[core[i].sizeC];

      for (int q=0; q<core[i].sizeC; q++) {
        String lut = lutNames.get(nextLut++).toLowerCase();
        if (!CHANNEL_PRIORITIES.containsKey(lut)) lut = "";
        realChannel[i][q] = CHANNEL_PRIORITIES.get(lut).intValue();
      }

      int[] sorted = new int[core[i].sizeC];
      Arrays.fill(sorted, -1);

      for (int q=0; q<sorted.length; q++) {
        int min = Integer.MAX_VALUE;
        int minIndex = -1;
        for (int n=0; n<core[i].sizeC; n++) {
          if (realChannel[i][n] < min && !DataTools.containsValue(sorted, n)) {
            min = realChannel[i][n];
            minIndex = n;
          }
        }

        sorted[q] = minIndex;
      }
    }

    MetadataTools.populatePixels(store, this, true, false);

    for (int i=0; i<getSeriesCount(); i++) {
      setSeries(i);

      String instrumentID = MetadataTools.createLSID("Instrument", i);
      store.setInstrumentID(instrumentID, i);

      store.setMicroscopeModel(microscopeModels[i], i);
      store.setMicroscopeType(getMicroscopeType("Unknown"), i);

      String objectiveID = MetadataTools.createLSID("Objective", i, 0);
      store.setObjectiveID(objectiveID, i, 0);
      store.setObjectiveLensNA(lensNA[i], i, 0);
      store.setObjectiveSerialNumber(serialNumber[i], i, 0);
      if (magnification[i] != null && magnification[i] > 0) {
        store.setObjectiveNominalMagnification(
          new PositiveInteger(magnification[i]), i, 0);
      }
      store.setObjectiveImmersion(getImmersion(immersions[i]), i, 0);
      store.setObjectiveCorrection(getCorrection(corrections[i]), i, 0);
      store.setObjectiveModel(objectiveModels[i], i, 0);

      if (cutIns[i] != null) {
        for (int filter=0; filter<cutIns[i].size(); filter++) {
          String filterID = MetadataTools.createLSID("Filter", i, filter);
          store.setFilterID(filterID, i, filter);
          if (filter < filterModels[i].size()) {
            store.setFilterModel(
              (String) filterModels[i].get(filter), i, filter);
          }
          int channel = filter - (cutIns[i].size() - getEffectiveSizeC());
          if (channel >= 0 && channel < getEffectiveSizeC()) {
            store.setLightPathEmissionFilterRef(filterID, i, channel, 0);
          }
          store.setTransmittanceRangeCutIn(
            (PositiveInteger) cutIns[i].get(filter), i, filter);
          store.setTransmittanceRangeCutOut(
            (PositiveInteger) cutOuts[i].get(filter), i, filter);
        }
      }

      Vector lasers = laserWavelength[i];
      Vector laserIntensities = laserIntensity[i];
      int nextChannel = getEffectiveSizeC() - 1;
      if (lasers != null) {
        for (int laser=0; laser<lasers.size(); laser++) {
          String id = MetadataTools.createLSID("LightSource", i, laser);
          store.setLaserID(id, i, laser);
          store.setLaserType(LaserType.OTHER, i, laser);
          store.setLaserLaserMedium(LaserMedium.OTHER, i, laser);
          Integer wavelength = (Integer) lasers.get(laser);
          if (wavelength > 0) {
            store.setLaserWavelength(new PositiveInteger(wavelength), i, laser);
          }

          if (laser < laserIntensities.size()) {
            double intensity = (Double) laserIntensities.get(laser);
            if (intensity < 100 && nextChannel >= 0 && wavelength != 0) {
              store.setChannelLightSourceSettingsID(id, i, nextChannel);
              store.setChannelLightSourceSettingsAttenuation(
                new PercentFraction((float) intensity / 100f), i, nextChannel);
              store.setChannelExcitationWavelength(
                new PositiveInteger(wavelength), i, nextChannel);

              nextChannel--;
            }
          }
        }
      }

      store.setImageInstrumentRef(instrumentID, i);
      store.setImageObjectiveSettingsID(objectiveID, i);
      store.setImageObjectiveSettingsRefractiveIndex(refractiveIndex[i], i);

      store.setImageDescription(descriptions[i], i);
      if (acquiredDate[i] > 0) {
        store.setImageAcquiredDate(DateTools.convertDate(
          (long) (acquiredDate[i] * 1000), DateTools.COBOL), i);
      }
      store.setImageName(imageNames[i].trim(), i);

      if (physicalSizeXs.get(i) > 0) {
        store.setPixelsPhysicalSizeX(
          new PositiveFloat(physicalSizeXs.get(i)), i);
      }
      if (physicalSizeYs.get(i) > 0) {
        store.setPixelsPhysicalSizeY(
          new PositiveFloat(physicalSizeYs.get(i)), i);
      }
      if (zSteps[i] != null && zSteps[i] > 0) {
        store.setPixelsPhysicalSizeZ(new PositiveFloat(zSteps[i]), i);
      }
      store.setPixelsTimeIncrement(tSteps[i], i);

      Vector detectors = detectorModels[i];
      if (detectors != null) {
        nextChannel = 0;
        for (int detector=0; detector<detectors.size(); detector++) {
          String detectorID = MetadataTools.createLSID("Detector", i, detector);
          store.setDetectorID(detectorID, i, detector);
          store.setDetectorModel((String) detectors.get(detector), i, detector);

          store.setDetectorZoom(zooms[i], i, detector);
          store.setDetectorType(DetectorType.PMT, i, detector);
          if (voltages[i] != null && detector < voltages[i].size()) {
            store.setDetectorVoltage(
              (Double) voltages[i].get(detector), i, detector);
          }

          if (activeDetector[i] != null) {
            if (detector < activeDetector[i].size() &&
              (Boolean) activeDetector[i].get(detector) &&
              detectorOffsets[i] != null &&
              nextChannel < detectorOffsets[i].size())
            {
              store.setDetectorOffset(
                (Double) detectorOffsets[i].get(nextChannel++), i, detector);
            }
          }
        }
      }

      Vector activeDetectors = activeDetector[i];
      int nextDetector = 0;

      for (int c=0; c<getEffectiveSizeC(); c++) {
        if (activeDetectors != null) {
          while (nextDetector < activeDetectors.size() &&
            !(Boolean) activeDetectors.get(nextDetector))
          {
            nextDetector++;
          }
          if (nextDetector < activeDetectors.size()) {
            String detectorID =
              MetadataTools.createLSID("Detector", i, nextDetector);
            store.setDetectorSettingsID(detectorID, i, c);
            nextDetector++;

            if (detectorOffsets[i] != null && c < detectorOffsets[i].size()) {
              store.setDetectorSettingsOffset(
                (Double) detectorOffsets[i].get(c), i, c);
            }

            if (gains[i] != null) {
              store.setDetectorSettingsGain(gains[i][c], i, c);
            }
          }
        }

        if (channelNames[i] != null) {
          store.setChannelName(channelNames[i][c], i, c);
        }
        store.setChannelPinholeSize(pinholes[i], i, c);
        if (exWaves[i] != null && exWaves[i][c] != null && exWaves[i][c] > 1) {
          store.setChannelExcitationWavelength(
            new PositiveInteger(exWaves[i][c]), i, c);
        }
        if (expTimes[i] != null) {
          store.setPlaneExposureTime(expTimes[i][c], i, c);
        }
      }

      for (int image=0; image<getImageCount(); image++) {
        store.setPlanePositionX(posX[i], i, image);
        store.setPlanePositionY(posY[i], i, image);
        store.setPlanePositionZ(posZ[i], i, image);
        if (timestamps[i] != null) {
          double timestamp = timestamps[i][image];
          if (timestamps[i][0] == acquiredDate[i]) {
            timestamp -= acquiredDate[i];
          }
          store.setPlaneDeltaT(timestamp, i, image);
        }
      }

      if (imageROIs[i] != null) {
        for (int roi=0; roi<imageROIs[i].length; roi++) {
          if (imageROIs[i][roi] != null) {
            imageROIs[i][roi].storeROI(store, i, roi);
          }
        }
      }
    }
  }

  private Element getMetadataRoot(String xml)
    throws FormatException, IOException
  {
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder parser = factory.newDocumentBuilder();
      ByteArrayInputStream s = new ByteArrayInputStream(xml.getBytes());
      Element root = parser.parse(s).getDocumentElement();
      s.close();
      return root;
    }
    catch (ParserConfigurationException e) {
      throw new FormatException(e);
    }
    catch (SAXException e) {
      throw new FormatException(e);
    }
  }

  private void translateMetadata(Element root) throws FormatException {
    Element realRoot = (Element) root.getChildNodes().item(0);

    NodeList toPrune = getNodes(realRoot, "LDM_Block_Sequential_Master");
    if (toPrune != null) {
      for (int i=0; i<toPrune.getLength(); i++) {
        Element prune = (Element) toPrune.item(i);
        Element parent = (Element) prune.getParentNode();
        parent.removeChild(prune);
      }
    }

    NodeList imageNodes = getNodes(realRoot, "Image");

    core = new CoreMetadata[imageNodes.getLength()];
    acquiredDate = new double[imageNodes.getLength()];
    descriptions = new String[imageNodes.getLength()];
    laserWavelength = new Vector[imageNodes.getLength()];
    laserIntensity = new Vector[imageNodes.getLength()];
    timestamps = new double[imageNodes.getLength()][];
    activeDetector = new Vector[imageNodes.getLength()];
    voltages = new Vector[imageNodes.getLength()];
    detectorOffsets = new Vector[imageNodes.getLength()];
    serialNumber = new String[imageNodes.getLength()];
    lensNA = new Double[imageNodes.getLength()];
    magnification = new Integer[imageNodes.getLength()];
    immersions = new String[imageNodes.getLength()];
    corrections = new String[imageNodes.getLength()];
    objectiveModels = new String[imageNodes.getLength()];
    posX = new Double[imageNodes.getLength()];
    posY = new Double[imageNodes.getLength()];
    posZ = new Double[imageNodes.getLength()];
    refractiveIndex = new Double[imageNodes.getLength()];
    cutIns = new Vector[imageNodes.getLength()];
    cutOuts = new Vector[imageNodes.getLength()];
    filterModels = new Vector[imageNodes.getLength()];
    microscopeModels = new String[imageNodes.getLength()];
    detectorModels = new Vector[imageNodes.getLength()];
    zSteps = new Double[imageNodes.getLength()];
    tSteps = new Double[imageNodes.getLength()];
    pinholes = new Double[imageNodes.getLength()];
    zooms = new Double[imageNodes.getLength()];

    expTimes = new Double[imageNodes.getLength()][];
    gains = new Double[imageNodes.getLength()][];
    channelNames = new String[imageNodes.getLength()][];
    exWaves = new Integer[imageNodes.getLength()][];
    imageROIs = new ROI[imageNodes.getLength()][];
    imageNames = new String[imageNodes.getLength()];

    for (int i=0; i<imageNodes.getLength(); i++) {
      Element image = (Element) imageNodes.item(i);
      setSeries(i);

      translateImageNames(image, i);
      translateImageNodes(image, i);
      translateAttachmentNodes(image, i);
      translateScannerSettings(image, i);
      translateFilterSettings(image, i);
      translateTimestamps(image, i);
      translateLaserLines(image, i);
      translateROIs(image, i);
      translateSingleROIs(image, i);
      translateDetectors(image, i);

      Stack<String> nameStack = new Stack<String>();
      HashMap<String, Integer> indexes = new HashMap<String, Integer>();
      populateOriginalMetadata(image, nameStack, indexes);
      indexes.clear();
    }
    setSeries(0);
  }

  private void populateOriginalMetadata(Element root, Stack<String> nameStack,
    HashMap<String, Integer> indexes)
  {
    String name = root.getNodeName();
    if (root.hasAttributes() && !name.equals("Element") &&
      !name.equals("Attachment") && !name.equals("LMSDataContainerHeader"))
    {
      nameStack.push(name);

      String suffix = root.getAttribute("Identifier");
      String value = root.getAttribute("Variant");
      if (suffix == null || suffix.trim().length() == 0) {
        suffix = root.getAttribute("Description");
      }
      StringBuffer key = new StringBuffer();
      for (String k : nameStack) {
        key.append(k);
        key.append("|");
      }
      if (suffix != null && value != null && suffix.length() > 0 &&
        value.length() > 0 && !suffix.equals("HighInteger") &&
        !suffix.equals("LowInteger"))
      {
        Integer i = indexes.get(key.toString() + suffix);
        String storedKey = key.toString() + suffix + " " + (i == null ? 0 : i);
        indexes.put(key.toString() + suffix, i == null ? 1 : i + 1);
        addSeriesMeta(storedKey, value);
      }
      else {
        NamedNodeMap attributes = root.getAttributes();
        for (int i=0; i<attributes.getLength(); i++) {
          Attr attr = (Attr) attributes.item(i);
          if (!attr.getName().equals("HighInteger") &&
            !attr.getName().equals("LowInteger"))
          {
            Integer index = indexes.get(key.toString() + attr.getName());
            if (index == null) {
              index = 0;
            }
            String storedKey = key.toString() + attr.getName() + " " + index;
            indexes.put(key.toString() + attr.getName(), index + 1);
            addSeriesMeta(storedKey, attr.getValue());
          }
        }
      }
    }

    NodeList children = root.getChildNodes();
    for (int i=0; i<children.getLength(); i++) {
      Object child = children.item(i);
      if (child instanceof Element) {
        populateOriginalMetadata((Element) child, nameStack, indexes);
      }
    }

    if (root.hasAttributes() && !name.equals("Element") &&
      !name.equals("Attachment") && !name.equals("LMSDataContainerHeader"))
    {
      nameStack.pop();
    }
  }

  private void translateImageNames(Element imageNode, int image) {
    Vector<String> names = new Vector<String>();
    Element parent = imageNode;
    while (true) {
      parent = (Element) parent.getParentNode();
      if (parent == null || parent.getNodeName().equals("LEICA")) {
        break;
      }
      if (parent.getNodeName().equals("Element")) {
        names.add(parent.getAttribute("Name"));
      }
    }
    imageNames[image] = "";
    for (int i=names.size() - 2; i>=0; i--) {
      imageNames[image] += names.get(i);
      if (i > 0) imageNames[image] += "/";
    }
  }

  private void translateDetectors(Element imageNode, int image)
    throws FormatException
  {
    NodeList definitions = getNodes(imageNode, "ATLConfocalSettingDefinition");
    if (definitions == null) return;

    int nextChannel = 0;
    for (int definition=0; definition<definitions.getLength(); definition++) {
      Element definitionNode = (Element) definitions.item(definition);
      NodeList detectors = getNodes(definitionNode, "Detector");
      if (detectors == null) return;

      for (int d=0; d<detectors.getLength(); d++) {
        Element detector = (Element) detectors.item(d);
        NodeList multibands = getNodes(definitionNode, "MultiBand");

        String v = detector.getAttribute("Gain");
        Double gain =
          v == null || v.trim().length() == 0 ? null : new Double(v);
        v = detector.getAttribute("Offset");
        Double offset =
          v == null || v.trim().length() == 0 ? null : new Double(v);

        boolean active = "1".equals(detector.getAttribute("IsActive"));

        if (active) {
          String c = detector.getAttribute("Channel");
          int channel = c == null ? 0 : Integer.parseInt(c);

          Element multiband = null;

          if (multibands != null) {
            for (int i=0; i<multibands.getLength(); i++) {
              Element mb = (Element) multibands.item(i);
              if (channel == Integer.parseInt(mb.getAttribute("Channel"))) {
                multiband = mb;
                break;
              }
            }
          }

          if (multiband != null) {
            if (nextChannel < channelNames[image].length) {
              channelNames[image][nextChannel] =
                multiband.getAttribute("DyeName");
            }

            double cutIn = new Double(multiband.getAttribute("LeftWorld"));
            double cutOut = new Double(multiband.getAttribute("RightWorld"));
            cutIns[image].add(new PositiveInteger((int) Math.round(cutIn)));
            cutOuts[image].add(new PositiveInteger((int) Math.round(cutOut)));
          }

          if (nextChannel < getEffectiveSizeC()) {
            gains[image][nextChannel] = gain;
            detectorOffsets[image].add(offset);
          }

          nextChannel++;
        }

        if (active) {
          detectorModels[image].add("");
          activeDetector[image].add(active);
        }
      }
    }
  }

  private void translateROIs(Element imageNode, int image)
    throws FormatException
  {
    NodeList rois = getNodes(imageNode, "Annotation");
    if (rois == null) return;
    imageROIs[image] = new ROI[rois.getLength()];

    for (int r=0; r<rois.getLength(); r++) {
      Element roiNode = (Element) rois.item(r);

      ROI roi = new ROI();

      String type = roiNode.getAttribute("type");
      if (type != null) {
        roi.type = Integer.parseInt(type);
      }
      String color = roiNode.getAttribute("color");
      if (color != null) {
        roi.color = Long.parseLong(color);
      }
      roi.name = roiNode.getAttribute("name");
      roi.fontName = roiNode.getAttribute("fontName");
      roi.fontSize = roiNode.getAttribute("fontSize");
      roi.transX = parseDouble(roiNode.getAttribute("transTransX"));
      roi.transY = parseDouble(roiNode.getAttribute("transTransY"));
      roi.scaleX = parseDouble(roiNode.getAttribute("transScalingX"));
      roi.scaleY = parseDouble(roiNode.getAttribute("transScalingY"));
      roi.rotation = parseDouble(roiNode.getAttribute("transRotation"));
      String linewidth = roiNode.getAttribute("linewidth");
      if (linewidth != null) {
        try {
          roi.linewidth = Integer.parseInt(linewidth);
        }
        catch (NumberFormatException e) { }
      }
      roi.text = roiNode.getAttribute("text");

      NodeList vertices = getNodes(roiNode, "Vertex");
      if (vertices == null) {
        continue;
      }

      for (int v=0; v<vertices.getLength(); v++) {
        Element vertex = (Element) vertices.item(v);
        String xx = vertex.getAttribute("x");
        String yy = vertex.getAttribute("y");

        if (xx != null) {
          roi.x.add(parseDouble(xx));
        }
        if (yy != null) {
          roi.y.add(parseDouble(yy));
        }
      }
      imageROIs[image][r] = roi;

      if (getNodes(imageNode, "ROI") != null) {
        alternateCenter = true;
      }
    }
  }

  private void translateSingleROIs(Element imageNode, int image)
    throws FormatException
  {
    if (imageROIs[image] != null) return;
    NodeList children = getNodes(imageNode, "ROI");
    if (children == null) return;
    children = getNodes((Element) children.item(0), "Children");
    if (children == null) return;
    children = getNodes((Element) children.item(0), "Element");
    if (children == null) return;
    imageROIs[image] = new ROI[children.getLength()];

    for (int r=0; r<children.getLength(); r++) {
      NodeList rois = getNodes((Element) children.item(r), "ROISingle");

      Element roiNode = (Element) rois.item(0);
      ROI roi = new ROI();

      String type = roiNode.getAttribute("RoiType");
      if (type != null) {
        roi.type = Integer.parseInt(type);
      }
      String color = roiNode.getAttribute("Color");
      if (color != null) {
        roi.color = Long.parseLong(color);
      }
      Element parent = (Element) roiNode.getParentNode();
      parent = (Element) parent.getParentNode();
      roi.name = parent.getAttribute("Name");

      NodeList vertices = getNodes(roiNode, "P");

      double sizeX = physicalSizeXs.get(image);
      double sizeY = physicalSizeYs.get(image);

      for (int v=0; v<vertices.getLength(); v++) {
        Element vertex = (Element) vertices.item(v);
        String xx = vertex.getAttribute("X");
        String yy = vertex.getAttribute("Y");

        if (xx != null) {
          roi.x.add(parseDouble(xx) / sizeX);
        }
        if (yy != null) {
          roi.y.add(parseDouble(yy) / sizeY);
        }
      }

      Element transform = (Element) getNodes(roiNode, "Transformation").item(0);

      roi.rotation = parseDouble(transform.getAttribute("Rotation"));

      Element scaling = (Element) getNodes(transform, "Scaling").item(0);
      roi.scaleX = parseDouble(scaling.getAttribute("XScale"));
      roi.scaleY = parseDouble(scaling.getAttribute("YScale"));

      Element translation =
        (Element) getNodes(transform, "Translation").item(0);
      roi.transX = parseDouble(translation.getAttribute("X")) / sizeX;
      roi.transY = parseDouble(translation.getAttribute("Y")) / sizeY;

      imageROIs[image][r] = roi;
    }
  }

  private void translateLaserLines(Element imageNode, int image)
    throws FormatException
  {
    NodeList laserLines = getNodes(imageNode, "LaserLineSetting");
    if (laserLines == null) return;

    laserWavelength[image] = new Vector<Integer>();
    laserIntensity[image] = new Vector<Double>();

    for (int laser=0; laser<laserLines.getLength(); laser++) {
      Element laserLine = (Element) laserLines.item(laser);

      String lineIndex = laserLine.getAttribute("LineIndex");
      String qual = laserLine.getAttribute("Qualifier");
      int index = lineIndex == null ? 0 : Integer.parseInt(lineIndex);
      int qualifier = qual == null ? 0: Integer.parseInt(qual);

      index += (2 - (qualifier / 10));
      if (index < 0) index = 0;

      Integer wavelength = new Integer(laserLine.getAttribute("LaserLine"));
      if (index < laserWavelength[image].size()) {
        laserWavelength[image].setElementAt(wavelength, index);
      }
      else {
        for (int i=laserWavelength[image].size(); i<index; i++) {
          laserWavelength[image].add(new Integer(0));
        }
        laserWavelength[image].add(wavelength);
      }

      String intensity = laserLine.getAttribute("IntensityDev");
      double realIntensity = intensity == null ? 0d : new Double(intensity);
      if (realIntensity > 0) {
        realIntensity = 100d - realIntensity;

        if (index < laserIntensity[image].size()) {
          laserIntensity[image].setElementAt(realIntensity, index);
        }
        else {
          for (int i=laserIntensity[image].size(); i<index; i++) {
            laserIntensity[image].add(new Double(0));
          }
          laserIntensity[image].add(realIntensity);
        }
      }
    }
  }

  private void translateTimestamps(Element imageNode, int image)
    throws FormatException
  {
    NodeList timestampNodes = getNodes(imageNode, "TimeStamp");
    if (timestampNodes == null) return;

    timestamps[image] = new double[getImageCount()];

    if (timestampNodes != null) {
      for (int stamp=0; stamp<timestampNodes.getLength(); stamp++) {
        if (stamp < getImageCount()) {
          Element timestamp = (Element) timestampNodes.item(stamp);
          String stampHigh = timestamp.getAttribute("HighInteger");
          String stampLow = timestamp.getAttribute("LowInteger");
          long high = stampHigh == null ? 0 : Long.parseLong(stampHigh);
          long low = stampLow == null ? 0 : Long.parseLong(stampLow);

          long ms = DateTools.getMillisFromTicks(high, low);

          timestamps[image][stamp] = ms / 1000.0;
        }
      }
    }
    acquiredDate[image] = timestamps[image][0];

    NodeList relTimestampNodes = getNodes(imageNode, "RelTimeStamp");
    if (relTimestampNodes != null) {
      for (int stamp=0; stamp<relTimestampNodes.getLength(); stamp++) {
        if (stamp < getImageCount()) {
          Element timestamp = (Element) relTimestampNodes.item(stamp);
          timestamps[image][stamp] =
            new Double(timestamp.getAttribute("Time"));
        }
      }
    }
  }

  private void translateFilterSettings(Element imageNode, int image)
    throws FormatException
  {
    NodeList filterSettings = getNodes(imageNode, "FilterSettingRecord");
    if (filterSettings == null) return;

    activeDetector[image] = new Vector<Boolean>();
    voltages[image] = new Vector<Double>();
    detectorOffsets[image] = new Vector<Double>();
    cutIns[image] = new Vector<PositiveInteger>();
    cutOuts[image] = new Vector<PositiveInteger>();
    filterModels[image] = new Vector<String>();

    for (int i=0; i<filterSettings.getLength(); i++) {
      Element filterSetting = (Element) filterSettings.item(i);

      String object = filterSetting.getAttribute("ObjectName");
      String attribute = filterSetting.getAttribute("Attribute");
      String objectClass = filterSetting.getAttribute("ClassName");
      String variant = filterSetting.getAttribute("Variant");

      if (attribute.equals("NumericalAperture")) {
        lensNA[image] = new Double(variant);
      }
      else if (attribute.equals("OrderNumber")) {
        serialNumber[image] = variant;
      }
      else if (objectClass.equals("CDetectionUnit")) {
        if (attribute.equals("State")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;

          detectorModels[image].add(object);
          activeDetector[image].add(variant.equals("Active"));
        }
        else if (attribute.equals("HighVoltage")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;
          if (channel < voltages[image].size()) {
            voltages[image].setElementAt(new Double(variant), channel);
          }
          else {
            while (channel > voltages[image].size()) {
              voltages[image].add(new Double(0));
            }
            voltages[image].add(new Double(variant));
          }
        }
        else if (attribute.equals("VideoOffset")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;
          detectorOffsets[image].add(new Double(variant));
        }
      }
      else if (attribute.equals("Objective")) {
        StringTokenizer tokens = new StringTokenizer(variant, " ");
        boolean foundMag = false;
        StringBuffer model = new StringBuffer();
        while (!foundMag) {
          String token = tokens.nextToken();
          int x = token.indexOf("x");
          if (x != -1) {
            foundMag = true;

            int mag = (int) Double.parseDouble(token.substring(0, x));
            String na = token.substring(x + 1);

            magnification[image] = mag;
            lensNA[image] = new Double(na);
          }
          else {
            model.append(token);
            model.append(" ");
          }
        }

        String immersion = "Other";
        if (tokens.hasMoreTokens()) {
          immersion = tokens.nextToken();
          if (immersion == null || immersion.trim().equals("")) {
            immersion = "Other";
          }
        }
        immersions[image] = immersion;

        String correction = "Other";
        if (tokens.hasMoreTokens()) {
          correction = tokens.nextToken();
          if (correction == null || correction.trim().equals("")) {
            correction = "Other";
          }
        }
        corrections[image] = correction;

        objectiveModels[image] = model.toString().trim();
      }
      else if (attribute.equals("RefractionIndex")) {
        refractiveIndex[image] = new Double(variant);
      }
      else if (attribute.equals("XPos")) {
        posX[image] = new Double(variant);
      }
      else if (attribute.equals("YPos")) {
        posY[image] = new Double(variant);
      }
      else if (attribute.equals("ZPos")) {
        posZ[image] = new Double(variant);
      }
      else if (objectClass.equals("CSpectrophotometerUnit")) {
        Integer v = null;
        try {
          v = new Integer((int) Double.parseDouble(variant));
        }
        catch (NumberFormatException e) { }
        String description = filterSetting.getAttribute("Description");
        if (description.endsWith("(left)")) {
          filterModels[image].add(object);
          if (v != null) {
            cutIns[image].add(new PositiveInteger(v));
          }
        }
        else if (description.endsWith("(right)")) {
          if (v != null) {
            cutOuts[image].add(new PositiveInteger(v));
          }
        }
      }
    }
  }

  private void translateScannerSettings(Element imageNode, int image)
    throws FormatException
  {
    NodeList scannerSettings = getNodes(imageNode, "ScannerSettingRecord");
    if (scannerSettings == null) return;

    expTimes[image] = new Double[getEffectiveSizeC()];
    gains[image] = new Double[getEffectiveSizeC()];
    channelNames[image] = new String[getEffectiveSizeC()];
    exWaves[image] = new Integer[getEffectiveSizeC()];
    detectorModels[image] = new Vector<String>();

    for (int i=0; i<scannerSettings.getLength(); i++) {
      Element scannerSetting = (Element) scannerSettings.item(i);
      String id = scannerSetting.getAttribute("Identifier");
      if (id == null) id = "";
      String suffix = scannerSetting.getAttribute("Identifier");
      String value = scannerSetting.getAttribute("Variant");

      if (id.equals("SystemType")) {
        microscopeModels[image] = value;
      }
      else if (id.equals("dblPinhole")) {
        pinholes[image] = Double.parseDouble(value) * 1000000;
      }
      else if (id.equals("dblZoom")) {
        zooms[image] = new Double(value);
      }
      else if (id.equals("dblStepSize")) {
        zSteps[image] = Double.parseDouble(value) * 1000000;
      }
      else if (id.equals("nDelayTime_s")) {
        tSteps[image] = new Double(value);
      }
      else if (id.equals("CameraName")) {
        detectorModels[image].add(value);
      }
      else if (id.indexOf("WFC") == 1) {
        int c = 0;
        try {
          c = Integer.parseInt(id.replaceAll("\\D", ""));<commit_msg>Add new metadata keys that record the image orientation.<commit_after>//
// LIFReader.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.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.ImageTools;
import loci.formats.MetadataTools;
import loci.formats.meta.IMetadata;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import loci.formats.services.OMEXMLService;

import ome.xml.model.enums.DetectorType;
import ome.xml.model.enums.LaserMedium;
import ome.xml.model.enums.LaserType;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PercentFraction;
import ome.xml.model.primitives.PositiveInteger;

import org.xml.sax.SAXException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * LIFReader is the file format reader for Leica LIF files.
 *
 * <dl><dt><b>Source code:</b></dt>
 * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/LIFReader.java">Trac</a>,
 * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/LIFReader.java;hb=HEAD">Gitweb</a></dd></dl>
 *
 * @author <NAME> <EMAIL>
 */
public class LIFReader extends FormatReader {

  // -- Constants --

  public static final byte LIF_MAGIC_BYTE = 0x70;
  public static final byte LIF_MEMORY_BYTE = 0x2a;

  private static final HashMap<String, Integer> CHANNEL_PRIORITIES =
    createChannelPriorities();

  private static HashMap<String, Integer> createChannelPriorities() {
    HashMap<String, Integer> h = new HashMap<String, Integer>();

    h.put("red", new Integer(0));
    h.put("green", new Integer(1));
    h.put("blue", new Integer(2));
    h.put("cyan", new Integer(3));
    h.put("magenta", new Integer(4));
    h.put("yellow", new Integer(5));
    h.put("black", new Integer(6));
    h.put("gray", new Integer(7));
    h.put("", new Integer(8));

    return h;
  }

  private static final byte[][][] BYTE_LUTS = createByteLUTs();

  private static byte[][][] createByteLUTs() {
    byte[][][] lut = new byte[9][3][256];
    for (int i=0; i<256; i++) {
      // red
      lut[0][0][i] = (byte) (i & 0xff);
      // green
      lut[1][1][i] = (byte) (i & 0xff);
      // blue
      lut[2][2][i] = (byte) (i & 0xff);

      // cyan
      lut[3][1][i] = (byte) (i & 0xff);
      lut[3][2][i] = (byte) (i & 0xff);

      // magenta
      lut[4][0][i] = (byte) (i & 0xff);
      lut[4][2][i] = (byte) (i & 0xff);

      // yellow
      lut[5][0][i] = (byte) (i & 0xff);
      lut[5][1][i] = (byte) (i & 0xff);

      // gray
      lut[6][0][i] = (byte) (i & 0xff);
      lut[6][1][i] = (byte) (i & 0xff);
      lut[6][2][i] = (byte) (i & 0xff);
      lut[7][0][i] = (byte) (i & 0xff);
      lut[7][1][i] = (byte) (i & 0xff);
      lut[7][2][i] = (byte) (i & 0xff);
      lut[8][0][i] = (byte) (i & 0xff);
      lut[8][1][i] = (byte) (i & 0xff);
      lut[8][2][i] = (byte) (i & 0xff);
    }
    return lut;
  }

  private static final short[][][] SHORT_LUTS = createShortLUTs();

  private static short[][][] createShortLUTs() {
    short[][][] lut = new short[9][3][65536];
    for (int i=0; i<65536; i++) {
      // red
      lut[0][0][i] = (short) (i & 0xffff);
      // green
      lut[1][1][i] = (short) (i & 0xffff);
      // blue
      lut[2][2][i] = (short) (i & 0xffff);

      // cyan
      lut[3][1][i] = (short) (i & 0xffff);
      lut[3][2][i] = (short) (i & 0xffff);

      // magenta
      lut[4][0][i] = (short) (i & 0xffff);
      lut[4][2][i] = (short) (i & 0xffff);

      // yellow
      lut[5][0][i] = (short) (i & 0xffff);
      lut[5][1][i] = (short) (i & 0xffff);

      // gray
      lut[6][0][i] = (short) (i & 0xffff);
      lut[6][1][i] = (short) (i & 0xffff);
      lut[6][2][i] = (short) (i & 0xffff);
      lut[7][0][i] = (short) (i & 0xffff);
      lut[7][1][i] = (short) (i & 0xffff);
      lut[7][2][i] = (short) (i & 0xffff);
      lut[8][0][i] = (short) (i & 0xffff);
      lut[8][1][i] = (short) (i & 0xffff);
      lut[8][2][i] = (short) (i & 0xffff);
    }
    return lut;
  }

  // -- Fields --

  /** Offsets to memory blocks, paired with their corresponding description. */
  private Vector<Long> offsets;

  private int[][] realChannel;
  private int lastChannel = 0;

  private Vector<String> lutNames = new Vector<String>();
  private Vector<Double> physicalSizeXs = new Vector<Double>();
  private Vector<Double> physicalSizeYs = new Vector<Double>();

  private String[] descriptions, microscopeModels, serialNumber;
  private Double[] pinholes, zooms, zSteps, tSteps, lensNA;
  private Double[][] expTimes, gains;
  private Vector[] detectorOffsets;
  private String[][] channelNames;
  private Vector[] detectorModels, voltages;
  private Integer[][] exWaves;
  private Vector[] activeDetector;

  private String[] immersions, corrections, objectiveModels;
  private Integer[] magnification;
  private Double[] posX, posY, posZ;
  private Double[] refractiveIndex;
  private Vector[] cutIns, cutOuts, filterModels;
  private double[][] timestamps;
  private Vector[] laserWavelength, laserIntensity;
  private ROI[][] imageROIs;
  private boolean alternateCenter = false;
  private String[] imageNames;
  private double[] acquiredDate;

  // -- Constructor --

  /** Constructs a new Leica LIF reader. */
  public LIFReader() {
    super("Leica Image File Format", "lif");
    domains = new String[] {FormatTools.LM_DOMAIN};
  }

  // -- IFormatReader API methods --

  /* @see loci.formats.IFormatReader#getOptimalTileHeight() */
  public int getOptimalTileHeight() {
    FormatTools.assertId(currentId, true, 1);
    return getSizeY();
  }

  /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
  public boolean isThisType(RandomAccessInputStream stream) throws IOException {
    final int blockLen = 1;
    if (!FormatTools.validStream(stream, blockLen, true)) return false;
    return stream.read() == LIF_MAGIC_BYTE;
  }

  /* @see loci.formats.IFormatReader#get8BitLookupTable() */
  public byte[][] get8BitLookupTable() {
    FormatTools.assertId(currentId, true, 1);
    if (getPixelType() != FormatTools.UINT8 || !isIndexed()) return null;
    return lastChannel < BYTE_LUTS.length ? BYTE_LUTS[lastChannel] : null;
  }

  /* @see loci.formats.IFormatReader#get16BitLookupTable() */
  public short[][] get16BitLookupTable() {
    FormatTools.assertId(currentId, true, 1);
    if (getPixelType() != FormatTools.UINT16 || !isIndexed()) return null;
    return lastChannel < SHORT_LUTS.length ? SHORT_LUTS[lastChannel] : null;
  }

  /**
   * @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);

    if (!isRGB()) {
      int[] pos = getZCTCoords(no);
      lastChannel = realChannel[series][pos[1]];
    }

    if (series >= offsets.size()) {
      // truncated file; imitate LAS AF and return black planes
      Arrays.fill(buf, (byte) 0);
      return buf;
    }

    long offset = offsets.get(series).longValue();
    int bytes = FormatTools.getBytesPerPixel(getPixelType());
    int bpp = bytes * getRGBChannelCount();

    long planeSize = (long) getSizeX() * getSizeY() * bpp;

    long nextOffset = series + 1 < offsets.size() ?
      offsets.get(series + 1).longValue() : in.length();
    int bytesToSkip = (int) (nextOffset - offset - planeSize * getImageCount());
    bytesToSkip /= getSizeY();
    if ((getSizeX() % 4) == 0) bytesToSkip = 0;

    if (offset + (planeSize + bytesToSkip * getSizeY()) * no >= in.length()) {
      // truncated file; imitate LAS AF and return black planes
      Arrays.fill(buf, (byte) 0);
      return buf;
    }

    in.seek(offset + planeSize * no);
    in.skipBytes(bytesToSkip * getSizeY() * no);

    if (bytesToSkip == 0) {
      readPlane(in, x, y, w, h, buf);
    }
    else {
      in.skipBytes(y * (getSizeX() * bpp + bytesToSkip));
      for (int row=0; row<h; row++) {
        in.skipBytes(x * bpp);
        in.read(buf, row * w * bpp, w * bpp);
        in.skipBytes(bpp * (getSizeX() - w - x) + bytesToSkip);
      }
    }

    // color planes are stored in BGR order
    if (getRGBChannelCount() == 3) {
      ImageTools.bgrToRgb(buf, isInterleaved(), bytes, getRGBChannelCount());
    }

    return buf;
  }

  /* @see loci.formats.IFormatReader#close(boolean) */
  public void close(boolean fileOnly) throws IOException {
    super.close(fileOnly);
    if (!fileOnly) {
      offsets = null;
      realChannel = null;
      lastChannel = 0;
      lutNames.clear();
      physicalSizeXs.clear();
      physicalSizeYs.clear();
      descriptions = microscopeModels = serialNumber = null;
      pinholes = zooms = lensNA = null;
      zSteps = tSteps = null;
      expTimes = gains = null;
      detectorOffsets = null;
      channelNames = null;
      detectorModels = voltages = null;
      exWaves = null;
      activeDetector = null;
      immersions = corrections = null;
      magnification = null;
      objectiveModels = null;
      posX = posY = posZ = null;
      refractiveIndex = null;
      cutIns = cutOuts = filterModels = null;
      timestamps = null;
      laserWavelength = laserIntensity = null;
      imageROIs = null;
      alternateCenter = false;
      imageNames = null;
      acquiredDate = null;
    }
  }

  // -- Internal FormatReader API methods --

  /* @see loci.formats.FormatReader#initFile(String) */
  protected void initFile(String id) throws FormatException, IOException {
    super.initFile(id);
    in = new RandomAccessInputStream(id);
    offsets = new Vector<Long>();

    in.order(true);

    // read the header

    LOGGER.info("Reading header");

    byte checkOne = in.readByte();
    in.skipBytes(2);
    byte checkTwo = in.readByte();
    if (checkOne != LIF_MAGIC_BYTE && checkTwo != LIF_MAGIC_BYTE) {
      throw new FormatException(id + " is not a valid Leica LIF file");
    }

    in.skipBytes(4);

    // read and parse the XML description

    if (in.read() != LIF_MEMORY_BYTE) {
      throw new FormatException("Invalid XML description");
    }

    // number of Unicode characters in the XML block
    int nc = in.readInt();
    String xml = DataTools.stripString(in.readString(nc * 2));

    LOGGER.info("Finding image offsets");

    while (in.getFilePointer() < in.length()) {
      LOGGER.debug("Looking for a block at {}; {} blocks read",
        in.getFilePointer(), offsets.size());
      int check = in.readInt();
      if (check != LIF_MAGIC_BYTE) {
        throw new FormatException("Invalid Memory Block: found magic bytes " +
          check + ", expected " + LIF_MAGIC_BYTE);
      }

      in.skipBytes(4);
      check = in.read();
      if (check != LIF_MEMORY_BYTE) {
        throw new FormatException("Invalid Memory Description: found magic " +
          "byte " + check + ", expected " + LIF_MEMORY_BYTE);
      }

      long blockLength = in.readInt();
      if (in.read() != LIF_MEMORY_BYTE) {
        in.seek(in.getFilePointer() - 5);
        blockLength = in.readLong();
        check = in.read();
        if (check != LIF_MEMORY_BYTE) {
          throw new FormatException("Invalid Memory Description: found magic " +
            "byte " + check + ", expected " + LIF_MEMORY_BYTE);
        }
      }

      int descrLength = in.readInt() * 2;

      if (blockLength > 0) {
        offsets.add(new Long(in.getFilePointer() + descrLength));
      }

      in.seek(in.getFilePointer() + descrLength + blockLength);
    }
    initMetadata(xml);
    xml = null;

    // correct offsets, if necessary
    if (offsets.size() > getSeriesCount()) {
      Long[] storedOffsets = offsets.toArray(new Long[offsets.size()]);
      offsets.clear();
      int index = 0;
      for (int i=0; i<getSeriesCount(); i++) {
        setSeries(i);
        long nBytes = (long) FormatTools.getPlaneSize(this) * getImageCount();
        long start = storedOffsets[index];
        long end = index == storedOffsets.length - 1 ? in.length() :
          storedOffsets[index + 1];
        while (end - start < nBytes && ((end - start) / nBytes) != 1) {
          index++;
          start = storedOffsets[index];
          end = index == storedOffsets.length - 1 ? in.length() :
            storedOffsets[index + 1];
        }
        offsets.add(storedOffsets[index]);
        index++;
      }
      setSeries(0);
    }
  }

  // -- Helper methods --

  /** Parses a string of XML and puts the values in a Hashtable. */
  private void initMetadata(String xml) throws FormatException, IOException {
    IMetadata omexml = null;
    try {
      ServiceFactory factory = new ServiceFactory();
      OMEXMLService service = factory.getInstance(OMEXMLService.class);
      omexml = service.createOMEXMLMetadata();
    }
    catch (DependencyException exc) {
      throw new FormatException("Could not create OME-XML store.", exc);
    }
    catch (ServiceException exc) {
      throw new FormatException("Could not create OME-XML store.", exc);
    }
    MetadataStore store = makeFilterMetadata();
    MetadataLevel level = getMetadataOptions().getMetadataLevel();

    // the XML blocks stored in a LIF file are invalid,
    // because they don't have a root node

    xml = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><LEICA>" + xml +
      "</LEICA>";

    xml = XMLTools.sanitizeXML(xml);

    translateMetadata(getMetadataRoot(xml));

    for (int i=0; i<imageNames.length; i++) {
      setSeries(i);
      addSeriesMeta("Image name", imageNames[i]);
    }
    setSeries(0);

    // set up mapping to rearrange channels
    // for instance, the green channel may be #0, and the red channel may be #1
    realChannel = new int[getSeriesCount()][];
    int nextLut = 0;
    for (int i=0; i<getSeriesCount(); i++) {
      realChannel[i] = new int[core[i].sizeC];

      for (int q=0; q<core[i].sizeC; q++) {
        String lut = lutNames.get(nextLut++).toLowerCase();
        if (!CHANNEL_PRIORITIES.containsKey(lut)) lut = "";
        realChannel[i][q] = CHANNEL_PRIORITIES.get(lut).intValue();
      }

      int[] sorted = new int[core[i].sizeC];
      Arrays.fill(sorted, -1);

      for (int q=0; q<sorted.length; q++) {
        int min = Integer.MAX_VALUE;
        int minIndex = -1;
        for (int n=0; n<core[i].sizeC; n++) {
          if (realChannel[i][n] < min && !DataTools.containsValue(sorted, n)) {
            min = realChannel[i][n];
            minIndex = n;
          }
        }

        sorted[q] = minIndex;
      }
    }

    MetadataTools.populatePixels(store, this, true, false);

    for (int i=0; i<getSeriesCount(); i++) {
      setSeries(i);

      String instrumentID = MetadataTools.createLSID("Instrument", i);
      store.setInstrumentID(instrumentID, i);

      store.setMicroscopeModel(microscopeModels[i], i);
      store.setMicroscopeType(getMicroscopeType("Unknown"), i);

      String objectiveID = MetadataTools.createLSID("Objective", i, 0);
      store.setObjectiveID(objectiveID, i, 0);
      store.setObjectiveLensNA(lensNA[i], i, 0);
      store.setObjectiveSerialNumber(serialNumber[i], i, 0);
      if (magnification[i] != null && magnification[i] > 0) {
        store.setObjectiveNominalMagnification(
          new PositiveInteger(magnification[i]), i, 0);
      }
      store.setObjectiveImmersion(getImmersion(immersions[i]), i, 0);
      store.setObjectiveCorrection(getCorrection(corrections[i]), i, 0);
      store.setObjectiveModel(objectiveModels[i], i, 0);

      if (cutIns[i] != null) {
        for (int filter=0; filter<cutIns[i].size(); filter++) {
          String filterID = MetadataTools.createLSID("Filter", i, filter);
          store.setFilterID(filterID, i, filter);
          if (filter < filterModels[i].size()) {
            store.setFilterModel(
              (String) filterModels[i].get(filter), i, filter);
          }
          int channel = filter - (cutIns[i].size() - getEffectiveSizeC());
          if (channel >= 0 && channel < getEffectiveSizeC()) {
            store.setLightPathEmissionFilterRef(filterID, i, channel, 0);
          }
          store.setTransmittanceRangeCutIn(
            (PositiveInteger) cutIns[i].get(filter), i, filter);
          store.setTransmittanceRangeCutOut(
            (PositiveInteger) cutOuts[i].get(filter), i, filter);
        }
      }

      Vector lasers = laserWavelength[i];
      Vector laserIntensities = laserIntensity[i];
      int nextChannel = getEffectiveSizeC() - 1;
      if (lasers != null) {
        for (int laser=0; laser<lasers.size(); laser++) {
          String id = MetadataTools.createLSID("LightSource", i, laser);
          store.setLaserID(id, i, laser);
          store.setLaserType(LaserType.OTHER, i, laser);
          store.setLaserLaserMedium(LaserMedium.OTHER, i, laser);
          Integer wavelength = (Integer) lasers.get(laser);
          if (wavelength > 0) {
            store.setLaserWavelength(new PositiveInteger(wavelength), i, laser);
          }

          if (laser < laserIntensities.size()) {
            double intensity = (Double) laserIntensities.get(laser);
            if (intensity < 100 && nextChannel >= 0 && wavelength != 0) {
              store.setChannelLightSourceSettingsID(id, i, nextChannel);
              store.setChannelLightSourceSettingsAttenuation(
                new PercentFraction((float) intensity / 100f), i, nextChannel);
              store.setChannelExcitationWavelength(
                new PositiveInteger(wavelength), i, nextChannel);

              nextChannel--;
            }
          }
        }
      }

      store.setImageInstrumentRef(instrumentID, i);
      store.setImageObjectiveSettingsID(objectiveID, i);
      store.setImageObjectiveSettingsRefractiveIndex(refractiveIndex[i], i);

      store.setImageDescription(descriptions[i], i);
      if (acquiredDate[i] > 0) {
        store.setImageAcquiredDate(DateTools.convertDate(
          (long) (acquiredDate[i] * 1000), DateTools.COBOL), i);
      }
      store.setImageName(imageNames[i].trim(), i);

      if (physicalSizeXs.get(i) > 0) {
        store.setPixelsPhysicalSizeX(
          new PositiveFloat(physicalSizeXs.get(i)), i);
      }
      if (physicalSizeYs.get(i) > 0) {
        store.setPixelsPhysicalSizeY(
          new PositiveFloat(physicalSizeYs.get(i)), i);
      }
      if (zSteps[i] != null && zSteps[i] > 0) {
        store.setPixelsPhysicalSizeZ(new PositiveFloat(zSteps[i]), i);
      }
      store.setPixelsTimeIncrement(tSteps[i], i);

      Vector detectors = detectorModels[i];
      if (detectors != null) {
        nextChannel = 0;
        for (int detector=0; detector<detectors.size(); detector++) {
          String detectorID = MetadataTools.createLSID("Detector", i, detector);
          store.setDetectorID(detectorID, i, detector);
          store.setDetectorModel((String) detectors.get(detector), i, detector);

          store.setDetectorZoom(zooms[i], i, detector);
          store.setDetectorType(DetectorType.PMT, i, detector);
          if (voltages[i] != null && detector < voltages[i].size()) {
            store.setDetectorVoltage(
              (Double) voltages[i].get(detector), i, detector);
          }

          if (activeDetector[i] != null) {
            if (detector < activeDetector[i].size() &&
              (Boolean) activeDetector[i].get(detector) &&
              detectorOffsets[i] != null &&
              nextChannel < detectorOffsets[i].size())
            {
              store.setDetectorOffset(
                (Double) detectorOffsets[i].get(nextChannel++), i, detector);
            }
          }
        }
      }

      Vector activeDetectors = activeDetector[i];
      int nextDetector = 0;

      for (int c=0; c<getEffectiveSizeC(); c++) {
        if (activeDetectors != null) {
          while (nextDetector < activeDetectors.size() &&
            !(Boolean) activeDetectors.get(nextDetector))
          {
            nextDetector++;
          }
          if (nextDetector < activeDetectors.size()) {
            String detectorID =
              MetadataTools.createLSID("Detector", i, nextDetector);
            store.setDetectorSettingsID(detectorID, i, c);
            nextDetector++;

            if (detectorOffsets[i] != null && c < detectorOffsets[i].size()) {
              store.setDetectorSettingsOffset(
                (Double) detectorOffsets[i].get(c), i, c);
            }

            if (gains[i] != null) {
              store.setDetectorSettingsGain(gains[i][c], i, c);
            }
          }
        }

        if (channelNames[i] != null) {
          store.setChannelName(channelNames[i][c], i, c);
        }
        store.setChannelPinholeSize(pinholes[i], i, c);
        if (exWaves[i] != null && exWaves[i][c] != null && exWaves[i][c] > 1) {
          store.setChannelExcitationWavelength(
            new PositiveInteger(exWaves[i][c]), i, c);
        }
        if (expTimes[i] != null) {
          store.setPlaneExposureTime(expTimes[i][c], i, c);
        }
      }

      for (int image=0; image<getImageCount(); image++) {
        store.setPlanePositionX(posX[i], i, image);
        store.setPlanePositionY(posY[i], i, image);
        store.setPlanePositionZ(posZ[i], i, image);
        if (timestamps[i] != null) {
          double timestamp = timestamps[i][image];
          if (timestamps[i][0] == acquiredDate[i]) {
            timestamp -= acquiredDate[i];
          }
          store.setPlaneDeltaT(timestamp, i, image);
        }
      }

      if (imageROIs[i] != null) {
        for (int roi=0; roi<imageROIs[i].length; roi++) {
          if (imageROIs[i][roi] != null) {
            imageROIs[i][roi].storeROI(store, i, roi);
          }
        }
      }
    }
  }

  private Element getMetadataRoot(String xml)
    throws FormatException, IOException
  {
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder parser = factory.newDocumentBuilder();
      ByteArrayInputStream s = new ByteArrayInputStream(xml.getBytes());
      Element root = parser.parse(s).getDocumentElement();
      s.close();
      return root;
    }
    catch (ParserConfigurationException e) {
      throw new FormatException(e);
    }
    catch (SAXException e) {
      throw new FormatException(e);
    }
  }

  private void translateMetadata(Element root) throws FormatException {
    Element realRoot = (Element) root.getChildNodes().item(0);

    NodeList toPrune = getNodes(realRoot, "LDM_Block_Sequential_Master");
    if (toPrune != null) {
      for (int i=0; i<toPrune.getLength(); i++) {
        Element prune = (Element) toPrune.item(i);
        Element parent = (Element) prune.getParentNode();
        parent.removeChild(prune);
      }
    }

    NodeList imageNodes = getNodes(realRoot, "Image");

    core = new CoreMetadata[imageNodes.getLength()];
    acquiredDate = new double[imageNodes.getLength()];
    descriptions = new String[imageNodes.getLength()];
    laserWavelength = new Vector[imageNodes.getLength()];
    laserIntensity = new Vector[imageNodes.getLength()];
    timestamps = new double[imageNodes.getLength()][];
    activeDetector = new Vector[imageNodes.getLength()];
    voltages = new Vector[imageNodes.getLength()];
    detectorOffsets = new Vector[imageNodes.getLength()];
    serialNumber = new String[imageNodes.getLength()];
    lensNA = new Double[imageNodes.getLength()];
    magnification = new Integer[imageNodes.getLength()];
    immersions = new String[imageNodes.getLength()];
    corrections = new String[imageNodes.getLength()];
    objectiveModels = new String[imageNodes.getLength()];
    posX = new Double[imageNodes.getLength()];
    posY = new Double[imageNodes.getLength()];
    posZ = new Double[imageNodes.getLength()];
    refractiveIndex = new Double[imageNodes.getLength()];
    cutIns = new Vector[imageNodes.getLength()];
    cutOuts = new Vector[imageNodes.getLength()];
    filterModels = new Vector[imageNodes.getLength()];
    microscopeModels = new String[imageNodes.getLength()];
    detectorModels = new Vector[imageNodes.getLength()];
    zSteps = new Double[imageNodes.getLength()];
    tSteps = new Double[imageNodes.getLength()];
    pinholes = new Double[imageNodes.getLength()];
    zooms = new Double[imageNodes.getLength()];

    expTimes = new Double[imageNodes.getLength()][];
    gains = new Double[imageNodes.getLength()][];
    channelNames = new String[imageNodes.getLength()][];
    exWaves = new Integer[imageNodes.getLength()][];
    imageROIs = new ROI[imageNodes.getLength()][];
    imageNames = new String[imageNodes.getLength()];

    for (int i=0; i<imageNodes.getLength(); i++) {
      Element image = (Element) imageNodes.item(i);
      setSeries(i);

      translateImageNames(image, i);
      translateImageNodes(image, i);
      translateAttachmentNodes(image, i);
      translateScannerSettings(image, i);
      translateFilterSettings(image, i);
      translateTimestamps(image, i);
      translateLaserLines(image, i);
      translateROIs(image, i);
      translateSingleROIs(image, i);
      translateDetectors(image, i);

      Stack<String> nameStack = new Stack<String>();
      HashMap<String, Integer> indexes = new HashMap<String, Integer>();
      populateOriginalMetadata(image, nameStack, indexes);
      indexes.clear();
    }
    setSeries(0);
  }

  private void populateOriginalMetadata(Element root, Stack<String> nameStack,
    HashMap<String, Integer> indexes)
  {
    String name = root.getNodeName();
    if (root.hasAttributes() && !name.equals("Element") &&
      !name.equals("Attachment") && !name.equals("LMSDataContainerHeader"))
    {
      nameStack.push(name);

      String suffix = root.getAttribute("Identifier");
      String value = root.getAttribute("Variant");
      if (suffix == null || suffix.trim().length() == 0) {
        suffix = root.getAttribute("Description");
      }
      StringBuffer key = new StringBuffer();
      for (String k : nameStack) {
        key.append(k);
        key.append("|");
      }
      if (suffix != null && value != null && suffix.length() > 0 &&
        value.length() > 0 && !suffix.equals("HighInteger") &&
        !suffix.equals("LowInteger"))
      {
        Integer i = indexes.get(key.toString() + suffix);
        String storedKey = key.toString() + suffix + " " + (i == null ? 0 : i);
        indexes.put(key.toString() + suffix, i == null ? 1 : i + 1);
        addSeriesMeta(storedKey, value);
      }
      else {
        NamedNodeMap attributes = root.getAttributes();
        for (int i=0; i<attributes.getLength(); i++) {
          Attr attr = (Attr) attributes.item(i);
          if (!attr.getName().equals("HighInteger") &&
            !attr.getName().equals("LowInteger"))
          {
            Integer index = indexes.get(key.toString() + attr.getName());
            if (index == null) {
              index = 0;
            }
            String storedKey = key.toString() + attr.getName() + " " + index;
            indexes.put(key.toString() + attr.getName(), index + 1);
            addSeriesMeta(storedKey, attr.getValue());
          }
        }
      }
    }

    NodeList children = root.getChildNodes();
    for (int i=0; i<children.getLength(); i++) {
      Object child = children.item(i);
      if (child instanceof Element) {
        populateOriginalMetadata((Element) child, nameStack, indexes);
      }
    }

    if (root.hasAttributes() && !name.equals("Element") &&
      !name.equals("Attachment") && !name.equals("LMSDataContainerHeader"))
    {
      nameStack.pop();
    }
  }

  private void translateImageNames(Element imageNode, int image) {
    Vector<String> names = new Vector<String>();
    Element parent = imageNode;
    while (true) {
      parent = (Element) parent.getParentNode();
      if (parent == null || parent.getNodeName().equals("LEICA")) {
        break;
      }
      if (parent.getNodeName().equals("Element")) {
        names.add(parent.getAttribute("Name"));
      }
    }
    imageNames[image] = "";
    for (int i=names.size() - 2; i>=0; i--) {
      imageNames[image] += names.get(i);
      if (i > 0) imageNames[image] += "/";
    }
  }

  private void translateDetectors(Element imageNode, int image)
    throws FormatException
  {
    NodeList definitions = getNodes(imageNode, "ATLConfocalSettingDefinition");
    if (definitions == null) return;

    int nextChannel = 0;
    for (int definition=0; definition<definitions.getLength(); definition++) {
      Element definitionNode = (Element) definitions.item(definition);
      NodeList detectors = getNodes(definitionNode, "Detector");
      if (detectors == null) return;

      for (int d=0; d<detectors.getLength(); d++) {
        Element detector = (Element) detectors.item(d);
        NodeList multibands = getNodes(definitionNode, "MultiBand");

        String v = detector.getAttribute("Gain");
        Double gain =
          v == null || v.trim().length() == 0 ? null : new Double(v);
        v = detector.getAttribute("Offset");
        Double offset =
          v == null || v.trim().length() == 0 ? null : new Double(v);

        boolean active = "1".equals(detector.getAttribute("IsActive"));

        if (active) {
          String c = detector.getAttribute("Channel");
          int channel = c == null ? 0 : Integer.parseInt(c);

          Element multiband = null;

          if (multibands != null) {
            for (int i=0; i<multibands.getLength(); i++) {
              Element mb = (Element) multibands.item(i);
              if (channel == Integer.parseInt(mb.getAttribute("Channel"))) {
                multiband = mb;
                break;
              }
            }
          }

          if (multiband != null) {
            if (nextChannel < channelNames[image].length) {
              channelNames[image][nextChannel] =
                multiband.getAttribute("DyeName");
            }

            double cutIn = new Double(multiband.getAttribute("LeftWorld"));
            double cutOut = new Double(multiband.getAttribute("RightWorld"));
            cutIns[image].add(new PositiveInteger((int) Math.round(cutIn)));
            cutOuts[image].add(new PositiveInteger((int) Math.round(cutOut)));
          }

          if (nextChannel < getEffectiveSizeC()) {
            gains[image][nextChannel] = gain;
            detectorOffsets[image].add(offset);
          }

          nextChannel++;
        }

        if (active) {
          detectorModels[image].add("");
          activeDetector[image].add(active);
        }
      }
    }
  }

  private void translateROIs(Element imageNode, int image)
    throws FormatException
  {
    NodeList rois = getNodes(imageNode, "Annotation");
    if (rois == null) return;
    imageROIs[image] = new ROI[rois.getLength()];

    for (int r=0; r<rois.getLength(); r++) {
      Element roiNode = (Element) rois.item(r);

      ROI roi = new ROI();

      String type = roiNode.getAttribute("type");
      if (type != null) {
        roi.type = Integer.parseInt(type);
      }
      String color = roiNode.getAttribute("color");
      if (color != null) {
        roi.color = Long.parseLong(color);
      }
      roi.name = roiNode.getAttribute("name");
      roi.fontName = roiNode.getAttribute("fontName");
      roi.fontSize = roiNode.getAttribute("fontSize");
      roi.transX = parseDouble(roiNode.getAttribute("transTransX"));
      roi.transY = parseDouble(roiNode.getAttribute("transTransY"));
      roi.scaleX = parseDouble(roiNode.getAttribute("transScalingX"));
      roi.scaleY = parseDouble(roiNode.getAttribute("transScalingY"));
      roi.rotation = parseDouble(roiNode.getAttribute("transRotation"));
      String linewidth = roiNode.getAttribute("linewidth");
      if (linewidth != null) {
        try {
          roi.linewidth = Integer.parseInt(linewidth);
        }
        catch (NumberFormatException e) { }
      }
      roi.text = roiNode.getAttribute("text");

      NodeList vertices = getNodes(roiNode, "Vertex");
      if (vertices == null) {
        continue;
      }

      for (int v=0; v<vertices.getLength(); v++) {
        Element vertex = (Element) vertices.item(v);
        String xx = vertex.getAttribute("x");
        String yy = vertex.getAttribute("y");

        if (xx != null) {
          roi.x.add(parseDouble(xx));
        }
        if (yy != null) {
          roi.y.add(parseDouble(yy));
        }
      }
      imageROIs[image][r] = roi;

      if (getNodes(imageNode, "ROI") != null) {
        alternateCenter = true;
      }
    }
  }

  private void translateSingleROIs(Element imageNode, int image)
    throws FormatException
  {
    if (imageROIs[image] != null) return;
    NodeList children = getNodes(imageNode, "ROI");
    if (children == null) return;
    children = getNodes((Element) children.item(0), "Children");
    if (children == null) return;
    children = getNodes((Element) children.item(0), "Element");
    if (children == null) return;
    imageROIs[image] = new ROI[children.getLength()];

    for (int r=0; r<children.getLength(); r++) {
      NodeList rois = getNodes((Element) children.item(r), "ROISingle");

      Element roiNode = (Element) rois.item(0);
      ROI roi = new ROI();

      String type = roiNode.getAttribute("RoiType");
      if (type != null) {
        roi.type = Integer.parseInt(type);
      }
      String color = roiNode.getAttribute("Color");
      if (color != null) {
        roi.color = Long.parseLong(color);
      }
      Element parent = (Element) roiNode.getParentNode();
      parent = (Element) parent.getParentNode();
      roi.name = parent.getAttribute("Name");

      NodeList vertices = getNodes(roiNode, "P");

      double sizeX = physicalSizeXs.get(image);
      double sizeY = physicalSizeYs.get(image);

      for (int v=0; v<vertices.getLength(); v++) {
        Element vertex = (Element) vertices.item(v);
        String xx = vertex.getAttribute("X");
        String yy = vertex.getAttribute("Y");

        if (xx != null) {
          roi.x.add(parseDouble(xx) / sizeX);
        }
        if (yy != null) {
          roi.y.add(parseDouble(yy) / sizeY);
        }
      }

      Element transform = (Element) getNodes(roiNode, "Transformation").item(0);

      roi.rotation = parseDouble(transform.getAttribute("Rotation"));

      Element scaling = (Element) getNodes(transform, "Scaling").item(0);
      roi.scaleX = parseDouble(scaling.getAttribute("XScale"));
      roi.scaleY = parseDouble(scaling.getAttribute("YScale"));

      Element translation =
        (Element) getNodes(transform, "Translation").item(0);
      roi.transX = parseDouble(translation.getAttribute("X")) / sizeX;
      roi.transY = parseDouble(translation.getAttribute("Y")) / sizeY;

      imageROIs[image][r] = roi;
    }
  }

  private void translateLaserLines(Element imageNode, int image)
    throws FormatException
  {
    NodeList laserLines = getNodes(imageNode, "LaserLineSetting");
    if (laserLines == null) return;

    laserWavelength[image] = new Vector<Integer>();
    laserIntensity[image] = new Vector<Double>();

    for (int laser=0; laser<laserLines.getLength(); laser++) {
      Element laserLine = (Element) laserLines.item(laser);

      String lineIndex = laserLine.getAttribute("LineIndex");
      String qual = laserLine.getAttribute("Qualifier");
      int index = lineIndex == null ? 0 : Integer.parseInt(lineIndex);
      int qualifier = qual == null ? 0: Integer.parseInt(qual);

      index += (2 - (qualifier / 10));
      if (index < 0) index = 0;

      Integer wavelength = new Integer(laserLine.getAttribute("LaserLine"));
      if (index < laserWavelength[image].size()) {
        laserWavelength[image].setElementAt(wavelength, index);
      }
      else {
        for (int i=laserWavelength[image].size(); i<index; i++) {
          laserWavelength[image].add(new Integer(0));
        }
        laserWavelength[image].add(wavelength);
      }

      String intensity = laserLine.getAttribute("IntensityDev");
      double realIntensity = intensity == null ? 0d : new Double(intensity);
      if (realIntensity > 0) {
        realIntensity = 100d - realIntensity;

        if (index < laserIntensity[image].size()) {
          laserIntensity[image].setElementAt(realIntensity, index);
        }
        else {
          for (int i=laserIntensity[image].size(); i<index; i++) {
            laserIntensity[image].add(new Double(0));
          }
          laserIntensity[image].add(realIntensity);
        }
      }
    }
  }

  private void translateTimestamps(Element imageNode, int image)
    throws FormatException
  {
    NodeList timestampNodes = getNodes(imageNode, "TimeStamp");
    if (timestampNodes == null) return;

    timestamps[image] = new double[getImageCount()];

    if (timestampNodes != null) {
      for (int stamp=0; stamp<timestampNodes.getLength(); stamp++) {
        if (stamp < getImageCount()) {
          Element timestamp = (Element) timestampNodes.item(stamp);
          String stampHigh = timestamp.getAttribute("HighInteger");
          String stampLow = timestamp.getAttribute("LowInteger");
          long high = stampHigh == null ? 0 : Long.parseLong(stampHigh);
          long low = stampLow == null ? 0 : Long.parseLong(stampLow);

          long ms = DateTools.getMillisFromTicks(high, low);

          timestamps[image][stamp] = ms / 1000.0;
        }
      }
    }
    acquiredDate[image] = timestamps[image][0];

    NodeList relTimestampNodes = getNodes(imageNode, "RelTimeStamp");
    if (relTimestampNodes != null) {
      for (int stamp=0; stamp<relTimestampNodes.getLength(); stamp++) {
        if (stamp < getImageCount()) {
          Element timestamp = (Element) relTimestampNodes.item(stamp);
          timestamps[image][stamp] =
            new Double(timestamp.getAttribute("Time"));
        }
      }
    }
  }

  private void translateFilterSettings(Element imageNode, int image)
    throws FormatException
  {
    NodeList filterSettings = getNodes(imageNode, "FilterSettingRecord");
    if (filterSettings == null) return;

    activeDetector[image] = new Vector<Boolean>();
    voltages[image] = new Vector<Double>();
    detectorOffsets[image] = new Vector<Double>();
    cutIns[image] = new Vector<PositiveInteger>();
    cutOuts[image] = new Vector<PositiveInteger>();
    filterModels[image] = new Vector<String>();

    for (int i=0; i<filterSettings.getLength(); i++) {
      Element filterSetting = (Element) filterSettings.item(i);

      String object = filterSetting.getAttribute("ObjectName");
      String attribute = filterSetting.getAttribute("Attribute");
      String objectClass = filterSetting.getAttribute("ClassName");
      String variant = filterSetting.getAttribute("Variant");

      if (attribute.equals("NumericalAperture")) {
        lensNA[image] = new Double(variant);
      }
      else if (attribute.equals("OrderNumber")) {
        serialNumber[image] = variant;
      }
      else if (objectClass.equals("CDetectionUnit")) {
        if (attribute.equals("State")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;

          detectorModels[image].add(object);
          activeDetector[image].add(variant.equals("Active"));
        }
        else if (attribute.equals("HighVoltage")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;
          if (channel < voltages[image].size()) {
            voltages[image].setElementAt(new Double(variant), channel);
          }
          else {
            while (channel > voltages[image].size()) {
              voltages[image].add(new Double(0));
            }
            voltages[image].add(new Double(variant));
          }
        }
        else if (attribute.equals("VideoOffset")) {
          int channel = getChannelIndex(filterSetting);
          if (channel < 0) continue;
          detectorOffsets[image].add(new Double(variant));
        }
      }
      else if (attribute.equals("Objective")) {
        StringTokenizer tokens = new StringTokenizer(variant, " ");
        boolean foundMag = false;
        StringBuffer model = new StringBuffer();
        while (!foundMag) {
          String token = tokens.nextToken();
          int x = token.indexOf("x");
          if (x != -1) {
            foundMag = true;

            int mag = (int) Double.parseDouble(token.substring(0, x));
            String na = token.substring(x + 1);

            magnification[image] = mag;
            lensNA[image] = new Double(na);
          }
          else {
            model.append(token);
            model.append(" ");
          }
        }

        String immersion = "Other";
        if (tokens.hasMoreTokens()) {
          immersion = tokens.nextToken();
          if (immersion == null || immersion.trim().equals("")) {
            immersion = "Other";
          }
        }
        immersions[image] = immersion;

        String correction = "Other";
        if (tokens.hasMoreTokens()) {
          correction = tokens.nextToken();
          if (correction == null || correction.trim().equals("")) {
            correction = "Other";
          }
        }
        corrections[image] = correction;

        objectiveModels[image] = model.toString().trim();
      }
      else if (attribute.equals("RefractionIndex")) {
        refractiveIndex[image] = new Double(variant);
      }
      else if (attribute.equals("XPos")) {
        posX[image] = new Double(variant);
      }
      else if (attribute.equals("YPos")) {
        posY[image] = new Double(variant);
      }
      else if (attribute.equals("ZPos")) {
        posZ[image] = new Double(variant);
      }
      else if (objectClass.equals("CSpectrophotometerUnit")) {
        Integer v = null;
        try {
          v = new Integer((int) Double.parseDouble(variant));
        }
        catch (NumberFormatException e) { }
        String description = filterSetting.getAttribute("Description");
        if (description.endsWith("(left)")) {
          filterModels[image].add(object);
          if (v != null) {
            cutIns[image].add(new PositiveInteger(v));
          }
        }
        else if (description.endsWith("(right)")) {
          if (v != null) {
            cutOuts[image].add(new PositiveInteger(v));
          }
        }
      }
    }
  }

  private void translateScannerSettings(Element imageNode, int image)
    throws FormatException
  {
    NodeList scannerSettings = getNodes(imageNode, "ScannerSettingRecord");
    if (scannerSettings == null) return;

    expTimes[image] = new Double[getEffectiveSizeC()];
    gains[image] = new Double[getEffectiveSizeC()];
    channelNames[image] = new String[getEffectiveSizeC()];
    exWaves[image] = new Integer[getEffectiveSizeC()];
    detectorModels[image] = new Vector<String>();

    for (int i=0; i<scannerSettings.getLength(); i++) {
      Element scannerSetting = (Element) scannerSettings.item(i);
      String id = scannerSetting.getAttribute("Identifier");
      if (id == null) id = "";
      String suffix = scannerSetting.getAttribute("Identifier");
      String value = scannerSetting.getAttribute("Variant");

      if (id.equals("SystemType")) {
        microscopeModels[image] = value;
      }
      else if (id.equals("dblPinhole")) {
        pinholes[image] = Double.parseDouble(value) * 1000000;
      }
      else if (id.equals("dblZoom")) {
        zooms[image] = new Double(value);
      }
      else if (id.equals("dblStepSize")) {
        zSteps[image] = Double.parseDouble(value) * 1000000;
      }
      else if (id.equals("nDelayTime_s")) {
        tSteps[image] = new Double(value);
      }
      else if (id.equals("CameraName")) {
        detectorModels[image].add(value);
      }
      else if (id.equals("eDirectional")) {
        addSeriesMeta("Reverse X orientation", value.equals("1"));
      }
      else if (id.equals("eDirectionalY")) {
        addSeriesMeta("Reverse Y orientation", value.equals("1"));
      }
      else if (id.indexOf("WFC") == 1) {
        int c = 0;
        try {
          c = Integer.parseInt(id.replaceAll("\\D", ""));
{'fraction_non_alphanumeric': 0.14138921344879174, 'fraction_numerical': 0.007731198881815303, 'mean_word_length': 2.737419907766396, 'pattern_counts': {'":': 0, '<': 216, '<?xml version=': 2, '>': 132, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 52}, 'pii_count': 0, 'substrings_counts': 0, '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': '2765590', 'n_tokens_mistral': 28510, 'n_tokens_neox': 27770, 'n_words': 6160}