/* * #%L * OME Bio-Formats package for reading and converting biological file formats. * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package loci.formats.in; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; import java.util.HashMap; import java.util.Vector; import loci.common.DataTools; import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.common.xml.XMLTools; 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.TiffParser; import ome.xml.model.primitives.PositiveFloat; import ome.units.quantity.Length; /** * TCSReader is the file format reader for Leica TCS TIFF files and their * companion XML file. * * @author Melissa Linkert melissa at glencoesoftware.com */ public class TCSReader extends FormatReader { // -- Constants -- public static final String DATE_FORMAT = "yyyy:MM:dd HH:mm:ss"; public static final String PREFIX = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><LEICA>"; public static final String SUFFIX = "</LEICA>"; public static final String[] XML_SUFFIX = {"xml"}; // -- Fields -- /** List of TIFF files. */ private Vector<String> tiffs; /** Helper readers. */ private TiffReader[] tiffReaders; private TiffParser tiffParser; private int lastPlane = 0; private long datestamp; private String xmlFile; private double voxelX, voxelY, voxelZ; // -- Constructor -- public TCSReader() { super("Leica TCS TIFF", new String[] {"tif", "tiff", "xml"}); domains = new String[] {FormatTools.LM_DOMAIN}; hasCompanionFiles = true; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isSingleFile(String) */ @Override public boolean isSingleFile(String id) throws FormatException, IOException { if (checkSuffix(id, "xml")) return false; Location file = new Location(id); String[] list = file.getParentFile().list(); for (String f : list) { if (checkSuffix(f, "xml") && DataTools.samePrefix(file.getName(), f)) { return false; } } return true; } /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @Override public boolean isThisType(String name, boolean open) { if (!open) return false; // not allowed to touch the file system // check that there is no LEI file 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"); } } if (lei.exists()) return false; try { RandomAccessInputStream s = new RandomAccessInputStream(name); boolean isThisType = isThisType(s); s.close(); return isThisType; } catch (IOException e) { LOGGER.debug("", e); return false; } } /* @see loci.formats.IFormatReader#fileGroupOption(String) */ @Override public int fileGroupOption(String id) throws FormatException, IOException { return MUST_GROUP; } /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { // check for Leica TCS IFD directory entries TiffParser tp = new TiffParser(stream); IFD ifd = tp.getFirstIFD(); if (ifd == null) { stream.seek(0); return stream.readString(6).equals("<Data>"); } String document = ifd.getIFDTextValue(IFD.DOCUMENT_NAME); if (document == null) document = ""; String software = ifd.getIFDTextValue(IFD.SOFTWARE); if (software == null) software = ""; software = software.trim(); return document.startsWith("CHANNEL") || software.startsWith("TCS"); } /* @see loci.formats.IFormatReader#get8BitLookupTable() */ @Override public byte[][] get8BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); return tiffReaders[lastPlane].get8BitLookupTable(); } /* @see loci.formats.IFormatReader#get16BitLookupTable() */ @Override public short[][] get16BitLookupTable() throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); return tiffReaders[lastPlane].get16BitLookupTable(); } /** * @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); int n = no; for (int i=0; i<getSeries(); i++) { n += core.get(i).imageCount; } if (tiffReaders.length == 1) { return tiffReaders[0].openBytes(n, buf, x, y, w, h); } int plane = 0; if (tiffReaders[0].getImageCount() > 1) { n /= tiffReaders.length; plane = n % tiffReaders.length; } if (lastPlane != 0) { tiffReaders[lastPlane].close(); } lastPlane = n; tiffReaders[n].setId(tiffs.get(n)); return tiffReaders[n].openBytes(plane, buf, x, y, w, h); } /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */ @Override public String[] getSeriesUsedFiles(boolean noPixels) { FormatTools.assertId(currentId, true, 1); if (noPixels) { return xmlFile == null ? null : new String[] {xmlFile}; } Vector<String> v = new Vector<String>(); v.addAll(tiffs); if (xmlFile != null) v.add(xmlFile); 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 (!fileOnly) { tiffs = null; if (tiffReaders != null) { for (TiffReader r : tiffReaders) { if (r != null) r.close(); } } tiffReaders = null; tiffParser = null; datestamp = 0; xmlFile = null; lastPlane = 0; } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { Location l = new Location(id).getAbsoluteFile(); Location parent = l.getParentFile(); String[] list = parent.list(); Arrays.sort(list); boolean isXML = checkSuffix(id, XML_SUFFIX); if (list != null) { for (String file : list) { if (checkSuffix(file, XML_SUFFIX) && !isXML && isGroupFiles()) { xmlFile = new Location(parent, file).getAbsolutePath(); break; } else if (checkSuffix(file, TiffReader.TIFF_SUFFIXES) && isXML) { initFile(new Location(parent, file).getAbsolutePath()); return; } } } if (isXML) xmlFile = l.getAbsolutePath(); super.initFile(id); MetadataStore store = makeFilterMetadata(); in = new RandomAccessInputStream(id, 16); tiffParser = new TiffParser(in); tiffs = new Vector<String>(); IFDList ifds = tiffParser.getIFDs(); String date = ifds.get(0).getIFDStringValue(IFD.DATE_TIME); if (date != null) { datestamp = DateTools.getTime(date, "yyyy:MM:dd HH:mm:ss"); } groupFiles(); addGlobalMeta("Number of image files", tiffs.size()); tiffReaders = new TiffReader[tiffs.size()]; for (int i=0; i<tiffReaders.length; i++) { tiffReaders[i] = new TiffReader(); } tiffReaders[0].setId(tiffs.get(0)); int[] ch = new int[ifds.size()]; int[] idx = new int[ifds.size()]; long[] stamp = new long[ifds.size()]; int channelCount = 0; CoreMetadata ms0 = core.get(0); ms0.sizeZ = 1; ms0.sizeC = tiffReaders[0].getSizeC(); ms0.dimensionOrder = isRGB() ? "XYC" : "XY"; if (isGroupFiles()) { try { FilePattern fp = new FilePattern(new Location(currentId).getAbsoluteFile()); AxisGuesser guesser = new AxisGuesser(fp, "XYTZC", 1, ifds.size(), 1, true); int[] axisTypes = guesser.getAxisTypes(); int[] count = fp.getCount(); for (int i=axisTypes.length-1; i>=0; i--) { if (axisTypes[i] == AxisGuesser.Z_AXIS) { if (getDimensionOrder().indexOf("Z") == -1) { ms0.dimensionOrder += "Z"; } ms0.sizeZ *= count[i]; } else if (axisTypes[i] == AxisGuesser.C_AXIS) { if (getDimensionOrder().indexOf("C") == -1) { ms0.dimensionOrder += "C"; } ms0.sizeC *= count[i]; } } } catch (NullPointerException e) { } } for (int i=0; i<ifds.size(); i++) { String document = ifds.get(i).getIFDStringValue(IFD.DOCUMENT_NAME); if (document == null) continue; int index = document.indexOf("INDEX"); String s = document.substring(8, index).trim(); ch[i] = Integer.parseInt(s); if (ch[i] > channelCount) channelCount = ch[i]; int space = document.indexOf(" ", index + 6); if (space < 0) continue; String n = document.substring(index + 6, space).trim(); idx[i] = Integer.parseInt(n); date = document.substring(space, document.indexOf("FORMAT")).trim(); stamp[i] = DateTools.getTime(date, DATE_FORMAT, "."); addGlobalMetaList("Timestamp for plane", stamp[i]); } ms0.sizeT = 0; // determine the axis sizes and ordering boolean unique = true; for (int i=0; i<stamp.length; i++) { for (int j=i+1; j<stamp.length; j++) { if (stamp[j] == stamp[i]) { unique = false; break; } } if (unique) { ms0.sizeT++; if (getDimensionOrder().indexOf("T") < 0) { ms0.dimensionOrder += "T"; } } else if (i > 0) { if ((ch[i] != ch[i - 1]) && getDimensionOrder().indexOf("C") < 0) { ms0.dimensionOrder += "C"; } else if (getDimensionOrder().indexOf("Z") < 0) { ms0.dimensionOrder += "Z"; } } unique = true; } if (getDimensionOrder().indexOf("Z") < 0) ms0.dimensionOrder += "Z"; if (getDimensionOrder().indexOf("C") < 0) ms0.dimensionOrder += "C"; if (getDimensionOrder().indexOf("T") < 0) ms0.dimensionOrder += "T"; if (getSizeC() == 0) ms0.sizeC = 1; if (getSizeT() == 0) ms0.sizeT = 1; if (channelCount == 0) channelCount = 1; if (getSizeZ() <= 1) { ms0.sizeZ = ifds.size() / (getSizeT() * channelCount); } ms0.sizeC *= channelCount; ms0.imageCount = getSizeZ() * getSizeT() * getSizeC(); // cut up comment String comment = ifds.get(0).getComment(); if (comment != null && comment.startsWith("[") && getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) { String[] lines = comment.split("\n"); for (String line : lines) { if (!line.startsWith("[")) { int eq = line.indexOf("="); if (eq < 0) continue; String key = line.substring(0, eq).trim(); String value = line.substring(eq + 1).trim(); if (key.equals("VoxelSizeX")) { try { voxelX = Double.parseDouble(value); } catch (NumberFormatException e) { } } else if (key.equals("VoxelSizeY")) { try { voxelY = Double.parseDouble(value); } catch (NumberFormatException e) { } } else if (key.equals("VoxelSizeZ")) { try { voxelZ = Double.parseDouble(value); } catch (NumberFormatException e) { } } addGlobalMeta(key, value); } } metadata.remove("Comment"); } ms0.sizeX = tiffReaders[0].getSizeX(); ms0.sizeY = tiffReaders[0].getSizeY(); ms0.rgb = tiffReaders[0].isRGB(); ms0.pixelType = tiffReaders[0].getPixelType(); ms0.littleEndian = tiffReaders[0].isLittleEndian(); ms0.interleaved = tiffReaders[0].isInterleaved(); ms0.falseColor = true; ms0.indexed = tiffReaders[0].isIndexed(); if (isRGB()) ms0.imageCount /= (getSizeC() / channelCount); if (getSizeZ() * getSizeT() * getEffectiveSizeC() != (ifds.size() * tiffReaders.length)) { int c = getEffectiveSizeC(); if (c == 0) c = 1; ms0.sizeT = (ifds.size() * tiffReaders.length) / (c * getSizeZ()); ms0.imageCount = getSizeT() * c * getSizeZ(); if (getSizeT() == 0) { ms0.sizeT = 1; ms0.imageCount = ifds.size() * tiffReaders.length; } } if (getImageCount() == ifds.size() * getSizeZ() * getSizeT() && ifds.size() > 1) { if (getSizeZ() == 1) { ms0.sizeZ = ifds.size(); } else if (getSizeT() == 1) { ms0.sizeT = ifds.size(); } else ms0.sizeZ *= ifds.size(); } if (xmlFile != null) { // parse XML metadata String xml = DataTools.readFile(xmlFile); xml = XMLTools.sanitizeXML(PREFIX + xml + SUFFIX); LeicaHandler handler = new LeicaHandler(store, getMetadataOptions().getMetadataLevel()); XMLTools.parseXML(xml, handler); metadata = handler.getGlobalMetadata(); MetadataTools.merge(handler.getGlobalMetadata(), metadata, ""); core = handler.getCoreMetadataList(); for (int i=0; i<getSeriesCount(); i++) { CoreMetadata ms = core.get(i); if (tiffs.size() < ms.imageCount) { int div = ms.imageCount / ms.sizeC; ms.imageCount = tiffs.size(); if (div >= ms.sizeZ) ms.sizeZ /= div; else if (div >= ms.sizeT) ms.sizeT /= div; } ms.dimensionOrder = getSizeZ() > getSizeT() ? "XYCZT" : "XYCTZ"; ms.rgb = false; ms.interleaved = false; ms.indexed = tiffReaders[0].isIndexed(); } } MetadataTools.populatePixels(store, this, true); Length sizeX = FormatTools.getPhysicalSizeX(voxelX); Length sizeY = FormatTools.getPhysicalSizeY(voxelY); Length sizeZ = FormatTools.getPhysicalSizeZ(voxelZ); if (sizeX != null) { store.setPixelsPhysicalSizeX(sizeX, 0); } if (sizeY != null) { store.setPixelsPhysicalSizeY(sizeY, 0); } if (sizeZ != null) { store.setPixelsPhysicalSizeZ(sizeZ, 0); } } // -- Helper methods -- private void groupFiles() throws FormatException, IOException { // look for associated TIFF files // we assume that two files are grouped if all of the following are true: // // * the files are in the same directory // * the file names are the same length // * the time stamps are less than 60 seconds apart // * the files have the same number of bytes Location current = new Location(currentId).getAbsoluteFile(); if (!checkSuffix(currentId, XML_SUFFIX)) { tiffs.add(current.getAbsolutePath()); } if (!isGroupFiles()) return; Location parent = current.getParentFile(); String[] list = parent.list(); Arrays.sort(list); HashMap<String, Long> timestamps = new HashMap<String, Long>(); RandomAccessInputStream s = new RandomAccessInputStream(current.getAbsolutePath(), 16); TiffParser p = new TiffParser(s); IFD ifd = p.getIFDs().get(0); s.close(); int expectedIFDCount = p.getIFDs().size(); long width = ifd.getImageWidth(); long height = ifd.getImageLength(); int samples = ifd.getSamplesPerPixel(); for (String file : list) { file = new Location(parent, file).getAbsolutePath(); if (file.length() != current.getAbsolutePath().length()) continue; RandomAccessInputStream rais = new RandomAccessInputStream(file, 16); TiffParser tp = new TiffParser(rais); if (!tp.isValidHeader()) { continue; } ifd = tp.getIFDs().get(0); if (tp.getIFDs().size() != expectedIFDCount || ifd.getImageWidth() != width || ifd.getImageLength() != height || ifd.getSamplesPerPixel() != samples) { continue; } String date = ifd.getIFDStringValue(IFD.DATE_TIME); if (date != null) { long stamp = DateTools.getTime(date, "yyyy:MM:dd HH:mm:ss"); String software = ifd.getIFDStringValue(IFD.SOFTWARE); if (software != null && software.trim().startsWith("TCS")) { timestamps.put(file, new Long(stamp)); } } rais.close(); } String[] files = timestamps.keySet().toArray(new String[timestamps.size()]); Arrays.sort(files); for (String file : files) { long thisStamp = timestamps.get(file).longValue(); boolean match = false; for (String tiff : tiffs) { s = new RandomAccessInputStream(tiff, 16); TiffParser parser = new TiffParser(s); ifd = parser.getIFDs().get(0); s.close(); String date = ifd.getIFDStringValue(IFD.DATE_TIME); long nextStamp = DateTools.getTime(date, "yyyy:MM:dd HH:mm:ss"); if (Math.abs(thisStamp - nextStamp) < 600000) { match = true; break; } } if (match && !tiffs.contains(file)) tiffs.add(file); } } }