/* * #%L * OME Bio-Formats package for reading and converting biological file formats. * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package loci.formats.in; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import loci.common.DataTools; import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.formats.AxisGuesser; import loci.formats.CoreMetadata; import loci.formats.FilePattern; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.meta.MetadataStore; import loci.formats.tiff.IFD; import loci.formats.tiff.IFDList; import loci.formats.tiff.TiffConstants; import loci.formats.tiff.TiffParser; import ome.xml.model.enums.Correction; import ome.xml.model.enums.Immersion; import ome.xml.model.primitives.Color; import ome.xml.model.primitives.PositiveFloat; import ome.xml.model.primitives.PositiveInteger; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.ElectricPotential; import ome.units.quantity.Length; import ome.units.quantity.Time; import ome.units.UNITS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; /** * LeicaReader is the file format reader for Leica files. * * @author Melissa Linkert melissa at glencoesoftware.com */ public class LeicaReader extends FormatReader { // -- Constants - private static final Logger LOGGER = LoggerFactory.getLogger(LeicaReader.class); public static final String[] LEI_SUFFIX = {"lei"}; /** All Leica TIFFs have this tag. */ private static final int LEICA_MAGIC_TAG = 33923; /** Format for dates. */ private static final String DATE_FORMAT = "yyyy:MM:dd,HH:mm:ss"; /** IFD tags. */ private static final Integer SERIES = 10; private static final Integer IMAGES = 15; private static final Integer DIMDESCR = 20; private static final Integer FILTERSET = 30; private static final Integer TIMEINFO = 40; private static final Integer SCANNERSET = 50; private static final Integer EXPERIMENT = 60; private static final Integer LUTDESC = 70; private static final Integer CHANDESC = 80; private static final Integer SEQUENTIALSET = 90; private static final Integer SEQ_SCANNERSET = 200; private static final Integer SEQ_FILTERSET = 700; private static final int SEQ_SCANNERSET_END = 300; private static final int SEQ_FILTERSET_END = 800; private static final ImmutableMap<Integer, String> DIMENSION_NAMES = makeDimensionTable(); // -- Fields -- protected IFDList ifds; /** Array of IFD-like structures containing metadata. */ protected IFDList headerIFDs; /** Helper readers. */ protected MinimalTiffReader tiff; /** Array of image file names. */ protected List<String>[] files; /** Number of series in the file. */ private int numSeries; /** Name of current LEI file */ private String leiFilename; /** Length of each file name. */ private int fileLength; private boolean[] valid; private String[][] timestamps; private List<String> seriesNames; private List<String> seriesDescriptions; private int lastPlane = 0, nameLength = 0; private double[][] physicalSizes; private double[] pinhole, exposureTime; private int nextDetector = 0, nextChannel = 0; private List<Integer> activeChannelIndices = new ArrayList<Integer>(); private boolean sequential = false; private List[] channelNames; private List[] emWaves; private List[] exWaves; private boolean[][] cutInPopulated; private boolean[][] cutOutPopulated; private boolean[][] filterRefPopulated; private List<Detector> detectors = new ArrayList<Detector>(); private int[] tileWidth, tileHeight; private Color[][] channelColor; // -- Constructor -- /** Constructs a new Leica reader. */ public LeicaReader() { super("Leica", new String[] {"lei", "tif", "tiff", "raw"}); domains = new String[] {FormatTools.LM_DOMAIN}; hasCompanionFiles = true; datasetDescription = "One .lei file with at least one .tif/.tiff file " + "and an optional .txt file"; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isSingleFile(String) */ @Override public boolean isSingleFile(String id) throws FormatException, IOException { return false; } /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @Override public boolean isThisType(String name, boolean open) { if (checkSuffix(name, LEI_SUFFIX)) return true; if (!checkSuffix(name, TiffReader.TIFF_SUFFIXES) && !checkSuffix(name, "raw")) { return false; } if (!open) return false; // not allowed to touch the file system // check for that there is an .lei file in the same directory String prefix = name; if (prefix.indexOf(".") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf(".")); } Location lei = new Location(prefix + ".lei"); if (!lei.exists()) { lei = new Location(prefix + ".LEI"); while (!lei.exists() && prefix.indexOf("_") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf("_")); lei = new Location(prefix + ".lei"); if (!lei.exists()) lei = new Location(prefix + ".LEI"); } } return lei.exists(); } /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { TiffParser tp = new TiffParser(stream); IFD ifd = tp.getFirstIFD(); if (ifd == null) return false; return ifd.containsKey(new Integer(LEICA_MAGIC_TAG)); } /* @see loci.formats.IFormatReader#get8BitLookupTable() */ @Override public byte[][] get8BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); try { int index = (int) Math.min(lastPlane, files[getSeries()].size() - 1); tiff.setId(files[getSeries()].get(index)); return tiff.get8BitLookupTable(); } catch (FormatException e) { LOGGER.debug("Failed to retrieve lookup table", e); } catch (IOException e) { LOGGER.debug("Failed to retrieve lookup table", e); } return null; } /* @see loci.formats.IFormatReader#get16BitLookupTable() */ @Override public short[][] get16BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); try { int index = (int) Math.min(lastPlane, files[getSeries()].size() - 1); tiff.setId(files[getSeries()].get(index)); return tiff.get16BitLookupTable(); } catch (FormatException e) { LOGGER.debug("Failed to retrieve lookup table", e); } catch (IOException e) { LOGGER.debug("Failed to retrieve lookup table", e); } return null; } /* @see loci.formats.IFormatReader#fileGroupOption(String) */ @Override public int fileGroupOption(String id) throws FormatException, IOException { return FormatTools.MUST_GROUP; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); lastPlane = no; int fileIndex = no < files[getSeries()].size() ? no : 0; int planeIndex = no < files[getSeries()].size() ? 0 : no; String filename = (String) files[getSeries()].get(fileIndex); if (new Location(filename).exists()) { if (checkSuffix(filename, TiffReader.TIFF_SUFFIXES)) { tiff.setId(filename); return tiff.openBytes(planeIndex, buf, x, y, w, h); } else { RandomAccessInputStream s = new RandomAccessInputStream(filename); s.seek(planeIndex * FormatTools.getPlaneSize(this)); readPlane(s, x, y, w, h, buf); s.close(); } } // imitate Leica's software and return a blank plane if the // appropriate TIFF file is missing return buf; } /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */ @Override public String[] getSeriesUsedFiles(boolean noPixels) { FormatTools.assertId(currentId, true, 1); final List<String> v = new ArrayList<String>(); if (leiFilename != null) v.add(leiFilename); if (!noPixels && files != null) { for (Object file : files[getSeries()]) { if (file != null && new Location((String) file).exists()) { v.add((String) file); } } } return v.toArray(new String[v.size()]); } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (tiff != null) tiff.close(fileOnly); if (!fileOnly) { leiFilename = null; files = null; ifds = headerIFDs = null; tiff = null; seriesNames = null; numSeries = 0; lastPlane = 0; physicalSizes = null; seriesDescriptions = null; pinhole = exposureTime = null; nextDetector = 0; nextChannel = 0; sequential = false; activeChannelIndices.clear(); channelNames = null; emWaves = null; exWaves = null; cutInPopulated = null; cutOutPopulated = null; filterRefPopulated = null; detectors.clear(); } } /* @see loci.formats.IFormatReader#getOptimalTileWidth() */ @Override public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); if (tileWidth != null && tileWidth[getSeries()] != 0) { return tileWidth[getSeries()]; } return super.getOptimalTileWidth(); } /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); if (tileHeight != null && tileHeight[getSeries()] != 0) { return tileHeight[getSeries()]; } return super.getOptimalTileHeight(); } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { close(); String leiFile = findLEIFile(id); if (leiFile == null || leiFile.trim().length() == 0 || new Location(leiFile).isDirectory()) { if (checkSuffix(id, TiffReader.TIFF_SUFFIXES)) { super.initFile(id); TiffReader r = new TiffReader(); r.setMetadataStore(getMetadataStore()); r.setId(id); core = new ArrayList<CoreMetadata>(r.getCoreMetadataList()); metadataStore = r.getMetadataStore(); final Map<String, Object> globalMetadata = r.getGlobalMetadata(); for (final Map.Entry<String, Object> entry : globalMetadata.entrySet()) { addGlobalMeta(entry.getKey(), entry.getValue()); } r.close(); files = new List[] {new ArrayList<String>()}; files[0].add(id); tiff = new MinimalTiffReader(); return; } else { throw new FormatException("LEI file not found."); } } // parse the LEI file super.initFile(leiFile); leiFilename = new File(leiFile).exists()? new Location(leiFile).getAbsolutePath() : id; in = new RandomAccessInputStream(leiFile); byte[] data = null; try { data = new byte[(int) in.length()]; in.read(data); } finally { in.close(); } in = new RandomAccessInputStream(data); MetadataLevel metadataLevel = metadataOptions.getMetadataLevel(); seriesNames = new ArrayList<String>(); byte[] fourBytes = new byte[4]; in.read(fourBytes); core.get(0).littleEndian = (fourBytes[0] == TiffConstants.LITTLE && fourBytes[1] == TiffConstants.LITTLE && fourBytes[2] == TiffConstants.LITTLE && fourBytes[3] == TiffConstants.LITTLE); boolean realLittleEndian = isLittleEndian(); in.order(isLittleEndian()); LOGGER.info("Reading metadata blocks"); in.skipBytes(8); int addr = in.readInt(); headerIFDs = new IFDList(); while (addr != 0) { IFD ifd = new IFD(); headerIFDs.add(ifd); in.seek(addr + 4); int tag = in.readInt(); while (tag != 0) { // create the IFD structure int offset = in.readInt(); long pos = in.getFilePointer(); in.seek(offset + 12); int size = in.readInt(); ifd.putIFDValue(tag, in.getFilePointer()); in.seek(pos); tag = in.readInt(); } addr = in.readInt(); } numSeries = headerIFDs.size(); tileWidth = new int[numSeries]; tileHeight = new int[numSeries]; core.clear(); for (int i=0; i<numSeries; i++) { core.add(new CoreMetadata()); } files = new List[numSeries]; channelNames = new List[getSeriesCount()]; emWaves = new List[getSeriesCount()]; exWaves = new List[getSeriesCount()]; cutInPopulated = new boolean[getSeriesCount()][]; cutOutPopulated = new boolean[getSeriesCount()][]; filterRefPopulated = new boolean[getSeriesCount()][]; for (int i=0; i<getSeriesCount(); i++) { channelNames[i] = new ArrayList(); emWaves[i] = new ArrayList(); exWaves[i] = new ArrayList(); } // determine the length of a filename LOGGER.info("Parsing metadata blocks"); core.get(0).littleEndian = !isLittleEndian(); int seriesIndex = 0; int invalidCount = 0; valid = new boolean[numSeries]; timestamps = new String[headerIFDs.size()][]; for (int i=0; i<headerIFDs.size(); i++) { IFD ifd = headerIFDs.get(i); valid[i] = true; if (ifd.get(SERIES) != null) { long offset = ((Long) ifd.get(SERIES)).longValue(); in.seek(offset + 8); nameLength = in.readInt() * 2; } in.seek(((Long) ifd.get(IMAGES)).longValue()); parseFilenames(i); if (!valid[i]) invalidCount++; } numSeries -= invalidCount; if (numSeries <= 0) { throw new FormatException("TIFF files not found"); } int[] count = new int[getSeriesCount()]; for (int i=0; i<getSeriesCount(); i++) { count[i] = core.get(i).imageCount; } final List<String>[] tempFiles = files; IFDList tempIFDs = headerIFDs; core = new ArrayList<CoreMetadata>(numSeries); files = new List[numSeries]; headerIFDs = new IFDList(); int index = 0; core.clear(); for (int i=0; i<numSeries; i++) { CoreMetadata ms = new CoreMetadata(); while (index < valid.length && !valid[index]) index++; if (index >= valid.length) { break; } ms.imageCount = count[index]; files[i] = tempFiles[index]; Collections.sort(files[i]); headerIFDs.add(tempIFDs.get(index)); index++; core.add(ms); } tiff = new MinimalTiffReader(); LOGGER.info("Populating metadata"); if (headerIFDs == null) headerIFDs = ifds; seriesDescriptions = new ArrayList<String>(); physicalSizes = new double[headerIFDs.size()][5]; pinhole = new double[headerIFDs.size()]; exposureTime = new double[headerIFDs.size()]; channelColor = new Color[headerIFDs.size()][]; for (int i=0; i<headerIFDs.size(); i++) { IFD ifd = headerIFDs.get(i); CoreMetadata ms = core.get(i); ms.littleEndian = isLittleEndian(); setSeries(i); Integer[] keys = ifd.keySet().toArray(new Integer[ifd.size()]); Arrays.sort(keys); for (Integer key : keys) { long offset = ((Long) ifd.get(key)).longValue(); in.seek(offset); if (key.equals(SERIES)) { parseSeriesTag(); } else if (key.equals(IMAGES)) { parseImageTag(i); } else if (key.equals(DIMDESCR)) { parseDimensionTag(i); } else if (key.equals(TIMEINFO) && metadataLevel != MetadataLevel.MINIMUM) { parseTimeTag(i); } else if (key.equals(EXPERIMENT) && metadataLevel != MetadataLevel.MINIMUM) { parseExperimentTag(); } else if (key.equals(LUTDESC)) { parseLUT(i); } else if (key.equals(CHANDESC) && metadataLevel != MetadataLevel.MINIMUM) { parseChannelTag(); } } ms.orderCertain = true; ms.littleEndian = isLittleEndian(); ms.falseColor = true; ms.metadataComplete = true; ms.interleaved = false; String filename = (String) files[i].get(0); if (checkSuffix(filename, TiffReader.TIFF_SUFFIXES)) { RandomAccessInputStream s = new RandomAccessInputStream(filename, 16); try { TiffParser parser = new TiffParser(s); parser.setDoCaching(false); IFD firstIFD = parser.getFirstIFD(); parser.fillInIFD(firstIFD); ms.sizeX = (int) firstIFD.getImageWidth(); ms.sizeY = (int) firstIFD.getImageLength(); // override the .lei pixel type, in case a TIFF file was overwritten ms.pixelType = firstIFD.getPixelType(); // don't worry about changing the endianness when it // won't affect the pixel data if (FormatTools.getBytesPerPixel(ms.pixelType) > 1) { ms.littleEndian = firstIFD.isLittleEndian(); } else { ms.littleEndian = realLittleEndian; } tileWidth[i] = (int) firstIFD.getTileWidth(); tileHeight[i] = (int) firstIFD.getTileLength(); } finally { s.close(); } } else { ms.littleEndian = realLittleEndian; } } for (int i=0; i<getSeriesCount(); i++) { setSeries(i); CoreMetadata ms = core.get(i); if (getSizeZ() == 0) ms.sizeZ = 1; if (getSizeT() == 0) ms.sizeT = 1; if (getSizeC() == 0) ms.sizeC = 1; if (getImageCount() == 0) ms.imageCount = 1; if (getImageCount() == 1 && getSizeZ() * getSizeT() > 1) { ms.sizeZ = 1; ms.sizeT = 1; } if (getSizeY() == 1 || getSizeY() == getSizeZ() || getSizeY() == getSizeT()) { // XZ or XT scan if (getSizeZ() > 1 && getImageCount() == getSizeC() * getSizeT()) { ms.sizeY = getSizeZ(); ms.sizeZ = 1; } else if (getSizeT() > 1 && getImageCount() == getSizeC() * getSizeZ()) { ms.sizeY = getSizeT(); ms.sizeT = 1; } } if (isRGB()) ms.indexed = false; ms.dimensionOrder = MetadataTools.makeSaneDimensionOrder(getDimensionOrder()); } MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, true); // Ensure we populate Image names before returning due to a possible // minimum metadata level. for (int i=0; i<getSeriesCount(); i++) { store.setImageName(seriesNames.get(i), i); } if (metadataLevel == MetadataLevel.MINIMUM) return; for (int i=0; i<getSeriesCount(); i++) { CoreMetadata ms = core.get(i); IFD ifd = headerIFDs.get(i); long firstPlane = 0; if (i < timestamps.length && timestamps[i] != null && timestamps[i].length > 0) { firstPlane = DateTools.getTime(timestamps[i][0], DATE_FORMAT, ":"); String date = DateTools.formatDate(timestamps[i][0], DATE_FORMAT); if (date != null) { store.setImageAcquisitionDate(new Timestamp(date), i); } } store.setImageDescription(seriesDescriptions.get(i), i); String instrumentID = MetadataTools.createLSID("Instrument", i); store.setInstrumentID(instrumentID, i); // parse instrument data nextDetector = 0; nextChannel = 0; detectors.clear(); cutInPopulated[i] = new boolean[ms.sizeC]; cutOutPopulated[i] = new boolean[ms.sizeC]; filterRefPopulated[i] = new boolean[ms.sizeC]; Integer[] keys = ifd.keySet().toArray(new Integer[ifd.size()]); Arrays.sort(keys); int nextInstrumentBlock = 1; sequential = DataTools.indexOf(keys, SEQ_SCANNERSET) != -1; for (Integer key : keys) { if (key.equals(FILTERSET) || key.equals(SCANNERSET) || key.equals(SEQ_SCANNERSET) || key.equals(SEQ_FILTERSET) || (key > SEQ_SCANNERSET && key < SEQ_SCANNERSET_END) || (key > SEQ_FILTERSET && key < SEQ_FILTERSET_END)) { if (sequential && (key.equals(FILTERSET) || key.equals(SCANNERSET))) { continue; } long offset = ((Long) ifd.get(key)).longValue(); in.seek(offset); setSeries(i); parseInstrumentData(store, nextInstrumentBlock++); } } activeChannelIndices.clear(); // link Instrument and Image store.setImageInstrumentRef(instrumentID, i); Length sizeX = FormatTools.getPhysicalSizeX(physicalSizes[i][0]); Length sizeY = FormatTools.getPhysicalSizeY(physicalSizes[i][1]); Length sizeZ = FormatTools.getPhysicalSizeZ(physicalSizes[i][2]); if (sizeX != null) { store.setPixelsPhysicalSizeX(sizeX, i); } if (sizeY != null) { store.setPixelsPhysicalSizeY(sizeY, i); } if (sizeZ != null) { store.setPixelsPhysicalSizeZ(sizeZ, i); } if ((int) physicalSizes[i][4] > 0) { store.setPixelsTimeIncrement(new Time(physicalSizes[i][4], UNITS.S), i); } for (int j=0; j<ms.imageCount; j++) { if (timestamps[i] != null && j < timestamps[i].length) { long time = DateTools.getTime(timestamps[i][j], DATE_FORMAT, ":"); double elapsedTime = (double) (time - firstPlane) / 1000; store.setPlaneDeltaT(new Time(elapsedTime, UNITS.S), i, j); if (exposureTime[i] > 0) { store.setPlaneExposureTime(new Time(exposureTime[i], UNITS.S), i, j); } } } } setSeries(0); } // -- Helper methods -- /** Find the .lei file that belongs to the same dataset as the given file. */ private String findLEIFile(String baseFile) throws FormatException, IOException { if (checkSuffix(baseFile, LEI_SUFFIX)) { return baseFile; } else if (checkSuffix(baseFile, TiffReader.TIFF_SUFFIXES) && isGroupFiles()) { // need to find the associated .lei file if (ifds == null) super.initFile(baseFile); in = new RandomAccessInputStream(baseFile, 16); TiffParser tp = new TiffParser(in); in.order(tp.checkHeader().booleanValue()); in.seek(0); LOGGER.info("Finding companion file name"); // open the TIFF file and look for the "Image Description" field ifds = tp.getIFDs(); if (ifds == null) throw new FormatException("No IFDs found"); String descr = ifds.get(0).getComment(); // remove anything of the form "[blah]" descr = descr.replaceAll("\\[.*.\\]\n", ""); // each remaining line in descr is a (key, value) pair, // where '=' separates the key from the value String lei = baseFile.substring(0, baseFile.lastIndexOf(File.separator) + 1); StringTokenizer lines = new StringTokenizer(descr, "\n"); String line = null, key = null, value = null; while (lines.hasMoreTokens()) { line = lines.nextToken(); if (line.indexOf("=") == -1) continue; key = line.substring(0, line.indexOf("=")).trim(); value = line.substring(line.indexOf("=") + 1).trim(); addGlobalMeta(key, value); if (key.startsWith("Series Name")) lei += value; } // now open the LEI file Location l = new Location(lei).getAbsoluteFile(); if (l.exists()) { return lei; } else { if (!lei.endsWith("lei") && !lei.endsWith("LEI")) { lei = lei.substring(0, lei.lastIndexOf(".") + 1); String test = lei + "lei"; if (new Location(test).exists()) { return test; } test = lei + "LEI"; if (new Location(test).exists()) { return test; } } l = l.getParentFile(); String[] list = l.list(); for (int i=0; i<list.length; i++) { if (checkSuffix(list[i], LEI_SUFFIX)) { return new Location(l.getAbsolutePath(), list[i]).getAbsolutePath(); } } } } else if (checkSuffix(baseFile, "raw") && isGroupFiles()) { // check for that there is an .lei file in the same directory String prefix = baseFile; if (prefix.indexOf(".") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf(".")); } Location lei = new Location(prefix + ".lei"); if (!lei.exists()) { lei = new Location(prefix + ".LEI"); while (!lei.exists() && prefix.indexOf("_") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf("_")); lei = new Location(prefix + ".lei"); if (!lei.exists()) lei = new Location(prefix + ".LEI"); } } if (lei.exists()) return lei.getAbsolutePath(); } return null; } private String[] getTIFFList() { File dirFile = new File(currentId).getAbsoluteFile(); String[] listing = null; if (dirFile.exists()) { listing = dirFile.getParentFile().list(); } else { listing = Location.getIdMap().keySet().toArray(new String[0]); } Arrays.sort(listing); final List<String> list = new ArrayList<String>(); for (int k=0; k<listing.length; k++) { if (checkSuffix(listing[k], TiffReader.TIFF_SUFFIXES)) { list.add(listing[k]); } } return list.toArray(new String[list.size()]); } private void parseFilenames(int seriesIndex) throws IOException { int maxPlanes = 0; final List<String> f = new ArrayList<String>(); int tempImages = in.readInt(); if (((long) tempImages * nameLength) > in.length()) { in.order(!isLittleEndian()); tempImages = in.readInt(); in.order(isLittleEndian()); } CoreMetadata ms = core.get(seriesIndex); ms.sizeX = in.readInt(); ms.sizeY = in.readInt(); in.skipBytes(4); int samplesPerPixel = in.readInt(); ms.rgb = samplesPerPixel > 1; ms.sizeC = samplesPerPixel; boolean tiffsExist = false; String dirPrefix = new Location(currentId).getAbsoluteFile().getParent(); if (!dirPrefix.endsWith(File.separator)) dirPrefix += File.separator; String prefix = ""; for (int j=0; j<tempImages; j++) { // read in each filename prefix = getString(nameLength); f.add(dirPrefix + prefix); // test to make sure the path is valid Location test = new Location(f.get(f.size() - 1)).getAbsoluteFile(); LOGGER.debug("Expected to find TIFF file {}", test.getAbsolutePath()); if (!test.exists()) { LOGGER.debug(" file does not exist"); } if (!tiffsExist) tiffsExist = test.exists(); } // all of the TIFF files were renamed if (!tiffsExist) { // Strategy for handling renamed files: // 1) Assume that files for each series follow a pattern. // 2) Assign each file group to the first series with the correct count. LOGGER.info("Handling renamed TIFF files"); String[] listing = getTIFFList(); // grab the file patterns final List<String> filePatterns = new ArrayList<String>(); for (String q : listing) { Location l = new Location(q).getAbsoluteFile(); if (!l.exists()) { l = new Location(dirPrefix, q).getAbsoluteFile(); } FilePattern pattern = new FilePattern(l); if (!pattern.isValid()) continue; AxisGuesser guess = new AxisGuesser(pattern, "XYZCT", 1, 1, 1, false); String fp = pattern.getPattern(); if (guess.getAxisCountS() >= 1) { String pre = pattern.getPrefix(guess.getAxisCountS()); final List<String> fileList = new ArrayList<String>(); for (int n=0; n<listing.length; n++) { Location p = new Location(dirPrefix, listing[n]); if (p.getAbsolutePath().startsWith(pre)) { fileList.add(listing[n]); } } fp = FilePattern.findPattern(l.getAbsolutePath(), dirPrefix, fileList.toArray(new String[fileList.size()])); } if (fp != null && !filePatterns.contains(fp)) { filePatterns.add(fp); } } for (String q : filePatterns) { String[] pattern = new FilePattern(q).getFiles(); if (pattern.length == tempImages) { // make sure that this pattern hasn't already been used boolean validPattern = true; for (int n=0; n<seriesIndex; n++) { if (files[n] == null) continue; if (files[n].contains(pattern[0])) { validPattern = false; break; } } if (validPattern) { files[seriesIndex] = new ArrayList<String>(); files[seriesIndex].addAll(Arrays.asList(pattern)); } } } } else { files[seriesIndex] = f; // invalidate any previous series that tried to use this set of files for (int s=0; s<seriesIndex; s++) { if (files[s] != null) { if (files[s].get(0).equals(f.get(0))) { valid[s] = false; files[s] = null; } } } } if (files[seriesIndex] == null) valid[seriesIndex] = false; else { ms.imageCount = files[seriesIndex].size(); maxPlanes = (int) Math.max(maxPlanes, ms.imageCount); } } private void parseSeriesTag() throws IOException { addSeriesMeta("Version", in.readInt()); addSeriesMeta("Number of Series", in.readInt()); fileLength = in.readInt(); addSeriesMeta("Length of filename", fileLength); int extLen = in.readInt(); if (extLen > fileLength) { in.seek(8); core.get(0).littleEndian = !isLittleEndian(); in.order(isLittleEndian()); fileLength = in.readInt(); extLen = in.readInt(); } addSeriesMeta("Length of file extension", extLen); addSeriesMeta("Image file extension", getString(extLen)); } private void parseImageTag(int seriesIndex) throws IOException { CoreMetadata ms = core.get(seriesIndex); ms.imageCount = in.readInt(); ms.sizeX = in.readInt(); ms.sizeY = in.readInt(); addSeriesMeta("Number of images", getImageCount()); addSeriesMeta("Image width", getSizeX()); addSeriesMeta("Image height", getSizeY()); addSeriesMeta("Bits per Sample", in.readInt()); addSeriesMeta("Samples per pixel", in.readInt()); String name = getString(fileLength * 2); if (name.indexOf(".") != -1) { name = name.substring(0, name.lastIndexOf(".")); } String[] tokens = name.split("_"); StringBuffer buf = new StringBuffer(); for (int p=1; p<tokens.length; p++) { String lcase = tokens[p].toLowerCase(); if (!lcase.startsWith("ch0") && !lcase.startsWith("c0") && !lcase.startsWith("z0") && !lcase.startsWith("t0") && !lcase.startsWith("y0") && !lcase.startsWith("la0")) { if (buf.length() > 0) buf.append("_"); buf.append(tokens[p]); } } seriesNames.add(buf.toString()); } private void parseDimensionTag(int seriesIndex) throws FormatException, IOException { CoreMetadata ms = core.get(seriesIndex); addSeriesMeta("Voxel Version", in.readInt()); ms.rgb = in.readInt() == 20; addSeriesMeta("VoxelType", isRGB() ? "RGB" : "gray"); int bpp = in.readInt(); addSeriesMeta("Bytes per pixel", bpp); if (bpp % 3 == 0) { ms.sizeC = 3; ms.rgb = true; bpp /= 3; } ms.pixelType = FormatTools.pixelTypeFromBytes(bpp, false, false); ms.dimensionOrder = "XY"; int resolution = in.readInt(); ms.bitsPerPixel = resolution; addSeriesMeta("Real world resolution", resolution); addSeriesMeta("Maximum voxel intensity", getString(true)); addSeriesMeta("Minimum voxel intensity", getString(true)); int len = in.readInt(); in.skipBytes(len * 2 + 4); len = in.readInt(); for (int j=0; j<len; j++) { int dimId = in.readInt(); String dimType = DIMENSION_NAMES.get(dimId); if (dimType == null) dimType = ""; int size = in.readInt(); int distance = in.readInt(); int strlen = in.readInt() * 2; String[] sizeData = getString(strlen).split(" "); String physicalSize = sizeData[0]; String unit = ""; if (sizeData.length > 1) unit = sizeData[1]; double physical = Double.parseDouble(physicalSize) / size; if (unit.equals("m")) { physical *= 1000000; } if (dimType.equals("x")) { ms.sizeX = size; physicalSizes[seriesIndex][0] = physical; } else if (dimType.equals("y")) { ms.sizeY = size; physicalSizes[seriesIndex][1] = physical; } else if (dimType.equals("channel")) { if (getSizeC() == 0) ms.sizeC = 1; ms.sizeC *= size; if (getDimensionOrder().indexOf("C") == -1) { ms.dimensionOrder += "C"; } physicalSizes[seriesIndex][3] = physical; } else if (dimType.equals("z")) { ms.sizeZ = size; if (getDimensionOrder().indexOf("Z") == -1) { ms.dimensionOrder += "Z"; } physicalSizes[seriesIndex][2] = physical; } else { ms.sizeT = size; if (getDimensionOrder().indexOf("T") == -1) { ms.dimensionOrder += "T"; } physicalSizes[seriesIndex][4] = physical; } String dimPrefix = "Dim" + j; addSeriesMeta(dimPrefix + " type", dimType); addSeriesMeta(dimPrefix + " size", size); addSeriesMeta(dimPrefix + " distance between sub-dimensions", distance); addSeriesMeta(dimPrefix + " physical length", physicalSize + " " + unit); addSeriesMeta(dimPrefix + " physical origin", getString(true)); } addSeriesMeta("Series name", getString(false)); String description = getString(false); seriesDescriptions.add(description); addSeriesMeta("Series description", description); } private void parseTimeTag(int seriesIndex) throws IOException { int nDims = in.readInt(); addSeriesMeta("Number of time-stamped dimensions", nDims); addSeriesMeta("Time-stamped dimension", in.readInt()); for (int j=0; j<nDims; j++) { String dimPrefix = "Dimension " + j; addSeriesMeta(dimPrefix + " ID", in.readInt()); addSeriesMeta(dimPrefix + " size", in.readInt()); addSeriesMeta(dimPrefix + " distance", in.readInt()); } int numStamps = in.readInt(); addSeriesMeta("Number of time-stamps", numStamps); timestamps[seriesIndex] = new String[numStamps]; for (int j=0; j<numStamps; j++) { timestamps[seriesIndex][j] = DateTools.convertDate( DateTools.getTime(getString(64), DATE_FORMAT, ":"), DateTools.UNIX, DATE_FORMAT + ":SSS"); addSeriesMetaList("Timestamp", timestamps[seriesIndex][j]); } if (in.getFilePointer() < in.length()) { int numTMs = in.readInt(); addSeriesMeta("Number of time-markers", numTMs); for (int j=0; j<numTMs; j++) { if (in.getFilePointer() + 4 >= in.length()) break; int numDims = in.readInt(); String time = "Time-marker " + j + " Dimension "; for (int k=0; k<numDims; k++) { if (in.getFilePointer() + 4 < in.length()) { addSeriesMeta(time + k + " coordinate", in.readInt()); } else break; } if (in.getFilePointer() >= in.length()) break; addSeriesMetaList("Time-marker", getString(64)); } } } private void parseExperimentTag() throws IOException { in.skipBytes(8); String description = getString(true); addSeriesMeta("Image Description", description); addSeriesMeta("Main file extension", getString(true)); addSeriesMeta("Image format identifier", getString(true)); addSeriesMeta("Single image extension", getString(true)); } private void parseLUT(int seriesIndex) throws IOException { int nChannels = in.readInt(); if (nChannels > 0) core.get(seriesIndex).indexed = true; addSeriesMeta("Number of LUT channels", nChannels); addSeriesMeta("ID of colored dimension", in.readInt()); channelColor[seriesIndex] = new Color[nChannels]; for (int j=0; j<nChannels; j++) { String p = "LUT Channel " + j; addSeriesMeta(p + " version", in.readInt()); addSeriesMeta(p + " inverted?", in.read() == 1); addSeriesMeta(p + " description", getString(false)); addSeriesMeta(p + " filename", getString(false)); String lut = getString(false); addSeriesMeta(p + " name", lut); channelColor[seriesIndex][j] = new Color(255, 255, 255, 255); if (lut.equalsIgnoreCase("red")) { channelColor[seriesIndex][j] = new Color(255, 0, 0, 255); } else if (lut.equalsIgnoreCase("green")) { channelColor[seriesIndex][j] = new Color(0, 255, 0, 255); } else if (lut.equalsIgnoreCase("blue")) { channelColor[seriesIndex][j] = new Color(0, 0, 255, 255); } else if (lut.equalsIgnoreCase("yellow")) { channelColor[seriesIndex][j] = new Color(255, 255, 0, 255); } else if (lut.equalsIgnoreCase("cyan")) { channelColor[seriesIndex][j] = new Color(0, 255, 255, 255); } else if (lut.equalsIgnoreCase("magenta")) { channelColor[seriesIndex][j] = new Color(255, 0, 255, 255); } in.skipBytes(8); } } private void parseChannelTag() throws IOException { int nBands = in.readInt(); for (int band=0; band<nBands; band++) { String p = "Band #" + (band + 1) + " "; addSeriesMeta(p + "Lower wavelength", in.readDouble()); in.skipBytes(4); addSeriesMeta(p + "Higher wavelength", in.readDouble()); in.skipBytes(4); addSeriesMeta(p + "Gain", in.readDouble()); addSeriesMeta(p + "Offset", in.readDouble()); } } private void parseInstrumentData(MetadataStore store, int blockNum) throws FormatException, IOException { int series = getSeries(); // read 24 byte SAFEARRAY in.skipBytes(4); int cbElements = in.readInt(); in.skipBytes(8); int nElements = in.readInt(); in.skipBytes(4); long initialOffset = in.getFilePointer(); long elementOffset = 0; LOGGER.trace("Element LOOP; series {} at offset {}", series, initialOffset); for (int j=0; j<nElements; j++) { elementOffset = initialOffset + j * cbElements; LOGGER.trace("Seeking to: {}", elementOffset); in.seek(elementOffset); String contentID = getString(128); LOGGER.trace("contentID: {}", contentID); String description = getString(64); LOGGER.trace("description: {}", description); String data = getString(64); int dataType = in.readShort(); LOGGER.trace("dataType: {}", dataType); in.skipBytes(6); // read data switch (dataType) { case 2: data = String.valueOf(in.readShort()); break; case 3: data = String.valueOf(in.readInt()); break; case 4: data = String.valueOf(in.readFloat()); break; case 5: data = String.valueOf(in.readDouble()); break; case 7: case 11: data = in.read() == 0 ? "false" : "true"; break; case 17: data = in.readString(1); break; } LOGGER.trace("data: {}", data); if (data.trim().length() == 0) { LOGGER.trace("Zero length dat string, continuing..."); continue; } String[] tokens = contentID.split("\\|"); LOGGER.trace("Parsing tokens: {}", tokens); if (tokens[0].startsWith("CDetectionUnit")) { if (tokens[1].startsWith("PMT")) { // detector information Detector detector = new Detector(); int lastDetector = detectors.size() - 1; if (detectors.size() > 0 && detectors.get(lastDetector).id == Integer.parseInt(tokens[3])) { detector = detectors.get(lastDetector); } else { detectors.add(detector); } detector.id = Integer.parseInt(tokens[3]); detector.name = tokens[1]; try { if (tokens[2].equals("VideoOffset")) { detector.offset = new Double(data); } else if (tokens[2].equals("HighVoltage")) { detector.voltage = new Double(data); } else if (tokens[2].equals("State")) { detector.active = data.equals("Active"); String index = tokens[1].substring(tokens[1].indexOf(" ") + 1); detector.index = -1; try { detector.index = Integer.parseInt(index) - 1; } catch (NumberFormatException e) { } if (detector.active && detector.index >= 0) { activeChannelIndices.add(detector.index); } } } catch (NumberFormatException e) { LOGGER.debug("Failed to parse detector metadata", e); } } } else if (tokens[0].startsWith("CTurret")) { // objective information int objective = Integer.parseInt(tokens[3]); if (tokens[2].equals("NumericalAperture")) { store.setObjectiveLensNA(new Double(data), series, objective); } else if (tokens[2].equals("Objective")) { String[] objectiveData = data.split(" "); StringBuffer model = new StringBuffer(); String mag = null, na = null; String immersion = null, correction = null; for (int i=0; i<objectiveData.length; i++) { if (objectiveData[i].indexOf("x") != -1 && mag == null && na == null) { int xIndex = objectiveData[i].indexOf("x"); mag = objectiveData[i].substring(0, xIndex).trim(); na = objectiveData[i].substring(xIndex + 1).trim(); } else if (mag == null && na == null) { model.append(objectiveData[i]); model.append(" "); } else if (correction == null) { correction = objectiveData[i]; } else if (immersion == null) { immersion = objectiveData[i]; } } if (immersion != null) immersion = immersion.trim(); if (correction != null) correction = correction.trim(); Correction realCorrection = getCorrection(correction); Correction testCorrection = getCorrection(immersion); Immersion realImmersion = getImmersion(immersion); Immersion testImmersion = getImmersion(correction); // Correction and Immersion are reversed if ((testCorrection != Correction.OTHER && realCorrection == Correction.OTHER) || (testImmersion != Immersion.OTHER && realImmersion == Immersion.OTHER)) { String tmp = correction; correction = immersion; immersion = tmp; } store.setObjectiveImmersion( getImmersion(immersion), series, objective); store.setObjectiveCorrection( getCorrection(correction), series, objective); store.setObjectiveModel(model.toString().trim(), series, objective); store.setObjectiveLensNA(new Double(na), series, objective); Double magnification = Double.parseDouble(mag); store.setObjectiveNominalMagnification( magnification, series, objective); } else if (tokens[2].equals("OrderNumber")) { store.setObjectiveSerialNumber(data, series, objective); } else if (tokens[2].equals("RefractionIndex")) { store.setObjectiveSettingsRefractiveIndex(new Double(data), series); } // link Objective to Image String objectiveID = MetadataTools.createLSID("Objective", series, objective); store.setObjectiveID(objectiveID, series, objective); if (objective == 0) { store.setObjectiveSettingsID(objectiveID, series); } } else if (tokens[0].startsWith("CSpectrophotometerUnit")) { int ndx = tokens[1].lastIndexOf(" "); int channel = Integer.parseInt(tokens[1].substring(ndx + 1)) - 1; if (tokens[2].equals("Wavelength")) { Double wavelength = Double.parseDouble(data); store.setFilterModel(tokens[1], series, channel); String filterID = MetadataTools.createLSID("Filter", series, channel); store.setFilterID(filterID, series, channel); int index = activeChannelIndices.indexOf(channel); if (index >= 0 && index < getEffectiveSizeC()) { if (!filterRefPopulated[series][index]) { store.setLightPathEmissionFilterRef(filterID, series, index, 0); filterRefPopulated[series][index] = true; } if (tokens[3].equals("0") && !cutInPopulated[series][index]) { Length cutIn = FormatTools.getCutIn(wavelength); if (cutIn != null) { store.setTransmittanceRangeCutIn(cutIn, series, channel); } cutInPopulated[series][index] = true; } else if (tokens[3].equals("1") && !cutOutPopulated[series][index]) { Length cutOut = FormatTools.getCutOut(wavelength); if (cutOut != null) { store.setTransmittanceRangeCutOut(cutOut, series, channel); } cutOutPopulated[series][index] = true; } } } else if (tokens[2].equals("Stain")) { if (activeChannelIndices.contains(channel)) { int nNames = channelNames[series].size(); String prevValue = nNames == 0 ? "" : (String) channelNames[series].get(nNames - 1); if (!prevValue.equals(data)) { channelNames[series].add(data); } } } } else if (tokens[0].startsWith("CXYZStage")) { CoreMetadata ms = core.get(series); // NB: there is only one stage position specified for each series if (tokens[2].equals("XPos")) { for (int q=0; q<ms.imageCount; q++) { final Double number = Double.valueOf(data); final Length length = new Length(number, UNITS.REFERENCEFRAME); store.setPlanePositionX(length, series, q); if (q == 0) { addGlobalMetaList("X position for position", data); } } } else if (tokens[2].equals("YPos")) { for (int q=0; q<ms.imageCount; q++) { final Double number = Double.valueOf(data); final Length length = new Length(number, UNITS.REFERENCEFRAME); store.setPlanePositionY(length, series, q); if (q == 0) { addGlobalMetaList("Y position for position", data); } } } else if (tokens[2].equals("ZPos")) { final Double number = Double.valueOf(data); final Length length = new Length(number, UNITS.REFERENCEFRAME); store.setStageLabelName("Position", series); store.setStageLabelZ(length, series); addGlobalMetaList("Z position for position", data); } } else if (tokens[0].equals("CScanActuator") && tokens[1].equals("Z Scan Actuator") && tokens[2].equals("Position")) { final double pos = Double.parseDouble(data) * 1000000; store.setStageLabelName("Position", series); store.setStageLabelZ(new Length(pos, UNITS.REFERENCEFRAME), series); addGlobalMetaList("Z position for position", pos); } if (contentID.equals("dblVoxelX")) { physicalSizes[series][0] = Double.parseDouble(data); } else if (contentID.equals("dblVoxelY")) { physicalSizes[series][1] = Double.parseDouble(data); } else if (contentID.equals("dblStepSize")) { double size = Double.parseDouble(data); if (size > 0) { physicalSizes[series][2] = size; } } else if (contentID.equals("dblPinhole")) { // pinhole is stored in meters pinhole[series] = Double.parseDouble(data) * 1000000; } else if (contentID.startsWith("nDelayTime")) { exposureTime[series] = Double.parseDouble(data); if (contentID.endsWith("_ms")) { exposureTime[series] /= 1000; } } addSeriesMeta("Block " + blockNum + " " + contentID, data); } for (Detector detector : detectors) { // link Detector to Image, if the detector was actually used if (detector.active) { store.setDetectorOffset(detector.offset, series, nextDetector); store.setDetectorVoltage( new ElectricPotential(detector.voltage, UNITS.V), series, nextDetector); store.setDetectorType(getDetectorType("PMT"), series, nextDetector); String detectorID = MetadataTools.createLSID("Detector", series, nextDetector); store.setDetectorID(detectorID, series, nextDetector); if (nextDetector == 0) { // link every channel to the first detector in the beginning // if additional detectors are found, the links will be // overwritten for (int c=0; c<getEffectiveSizeC(); c++) { store.setDetectorSettingsID(detectorID, series, c); } } if (nextChannel < getEffectiveSizeC()) { store.setDetectorSettingsID(detectorID, series, nextChannel); if (nextChannel < channelNames[series].size()) { String name = (String) channelNames[series].get(nextChannel); if (name == null || name.trim().equals("") || name.equals("None")) { channelNames[series].set(nextChannel, detector.name); } } else { while (channelNames[series].size() < nextChannel) { channelNames[series].add(""); } channelNames[series].add(detector.name); } nextChannel++; } nextDetector++; } } detectors.clear(); // populate saved LogicalChannel data for (int i=0; i<getSeriesCount(); i++) { setSeries(i); for (int channel=0; channel<getEffectiveSizeC(); channel++) { if (channel < channelNames[i].size()) { String name = (String) channelNames[i].get(channel); if (name != null && !name.trim().equals("") && !name.equals("None")) { store.setChannelName(name, i, channel); } } if (channel < emWaves[i].size()) { Double wave = new Double(emWaves[i].get(channel).toString()); Length emission = FormatTools.getEmissionWavelength(wave); if (emission != null) { store.setChannelEmissionWavelength(emission, i, channel); } } if (channel < exWaves[i].size()) { Double wave = new Double(exWaves[i].get(channel).toString()); Length ex = FormatTools.getExcitationWavelength(wave); if (ex != null) { store.setChannelExcitationWavelength(ex, i, channel); } } if (i < pinhole.length) { store.setChannelPinholeSize(new Length(pinhole[i], UNITS.MICROM), i, channel); } if (channel < channelColor[i].length) { store.setChannelColor(channelColor[i][channel], i, channel); } } } setSeries(0); } private boolean usedFile(String s) { if (files == null) return false; for (int i=0; i<files.length; i++) { if (files[i] == null) continue; for (int j=0; j<files[i].size(); j++) { if (((String) files[i].get(j)).endsWith(s)) return true; } } return false; } private String getString(int len) throws IOException { return DataTools.stripString(in.readString(len)); } private String getString(boolean doubleLength) throws IOException { int len = in.readInt(); if (doubleLength) len *= 2; return getString(len); } private static ImmutableMap<Integer, String> makeDimensionTable() { ImmutableMap.Builder<Integer, String> table = ImmutableMap.builder(); table.put(0, "undefined"); table.put(120, "x"); table.put(121, "y"); table.put(122, "z"); table.put(116, "t"); table.put(6815843, "channel"); table.put(6357100, "wave length"); table.put(7602290, "rotation"); table.put(7798904, "x-wide for the motorized xy-stage"); table.put(7798905, "y-wide for the motorized xy-stage"); table.put(7798906, "z-wide for the z-stage-drive"); table.put(4259957, "user1 - unspecified"); table.put(4325493, "user2 - unspecified"); table.put(4391029, "user3 - unspecified"); table.put(6357095, "graylevel"); table.put(6422631, "graylevel1"); table.put(6488167, "graylevel2"); table.put(6553703, "graylevel3"); table.put(7864398, "logical x"); table.put(7929934, "logical y"); table.put(7995470, "logical z"); table.put(7602254, "logical t"); table.put(7077966, "logical lambda"); table.put(7471182, "logical rotation"); table.put(5767246, "logical x-wide"); table.put(5832782, "logical y-wide"); table.put(5898318, "logical z-wide"); return table.build(); } // -- Helper class -- class Detector { public int id; public int index; public boolean active; public Double offset; public Double voltage; public String name; } }