/* * #%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 loci.common.DateTools; import loci.common.RandomAccessInputStream; import loci.common.services.ServiceException; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.codec.JPEGTileDecoder; import loci.formats.meta.MetadataStore; import loci.formats.services.JPEGTurboService; import loci.formats.services.JPEGTurboServiceImpl; import loci.formats.tiff.IFD; import loci.formats.tiff.PhotoInterp; import loci.formats.tiff.TiffIFDEntry; import loci.formats.tiff.TiffParser; import ome.xml.model.primitives.PositiveFloat; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Length; /** * NDPIReader is the file format reader for Hamamatsu .ndpi files. */ public class NDPIReader extends BaseTiffReader { // -- Constants -- private static final int MAX_SIZE = 2048; private static final int SOURCE_LENS = 65421; private static final int MARKER_TAG = 65426; private static final int THUMB_TAG_2 = 65439; private static final int METADATA_TAG = 65449; // -- Fields -- private int initializedSeries = -1; private int initializedPlane = -1; private int sizeZ = 1; private int pyramidHeight = 1; private JPEGTurboService service = new JPEGTurboServiceImpl(); private Double magnification; private String serialNumber; private String instrumentModel; // -- Constructor -- /** Constructs a new NDPI reader. */ public NDPIReader() { super("Hamamatsu NDPI", new String[] {"ndpi"}); domains = new String[] {FormatTools.HISTOLOGY_DOMAIN}; } // -- IFormatReader API methods -- /* (non-Javadoc) * @see loci.formats.FormatReader#isThisType(java.lang.String, boolean) */ @Override public boolean isThisType(String name, boolean open) { boolean isThisType = super.isThisType(name, open); if (isThisType && open) { RandomAccessInputStream stream = null; TiffParser parser = null; try { stream = new RandomAccessInputStream(name); parser = new TiffParser(stream); parser.setDoCaching(false); parser.setUse64BitOffsets(stream.length() >= Math.pow(2, 32)); if (!parser.isValidHeader()) { return false; } IFD ifd = parser.getFirstIFD(); if (ifd == null) { return false; } return ifd.containsKey(MARKER_TAG); } catch (IOException e) { LOGGER.debug("I/O exception during isThisType() evaluation.", e); return false; } finally { try { if (stream != null) { stream.close(); } if (parser != null) { parser.getStream().close(); } } catch (IOException e) { LOGGER.debug("I/O exception during stream closure.", e); } } } return isThisType; } /** @see loci.formats.IFormatReader#fileGroupOption(String) */ @Override public int fileGroupOption(String id) throws FormatException, IOException { return MUST_GROUP; } /** * @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 (x == 0 && y == 0 && w == 1 && h == 1) { return buf; } else if (getSizeX() <= MAX_SIZE || getSizeY() <= MAX_SIZE) { int ifdIndex = getIFDIndex(getCoreIndex(), no); in = new RandomAccessInputStream(currentId); tiffParser = new TiffParser(in); tiffParser.setUse64BitOffsets(true); tiffParser.setYCbCrCorrection(false); byte[] b = tiffParser.getSamples(ifds.get(ifdIndex), buf, x, y, w, h); in.close(); tiffParser.getStream().close(); return b; } if (initializedSeries != getCoreIndex() || initializedPlane != no) { IFD ifd = ifds.get(getIFDIndex(getCoreIndex(), no)); long offset = ifd.getStripOffsets()[0]; long byteCount = ifd.getStripByteCounts()[0]; if (in != null) { in.close(); } in = new RandomAccessInputStream(currentId); in.seek(offset); in.setLength(offset + byteCount); try { service.close(); long[] markers = ifd.getIFDLongArray(MARKER_TAG); if (!use64Bit) { for (int i=0; i<markers.length; i++) { markers[i] = markers[i] & 0xffffffffL; } } if (markers != null) { service.setRestartMarkers(markers); } service.initialize(in, getSizeX(), getSizeY()); } catch (ServiceException e) { throw new FormatException(e); } initializedSeries = getCoreIndex(); initializedPlane = no; } service.getTile(buf, x, y, w, h); return buf; } /* @see loci.formats.IFormatReader#openThumbBytes(int) */ @Override public byte[] openThumbBytes(int no) throws FormatException, IOException { FormatTools.assertId(currentId, true, 1); int currentSeries = getSeries(); if (getCoreIndex() >= pyramidHeight) { return super.openThumbBytes(no); } int thumbX = getThumbSizeX(); int thumbY = getThumbSizeY(); int rgbCount = getRGBChannelCount(); if (hasFlattenedResolutions()) { setSeries(pyramidHeight - 1); } else { setResolution(pyramidHeight - 1); } byte[] thumb = null; if (thumbX == getThumbSizeX() && thumbY == getThumbSizeY() && rgbCount == getRGBChannelCount()) { thumb = FormatTools.openThumbBytes(this, no); setSeries(currentSeries); setResolution(0); } else { // find the smallest series with the same aspect ratio for (int s=getSeriesCount()-1; s>=0; s--) { setSeries(s); if (thumbX == getThumbSizeX() && thumbY == getThumbSizeY() && s != currentSeries && rgbCount == getRGBChannelCount()) { thumb = FormatTools.openThumbBytes(this, no); break; } } setSeries(currentSeries); if (thumb == null) { thumb = FormatTools.openThumbBytes(this, no); } } return thumb; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { if (!fileOnly) { service.close(); initializedSeries = -1; initializedPlane = -1; sizeZ = 1; pyramidHeight = 1; magnification = null; serialNumber = null; instrumentModel = null; if (tiffParser != null) { tiffParser.getStream().close(); } } super.close(fileOnly); } /* @see loci.formats.IFormatReader#getOptimalTileWidth() */ @Override public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); return 1024; } /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); return 1024; } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { RandomAccessInputStream s = new RandomAccessInputStream(id); use64Bit = s.length() >= Math.pow(2, 32); s.close(); super.initFile(id); } // -- Internal BaseTiffReader API methods -- /* @see loci.formats.BaseTiffReader#initStandardMetadata() */ @Override protected void initStandardMetadata() throws FormatException, IOException { super.initStandardMetadata(); ifds = tiffParser.getIFDs(); // fix the offsets for > 4 GB files RandomAccessInputStream stream = new RandomAccessInputStream(currentId); for (int i=0; i<ifds.size(); i++) { IFD ifd = ifds.get(i); long[] stripOffsets = ifd.getStripOffsets(); boolean neededAdjustment = false; for (int j=0; j<stripOffsets.length; j++) { long prevOffset = i == 0 ? 0 : ifds.get(i - 1).getStripOffsets()[0]; long prevByteCount = i == 0 ? 0 : ifds.get(i - 1).getStripByteCounts()[0]; while (stripOffsets[j] < prevOffset || stripOffsets[j] < prevOffset + prevByteCount) { long newOffset = stripOffsets[j] + 0x100000000L; if (newOffset < stream.length() && ((j > 0 && (stripOffsets[j] < stripOffsets[j - 1])) || (i > 0 && stripOffsets[j] < prevOffset + prevByteCount))) { stripOffsets[j] = newOffset; neededAdjustment = true; } } } if (neededAdjustment) { ifd.putIFDValue(IFD.STRIP_OFFSETS, stripOffsets); } neededAdjustment = false; long[] stripByteCounts = ifd.getStripByteCounts(); for (int j=0; j<stripByteCounts.length; j++) { long newByteCount = stripByteCounts[j] + 0x100000000L; if (stripByteCounts[j] < 0 || neededAdjustment || newByteCount + stripOffsets[j] < in.length()) { if (newByteCount < ifd.getImageWidth() * ifd.getImageLength()) { stripByteCounts[j] = newByteCount; neededAdjustment = true; } } } if (neededAdjustment) { ifd.putIFDValue(IFD.STRIP_BYTE_COUNTS, stripByteCounts); } } stream.close(); for (int i=1; i<ifds.size(); i++) { IFD ifd = ifds.get(i); if (ifd.getImageWidth() == ifds.get(0).getImageWidth() && ifd.getImageLength() == ifds.get(0).getImageLength()) { sizeZ++; } else if (sizeZ == 1) { boolean isPyramid; Object source_lens_value = ifd.getIFDValue(SOURCE_LENS); if (source_lens_value != null) { float source_lens = (Float) source_lens_value; // A value of -1 correspond to the macro image and a value of -2 // correspond to the map image isPyramid = (source_lens != -1 && source_lens != -2); } else { // Assume the last IFD is the macro image isPyramid = i < ifds.size() - 1; } if (isPyramid) pyramidHeight++; } } // repopulate core metadata int seriesCount = pyramidHeight + (ifds.size() - pyramidHeight * sizeZ); long prevMarkerOffset = 0; for (int i=0; i<ifds.size(); i++) { IFD ifd = ifds.get(i); ifd.remove(THUMB_TAG_2); ifds.set(i, ifd); TiffIFDEntry markerTag = (TiffIFDEntry) ifd.get(MARKER_TAG); if (markerTag != null) { if (markerTag.getValueOffset() > in.length()) { // can't rely upon the MARKER_TAG to be detected correctly ifds.get(i).remove(MARKER_TAG); } else { Object value = tiffParser.getIFDValue(markerTag); ifds.get(i).putIFDValue(MARKER_TAG, value); } } tiffParser.fillInIFD(ifds.get(i)); int[] bpp = ifds.get(i).getBitsPerSample(); for (int q=0; q<bpp.length; q++) { if (bpp[q] < 8) { bpp[q] = 8; } } ifds.get(i).putIFDValue(IFD.BITS_PER_SAMPLE, bpp); } core.clear(); for (int s=0; s<seriesCount; s++) { CoreMetadata ms = new CoreMetadata(); core.add(ms); if (s == 0) { ms.resolutionCount = pyramidHeight; } } for (int s=0; s<core.size(); s++) { IFD ifd = ifds.get(getIFDIndex(s, 0)); PhotoInterp p = ifd.getPhotometricInterpretation(); int samples = ifd.getSamplesPerPixel(); CoreMetadata ms = core.get(s); ms.rgb = samples > 1 || p == PhotoInterp.RGB; ms.sizeX = (int) ifd.getImageWidth(); ms.sizeY = (int) ifd.getImageLength(); ms.sizeZ = s < pyramidHeight ? sizeZ : 1; ms.sizeT = 1; ms.sizeC = ms.rgb ? samples : 1; ms.littleEndian = ifd.isLittleEndian(); ms.indexed = p == PhotoInterp.RGB_PALETTE && (get8BitLookupTable() != null || get16BitLookupTable() != null); ms.imageCount = ms.sizeZ * ms.sizeT; ms.pixelType = ifd.getPixelType(); ms.metadataComplete = true; ms.interleaved = ms.sizeX > MAX_SIZE && ms.sizeY > MAX_SIZE; ms.falseColor = false; ms.dimensionOrder = "XYCZT"; ms.thumbnail = s != 0; } String metadataTag = ifds.get(0).getIFDStringValue(METADATA_TAG); if (metadataTag != null) { String[] entries = metadataTag.split("\n"); for (String entry : entries) { int eq = entry.indexOf("="); if (eq < 0) { continue; } String key = entry.substring(0, eq).trim(); String value = entry.substring(eq + 1).trim(); addGlobalMeta(key, value); if (key.equals("Objective.Lens.Magnificant")) { // not a typo magnification = new Double(value); } else if (key.equals("NDP.S/N")) { serialNumber = value; } else if (key.equals("Product")) { instrumentModel = value; } } } } /* @see loci.formats.BaseTiffReader#initMetadataStore() */ @Override protected void initMetadataStore() throws FormatException { super.initMetadataStore(); MetadataStore store = makeFilterMetadata(); String instrumentID = MetadataTools.createLSID("Instrument", 0); String objectiveID = MetadataTools.createLSID("Objective", 0, 0); store.setInstrumentID(instrumentID, 0); store.setObjectiveID(objectiveID, 0, 0); if (instrumentModel != null) { store.setMicroscopeModel(instrumentModel, 0); } if (magnification != null) { store.setObjectiveNominalMagnification(magnification, 0, 0); } for (int i=0; i<getSeriesCount(); i++) { store.setImageName("Series " + (i + 1), i); store.setImageInstrumentRef(instrumentID, i); store.setObjectiveSettingsID(objectiveID, i); if (i > 0) { int ifdIndex = getIFDIndex(i, 0); String creationDate = ifds.get(ifdIndex).getIFDTextValue(IFD.DATE_TIME); creationDate = DateTools.formatDate(creationDate, DATE_FORMATS, "."); if (creationDate != null) { store.setImageAcquisitionDate(new Timestamp(creationDate), i); } double xResolution = ifds.get(ifdIndex).getXResolution(); double yResolution = ifds.get(ifdIndex).getYResolution(); Length sizeX = FormatTools.getPhysicalSizeX(xResolution); Length sizeY = FormatTools.getPhysicalSizeY(yResolution); if (sizeX != null) { store.setPixelsPhysicalSizeX(sizeX, i); } if (sizeY != null) { store.setPixelsPhysicalSizeY(sizeY, i); } } else { store.setImageDescription(serialNumber, i); } } } // -- Helper methods -- private int getIFDIndex(int seriesIndex, int zIndex) { if (seriesIndex < pyramidHeight) { return zIndex * pyramidHeight + seriesIndex; } return sizeZ * pyramidHeight + (seriesIndex - pyramidHeight); } }