/* * #%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.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ome.units.UNITS; import ome.units.quantity.Length; import ome.xml.model.enums.IlluminationType; import ome.xml.model.primitives.NonNegativeInteger; import ome.xml.model.primitives.PositiveFloat; import ome.xml.model.primitives.Timestamp; import loci.common.RandomAccessInputStream; import loci.common.xml.XMLTools; import loci.formats.CoreMetadata; import loci.formats.MetadataTools; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.meta.MetadataStore; import loci.formats.tiff.IFD; import loci.formats.tiff.PhotoInterp; import loci.formats.tiff.TiffParser; /** * LeicaSCNReader is the file format reader for Leica SCN TIFF files. */ public class LeicaSCNReader extends BaseTiffReader { // -- Constants -- /** Logger for this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(LeicaSCNReader.class); private static final String SCHEMA_2010_03 = "http://www.leica-microsystems.com/scn/2010/03/10"; private static final String SCHEMA_2010_10 = "http://www.leica-microsystems.com/scn/2010/10/01"; // -- Fields -- LeicaSCNHandler handler; // -- Constructor -- /** Constructs a new LeicaSCN reader. */ public LeicaSCNReader() { super("Leica SCN", new String[] {"scn"}); domains = new String[] {FormatTools.HISTOLOGY_DOMAIN}; suffixNecessary = false; suffixSufficient = false; } // -- IFormatReader API methods -- /* (non-Javadoc) * @see loci.formats.FormatReader#isThisType(java.lang.String, boolean) */ @Override public boolean isThisType(String name, boolean open) { if (super.isThisType(name, open) && open) { RandomAccessInputStream stream = null; try { stream = new RandomAccessInputStream(name); TiffParser tiffParser = new TiffParser(stream); if (!tiffParser.isValidHeader()) { return false; } String imageDescription = tiffParser.getComment(); if (imageDescription != null) { try { // Test if XML is valid SCN metadata LeicaSCNHandler handler = new LeicaSCNHandler(); XMLTools.parseXML(imageDescription, handler); return true; } catch (Exception se) { LOGGER.debug("XML parsing failed", se); } } } catch (IOException e) { LOGGER.debug("I/O exception during isThisType() evaluation.", e); } finally { try { if (stream != null) { stream.close(); } } catch (IOException e) { LOGGER.debug("I/O exception during stream closure.", e); } } } return false; } private int imageIFD(int no) { int s = getCoreIndex(); Image i = handler.imageMap.get(s); int[] dims = getZCTCoords(no); int dz = dims[0]; int dc = dims[1]; int dr = s - getParent(s); return i.pixels.lookupDimension(dz, dc, dr).ifd; } /** * @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 { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); if (tiffParser == null) { initTiffParser(); } int ifd = imageIFD(no); tiffParser.getSamples(ifds.get(ifd), buf, x, y, w, h); return buf; } /* @see loci.formats.IFormatReader#openThumbBytes(int) */ @Override public byte[] openThumbBytes(int no) throws FormatException, IOException { int originalIndex = getCoreIndex(); Image i = handler.imageMap.get(getCoreIndex()); setCoreIndex(getParent(originalIndex) + i.imageThumbnail); byte[] thumb = FormatTools.openThumbBytes(this, no); setCoreIndex(originalIndex); return thumb; } @Override public int getThumbSizeX() { int originalIndex = getCoreIndex(); Image i = handler.imageMap.get(getCoreIndex()); setCoreIndex(getParent(originalIndex) + i.imageThumbnail); int size = super.getThumbSizeX(); setCoreIndex(originalIndex); return size; } @Override public int getThumbSizeY() { int originalIndex = getCoreIndex(); Image i = handler.imageMap.get(getCoreIndex()); setCoreIndex(getParent(originalIndex) + i.imageThumbnail); int size = super.getThumbSizeY(); setCoreIndex(originalIndex); return size; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); handler = null; if (!fileOnly) { } } /* @see loci.formats.IFormatReader#getOptimalTileWidth() */ @Override public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); try { return (int) ifds.get(imageIFD(0)).getTileWidth(); } catch (FormatException e) { LOGGER.debug("", e); } return super.getOptimalTileWidth(); } /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); try { return (int) ifds.get(imageIFD(0)).getTileLength(); } catch (FormatException e) { LOGGER.debug("", e); } return super.getOptimalTileHeight(); } // -- Internal BaseTiffReader API methods -- protected void initCoreMetadata(int s, int resolution) throws FormatException, IOException { ImageCollection c = handler.collection; Image i = handler.imageMap.get(s); if (c == null || i == null) { throw new FormatException("Error setting core metadata for image number " + s); } CoreMetadata ms = core.get(s); // repopulate core metadata if (resolution == 0) { ms.resolutionCount = i.pixels.sizeR; } Dimension dimension = i.pixels.lookupDimension(0, 0, resolution); if (dimension == null) { throw new FormatException( "No dimension information for subresolution=" + resolution); } IFD ifd = ifds.get(dimension.ifd); PhotoInterp pi = ifd.getPhotometricInterpretation(); int samples = ifd.getSamplesPerPixel(); ms.rgb = samples > 1 || pi == PhotoInterp.RGB; ms.sizeX = (int) dimension.sizeX; ms.sizeY = (int) dimension.sizeY; ms.sizeZ = (int) i.pixels.sizeZ; ms.sizeT = 1; ms.sizeC = ms.rgb ? samples : i.pixels.sizeC; if ((ifd.getImageWidth() != ms.sizeX) || (ifd.getImageLength() != ms.sizeY)) { throw new FormatException("IFD dimensions do not match XML dimensions for image " + s + ": x=" + ifd.getImageWidth() + ", " + ms.sizeX + ", y=" + ifd.getImageLength() + ", " + ms.sizeY); } ms.orderCertain = true; ms.littleEndian = ifd.isLittleEndian(); ms.indexed = pi == PhotoInterp.RGB_PALETTE && (get8BitLookupTable() != null || get16BitLookupTable() != null); ms.imageCount = i.pixels.sizeZ * i.pixels.sizeC; ms.pixelType = ifd.getPixelType(); ms.metadataComplete = true; ms.interleaved = false; ms.falseColor = false; ms.dimensionOrder = "XYCZT"; ms.thumbnail = i.imageThumbnail == resolution; } /* @see loci.formats.BaseTiffReader#initStandardMetadata() */ @Override protected void initStandardMetadata() throws FormatException, IOException { super.initStandardMetadata(); tiffParser.setDoCaching(true); // Otherwise getComment() doesn't return the comment. String imageDescription = tiffParser.getComment(); handler = new LeicaSCNHandler(); if (imageDescription != null) { try { // parse the XML description XMLTools.parseXML(imageDescription, handler); } catch (Exception se) { throw new FormatException("Failed to parse XML", se); } } int count = handler.count(); ifds = tiffParser.getIFDs(); if (ifds.size() < count) { count = ifds.size(); } core.clear(); int resolution = 0; int parent = 0; for (int i=0; i<count; i++) { if (resolution == 0) { parent = i; } CoreMetadata ms = new CoreMetadata(); core.add(ms); tiffParser.fillInIFD(ifds.get(handler.IFDMap.get(i))); initCoreMetadata(i, resolution); resolution++; if (resolution == core.get(parent).resolutionCount) { resolution = 0; } } } /* @see loci.formats.BaseTiffReader#initMetadataStore() */ @Override protected void initMetadataStore() throws FormatException { super.initMetadataStore(); MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, true); HashMap<String,Integer> instrumentIDs = new HashMap<String,Integer>(); int instrumentidno = 0; HashMap<String,String> objectives = new HashMap<String,String>(); int objectiveidno = 0; int parent = 0; for (int s=0; s<getSeriesCount(); s++) { int coreIndex = seriesToCoreIndex(s); ImageCollection c = handler.collection; Image i = handler.imageMap.get(coreIndex); int subresolution = coreIndex - parent; if (!hasFlattenedResolutions()) { subresolution = 0; } if (core.get(s).resolutionCount > 1) { parent = s; } else if (core.get(parent).resolutionCount -1 == subresolution) { parent = s + 1; } Dimension dimension = i.pixels.lookupDimension(0, 0, subresolution); if (dimension == null) { throw new FormatException( "No dimension information for subresolution=" + subresolution); } // Leica units are nanometres; convert to µm double sizeX = i.vSizeX / 1000.0; double sizeY = i.vSizeY / 1000.0; final Length offsetX = new Length(i.vOffsetX, UNITS.REFERENCEFRAME); final Length offsetY = new Length(i.vOffsetY, UNITS.REFERENCEFRAME); double sizeZ = i.vSpacingZ / 1000.0; store.setPixelsPhysicalSizeX( FormatTools.getPhysicalSizeX(sizeX / dimension.sizeX), s); store.setPixelsPhysicalSizeY( FormatTools.getPhysicalSizeY(sizeY / dimension.sizeY), s); store.setPixelsPhysicalSizeZ(FormatTools.getPhysicalSizeZ(sizeZ), s); if (instrumentIDs.get(i.devModel) == null) { String instrumentID = MetadataTools.createLSID("Instrument", instrumentidno); instrumentIDs.put(i.devModel, instrumentidno); store.setInstrumentID(instrumentID, instrumentidno); instrumentidno++; } int inst = instrumentIDs.get(i.devModel); String objectiveName = i.devModel + ":" + i.objMag; if (objectives.get(objectiveName) == null) { String objectiveID = MetadataTools.createLSID("Objective", inst, objectiveidno); objectives.put(objectiveName, objectiveID); store.setObjectiveID(objectiveID, inst, objectiveidno); Double mag = Double.parseDouble(i.objMag); store.setObjectiveNominalMagnification(mag, inst, objectiveidno); store.setObjectiveCalibratedMagnification(mag, inst, objectiveidno); store.setObjectiveLensNA(new Double(i.illumNA), inst, objectiveidno); objectiveidno++; } store.setImageInstrumentRef( MetadataTools.createLSID("Instrument", inst), s); store.setObjectiveSettingsID(objectives.get(objectiveName), s); // TODO: Only "brightfield" has been seen in example files if (i.illumSource.equals("brightfield")) { store.setChannelIlluminationType(IlluminationType.TRANSMITTED, s, 0); } else { store.setChannelIlluminationType(IlluminationType.OTHER, s, 0); LOGGER.debug("Unknown illumination source {}", i.illumSource); } CoreMetadata ms = core.get(s); for (int q=0; q<ms.imageCount; q++) { store.setPlanePositionX(offsetX, s, q); store.setPlanePositionY(offsetY, s, q); } store.setImageName(i.name + " (R" + subresolution + ")", s); store.setImageDescription("Collection " + c.name, s); store.setImageAcquisitionDate(new Timestamp(i.creationDate), s); // Original metadata... addSeriesMeta("collection.name", c.name); addSeriesMeta("collection.uuid", c.uuid); addSeriesMeta("collection.barcode", c.barcode); addSeriesMeta("collection.ocr", c.ocr); addSeriesMeta("creationDate", i.creationDate); addSeriesMeta("device.model for image", i.devModel); addSeriesMeta("device.version for image", i.devVersion); addSeriesMeta("view.sizeX for image", i.vSizeX); addSeriesMeta("view.sizeY for image", i.vSizeY); addSeriesMeta("view.offsetX for image", i.vOffsetX); addSeriesMeta("view.offsetY for image", i.vOffsetY); addSeriesMeta("view.spacingZ for image", i.vSpacingZ); addSeriesMeta("scanSettings.objectiveSettings.objective for image", i.objMag); addSeriesMeta("scanSettings.illuminationSettings.numericalAperture for image", i.illumNA); addSeriesMeta("scanSettings.illuminationSettings.illuminationSource for image", i.illumSource); } } private int getParent(int coreIndex) { for (int parent=0; parent<core.size(); ) { int resCount = core.get(parent).resolutionCount; if (parent + resCount > coreIndex) { return parent; } parent += resCount; } return -1; } /** * SAX handler for parsing XML in Leica SCN files. * * @author Roger Leigh <r.leigh at dundee.ac.uk> */ class LeicaSCNHandler extends DefaultHandler { // -- Fields -- boolean valid = false; public ImageCollection collection; public Image currentImage; public int seriesIndex; public ArrayList<Integer> IFDMap = new ArrayList<Integer>(); public ArrayList<Image> imageMap = new ArrayList<Image>(); // Stack of XML elements to keep track of placement in the tree. public Deque<String> nameStack = new ArrayDeque<String>(); // CDATA text stored while parsing. Note that this is limited to a // single span between two tags, and CDATA with embedded elements is // not supported. public String cdata; public int resolutionCount = 0; // -- DefaultHandler API methods -- @Override public void endElement(String uri, String localName, String qName) { if (!nameStack.isEmpty() && nameStack.peek().equals(qName)) { nameStack.pop(); } if (qName.equals("image")) { currentImage.imageNumStart = seriesIndex; seriesIndex += currentImage.pixels.sizeR * currentImage.pixels.sizeC * currentImage.pixels.sizeZ; currentImage.imageNumEnd = seriesIndex - 1; resolutionCount += currentImage.pixels.sizeR; currentImage = null; } else if (qName.equals("creationDate")) { currentImage.creationDate = cdata; } else if (qName.equals("pixels")) { // Compute size of C, R and Z Pixels p = currentImage.pixels; int sizeC = 0; int sizeR = 0; int sizeZ = 0; for (Dimension d : p.dims) { if (d.c > sizeC) { sizeC = d.c; } if (d.r > sizeR) { sizeR = d.r; } if (d.z > sizeZ) { sizeZ = d.z; } } sizeC++; sizeR++; sizeZ++; // Set up storage for all dimensions. p.sizeC = sizeC; p.sizeR = sizeR; p.sizeZ = sizeZ; for (Dimension d : p.dims) { if (d.r == 0 || currentImage.thumbSizeX > d.sizeX) { currentImage.thumbSizeX = d.sizeX; currentImage.imageThumbnail = d.r; } } // Dimension ordering indirection (R=image, then Z, then C) for (int cr = 0; cr < sizeR; cr++) { imageMap.add(currentImage); for (int cc = 0; cc < sizeC; cc++) { for (int cz = 0; cz < sizeZ; cz++) { IFDMap.add(p.lookupDimension(cz, cc, cr).ifd); } } } } else if (qName.equals("objective")) { currentImage.objMag = cdata; } else if (qName.equals("numericalAperture")) { currentImage.illumNA = cdata; } else if (qName.equals("illuminationSource")) { currentImage.illumSource = cdata; } cdata = null; } @Override public void characters(char[] ch, int start, int length) { String s = new String(ch, start, length); if (cdata == null) { cdata = s; } else { cdata += s; } } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { cdata = null; if (qName.equals("scn")) { String ns = attributes.getValue("xmlns"); if (ns == null) { throw new SAXException("Invalid Leica SCN XML"); } if (!(ns.equals(SCHEMA_2010_03) || ns.equals(SCHEMA_2010_10))) { LOGGER.warn("Unknown Leica SCN XML schema: " + ns + "; this file may not be read correctly"); } valid = true; seriesIndex = 0; } if (!valid) { throw new SAXException("Invalid Leica SCN XML"); } if (qName.equals("collection")) { collection = new ImageCollection(attributes); } else if (qName.equals("image")) { currentImage = new Image(attributes); collection.images.add(currentImage); } else if (qName.equals("device")) { currentImage.devModel = attributes.getValue("model"); currentImage.devVersion = attributes.getValue("version"); } else if (qName.equals("pixels")) { if (currentImage.pixels == null) { currentImage.pixels = new Pixels(attributes); } else { throw new SAXException("Invalid Leica SCN XML: Multiple pixels elements for single image"); } } else if (qName.equals("dimension")) { currentImage.pixels.dims.add(new Dimension(attributes)); } else if (qName.equals("view")) { currentImage.setView(attributes); } nameStack.push(qName); } int count() { return resolutionCount; } } class ImageCollection { String name; String uuid; long sizeX; long sizeY; String barcode; String ocr; ArrayList<Image> images; ImageCollection(Attributes attrs) { name = attrs.getValue("name"); uuid = attrs.getValue("uuid"); String s = attrs.getValue("sizeX"); if (s != null) { sizeX = Long.parseLong(s); } s = attrs.getValue("sizeY"); if (s != null) { sizeY = Long.parseLong(s); } barcode = attrs.getValue("barcode"); ocr = attrs.getValue("ocr"); images = new ArrayList<Image>(); } } class Image { int imageNumStart; // first image number int imageNumEnd; // last image number (subresolutions) int imageThumbnail; // image for thumbnailing long thumbSizeX; String name; String uuid; String creationDate; String devModel; // device model String devVersion; // device version Pixels pixels; // pixel metadata for each subresolution long vSizeX; // view size x (nm) long vSizeY; // view size y (nm) long vOffsetX; // view offset x (nm?) long vOffsetY; // view offset y (nm?) long vSpacingZ; // view spacing z (nm?) String objMag; // objective magnification String illumNA; // illumination NA (why not objective NA?) String illumSource; // illumination source Image(Attributes attrs) { name = attrs.getValue("name"); uuid = attrs.getValue("uuid"); } void setView(Attributes attrs) { String s = attrs.getValue("sizeX"); if (s != null) { vSizeX = Long.parseLong(s); } s = attrs.getValue("sizeY"); if (s != null) { vSizeY = Long.parseLong(s); } s = attrs.getValue("offsetX"); if (s != null) { vOffsetX = Long.parseLong(s); } s = attrs.getValue("offsetY"); if (s != null) { vOffsetY = Long.parseLong(s); } s = attrs.getValue("spacingZ"); if (s != null) { vSpacingZ = Long.parseLong(s); } } } class Pixels { // Set up storage for each resolution and each dimension. Set main resolution. // data order (XYCRZ) [unused; force to XYCZT] // sizeX, sizeY // sizeZ, sizeC, sizeR [unused; compute from dimensions] // firstIFD (number) [unused] // dimension->IFD mapping (RZC to sizeX, sizeY, IFD) // use 3 arrays of size C*Z*R ArrayList<Dimension> dims = new ArrayList<Dimension>(); long sizeX; long sizeY; int sizeZ; int sizeC; int sizeR; int lastIFD; Pixels(Attributes attrs) { // Set main resolution. String s = attrs.getValue("sizeX"); if (s != null) { sizeX = Long.parseLong(s); } s = attrs.getValue("sizeY"); if (s != null) { sizeY = Long.parseLong(s); } } public Dimension lookupDimension(int z, int c, int resolution) { for (Dimension d : dims) { if (d.z == z && d.c == c && d.r == resolution) { return d; } } return null; } } class Dimension { // Single image plane for given Z, C, R dimensions long sizeX = 0; long sizeY = 0; int z = 0; int c = 0; int r = 0; int ifd = 0; Dimension(Attributes attrs) { String s = attrs.getValue("r"); if (s != null) { r = Integer.parseInt(s); } s = attrs.getValue("z"); if (s != null) { z = Integer.parseInt(s); } s = attrs.getValue("c"); if (s != null) { c = Integer.parseInt(s); } s = attrs.getValue("sizeX"); if (s != null) { sizeX = Long.parseLong(s); } s = attrs.getValue("sizeY"); if (s != null) { sizeY = Long.parseLong(s); } s = attrs.getValue("ifd"); if (s != null) { ifd = Integer.parseInt(s); } } } }