/*
* #%L
* BSD implementations of Bio-Formats readers and writers
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
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.Constants;
import loci.common.DataTools;
import loci.common.Location;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.TiffCompression;
import ome.xml.model.primitives.PositiveFloat;
import ome.units.quantity.Time;
import ome.units.quantity.Length;
import ome.units.UNITS;
/**
* TiffReader is the file format reader for regular TIFF files,
* not of any specific TIFF variant.
*
* @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 Time 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) */
@Override
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) */
@Override
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() */
@Override
protected void initStandardMetadata() throws FormatException, IOException {
super.initStandardMetadata();
String comment = ifds.get(0).getComment();
LOGGER.info("Checking comment style");
CoreMetadata m = core.get(0);
if (ifds.size() > 1) m.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, Constants.ENCODING));
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() */
@Override
protected void initMetadataStore() throws FormatException {
super.initMetadataStore();
MetadataStore store = makeFilterMetadata();
if (description != null) {
description = description.replaceAll("\n", "; ");
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();
CoreMetadata m = core.get(0);
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=")) {
Double valueDouble = parseDouble(value);
if (valueDouble != null) {
timeIncrement = new Time(valueDouble, UNITS.S);
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();
}
m.dimensionOrder = "XYCZT";
if (z * t * (isRGB() ? 1 : c) == ifds.size()) {
m.sizeZ = z;
m.sizeT = t;
m.sizeC = isRGB() ? getSizeC() : c;
}
else if (z * c * t == ifds.size() && isRGB()) {
m.sizeZ = z;
m.sizeT = t;
m.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()) {
m.sizeZ = z;
m.sizeT = t;
m.sizeC = c;
}
else if (z * t == ifds.size()) {
m.sizeZ = z;
m.sizeT = t;
}
else m.sizeZ = ifds.size();
m.imageCount = ifds.size();
}
else {
m.sizeT = ifds.size();
m.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;
Length z = FormatTools.getPhysicalSizeZ(zDepth);
if (z != null) {
store.setPixelsPhysicalSizeZ(z, 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;
}
}