/* * #%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.BufferedReader; import java.io.DataInputStream; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.zip.GZIPInputStream; import ome.xml.model.primitives.Timestamp; import loci.common.DataTools; import loci.common.DateTools; import loci.common.IniList; import loci.common.IniParser; import loci.common.IniTable; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.UnsupportedCompressionException; import loci.formats.meta.MetadataStore; import ome.units.quantity.Time; import ome.units.UNITS; /** * LiFlimReader is the file format reader for LI-FLIM files. */ public class LiFlimReader extends FormatReader { // -- Constants -- // INI tables public static final String INFO_TABLE = "FLIMIMAGE: INFO"; public static final String LAYOUT_TABLE = "FLIMIMAGE: LAYOUT"; public static final String BACKGROUND_TABLE = "FLIMIMAGE: BACKGROUND"; // relevant keys in info table public static final String VERSION_KEY = "version"; public static final String COMPRESSION_KEY = "compression"; // relevant keys in layout and background tables public static final String DATATYPE_KEY = "datatype"; public static final String C_KEY = "channels"; public static final String X_KEY = "x"; public static final String Y_KEY = "y"; public static final String Z_KEY = "z"; public static final String P_KEY = "phases"; public static final String F_KEY = "frequencies"; public static final String T_KEY = "timestamps"; // relevant keys in timestamp table public static final String TIMESTAMP_KEY = "FLIMIMAGE: TIMESTAMPS - t"; // supported versions public static final String[] KNOWN_VERSIONS = {"1.0"}; // compression types public static final String COMPRESSION_NONE = "0"; public static final String COMPRESSION_GZIP = "1"; // data types public static final String DATATYPE_UINT8 = "UINT8"; public static final String DATATYPE_INT8 = "INT8"; public static final String DATATYPE_UINT16 = "UINT16"; public static final String DATATYPE_INT16 = "INT16"; public static final String DATATYPE_UINT32 = "UINT32"; public static final String DATATYPE_INT32 = "INT32"; public static final String DATATYPE_REAL32 = "REAL32"; public static final String DATATYPE_REAL64 = "REAL64"; // -- Fields -- /** Offset to start of pixel data. */ private long dataOffset; /** Parsed configuration data. */ private IniList ini; private String version; private String compression; private String datatype; private String channels; private String xLen; private String yLen; private String zLen; private String phases; private String frequencies; private String timestamps; private String backgroundDatatype; private String backgroundX; private String backgroundY; private String backgroundC; private String backgroundZ; private String backgroundT; private String backgroundP; private String backgroundF; private int numRegions = 0; /* note: set but not used */ private Map<Integer, ROI> rois; private Map<Integer, String> stampValues; private Double exposureTime; /** True if gzip compression was used to deflate the pixels. */ private boolean gzip; /** Stream to use for reading gzip-compressed pixel data. */ private DataInputStream gz; /** Image number indicating position in gzip stream. */ private int gzPos; /** Series number indicating position in gzip stream. */ private int gzSeries; // -- Constructor -- /** Constructs a new LI-FLIM reader. */ public LiFlimReader() { super("LI-FLIM", "fli"); domains = new String[] {FormatTools.FLIM_DOMAIN}; } // -- IFormatReader API methods -- /** * @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 bytesPerPlane = FormatTools.getPlaneSize(this); if (gzip) { prepareGZipStream(no); // read compressed data byte[] bytes = new byte[bytesPerPlane]; try { gz.readFully(bytes); } catch (EOFException e) { LOGGER.debug("Could not read full plane", e); } RandomAccessInputStream s = new RandomAccessInputStream(bytes); readPlane(s, x, y, w, h, buf); s.close(); } else { in.seek(dataOffset + bytesPerPlane * no); int thisSeries = getSeries(); for (int i=0; i<thisSeries; i++) { setSeries(i); in.skipBytes(getImageCount() * FormatTools.getPlaneSize(this)); } setSeries(thisSeries); readPlane(in, x, y, w, h, buf); } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { dataOffset = 0; ini = null; gzip = false; if (gz != null) gz.close(); gz = null; gzPos = 0; gzSeries = 0; version = null; compression = null; datatype = null; channels = null; xLen = null; yLen = null; zLen = null; phases = null; frequencies = null; timestamps = null; backgroundDatatype = null; backgroundX = null; backgroundY = null; backgroundC = null; backgroundZ = null; backgroundT = null; backgroundP = null; backgroundF = null; numRegions = 0; rois = null; stampValues = null; exposureTime = null; } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); LOGGER.info("Parsing header"); in = new RandomAccessInputStream(id); parseHeader(); LOGGER.info("Parsing metadata"); initOriginalMetadata(); initCoreMetadata(); initOMEMetadata(); } // -- Helper methods -- private void parseHeader() throws IOException { String headerData = in.findString("{END}"); dataOffset = in.getFilePointer(); IniParser parser = new IniParser(); ini = parser.parseINI(new BufferedReader(new StringReader(headerData))); } private void initOriginalMetadata() { rois = new HashMap<Integer, ROI>(); stampValues = new HashMap<Integer, String>(); IniTable layoutTable = ini.getTable(LAYOUT_TABLE); datatype = layoutTable.get(DATATYPE_KEY); channels = layoutTable.get(C_KEY); xLen = layoutTable.get(X_KEY); yLen = layoutTable.get(Y_KEY); zLen = layoutTable.get(Z_KEY); phases = layoutTable.get(P_KEY); frequencies = layoutTable.get(F_KEY); timestamps = layoutTable.get(T_KEY); IniTable backgroundTable = ini.getTable(BACKGROUND_TABLE); if (backgroundTable != null) { backgroundDatatype = backgroundTable.get(DATATYPE_KEY); backgroundC = backgroundTable.get(C_KEY); backgroundX = backgroundTable.get(X_KEY); backgroundY = backgroundTable.get(Y_KEY); backgroundZ = backgroundTable.get(Z_KEY); backgroundT = backgroundTable.get(T_KEY); backgroundP = backgroundTable.get(P_KEY); backgroundF = backgroundTable.get(F_KEY); } IniTable infoTable = ini.getTable(INFO_TABLE); version = infoTable.get(VERSION_KEY); compression = infoTable.get(COMPRESSION_KEY); MetadataLevel level = getMetadataOptions().getMetadataLevel(); if (level != MetadataLevel.MINIMUM) { // add all INI entries to the global metadata list for (IniTable table : ini) { String name = table.get(IniTable.HEADER_KEY); for (String key : table.keySet()) { if (key.equals(IniTable.HEADER_KEY)) continue; String value = table.get(key); String metaKey = name + " - " + key; addGlobalMeta(metaKey, value); if (metaKey.startsWith(TIMESTAMP_KEY)) { Integer index = new Integer(metaKey.replaceAll(TIMESTAMP_KEY, "")); stampValues.put(index, value); } else if (metaKey.equals("ROI: INFO - numregions")) { numRegions = Integer.parseInt(value); } else if (metaKey.startsWith("ROI: ROI") && level != MetadataLevel.NO_OVERLAYS) { int start = metaKey.lastIndexOf("ROI") + 3; int end = metaKey.indexOf(" ", start); Integer index = new Integer(metaKey.substring(start, end)); ROI roi = rois.get(index); if (roi == null) roi = new ROI(); if (metaKey.endsWith("name")) { roi.name = value; } else if (metaKey.indexOf(" - p") >= 0) { String p = metaKey.substring(metaKey.indexOf(" - p") + 4); roi.points.put(new Integer(p), value.replaceAll(" ", ",")); } rois.put(index, roi); } else if (metaKey.endsWith("ExposureTime")) { int space = value.indexOf(" "); double expTime = Double.parseDouble(value.substring(0, space)); String units = value.substring(space + 1).toLowerCase(); if (units.equals("ms")) { expTime /= 1000; } exposureTime = new Double(expTime); } } } } } private void initCoreMetadata() throws FormatException { // check version number if (DataTools.indexOf(KNOWN_VERSIONS, version) < 0) { LOGGER.warn("Unknown LI-FLIM version: {}", version); } // check compression type if (COMPRESSION_NONE.equals(compression)) gzip = false; else if (COMPRESSION_GZIP.equals(compression)) gzip = true; else { throw new UnsupportedCompressionException( "Unknown compression type: " + compression); } // check dimensional extents int sizeP = Integer.parseInt(phases); int sizeF = Integer.parseInt(frequencies); int p = backgroundP == null ? 1 : Integer.parseInt(backgroundP); int f = backgroundF == null ? 1 : Integer.parseInt(backgroundF); // populate core metadata CoreMetadata ms = core.get(0); ms.sizeX = Integer.parseInt(xLen); ms.sizeY = Integer.parseInt(yLen); ms.sizeZ = Integer.parseInt(zLen) * sizeF; ms.sizeC = Integer.parseInt(channels); ms.sizeT = Integer.parseInt(timestamps) * sizeP; ms.imageCount = getSizeZ() * getSizeT(); ms.rgb = getSizeC() > 1; ms.indexed = false; ms.dimensionOrder = "XYCZT"; ms.pixelType = getPixelTypeFromString(datatype); ms.littleEndian = true; ms.interleaved = true; ms.falseColor = false; ms.moduloZ.type = FormatTools.FREQUENCY; ms.moduloZ.step = ms.sizeZ / sizeF; ms.moduloZ.start = 0; ms.moduloZ.end = ms.sizeZ - 1; ms.moduloT.type = FormatTools.PHASE; ms.moduloT.step = ms.sizeT / sizeP; ms.moduloT.start = 0; ms.moduloT.end = ms.sizeT - 1; if (backgroundX != null) { ms = new CoreMetadata(); ms.sizeX = Integer.parseInt(backgroundX); ms.sizeY = Integer.parseInt(backgroundY); ms.sizeZ = Integer.parseInt(backgroundZ) * f; ms.sizeC = Integer.parseInt(backgroundC); ms.sizeT = Integer.parseInt(backgroundT) * p; ms.imageCount = ms.sizeZ * ms.sizeT; ms.rgb = ms.sizeC > 1; ms.indexed = false; ms.dimensionOrder = "XYCZT"; ms.pixelType = getPixelTypeFromString(backgroundDatatype); ms.littleEndian = true; ms.interleaved = true; ms.falseColor = false; ms.moduloZ.type = FormatTools.FREQUENCY; ms.moduloZ.step = ms.sizeZ / f; ms.moduloZ.start = 0; ms.moduloZ.end = ms.sizeZ - 1; ms.moduloT.type = FormatTools.PHASE; ms.moduloT.step = ms.sizeT / p; ms.moduloT.start = 0; ms.moduloT.end = ms.sizeT - 1; core.add(ms); } } private void initOMEMetadata() { int times = timestamps == null ? 0 : Integer.parseInt(timestamps); MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, times > 0); String path = new Location(getCurrentFile()).getName(); store.setImageName(path + " Primary Image #1", 0); if (getSeriesCount() > 1) { store.setImageName(path + " Background Image #1", 1); } if (getMetadataOptions().getMetadataLevel() == MetadataLevel.MINIMUM) { return; } // timestamps long firstStamp = 0; for (int t=0; t<times; t++) { if (stampValues.get(t) == null) break; String[] stampWords = stampValues.get(t).split(" "); long stampHi = Long.parseLong(stampWords[0]); long stampLo = Long.parseLong(stampWords[1]); long stamp = DateTools.getMillisFromTicks(stampHi, stampLo); Double deltaT; if (t == 0) { String date = DateTools.convertDate(stamp, DateTools.COBOL); if (date != null) { store.setImageAcquisitionDate(new Timestamp(date), 0); } firstStamp = stamp; deltaT = Double.valueOf(0); } else { long ms = stamp - firstStamp; deltaT = new Double(ms / 1000.0); } for (int c=0; c<getEffectiveSizeC(); c++) { for (int z=0; z<getSizeZ(); z++) { int index = getIndex(z, c, t); if (deltaT != null) { store.setPlaneDeltaT(new Time(deltaT, UNITS.S), 0, index); } if (exposureTime != null) { store.setPlaneExposureTime(new Time(exposureTime, UNITS.S), 0, index); } } } } if (getMetadataOptions().getMetadataLevel() == MetadataLevel.NO_OVERLAYS) { return; } // regions of interest Integer[] roiIndices = rois.keySet().toArray(new Integer[rois.size()]); Arrays.sort(roiIndices); for (int roi=0; roi<roiIndices.length; roi++) { ROI r = rois.get(roiIndices[roi]); String polylineID = MetadataTools.createLSID("Shape", roi, 0); store.setPolygonID(polylineID, roi, 0); store.setPolygonPoints(r.pointsToString(), roi, 0); String roiID = MetadataTools.createLSID("ROI", roi); store.setROIID(roiID, roi); for (int s=0; s<getSeriesCount(); s++) { store.setImageROIRef(roiID, s, roi); } } } private int getPixelTypeFromString(String type) throws FormatException { // check data type if (DATATYPE_UINT8.equals(type)) return FormatTools.UINT8; else if (DATATYPE_INT8.equals(type)) return FormatTools.INT8; else if (DATATYPE_UINT16.equals(type)) return FormatTools.UINT16; else if (DATATYPE_INT16.equals(type)) return FormatTools.INT16; else if (DATATYPE_UINT32.equals(type)) return FormatTools.UINT32; else if (DATATYPE_INT32.equals(type)) return FormatTools.INT32; else if (DATATYPE_REAL32.equals(type)) return FormatTools.FLOAT; else if (DATATYPE_REAL64.equals(type)) return FormatTools.DOUBLE; throw new FormatException("Unknown data type: " + type); } private void prepareGZipStream(int no) throws IOException { int bytesPerPlane = FormatTools.getPlaneSize(this); if (gz == null || (no < gzPos && getSeries() == gzSeries) || gzSeries > getSeries()) { // reinitialize gzip stream if (gz != null) gz.close(); // seek to start of pixel data String path = Location.getMappedId(currentId); FileInputStream fis = new FileInputStream(path); skip(fis, dataOffset); // create gzip stream gz = new DataInputStream(new GZIPInputStream(fis)); gzPos = 0; gzSeries = 0; } // seek to correct image number if (getSeries() >= 1 && gzSeries < getSeries()) { int originalSeries = getSeries(); for (int i=gzSeries; i<originalSeries; i++) { setSeries(i); int nPlanes = getImageCount() - gzPos; int nBytes = FormatTools.getPlaneSize(this) * nPlanes; skip(gz, nBytes); gzPos = 0; } setSeries(originalSeries); gzSeries = getSeries(); } skip(gz, bytesPerPlane * (no - gzPos)); gzPos = no + 1; } private void skip(InputStream is, long num) throws IOException { long skipLeft = num; while (skipLeft > 0) { long skip = is.skip(skipLeft); if (skip <= 0) throw new IOException("Cannot skip bytes"); skipLeft -= skip; } } // -- Helper class -- private class ROI { public String name; public final Map<Integer, String> points = new HashMap<Integer, String>(); public String pointsToString() { StringBuilder s = new StringBuilder(); Integer[] pointIndices = points.keySet().toArray(new Integer[0]); Arrays.sort(pointIndices); for (Integer point : pointIndices) { if (point == null) continue; String p = points.get(point); if (s.length() > 0) s.append(" "); s.append(p); } return s.toString(); } } }