// // OpenlabReader.java // /* LOCI Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan, Eric Kjellman and Brian Loranger. This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a copy of the GNU Library 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.awt.image.BufferedImage; import java.io.*; import java.util.Arrays; import java.util.Vector; import loci.formats.*; import loci.formats.codec.LZOCodec; /** * OpenlabReader is the file format reader for Openlab LIFF files. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/OpenlabReader.java">Trac</a>, * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/OpenlabReader.java">SVN</a></dd></dl> * * @author Melissa Linkert linkert at wisc.edu * @author Eric Kjellman egkjellman at wisc.edu * @author Curtis Rueden ctrueden at wisc.edu */ public class OpenlabReader extends FormatReader { // -- Constants -- /** Image types. */ private static final int MAC_1_BIT = 1; private static final int MAC_256_GREYS = 5; private static final int MAC_256_COLORS = 6; private static final int MAC_24_BIT = 8; private static final int GREY_16_BIT = 16; // -- Static fields -- /** Helper reader to read PICT data. */ private static PictReader pict = new PictReader(); // -- Fields -- /** LIFF version (should be 2 or 5). */ private int version; /** Number of series. */ private int numSeries; private Vector[] layerInfoList; private float xCal, yCal, zCal; private int bytesPerPixel; private int tag = 0, subTag = 0; private String fmt = ""; // -- Constructor -- /** Constructs a new OpenlabReader. */ public OpenlabReader() { super("Openlab LIFF", "liff"); } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(byte[]) */ public boolean isThisType(byte[] block) { return block.length >= 8 && block[0] == 0 && block[1] == 0 && block[2] == -1 && block[3] == -1 && block[4] == 105 && block[5] == 109 && block[6] == 112 && block[7] == 114; } /* @see loci.formats.IFormatReader#openBytes(int, byte[]) */ public byte[] openBytes(int no, byte[] buf) throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); FormatTools.checkPlaneNumber(this, no); FormatTools.checkBufferSize(this, buf.length); buf = openBytes(no); return buf; } /* @see loci.formats.IFormatReader#openBytes(int) */ public byte[] openBytes(int no) throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); FormatTools.checkPlaneNumber(this, no); LayerInfo info = (LayerInfo) layerInfoList[series].get(no); in.seek(info.layerStart); readTagHeader(); if ((tag != 67 && tag != 68) || (!fmt.equals("PICT") && !fmt.equals("RAWi"))) { throw new FormatException("Corrupt LIFF file."); } in.skipBytes(24); int volumeType = in.readShort(); in.skipBytes(272); int top, left, bottom, right; if (version == 2) { in.skipBytes(2); top = in.readShort(); left = in.readShort(); bottom = in.readShort(); right = in.readShort(); if (core.sizeX[series] == 0) core.sizeX[series] = right - left; if (core.sizeY[series] == 0) core.sizeY[series] = bottom - top; } else { core.sizeX[series] = in.readInt(); core.sizeY[series] = in.readInt(); } in.seek(info.layerStart); byte[] b = new byte[0]; if (version == 2) { long nextTag = readTagHeader(); if ((tag != 67 && tag != 68) || !fmt.equals("PICT")) { throw new FormatException("Corrupt LIFF file."); } in.skipBytes(298); // open image using pict reader Exception exception = null; try { b = new byte[(int) (nextTag - in.getFilePointer())]; in.read(b); BufferedImage img = pict.open(b); byte[][] tmp = ImageTools.getBytes(img); b = new byte[tmp.length * tmp[0].length]; int pt = 0; for (int i=0; i<tmp[0].length; i++) { for (int j=0; j<tmp.length; j++) { b[pt++] = tmp[j][i]; } } } catch (FormatException exc) { exception = exc; } catch (IOException exc) { exception = exc; } if (exception != null) { if (debug) trace(exception); b = null; in.seek(info.layerStart + 12); int blockSize = DataTools.read4SignedBytes(in, false); byte toRead = (byte) in.read(); // right now I'm gonna skip all the header info // check to see whether or not this is v2 data if (toRead == 1) in.skipBytes(128); in.skipBytes(169); // read in the block of data byte[] q = new byte[blockSize]; in.read(q); byte[] pixelData = new byte[blockSize]; int pixPos = 0; int length = q.length; int num, size; int totalBlocks = -1; // set to allow loop to start. int expectedBlock = 0; int pos = 0; while (expectedBlock != totalBlocks) { while (pos + 7 < length && (q[pos] != 73 || q[pos + 1] != 86 || q[pos + 2] != 69 || q[pos + 3] != 65 || q[pos + 4] != 100 || q[pos + 5] != 98 || q[pos + 6] != 112 || q[pos + 7] != 113)) { pos++; } pos += 8; // skip the block type we just found // Read info from the iPic comment. This serves as a // starting point to read the rest. num = DataTools.bytesToInt(q, pos, 4, false); if (num != expectedBlock) { throw new FormatException("Expected iPic block not found"); } expectedBlock++; if (totalBlocks == -1) { totalBlocks = DataTools.bytesToInt(q, pos + 4, 4, false); } else { if (DataTools.bytesToInt(q, pos + 4, 4, false) != totalBlocks) { throw new FormatException("Unexpected totalBlocks numbein.read"); } } // skip to size pos += 16; size = DataTools.bytesToInt(q, pos, 4, false); pos += 8; // copy into our data array. System.arraycopy(q, pos, pixelData, pixPos, size); pixPos += size; } System.gc(); b = new byte[pixPos]; System.arraycopy(pixelData, 0, b, 0, b.length); } } else { readTagHeader(); if (tag != 68 || !fmt.equals("RAWi")) { throw new FormatException("Corrupt LIFF file."); } if (subTag != 0) { throw new FormatException("Wrong compression type."); } in.skipBytes(24); volumeType = in.readShort(); in.skipBytes(280); int size = in.readInt(); int compressedSize = in.readInt(); b = new byte[size]; byte[] c = new byte[compressedSize]; in.read(c); LZOCodec lzoc = new LZOCodec(); b = lzoc.decompress(c); if (b.length != size) { LogTools.println("LZOCodec failed to predict image size"); LogTools.println(size + " expected, got " + b.length + ". The image displayed may not be correct."); } if (volumeType == MAC_24_BIT) { bytesPerPixel = b.length >= core.sizeX[series] * core.sizeY[series] * 4 ? 4 : 3; int destRowBytes = core.sizeX[series] * bytesPerPixel; int srcRowBytes = b.length / core.sizeY[series]; byte[] tmp = new byte[destRowBytes * core.sizeY[series]]; int src = 0; int dest = 0; for (int y=0; y<core.sizeY[series]; y++) { System.arraycopy(b, src, tmp, dest, destRowBytes); src += srcRowBytes; dest += destRowBytes; } // strip out alpha channel and force channel separation if (bytesPerPixel == 4) { b = new byte[(3 * tmp.length) / 4]; dest = 0; for (int i=0; i<tmp.length; i+=4) { b[dest] = tmp[i + 1]; b[dest + (b.length / 3)] = tmp[i + 2]; b[dest + ((2 * b.length) / 3)] = tmp[i + 3]; dest++; } bytesPerPixel = 3; } } else if (volumeType == MAC_256_GREYS) { byte[] tmp = b; b = new byte[core.sizeX[series] * core.sizeY[series]]; for (int y=0; y<core.sizeY[series]; y++) { System.arraycopy(tmp, y*(core.sizeX[series] + 16), b, y*core.sizeX[series], core.sizeX[series]); } } else if (volumeType < MAC_24_BIT) { throw new FormatException("Unsupported image type : " + volumeType); } } int bpp = b.length / (core.sizeX[series] * core.sizeY[series]); int expected = core.sizeX[series] * core.sizeY[series] * bpp; if (b.length > expected) { byte[] tmp = b; b = new byte[expected]; System.arraycopy(tmp, 0, b, 0, b.length); } return b; } /* @see loci.formats.IFormatReader#close(boolean) */ public void close(boolean fileOnly) throws IOException { if (fileOnly) { if (in != null) in.close(); if (pict != null) pict.close(fileOnly); } else close(); } // -- IFormatHandler API methods -- /* @see loci.formats.IFormatHandler#isThisType(String, boolean) */ public boolean isThisType(String name, boolean open) { if (super.isThisType(name, open)) return true; // check extension if (open) { byte[] b = new byte[8]; try { in = new RandomAccessStream(name); in.read(b); } catch (IOException exc) { if (debug) trace(exc); return false; } return isThisType(b); } else { // not allowed to check the file contents return name.indexOf(".") < 0; // file appears to have no extension } } /* @see loci.formats.IFormatHandler#close() */ public void close() throws IOException { super.close(); if (pict != null) pict.close(); layerInfoList = null; } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ protected void initFile(String id) throws FormatException, IOException { if (debug) debug("OpenlabReader.initFile(" + id + ")"); super.initFile(id); in = new RandomAccessStream(id); status("Verifying Openlab LIFF format"); in.order(false); in.skipBytes(4); if (!in.readString(4).equals("impr")) { throw new FormatException("Invalid LIFF file."); } version = in.readInt(); if (version != 2 && version != 5) { throw new FormatException("Invalid version : " + version); } // skip the layer count and ID seed in.skipBytes(4); // read offset to first plane int offset = in.readInt(); in.seek(offset); status("Finding image offsets"); layerInfoList = new Vector[2]; for (int i=0; i<layerInfoList.length; i++) layerInfoList[i] = new Vector(); xCal = yCal = zCal = (float) 0.0; // scan through the file, and read image information while (in.getFilePointer() < in.length()) { long nextTag, startPos; subTag = tag = 0; try { startPos = in.getFilePointer(); nextTag = readTagHeader(); } catch (IOException exc) { if (debug) trace(exc); if (in.getFilePointer() >= in.length()) break; else throw new FormatException(exc.getMessage()); } try { if (tag == 67 || tag == 68 || fmt.equals("PICT") || fmt.equals("RAWi")) { LayerInfo info = new LayerInfo(); info.layerStart = (int) startPos; info.zPosition = -1; info.wavelength = -1; in.skipBytes(24); int volumeType = in.readShort(); if (volumeType == MAC_1_BIT || volumeType == MAC_256_GREYS || volumeType == MAC_256_COLORS || (volumeType >= MAC_24_BIT && volumeType <= GREY_16_BIT)) { in.skipBytes(16); info.layerName = in.readString(128); if (!info.layerName.trim().equals("Original Image")) { info.timestamp = in.readLong(); layerInfoList[0].add(info); } } } else if (tag == 69) { in.skipBytes(18); xCal = in.readFloat(); yCal = in.readFloat(); } else if (tag == 72 || fmt.equals("USER")) { char aChar = (char) in.read(); StringBuffer sb = new StringBuffer(); while (aChar != 0) { sb = sb.append(aChar); aChar = (char) in.read(); } String className = sb.toString(); if (className.equals("CVariableList")) { aChar = (char) in.read(); if (aChar == 1) { int numVars = in.readShort(); while (numVars > 0) { aChar = (char) in.read(); sb = new StringBuffer(); while (aChar != 0) { sb = sb.append(aChar); aChar = (char) in.read(); } //in.read(); String varName = ""; String varStringValue = ""; double varNumValue = 0.0; className = sb.toString(); int derivedClassVersion = in.read(); if (derivedClassVersion != 1) { throw new FormatException("Invalid revision."); } if (className.equals("CStringVariable")) { int strSize = in.readInt(); varStringValue = in.readString(strSize); varNumValue = Float.parseFloat(varStringValue); in.skipBytes(1); } else if (className.equals("CFloatVariable")) { varNumValue = in.readDouble(); varStringValue = "" + varNumValue; } int baseClassVersion = in.read(); if (baseClassVersion == 1 || baseClassVersion == 2) { int strSize = in.readInt(); varName = in.readString(strSize); in.skipBytes(baseClassVersion == 1 ? 3 : 2); } else { throw new FormatException("Invalid revision."); } addMeta(varName, varStringValue); numVars--; } } } } in.seek(nextTag); } catch (Exception exc) { // CTR TODO - eliminate catch-all exception handling if (debug) trace(exc); in.seek(nextTag); } } Vector tmp = new Vector(); for (int i=0; i<layerInfoList[0].size(); i++) { tmp.add(layerInfoList[0].get(i)); } core = new CoreMetadata(2); core.imageCount[0] = tmp.size(); // determine if we have a multi-series file status("Determining series count"); int oldChannels = openBytes(0).length / (core.sizeX[0] * core.sizeY[0] * 3); int oldWidth = core.sizeX[0]; for (int i=0; i<tmp.size(); i++) { LayerInfo layer = (LayerInfo) tmp.get(i); in.seek(layer.layerStart); long nextTag = readTagHeader(); if (fmt.equals("PICT")) { in.skipBytes(298); int top, left, bottom, right; if (version == 2) { in.skipBytes(2); top = in.readShort(); left = in.readShort(); bottom = in.readShort(); right = in.readShort(); if (core.sizeX[series] == 0) core.sizeX[series] = right - left; if (core.sizeY[series] == 0) core.sizeY[series] = bottom - top; } else { core.sizeX[series] = in.readInt(); core.sizeY[series] = in.readInt(); } in.seek(layer.layerStart); if (version == 2) { nextTag = readTagHeader(); if ((tag != 67 && tag != 68) || !fmt.equals("PICT")) { throw new FormatException("Corrupt LIFF file."); } in.skipBytes(298); // open image using pict reader try { byte[] b = new byte[(int) (nextTag - in.getFilePointer())]; in.read(b); BufferedImage img = pict.open(b); if (img.getRaster().getNumBands() != oldChannels || img.getWidth() != oldWidth) { layerInfoList[1].add(tmp.get(i)); layerInfoList[0].remove(tmp.get(i)); } } catch (FormatException e) { } } } else { in.skipBytes(24); int type = DataTools.read2SignedBytes(in, false); if (type == MAC_24_BIT) { layerInfoList[1].add(tmp.get(i)); layerInfoList[0].remove(tmp.get(i)); } } } if (layerInfoList[1].size() == 0 || layerInfoList[0].size() == 0) { core.sizeC = new int[1]; core.sizeC[0] = layerInfoList[1].size() == 0 ? 1 : 3; if (core.sizeC[0] == 1 && oldChannels == 1) core.sizeC[0] = 3; int oldImages = core.imageCount[0]; core.imageCount = new int[1]; core.imageCount[0] = oldImages; if (layerInfoList[0].size() == 0) layerInfoList[0] = layerInfoList[1]; int x = core.sizeX[0]; core.sizeX = new int[1]; core.sizeX[0] = x; } else { core.imageCount[0] = layerInfoList[0].size(); core.imageCount[1] = layerInfoList[1].size(); core.sizeC[0] = 1; core.sizeC[1] = 3; int oldW = core.sizeX[0]; int oldH = core.sizeY[0]; core.sizeX = new int[2]; core.sizeY = new int[2]; core.sizeX[0] = oldW; core.sizeX[1] = oldW; core.sizeY[0] = oldH; core.sizeY[1] = oldH; } Arrays.fill(core.metadataComplete, true); status("Populating metadata"); numSeries = core.imageCount.length; int[] bpp = new int[numSeries]; Arrays.fill(core.orderCertain, true); int oldSeries = getSeries(); for (int i=0; i<bpp.length; i++) { setSeries(i); if (core.sizeC[i] == 0) core.sizeC[i] = 1; bpp[i] = openBytes(0).length / (core.sizeX[i] * core.sizeY[i]); } setSeries(oldSeries); if (bytesPerPixel == 3) bytesPerPixel = 1; if (bytesPerPixel == 0) bytesPerPixel++; // finish populating metadata hashtable addMeta("Version", new Integer(version)); addMeta("Number of Series", new Integer(numSeries)); for (int i=0; i<numSeries; i++) { addMeta("Width (Series " + i + ")", new Integer(core.sizeX[i])); addMeta("Height (Series " + i + ")", new Integer(core.sizeY[i])); addMeta("Bit depth (Series " + i + ")", new Integer(bpp[i] * 8)); addMeta("Number of channels (Series " + i + ")", new Integer(core.sizeC[i])); addMeta("Number of images (Series " + i + ")", new Integer(core.imageCount[i])); } // populate MetadataStore MetadataStore store = getMetadataStore(); for (int i=0; i<numSeries; i++) { core.sizeT[i] += 1; core.sizeZ[i] = core.imageCount[i] / core.sizeT[i]; core.currentOrder[i] = isRGB() ? "XYCZT" : "XYZCT"; core.rgb[i] = core.sizeC[i] > 1; core.interleaved[i] = true; core.littleEndian[i] = false; try { if (i != 0) { if (bpp[i] == bpp[0]) bpp[i] = bpp[i + 1]; } } catch (ArrayIndexOutOfBoundsException a) { } switch (bpp[i]) { case 1: case 3: core.pixelType[i] = FormatTools.UINT8; break; case 2: case 6: core.pixelType[i] = FormatTools.UINT16; break; case 4: core.pixelType[i] = FormatTools.UINT32; break; } store.setImage("Series " + i, null, null, new Integer(i)); store.setDimensions(new Float(xCal), new Float(yCal), new Float(zCal), null, null, new Integer(i)); } FormatTools.populatePixels(store, this); for (int i=0; i<numSeries; i++) { for (int j=0; j<core.sizeC[i]; j++) { store.setLogicalChannel(j, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, new Integer(i)); } } } // -- Helper methods -- /** Read the next tag. */ private long readTagHeader() throws IOException { tag = in.readShort(); subTag = in.readShort(); long nextTag = (version == 2 ? in.readInt() : in.readLong()); byte[] b = new byte[4]; in.read(b); fmt = new String(b); if (version == 2) in.skipBytes(4); else in.skipBytes(8); return nextTag; } // -- Helper classes -- /** Helper class for storing layer info. */ protected class LayerInfo { protected int layerStart; protected int zPosition; protected int wavelength; protected String layerName; protected long timestamp; } }