//
// TiffReader.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.IOException;
import java.util.Hashtable;
import java.util.StringTokenizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import loci.common.DataTools;
import loci.common.Location;
import loci.common.xml.XMLTools;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.TiffCompression;
/**
* TiffReader is the file format reader for regular TIFF files,
* not of any specific TIFF variant.
*
* <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/TiffReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/TiffReader.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @author Curtis Rueden ctrueden at wisc.edu
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class TiffReader extends BaseTiffReader {
// -- Constants --
/** Logger for this class. */
private static final Logger LOGGER =
LoggerFactory.getLogger(TiffReader.class);
public static final String[] TIFF_SUFFIXES =
{"tif", "tiff", "tf2", "tf8", "btf"};
public static final String[] COMPANION_SUFFIXES = {"xml", "txt"};
public static final int IMAGEJ_TAG = 50839;
// -- Fields --
private String companionFile;
private String description;
private String calibrationUnit;
private Double physicalSizeZ;
private Double timeIncrement;
private Integer xOrigin, yOrigin;
// -- Constructor --
/** Constructs a new Tiff reader. */
public TiffReader() {
super("Tagged Image File Format", TIFF_SUFFIXES);
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
public String[] getSeriesUsedFiles(boolean noPixels) {
if (noPixels) {
return companionFile == null ? null : new String[] {companionFile};
}
if (companionFile != null) return new String[] {companionFile, currentId};
return new String[] {currentId};
}
/* @see loci.formats.IFormatReader#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
companionFile = null;
description = null;
calibrationUnit = null;
physicalSizeZ = null;
timeIncrement = null;
xOrigin = null;
yOrigin = null;
}
}
// -- Internal BaseTiffReader API methods --
/* @see BaseTiffReader#initStandardMetadata() */
protected void initStandardMetadata() throws FormatException, IOException {
super.initStandardMetadata();
String comment = ifds.get(0).getComment();
LOGGER.info("Checking comment style");
if (ifds.size() > 1) core[0].orderCertain = false;
description = null;
calibrationUnit = null;
physicalSizeZ = null;
timeIncrement = null;
xOrigin = null;
yOrigin = null;
// check for reusable proprietary tags (65000-65535),
// which may contain additional metadata
MetadataLevel level = getMetadataOptions().getMetadataLevel();
if (level != MetadataLevel.MINIMUM) {
Integer[] tags = ifds.get(0).keySet().toArray(new Integer[0]);
for (Integer tag : tags) {
if (tag.intValue() >= 65000) {
Object value = ifds.get(0).get(tag);
if (value instanceof short[]) {
short[] s = (short[]) value;
byte[] b = new byte[s.length];
for (int i=0; i<b.length; i++) {
b[i] = (byte) s[i];
}
String metadata = DataTools.stripString(new String(b));
if (metadata.indexOf("xml") != -1) {
metadata = metadata.substring(metadata.indexOf("<"));
metadata = "<root>" + XMLTools.sanitizeXML(metadata) + "</root>";
try {
Hashtable<String, String> xmlMetadata =
XMLTools.parseXML(metadata);
for (String key : xmlMetadata.keySet()) {
addGlobalMeta(key, xmlMetadata.get(key));
}
}
catch (IOException e) { }
}
else {
addGlobalMeta(tag.toString(), metadata);
}
}
}
}
}
// check for ImageJ-style TIFF comment
boolean ij = checkCommentImageJ(comment);
if (ij) parseCommentImageJ(comment);
// check for MetaMorph-style TIFF comment
boolean metamorph = checkCommentMetamorph(comment);
if (metamorph && level != MetadataLevel.MINIMUM) {
parseCommentMetamorph(comment);
}
put("MetaMorph", metamorph ? "yes" : "no");
// check for other INI-style comment
if (!ij && !metamorph && level != MetadataLevel.MINIMUM) {
parseCommentGeneric(comment);
}
// check for another file with the same name
if (isGroupFiles()) {
Location currentFile = new Location(currentId).getAbsoluteFile();
String currentName = currentFile.getName();
Location directory = currentFile.getParentFile();
String[] files = directory.list(true);
if (files != null) {
for (String file : files) {
String name = file;
if (name.indexOf(".") != -1) {
name = name.substring(0, name.indexOf("."));
}
if (currentName.startsWith(name) &&
checkSuffix(name, COMPANION_SUFFIXES))
{
companionFile = new Location(directory, file).getAbsolutePath();
break;
}
}
}
}
// TODO : parse companion file once loci.parsers package is in place
}
/* @see BaseTiffReader#initMetadataStore() */
protected void initMetadataStore() throws FormatException {
super.initMetadataStore();
MetadataStore store = makeFilterMetadata();
if (description != null) {
store.setImageDescription(description, 0);
}
populateMetadataStoreImageJ(store);
}
// -- Helper methods --
private boolean checkCommentImageJ(String comment) {
return comment != null && comment.startsWith("ImageJ=");
}
private boolean checkCommentMetamorph(String comment) {
String software = ifds.get(0).getIFDTextValue(IFD.SOFTWARE);
return comment != null && software != null &&
software.indexOf("MetaMorph") != -1;
}
private void parseCommentImageJ(String comment)
throws FormatException, IOException
{
int nl = comment.indexOf("\n");
put("ImageJ", nl < 0 ? comment.substring(7) : comment.substring(7, nl));
metadata.remove("Comment");
description = "";
int z = 1, t = 1;
int c = getSizeC();
if (ifds.get(0).containsKey(IMAGEJ_TAG)) {
comment += "\n" + ifds.get(0).getIFDTextValue(IMAGEJ_TAG);
}
// parse ImageJ metadata (ZCT sizes, calibration units, etc.)
StringTokenizer st = new StringTokenizer(comment, "\n");
while (st.hasMoreTokens()) {
String token = st.nextToken();
String value = null;
int eq = token.indexOf("=");
if (eq >= 0) value = token.substring(eq + 1);
if (token.startsWith("channels=")) c = parseInt(value);
else if (token.startsWith("slices=")) z = parseInt(value);
else if (token.startsWith("frames=")) t = parseInt(value);
else if (token.startsWith("mode=")) {
put("Color mode", value);
}
else if (token.startsWith("unit=")) {
calibrationUnit = value;
put("Unit", calibrationUnit);
}
else if (token.startsWith("finterval=")) {
timeIncrement = parseDouble(value);
put("Frame Interval", timeIncrement);
}
else if (token.startsWith("spacing=")) {
physicalSizeZ = parseDouble(value);
put("Spacing", physicalSizeZ);
}
else if (token.startsWith("xorigin=")) {
xOrigin = parseInt(value);
put("X Origin", xOrigin);
}
else if (token.startsWith("yorigin=")) {
yOrigin = parseInt(value);
put("Y Origin", yOrigin);
}
else if (eq > 0) {
put(token.substring(0, eq).trim(), value);
}
}
if (z * c * t == c && isRGB()) {
t = getImageCount();
}
core[0].dimensionOrder = "XYCZT";
if (z * t * (isRGB() ? 1 : c) == ifds.size()) {
core[0].sizeZ = z;
core[0].sizeT = t;
core[0].sizeC = c;
}
else if (ifds.size() == 1 && z * t > ifds.size() &&
ifds.get(0).getCompression() == TiffCompression.UNCOMPRESSED)
{
// file is likely corrupt (missing end IFDs)
//
// ImageJ writes TIFF files like this:
// IFD #0
// comment
// all pixel data
// IFD #1
// IFD #2
// ...
//
// since we know where the pixel data is, we can create fake
// IFDs in an attempt to read the rest of the pixels
IFD firstIFD = ifds.get(0);
int planeSize = getSizeX() * getSizeY() * getRGBChannelCount() *
FormatTools.getBytesPerPixel(getPixelType());
long[] stripOffsets = firstIFD.getStripOffsets();
long[] stripByteCounts = firstIFD.getStripByteCounts();
long endOfFirstPlane = stripOffsets[stripOffsets.length - 1] +
stripByteCounts[stripByteCounts.length - 1];
long totalBytes = in.length() - endOfFirstPlane;
int totalPlanes = (int) (totalBytes / planeSize) + 1;
ifds = new IFDList();
ifds.add(firstIFD);
for (int i=1; i<totalPlanes; i++) {
IFD ifd = new IFD(firstIFD);
ifds.add(ifd);
long[] prevOffsets = ifds.get(i - 1).getStripOffsets();
long[] offsets = new long[stripOffsets.length];
offsets[0] = prevOffsets[prevOffsets.length - 1] +
stripByteCounts[stripByteCounts.length - 1];
for (int j=1; j<offsets.length; j++) {
offsets[j] = offsets[j - 1] + stripByteCounts[j - 1];
}
ifd.putIFDValue(IFD.STRIP_OFFSETS, offsets);
}
if (z * c * t == ifds.size()) {
core[0].sizeZ = z;
core[0].sizeT = t;
core[0].sizeC = c;
}
else if (z * t == ifds.size()) {
core[0].sizeZ = z;
core[0].sizeT = t;
}
else core[0].sizeZ = ifds.size();
core[0].imageCount = ifds.size();
}
else {
core[0].sizeT = ifds.size();
core[0].imageCount = ifds.size();
}
}
/**
* Checks the original metadata table for ImageJ-specific information
* to propagate into the metadata store.
*/
private void populateMetadataStoreImageJ(MetadataStore store) {
// TODO: Perhaps we should only populate the physical Z size if the unit is
// a known, physical quantity such as "micron" rather than "pixel".
// e.g.: if (calibrationUnit.equals("micron"))
if (physicalSizeZ != null) {
double zDepth = physicalSizeZ.doubleValue();
if (zDepth < 0) zDepth = -zDepth;
store.setPixelsPhysicalSizeZ(new PositiveFloat(zDepth), 0);
}
if (timeIncrement != null) {
store.setPixelsTimeIncrement(timeIncrement, 0);
}
}
private void parseCommentMetamorph(String comment) {
// parse key/value pairs
StringTokenizer st = new StringTokenizer(comment, "\n");
while (st.hasMoreTokens()) {
String line = st.nextToken();
int colon = line.indexOf(":");
if (colon < 0) {
addGlobalMeta("Comment", line);
description = line;
continue;
}
String key = line.substring(0, colon);
String value = line.substring(colon + 1);
addGlobalMeta(key, value);
}
}
private void parseCommentGeneric(String comment) {
if (comment == null) return;
String[] lines = comment.split("\n");
if (lines.length > 1) {
comment = "";
for (String line : lines) {
int eq = line.indexOf("=");
if (eq != -1) {
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
addGlobalMeta(key, value);
}
else if (!line.startsWith("[")) {
comment += line + "\n";
}
}
addGlobalMeta("Comment", comment);
description = comment;
}
}
private int parseInt(String s) {
try {
return Integer.parseInt(s);
}
catch (NumberFormatException e) {
LOGGER.debug("Failed to parse integer value", e);
}
return 0;
}
private double parseDouble(String s) {
try {
return Double.parseDouble(s);
}
catch (NumberFormatException e) {
LOGGER.debug("Failed to parse floating point value", e);
}
return 0;
}
}