/*
* #%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.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.InflaterInputStream;
import loci.common.Constants;
import loci.common.DataTools;
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.meta.MetadataStore;
import loci.formats.tools.AmiraParameters;
import ome.xml.model.primitives.PositiveFloat;
import ome.units.quantity.Length;
/**
* This is a file format reader for AmiraMesh data.
*
* @author Gregory Jefferis jefferis at gmail.com
* @author Johannes Schindelin johannes.schindelin at gmx.de
*/
public class AmiraReader extends FormatReader {
// -- Fields --
long offsetOfFirstStream;
// for non-raw plane formats
transient PlaneReader planeReader;
private boolean hasPlaneReader = false;
private String compression;
private boolean ascii = false;
// for labels
byte[][] lut;
// -- Constructor --
public AmiraReader() {
super("Amira", new String[] {"am", "amiramesh", "grey", "hx", "labels"});
domains = new String[] {FormatTools.UNKNOWN_DOMAIN};
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#getOptimalTileHeight() */
@Override
public int getOptimalTileHeight() {
FormatTools.assertId(currentId, true, 1);
return getSizeY();
}
/**
* @see loci.formats.FormatReader#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 planeSize = FormatTools.getPlaneSize(this);
if (hasPlaneReader) {
if (planeReader == null) {
initPlaneReader();
}
// plane readers can only read whole planes, so we need to blit
int bytesPerPixel = FormatTools.getBytesPerPixel(getPixelType());
byte[] planeBuf = new byte[planeSize];
planeReader.read(no, planeBuf);
int srcWidth = getSizeX() * bytesPerPixel;
int destWidth = w * bytesPerPixel;
for (int j = y; j < y + h; j++) {
int src = j * srcWidth + x * bytesPerPixel;
int dest = (j - y) * destWidth;
System.arraycopy(planeBuf, src, buf, dest, destWidth);
}
}
else {
in.seek(offsetOfFirstStream + (long) no * planeSize);
readPlane(in, x, y, w, h, buf);
}
return buf;
}
/* @see loci.formats.FormatReader#close(boolean) */
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
offsetOfFirstStream = 0;
planeReader = null;
hasPlaneReader = false;
compression = null;
ascii = false;
lut = null;
}
/* (non-Javadoc)
* @see loci.formats.FormatReader#initFile(java.lang.String)
*/
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
AmiraParameters parameters = new AmiraParameters(in);
offsetOfFirstStream = in.getFilePointer();
LOGGER.info("Populating metadata hashtable");
addGlobalMeta("Image width", parameters.width);
addGlobalMeta("Image height", parameters.height);
addGlobalMeta("Number of planes", parameters.depth);
addGlobalMeta("Bits per pixel", 8);
LOGGER.info("Populating core metadata");
int channelIndex = 1;
while (parameters.getStreams().get("@" + channelIndex) != null) {
channelIndex++;
}
CoreMetadata m = core.get(0);
m.sizeX = parameters.width;
m.sizeY = parameters.height;
m.sizeZ = parameters.depth;
m.sizeT = 1;
m.sizeC = channelIndex - 1;
m.imageCount = getSizeZ() * getSizeC();
m.littleEndian = parameters.littleEndian;
m.dimensionOrder = "XYZCT";
String streamType = parameters.streamTypes[0].toLowerCase();
if (streamType.equals("byte")) {
m.pixelType = FormatTools.UINT8;
}
else if (streamType.equals("short")) {
m.pixelType = FormatTools.INT16;
addGlobalMeta("Bits per pixel", 16);
}
else if (streamType.equals("ushort")) {
m.pixelType = FormatTools.UINT16;
addGlobalMeta("Bits per pixel", 16);
}
else if (streamType.equals("int")) {
m.pixelType = FormatTools.INT32;
addGlobalMeta("Bits per pixel", 32);
}
else if (streamType.equals("float")) {
m.pixelType = FormatTools.FLOAT;
addGlobalMeta("Bits per pixel", 32);
}
else {
LOGGER.warn("Assuming data type is byte");
m.pixelType = FormatTools.UINT8;
}
LOGGER.info("Populating metadata store");
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
// Note that Amira specifies a bounding box, not pixel sizes.
// The bounding box is the range of the centre of the voxels
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
double pixelWidth = (double) (parameters.x1 - parameters.x0) /
(parameters.width - 1);
double pixelHeight = (double) (parameters.y1 - parameters.y0) /
(parameters.height - 1);
// TODO - what is correct setting if single slice?
double pixelDepth = (double) (parameters.z1 - parameters.z0) /
(parameters.depth - 1);
// Amira does not have a standard form for encoding units, so we just
// have to assume microns for microscopy data
addGlobalMeta("Pixels per meter (X)", 1e6 / pixelWidth);
addGlobalMeta("Pixels per meter (Y)", 1e6 / pixelHeight);
addGlobalMeta("Pixels per meter (Z)", 1e6 / pixelDepth);
Length sizeX = FormatTools.getPhysicalSizeX(pixelWidth);
Length sizeY = FormatTools.getPhysicalSizeY(pixelHeight);
Length sizeZ = FormatTools.getPhysicalSizeZ(pixelDepth);
if (sizeX != null) {
store.setPixelsPhysicalSizeX(sizeX, 0);
}
if (sizeY != null) {
store.setPixelsPhysicalSizeY(sizeY, 0);
}
if (sizeZ != null) {
store.setPixelsPhysicalSizeZ(sizeZ, 0);
}
}
ascii = parameters.ascii;
ArrayList streamData = (ArrayList) parameters.getStreams().get("@1");
if (streamData.size() > 2) {
compression = (String) streamData.get(2);
}
initPlaneReader();
hasPlaneReader = planeReader != null;
addGlobalMeta("Compression", compression);
Map params = (Map) parameters.getMap().get("Parameters");
if (params != null) {
Map materials = (Map) params.get("Materials");
if (materials != null) {
lut = getLookupTable(materials);
m.indexed = true;
}
}
}
private void initPlaneReader() {
if (ascii) {
planeReader = new ASCII(getPixelType(), getSizeX() * getSizeY());
}
if (compression != null) {
if (compression.startsWith("HxZip,")) {
long size = Long.parseLong(compression.substring("HxZip,".length()));
planeReader = new HxZip(size);
}
else if (compression.startsWith("HxByteRLE,")) {
long size =
Long.parseLong(compression.substring("HxByteRLE,".length()));
planeReader = new HxRLE(getSizeZ(), size);
}
}
}
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
@Override
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
if (!FormatTools.validStream(stream, 50, false)) return false;
String c = stream.readLine();
Matcher amiraMeshDef = Pattern.compile("#\\s+AmiraMesh.*?" +
"(BINARY|ASCII)(-LITTLE-ENDIAN)*").matcher(c);
return amiraMeshDef.find();
}
/* @see IFormatReader#get8BitLookupTable() */
@Override
public byte[][] get8BitLookupTable() {
FormatTools.assertId(currentId, true ,1);
return lut;
}
// -- Helper methods --
byte[][] getLookupTable(Map materials) throws FormatException {
byte[][] result = new byte[3][256];
int i = -1;
for (Object label : materials.keySet()) {
i++;
Object object = materials.get(label);
if (!(object instanceof Map)) {
throw new FormatException("Invalid material: " + label);
}
Map material = (Map) object;
object = material.get("Color");
if (object == null) continue; // black
if (!(object instanceof Number[])) {
throw new FormatException("Invalid material: " + label);
}
Number[] color = (Number[]) object;
if (color.length != 3) {
throw new FormatException("Invalid color: " +
color.length + " channels");
}
for (int j = 0; j < 3; j++) {
result[j][i] = (byte) (int) (255 * color[j].floatValue());
}
}
return result;
}
/**
* This is the common interface for all formats, compressed or not.
*/
interface PlaneReader {
byte[] read(int no, byte[] buf) throws FormatException, IOException;
}
/**
* This class provides a not-quite-efficient, but simple reader for
* planes stored in ASCII-encoded numbers.
*
* Should efficiency ever become a concern, we'll need to have
* specializations for every pixel type.
*/
class ASCII implements PlaneReader {
int pixelType, bytesPerPixel, pixelsPerPlane;
long[] offsets;
byte[] numberBuffer = new byte[32];
ASCII(int pixelType, int pixelsPerPlane) {
this.pixelType = pixelType;
this.pixelsPerPlane = pixelsPerPlane;
bytesPerPixel = FormatTools.getBytesPerPixel(pixelType);
offsets = new long[getSizeZ() + 1];
offsets[0] = offsetOfFirstStream;
}
@Override
public byte[] read(int no, byte[] buf) throws FormatException, IOException {
if (offsets[no] == 0) {
int i = no - 1;
while (offsets[i] == 0) {
i--;
}
in.seek(offsets[i]);
while (i < no) {
for (int j = 0; j < pixelsPerPlane; j++) {
readNumberString();
}
offsets[++i] = in.getFilePointer();
}
}
else {
in.seek(offsets[no]);
}
for (int j = 0; j < pixelsPerPlane; j++) {
int offset = j * bytesPerPixel;
double number = readNumberString();
long value = pixelType == FormatTools.DOUBLE ?
Double.doubleToLongBits(number) :
pixelType == FormatTools.FLOAT ?
Float.floatToIntBits((float) number) :
(long) number;
DataTools.unpackBytes(value, buf, offset, bytesPerPixel, false);
}
offsets[no + 1] = in.getFilePointer();
return buf;
}
double readNumberString() throws IOException {
numberBuffer[0] = skipWhiteSpace();
for (int i = 1;; i++) {
byte c = in.readByte();
if (!(c >= '0' && c <= '9') && c != '.') {
return Double.parseDouble(
new String(numberBuffer, 0, i, Constants.ENCODING));
}
numberBuffer[i] = c;
}
}
byte skipWhiteSpace() throws IOException {
for (;;) {
byte c = in.readByte();
if (c != ' ' && c != '\t' && c != '\n') {
return c;
}
}
}
}
/**
* This is the reader for GZip-compressed AmiraMeshes.
*
* As such files contain a single GZipped stream for the complete stack,
* we cannot really access the slices randomly, but have to decompress
* instead of seeking.
*/
class HxZip implements PlaneReader {
long offsetOfStream, compressedSize;
int currentNo, planeSize;
transient InflaterInputStream decompressor;
HxZip(long compressedSize) {
this.compressedSize = compressedSize;
planeSize = FormatTools.getPlaneSize(AmiraReader.this);
offsetOfStream = offsetOfFirstStream;
currentNo = Integer.MAX_VALUE;
}
void initDecompressor() throws IOException {
currentNo = 0;
in.seek(offsetOfStream);
decompressor = new InflaterInputStream(in);
}
@Override
public byte[] read(int no, byte[] buf) throws FormatException, IOException {
if (no < currentNo) {
initDecompressor();
}
for (; currentNo <= no; currentNo++) {
int offset = 0, len = planeSize;
while (len > 0) {
int count = decompressor.read(buf, offset, len);
if (count <= 0) return null;
offset += count;
len -= count;
}
}
return buf;
}
}
/**
* This is the reader for RLE-compressed AmiraMeshes.
*/
class HxRLE implements PlaneReader {
long compressedSize;
long[] offsets;
int[] internalOffsets;
int currentNo, maxOffsetIndex, planeSize;
long lastCodeOffset = 0;
HxRLE(int sliceCount, long compressedSize) {
this.compressedSize = compressedSize;
offsets = new long[sliceCount + 1];
internalOffsets = new int[sliceCount + 1];
offsets[0] = offsetOfFirstStream;
internalOffsets[0] = 0;
planeSize = FormatTools.getPlaneSize(AmiraReader.this);
maxOffsetIndex = currentNo = 0;
}
void read(byte[] buf, int len) throws FormatException, IOException {
int off = 0;
while (len > 0 && in.getFilePointer() < in.length()) {
lastCodeOffset = in.getFilePointer();
int insn = in.readByte();
if (insn < 0) {
insn = (insn & 0x7f);
if (insn > len) {
throw new FormatException("Slice " + currentNo + " is unaligned!");
}
while (insn > 0) {
int count = in.read(buf, off, insn);
if (count < 0) throw new IOException("End of file!");
insn -= count;
len -= count;
off += count;
}
}
else {
if (insn > len) {
internalOffsets[currentNo] = len;
insn = len;
}
else if (insn == len) lastCodeOffset += 2;
if (off == 0 && currentNo > 0 && internalOffsets[currentNo - 1] > 0) {
insn -= internalOffsets[currentNo - 1];
}
Arrays.fill(buf, off, off + insn, in.readByte());
len -= insn;
off += insn;
}
}
}
@Override
public byte[] read(int no, byte[] buf) throws FormatException, IOException {
if (maxOffsetIndex < no) {
in.seek(offsets[maxOffsetIndex]);
while (maxOffsetIndex <= no) {
Arrays.fill(buf, (byte) 0);
read(currentNo, buf);
currentNo++;
}
}
else {
in.seek(offsets[no]);
currentNo = no;
Arrays.fill(buf, (byte) 0);
read(buf, planeSize);
if (maxOffsetIndex == no) {
offsets[++maxOffsetIndex] = lastCodeOffset;
}
}
return buf;
}
}
}