/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) Max Planck Institute for Biophysical Chemistry, * Goettingen, 2014 - 2015 * * Copyright (C) 2014 - 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.ArrayList; import java.util.List; import java.util.zip.Inflater; import java.util.zip.DataFormatException; import javax.xml.parsers.ParserConfigurationException; import loci.common.RandomAccessInputStream; import loci.common.xml.XMLTools; 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 ome.units.quantity.Length; import ome.units.UNITS; import ome.xml.model.primitives.PositiveFloat; import org.xml.sax.SAXException; import org.w3c.dom.Element; import org.w3c.dom.NodeList; /** * OBFReader is the file format reader for Imspector OBF files. * * @author Bjoern Thiel bjoern.thiel at mpibpc.mpg.de */ public class OBFReader extends FormatReader { private static final boolean LITTLE_ENDIAN = true; private static final String FILE_MAGIC_STRING = "OMAS_BF\n"; private static final String STACK_MAGIC_STRING = "OMAS_BF_STACK\n"; private static final short MAGIC_NUMBER = (short) 0xFFFF; private static final int FILE_VERSION = 2; private static final int STACK_VERSION = 5; private static final int MAXIMAL_NUMBER_OF_DIMENSIONS = 15; private class Stack { long position; long length; boolean compression; } private List<Stack> stacks = new ArrayList<Stack>(); private class Frame { byte[] bytes; int series; int number; } private Frame currentInflatedFrame = new Frame(); private transient Inflater inflater; public OBFReader() { super("OBF", new String[] {"obf", "msr"}); suffixNecessary = false; suffixSufficient = false; datasetDescription = "OBF file"; } private int getFileVersion(RandomAccessInputStream stream) throws IOException { stream.seek(0); stream.order(LITTLE_ENDIAN); try { final String magicString = stream.readString(FILE_MAGIC_STRING.length()); final short magicNumber = stream.readShort(); final int version = stream.readInt(); if (magicString.equals(FILE_MAGIC_STRING) && magicNumber == MAGIC_NUMBER) { return version; } } catch(IOException exception) { } return -1; } @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int fileVersion = getFileVersion(stream); return fileVersion >= 0 && fileVersion <= FILE_VERSION; } @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); currentInflatedFrame.series = -1; currentInflatedFrame.number = -1; in = new RandomAccessInputStream(id); final int fileVersion = getFileVersion(in); long stackPosition = in.readLong(); final int lengthOfDescription = in.readInt(); final String description = in.readString(lengthOfDescription); metadata.put("Description", description); if (fileVersion > 1) { in.readLong(); } if (stackPosition != 0) { core.clear(); do { stackPosition = initStack(stackPosition, fileVersion); } while (stackPosition != 0); } MetadataStore ome = makeFilterMetadata(); MetadataTools.populatePixels(ome, this); for (int series = 0; series != core.size(); ++ series) { CoreMetadata obf = core.get(series); final String name = obf.seriesMetadata.get("Name").toString(); ome.setImageName(name, series); @SuppressWarnings("unchecked") final List<Double> lengths = (List<Double>) obf.seriesMetadata.get("Lengths"); if (lengths.size() > 0) { double lengthX = Math.abs(lengths.get(0)); if (lengthX < 0.01) { lengthX *= 1000000; } if (lengthX > 0) { Length physicalSizeX = FormatTools.getPhysicalSizeX(lengthX / obf.sizeX, UNITS.MICROM); if (physicalSizeX != null) { ome.setPixelsPhysicalSizeX(physicalSizeX, series); } } } if (lengths.size() > 1) { double lengthY = Math.abs(lengths.get(1)); if (lengthY < 0.01) { lengthY *= 1000000; } if (lengthY > 0) { Length physicalSizeY = FormatTools.getPhysicalSizeY(lengthY / obf.sizeY, UNITS.MICROM); if (physicalSizeY != null) { ome.setPixelsPhysicalSizeY(physicalSizeY, series); } } } if (lengths.size() > 2) { double lengthZ = Math.abs(lengths.get(2)); if (lengthZ < 0.01) { lengthZ *= 1000000; } if (lengthZ > 0) { Length physicalSizeZ = FormatTools.getPhysicalSizeZ(lengthZ / obf.sizeZ, UNITS.MICROM); if (physicalSizeZ != null) { ome.setPixelsPhysicalSizeZ(physicalSizeZ, series); } } } } } private long initStack(long current, int fileVersion) throws FormatException, IOException { in.seek(current); final String magicString = in.readString(STACK_MAGIC_STRING.length()); final short magicNumber = in.readShort(); final int version = in.readInt(); if (magicString.equals(STACK_MAGIC_STRING) && magicNumber == MAGIC_NUMBER && version <= STACK_VERSION) { CoreMetadata obf = new CoreMetadata(); core.add(obf); obf.littleEndian = LITTLE_ENDIAN; obf.thumbnail = false; final int numberOfDimensions = in.readInt(); if (numberOfDimensions > 5) { throw new FormatException("Unsupported number of " + numberOfDimensions + " dimensions"); } int[] sizes = new int[MAXIMAL_NUMBER_OF_DIMENSIONS]; for (int dimension = 0; dimension != MAXIMAL_NUMBER_OF_DIMENSIONS; ++ dimension) { final int size = in.readInt(); sizes[dimension] = dimension < numberOfDimensions ? size : 1; } obf.sizeX = sizes[0]; obf.sizeY = sizes[1]; obf.sizeZ = sizes[2]; obf.sizeC = sizes[3]; obf.sizeT = sizes[4]; obf.imageCount = sizes[2] * sizes[3] * sizes[4]; obf.dimensionOrder = "XYZCT"; obf.orderCertain = false; List<Double> lengths = new ArrayList<Double>(); for (int dimension = 0; dimension != MAXIMAL_NUMBER_OF_DIMENSIONS; ++ dimension) { final double length = in.readDouble(); if (dimension < numberOfDimensions) { lengths.add(new Double(length)); } } obf.seriesMetadata.put("Lengths", lengths); List<Double> offsets = new ArrayList<Double>(); for (int dimension = 0; dimension != MAXIMAL_NUMBER_OF_DIMENSIONS; ++ dimension) { final double offset = in.readDouble(); if (dimension < numberOfDimensions) { offsets.add(new Double(offset)); } } obf.seriesMetadata.put("Offsets", offsets); final int type = in.readInt(); obf.pixelType = getPixelType(type); obf.bitsPerPixel = getBitsPerPixel(type); obf.indexed = false; obf.rgb = false; obf.interleaved = false; Stack stack = new Stack(); final int compression = in.readInt(); stack.compression = getCompression(compression); in.skipBytes(4); final int lengthOfName = in.readInt(); final int lengthOfDescription = in.readInt(); in.skipBytes(8); final long lengthOfData = in.readLong(); stack.length = getLength(lengthOfData); final long next = in.readLong(); final String name = in.readString(lengthOfName); obf.seriesMetadata.put("Name", name); String description = in.readString(lengthOfDescription); if (description != null) { description = XMLTools.sanitizeXML(description); // some XML node names may contain white space, which prevents parsing description = description.replaceAll("<Time Lapse ", "<TimeLapse "); description = description.replaceAll("</Time Lapse", "</TimeLapse"); boolean xml = false; try { Element root = XMLTools.parseDOM(description).getDocumentElement(); root = getChildNodes(root).get(0); ArrayList<Element> children = getChildNodes(root); for (Element child : children) { String nodeName = child.getNodeName(); ArrayList<Element> grandchildren = getChildNodes(child); for (Element grandchild : grandchildren) { String key = grandchild.getNodeName(); String value = grandchild.getTextContent().trim(); if (!key.equals("doc") && !key.equals("hwr")) { addSeriesMeta(nodeName + " " + key, value); } else { ArrayList<Element> docs = getChildNodes(grandchild); for (Element doc : docs) { key = doc.getNodeName(); value = doc.getTextContent().trim(); addSeriesMeta(nodeName + " " + key, value); } } } } xml = true; } catch (ParserConfigurationException e) { LOGGER.warn("Could parse description as XML", e); } catch (SAXException e) { LOGGER.warn("Could parse description as XML", e); } if (!xml) { obf.seriesMetadata.put("Description", description); } } stack.position = in.getFilePointer(); stacks.add(stack); if (fileVersion >= 1) { in.skip(lengthOfData); final long footer = in.getFilePointer(); final int offset = in.readInt(); List<Boolean> stepsPresent = new ArrayList<Boolean>(); for (int dimension = 0; dimension != MAXIMAL_NUMBER_OF_DIMENSIONS; ++ dimension) { final int present = in.readInt(); if (dimension < numberOfDimensions) { stepsPresent.add(new Boolean(present != 0)); } } List<Boolean> stepLabelsPresent = new ArrayList<Boolean>(); for (int dimension = 0; dimension != MAXIMAL_NUMBER_OF_DIMENSIONS; ++ dimension) { final int present = in.readInt(); if (dimension < numberOfDimensions) { stepLabelsPresent.add(new Boolean(present != 0)); } } in.seek(footer + offset); List<String> labels = new ArrayList<String>(); for (int dimension = 0; dimension != numberOfDimensions; ++ dimension) { final int length = in.readInt(); final String label = in.readString(length); labels.add(label); } obf.seriesMetadata.put("Labels", labels); List<List<Double>> steps = new ArrayList<List<Double>>(); for (int dimension = 0; dimension != numberOfDimensions; ++ dimension) { List<Double> list = new ArrayList<Double>(); if (stepsPresent.get(dimension)) { for (int position = 0; position != sizes[dimension]; ++ position) { final double step = in.readDouble(); list.add(new Double(step)); } } steps.add(list); } obf.seriesMetadata.put("Steps", steps); List<List<String>> stepLabels = new ArrayList<List<String>>(); for (int dimension = 0; dimension != numberOfDimensions; ++ dimension) { List<String> list = new ArrayList<String>(); if (stepLabelsPresent.get(dimension)) { for (int position = 0; position != sizes[dimension]; ++ position) { final int length = in.readInt(); final String label = in.readString(length); list.add(label); } } stepLabels.add(list); } obf.seriesMetadata.put("StepLabels", stepLabels); } return next; } else { throw new FormatException("Unsupported stack format"); } } private int getPixelType(int type) throws FormatException { switch (type) { case 0x01: return FormatTools.UINT8; case 0x02: return FormatTools.INT8; case 0x04: return FormatTools.UINT16; case 0x08: return FormatTools.INT16; case 0x10: return FormatTools.UINT32; case 0x20: return FormatTools.INT32; case 0x40: return FormatTools.FLOAT; case 0x80: return FormatTools.DOUBLE; default: throw new FormatException("Unsupported data type " + type); } } private int getBitsPerPixel(int type) throws FormatException { switch (type) { case 0x01: case 0x02: return 8; case 0x04: case 0x08: return 16; case 0x10: case 0x20: return 32; case 0x40: return 32; case 0x80: return 64; default: throw new FormatException("Unsupported data type " + type); } } private long getLength(long length) throws FormatException { if (length >= 0) { return length; } else { throw new FormatException("Negative stack length on disk"); } } private boolean getCompression(int compression) throws FormatException { switch (compression) { case 0: return false; case 1: return true; default: throw new FormatException("Unsupported compression " + compression); } } @Override public byte[] openBytes(int no, byte[] buffer, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buffer.length, x, y, w, h); final int rows = getSizeY(); final int columns = getSizeX(); final int bytesPerPixel = getBitsPerPixel() / 8; final int series = getSeries(); final Stack stack = stacks.get(series); if (stack.compression) { if (series != currentInflatedFrame.series) { currentInflatedFrame.bytes = new byte[rows * columns * bytesPerPixel]; currentInflatedFrame.series = series; currentInflatedFrame.number = - 1; } if (inflater == null) { inflater = new Inflater(); } byte[] bytes = currentInflatedFrame.bytes; if (no != currentInflatedFrame.number) { if (no < currentInflatedFrame.number) { currentInflatedFrame.number = - 1; } if (currentInflatedFrame.number == - 1) { in.seek(stack.position); inflater.reset(); } byte[] input = new byte[8192]; while (no != currentInflatedFrame.number) { int offset = 0; while (offset != bytes.length) { if (inflater.needsInput()) { final long remainder = stack.position + stack.length - in.getFilePointer(); if (remainder > 0) { final int length = remainder > input.length ? input.length : (int) remainder; in.read(input, 0, length); inflater.setInput(input, 0, length); } else { throw new FormatException("Corrupted zlib compression"); } } else if (inflater.needsDictionary()) { throw new FormatException("Unsupported zlib compression"); } try { offset += inflater.inflate(bytes, offset, bytes.length - offset); } catch (DataFormatException exception) { throw new FormatException(exception.getMessage()); } } ++ currentInflatedFrame.number; } } for (int row = 0; row != h; ++ row) { System.arraycopy(bytes, ((row + y) * columns + x) * bytesPerPixel, buffer, row * w * bytesPerPixel, w * bytesPerPixel); } } else { for (int row = 0; row != h; ++ row) { in.seek(stack.position + ((no * rows + row + y) * columns + x) * bytesPerPixel); in.read(buffer, row * w * bytesPerPixel, w * bytesPerPixel); } } return buffer; } @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { stacks.clear(); currentInflatedFrame = new Frame(); inflater = null; } } private ArrayList<Element> getChildNodes(Element root) { ArrayList<Element> list = new ArrayList<Element>(); NodeList children = root.getChildNodes(); for (int i=0; i<children.getLength(); i++) { Object child = children.item(i); if (child instanceof Element) { list.add((Element) child); } } return list; } }