/*
* #%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.ByteArrayInputStream;
import java.io.IOException;
import java.util.Vector;
import loci.common.CBZip2InputStream;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.BaseHandler;
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.MissingLibraryException;
import loci.formats.codec.CodecOptions;
import loci.formats.codec.JPEG2000Codec;
import loci.formats.codec.JPEGCodec;
import loci.formats.codec.ZlibCodec;
import loci.formats.meta.MetadataStore;
import loci.formats.ome.OMEXMLMetadata;
import loci.formats.services.OMEXMLService;
import loci.formats.services.OMEXMLServiceImpl;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;
import com.google.common.io.BaseEncoding;
/**
* OMEXMLReader is the file format reader for OME-XML files.
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class OMEXMLReader extends FormatReader {
// -- Fields --
// compression value and offset for each BinData element
private Vector<BinData> binData;
private Vector<Long> binDataOffsets;
private Vector<String> compression;
private String omexml;
private boolean hasSPW = false;
// -- Constructor --
/** Constructs a new OME-XML reader. */
public OMEXMLReader() {
super("OME-XML", "ome");
domains = FormatTools.NON_GRAPHICS_DOMAINS;
suffixNecessary = false;
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
@Override
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
final int blockLen = 64;
String xml = stream.readString(blockLen);
return xml.startsWith("<?xml") && xml.indexOf("<OME") >= 0;
}
/* @see loci.formats.IFormatReader#isThisType(String, boolean) */
@Override
public boolean isThisType(String name, boolean open) {
if (checkSuffix(name, "companion.ome")) {
// pass binary-only files along to the OME-TIFF reader
return false;
}
return super.isThisType(name, open);
}
/* @see loci.formats.IFormatReader#getDomains() */
@Override
public String[] getDomains() {
FormatTools.assertId(currentId, true, 1);
return hasSPW ? new String[] {FormatTools.HCS_DOMAIN} :
FormatTools.NON_SPECIAL_DOMAINS;
}
/**
* @see loci.formats.IFormatReader#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
{
if (binDataOffsets.size() == 0) return buf;
FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
int index = no;
int series = getSeries();
for (int i=0; i<series; i++) {
index += core.get(i).imageCount;
}
if (index >= binDataOffsets.size()) {
index = binDataOffsets.size() - 1;
}
long offset = binDataOffsets.get(index).longValue();
String compress = compression.get(index);
in.seek(offset);
int depth = FormatTools.getBytesPerPixel(getPixelType());
int planeSize = getSizeX() * getSizeY() * depth;
CodecOptions options = new CodecOptions();
options.width = getSizeX();
options.height = getSizeY();
options.bitsPerSample = depth * 8;
options.channels = getRGBChannelCount();
options.maxBytes = planeSize;
options.littleEndian = isLittleEndian();
options.interleaved = isInterleaved();
String encoded = in.readString("<");
encoded = encoded.trim();
if (encoded.length() == 0 || encoded.equals("<")) {
LOGGER.debug("No pixel data for plane #{}", no);
return buf;
}
encoded = encoded.substring(0, encoded.length() - 1);
byte[] pixels = BaseEncoding.base64().decode(encoded);
// return a blank plane if no pixel data was stored
if (pixels.length == 0) {
LOGGER.debug("No pixel data for plane #{}", no);
return buf;
}
// TODO: Create a method uncompress to handle all compression methods
if (compress.equals("bzip2")) {
byte[] tempPixels = pixels;
pixels = new byte[tempPixels.length - 2];
System.arraycopy(tempPixels, 2, pixels, 0, pixels.length);
ByteArrayInputStream bais = new ByteArrayInputStream(pixels);
CBZip2InputStream bzip = new CBZip2InputStream(bais);
pixels = new byte[planeSize];
bzip.read(pixels, 0, pixels.length);
tempPixels = null;
bais.close();
bzip.close();
bais = null;
bzip = null;
}
else if (compress.equals("zlib")) {
pixels = new ZlibCodec().decompress(pixels, options);
}
else if (compress.equals("J2K")) {
pixels = new JPEG2000Codec().decompress(pixels, options);
}
else if (compress.equals("JPEG")) {
pixels = new JPEGCodec().decompress(pixels, options);
}
for (int row=0; row<h; row++) {
int off = (row + y) * getSizeX() * depth + x * depth;
System.arraycopy(pixels, off, buf, row * w * depth, w * depth);
}
pixels = null;
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
compression = null;
binDataOffsets = null;
binData = null;
omexml = null;
hasSPW = false;
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
in = new RandomAccessInputStream(id);
in.setEncoding("ASCII");
binData = new Vector<BinData>();
binDataOffsets = new Vector<Long>();
compression = new Vector<String>();
DefaultHandler handler = new OMEXMLHandler();
try {
RandomAccessInputStream s = new RandomAccessInputStream(id);
XMLTools.parseXML(s, handler);
s.close();
}
catch (IOException e) {
throw new FormatException("Malformed OME-XML", e);
}
int lineNumber = 1;
for (BinData bin : binData) {
int line = bin.getRow();
int col = bin.getColumn();
while (lineNumber < line) {
in.readLine();
lineNumber++;
}
binDataOffsets.add(in.getFilePointer() + col - 1);
}
LOGGER.info("Populating metadata");
OMEXMLMetadata omexmlMeta;
OMEXMLService service;
try {
ServiceFactory factory = new ServiceFactory();
service = factory.getInstance(OMEXMLService.class);
omexmlMeta = service.createOMEXMLMetadata(omexml);
}
catch (DependencyException de) {
throw new MissingLibraryException(OMEXMLServiceImpl.NO_OME_XML_MSG, de);
}
catch (ServiceException se) {
throw new FormatException(se);
}
hasSPW = omexmlMeta.getPlateCount() > 0;
addGlobalMeta("Is SPW file", hasSPW);
// TODO
//Hashtable originalMetadata = omexmlMeta.getOriginalMetadata();
//if (originalMetadata != null) metadata = originalMetadata;
int numDatasets = omexmlMeta.getImageCount();
int oldSeries = getSeries();
core.clear();
for (int i=0; i<numDatasets; i++) {
CoreMetadata ms = new CoreMetadata();
core.add(ms);
setSeries(i);
Integer w = omexmlMeta.getPixelsSizeX(i).getValue();
Integer h = omexmlMeta.getPixelsSizeY(i).getValue();
Integer t = omexmlMeta.getPixelsSizeT(i).getValue();
Integer z = omexmlMeta.getPixelsSizeZ(i).getValue();
Integer c = omexmlMeta.getPixelsSizeC(i).getValue();
if (w == null || h == null || t == null || z == null | c == null) {
throw new FormatException("Image dimensions not found");
}
Boolean endian = null;
if (binData.size() > 0) {
endian = omexmlMeta.getPixelsBinDataBigEndian(i, 0);
}
String pixType = omexmlMeta.getPixelsType(i).toString();
ms.dimensionOrder = omexmlMeta.getPixelsDimensionOrder(i).toString();
ms.sizeX = w.intValue();
ms.sizeY = h.intValue();
ms.sizeT = t.intValue();
ms.sizeZ = z.intValue();
ms.sizeC = c.intValue();
ms.imageCount = getSizeZ() * getSizeC() * getSizeT();
ms.littleEndian = endian == null ? false : !endian.booleanValue();
ms.rgb = false;
ms.interleaved = false;
ms.indexed = false;
ms.falseColor = true;
ms.pixelType = FormatTools.pixelTypeFromString(pixType);
ms.orderCertain = true;
if (omexmlMeta.getPixelsSignificantBits(i) != null) {
ms.bitsPerPixel = omexmlMeta.getPixelsSignificantBits(i).getValue();
}
}
setSeries(oldSeries);
// populate assigned metadata store with the
// contents of the internal OME-XML metadata object
MetadataStore store = getMetadataStore();
service.convertMetadata(omexmlMeta, store);
MetadataTools.populatePixels(store, this, false, false);
}
// -- Helper class --
class OMEXMLHandler extends BaseHandler {
private StringBuffer xmlBuffer;
private String currentQName;
private Locator locator;
public OMEXMLHandler() {
xmlBuffer = new StringBuffer();
}
@Override
public void characters(char[] ch, int start, int length) {
if (currentQName.indexOf("BinData") < 0) {
xmlBuffer.append(new String(ch, start, length));
}
}
@Override
public void endElement(String uri, String localName, String qName) {
xmlBuffer.append("</");
xmlBuffer.append(qName);
xmlBuffer.append(">");
}
@Override
public void startElement(String ur, String localName, String qName,
Attributes attributes)
{
currentQName = qName;
if (qName.indexOf("BinData") == -1) {
xmlBuffer.append("<");
xmlBuffer.append(qName);
for (int i=0; i<attributes.getLength(); i++) {
String key = XMLTools.escapeXML(attributes.getQName(i));
String value = XMLTools.escapeXML(attributes.getValue(i));
if (key.equals("BigEndian")) {
String endian = value.toLowerCase();
if (!endian.equals("true") && !endian.equals("false")) {
// hack for files that specify 't' or 'f' instead of
// 'true' or 'false'
if (endian.startsWith("t")) endian = "true";
else if (endian.startsWith("f")) endian = "false";
}
value = endian;
}
xmlBuffer.append(" ");
xmlBuffer.append(key);
xmlBuffer.append("=\"");
xmlBuffer.append(value);
xmlBuffer.append("\"");
}
xmlBuffer.append(">");
}
else {
binData.add(
new BinData(locator.getLineNumber(), locator.getColumnNumber()));
String compress = attributes.getValue("Compression");
compression.add(compress == null ? "" : compress);
xmlBuffer.append("<");
xmlBuffer.append(qName);
for (int i=0; i<attributes.getLength(); i++) {
String key = XMLTools.escapeXML(attributes.getQName(i));
String value = XMLTools.escapeXML(attributes.getValue(i));
if (key.equals("Length")) value = "0";
xmlBuffer.append(" ");
xmlBuffer.append(key);
xmlBuffer.append("=\"");
xmlBuffer.append(value);
xmlBuffer.append("\"");
}
xmlBuffer.append(">");
}
}
@Override
public void endDocument() {
omexml = xmlBuffer.toString();
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
}
class BinData {
private int row;
private int column;
public BinData(int row, int column) {
this.row = row;
this.column = column;
}
public int getRow() { return row; }
public int getColumn() { return column; }
}
}