/*
* #%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.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import loci.common.ByteArrayHandle;
import loci.common.Constants;
import loci.common.RandomAccessInputStream;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.ImageTools;
import loci.formats.MetadataTools;
import loci.formats.codec.NikonCodec;
import loci.formats.codec.NikonCodecOptions;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.IFDList;
import loci.formats.tiff.PhotoInterp;
import loci.formats.tiff.TiffCompression;
import loci.formats.tiff.TiffParser;
import loci.formats.tiff.TiffRational;
/**
* NikonReader is the file format reader for Nikon NEF (TIFF) files.
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class NikonReader extends BaseTiffReader {
// -- Constants --
/** Logger for this class. */
private static final Logger LOGGER =
LoggerFactory.getLogger(NikonReader.class);
public static final String[] NEF_SUFFIX = {"nef"};
// Tags that give a good indication of whether this is an NEF file.
private static final int TIFF_EPS_STANDARD = 37398;
private static final int COLOR_MAP = 33422;
// Maker Note tags.
private static final int FIRMWARE_VERSION = 1;
private static final int ISO = 2;
private static final int QUALITY = 4;
private static final int MAKER_WHITE_BALANCE = 5;
private static final int SHARPENING = 6;
private static final int FOCUS_MODE = 7;
private static final int FLASH_SETTING = 8;
private static final int FLASH_MODE = 9;
private static final int WHITE_BALANCE_FINE = 11;
private static final int WHITE_BALANCE_RGB_COEFFS = 12;
private static final int FLASH_COMPENSATION = 18;
private static final int TONE_COMPENSATION = 129;
private static final int LENS_TYPE = 131;
private static final int LENS = 132;
private static final int FLASH_USED = 135;
private static final int CURVE = 140;
private static final int COLOR_MODE = 141;
private static final int LIGHT_TYPE = 144;
private static final int HUE = 146;
private static final int CAPTURE_EDITOR_DATA = 3585;
// -- Fields --
/** Offset to the Nikon Maker Note. */
protected int makerNoteOffset;
/** The original IFD. */
protected IFD original;
private TiffRational[] whiteBalance;
private Object cfaPattern;
private int[] curve;
private int[] vPredictor;
private boolean lossyCompression;
private int split = -1;
private byte[] lastPlane = null;
private int lastIndex = -1;
// -- Constructor --
/** Constructs a new Nikon reader. */
public NikonReader() {
super("Nikon NEF", new String[] {"nef", "tif", "tiff"});
suffixSufficient = false;
domains = new String[] {FormatTools.GRAPHICS_DOMAIN};
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(String, boolean) */
@Override
public boolean isThisType(String name, boolean open) {
// extension is sufficient as long as it is NEF
if (checkSuffix(name, NEF_SUFFIX)) return true;
return super.isThisType(name, open);
}
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
@Override
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
TiffParser tp = new TiffParser(stream);
IFD ifd = tp.getFirstIFD();
if (ifd == null) return false;
if (ifd.containsKey(TIFF_EPS_STANDARD)) return true;
String make = ifd.getIFDTextValue(IFD.MAKE);
return make != null && make.indexOf("Nikon") != -1;
}
/**
* @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);
IFD ifd = ifds.get(no);
int[] bps = ifd.getBitsPerSample();
int dataSize = bps[0];
long[] byteCounts = ifd.getStripByteCounts();
long totalBytes = 0;
for (long b : byteCounts) {
totalBytes += b;
}
if (totalBytes == FormatTools.getPlaneSize(this) || bps.length > 1) {
return super.openBytes(no, buf, x, y, w, h);
}
if (lastPlane == null || lastIndex != no) {
long[] offsets = ifd.getStripOffsets();
boolean maybeCompressed = ifd.getCompression() == TiffCompression.NIKON;
boolean compressed =
vPredictor != null && curve != null && maybeCompressed;
if (!maybeCompressed && dataSize == 14) dataSize = 16;
ByteArrayOutputStream src = new ByteArrayOutputStream();
NikonCodec codec = new NikonCodec();
NikonCodecOptions options = new NikonCodecOptions();
options.width = getSizeX();
options.height = getSizeY();
options.bitsPerSample = dataSize;
options.curve = curve;
if (vPredictor != null) {
options.vPredictor = new int[vPredictor.length];
}
options.lossless = !lossyCompression;
options.split = split;
for (int i=0; i<byteCounts.length; i++) {
byte[] t = new byte[(int) byteCounts[i]];
in.seek(offsets[i]);
in.read(t);
if (compressed) {
options.maxBytes = (int) byteCounts[i];
System.arraycopy(vPredictor, 0, options.vPredictor, 0,
vPredictor.length);
t = codec.decompress(t, options);
}
src.write(t);
}
RandomAccessInputStream bb = new RandomAccessInputStream(
new ByteArrayHandle(src.toByteArray()));
short[] pix = new short[getSizeX() * getSizeY() * 3];
src.close();
int[] colorMap = {1, 0, 2, 1}; // default color map
short[] ifdColors = (short[]) ifd.get(COLOR_MAP);
if (ifdColors != null && ifdColors.length >= colorMap.length) {
boolean colorsValid = true;
for (int q=0; q<colorMap.length; q++) {
if (ifdColors[q] < 0 || ifdColors[q] > 2) {
// found invalid channel index, use default color map instead
colorsValid = false;
break;
}
}
if (colorsValid) {
for (int q=0; q<colorMap.length; q++) {
colorMap[q] = ifdColors[q];
}
}
}
boolean interleaveRows =
offsets.length == 1 && !maybeCompressed && colorMap[0] != 0;
for (int row=0; row<getSizeY(); row++) {
int realRow = interleaveRows ? (row < (getSizeY() / 2) ?
row * 2 : (row - (getSizeY() / 2)) * 2 + 1) : row;
for (int col=0; col<getSizeX(); col++) {
short val = (short) (bb.readBits(dataSize) & 0xffff);
int mapIndex = (realRow % 2) * 2 + (col % 2);
int redOffset = realRow * getSizeX() + col;
int greenOffset = (getSizeY() + realRow) * getSizeX() + col;
int blueOffset = (2 * getSizeY() + realRow) * getSizeX() + col;
if (colorMap[mapIndex] == 0) {
pix[redOffset] = adjustForWhiteBalance(val, 0);
}
else if (colorMap[mapIndex] == 1) {
pix[greenOffset] = adjustForWhiteBalance(val, 1);
}
else if (colorMap[mapIndex] == 2) {
pix[blueOffset] = adjustForWhiteBalance(val, 2);
}
if (maybeCompressed && !compressed) {
int toSkip = 0;
if ((col % 10) == 9) {
toSkip = 1;
}
if (col == getSizeX() - 1) {
toSkip = 10;
}
bb.skipBits(toSkip * 8);
}
}
}
bb.close();
lastPlane = new byte[FormatTools.getPlaneSize(this)];
ImageTools.interpolate(pix, lastPlane, colorMap, getSizeX(), getSizeY(),
isLittleEndian());
lastIndex = no;
}
int bpp = FormatTools.getBytesPerPixel(getPixelType()) * 3;
int rowLen = w * bpp;
int width = getSizeX() * bpp;
for (int row=0; row<h; row++) {
System.arraycopy(
lastPlane, (row + y) * width + x * bpp, buf, row * rowLen, rowLen);
}
return buf;
}
/* @see loci.formats.IFormatReader#close(boolean) */
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
makerNoteOffset = 0;
original = null;
split = -1;
whiteBalance = null;
cfaPattern = null;
curve = null;
vPredictor = null;
lossyCompression = false;
lastPlane = null;
lastIndex = -1;
}
}
// -- Internal BaseTiffReader API methods --
/* @see BaseTiffReader#initStandardMetadata() */
@Override
protected void initStandardMetadata() throws FormatException, IOException {
super.initStandardMetadata();
// reset image dimensions
// the actual image data is stored in IFDs referenced by the SubIFD tag
// in the 'real' IFD
CoreMetadata m = core.get(0);
m.imageCount = ifds.size();
IFD firstIFD = ifds.get(0);
PhotoInterp photo = firstIFD.getPhotometricInterpretation();
int samples = firstIFD.getSamplesPerPixel();
m.rgb = samples > 1 || photo == PhotoInterp.RGB ||
photo == PhotoInterp.CFA_ARRAY;
if (photo == PhotoInterp.CFA_ARRAY) samples = 3;
m.sizeX = (int) firstIFD.getImageWidth();
m.sizeY = (int) firstIFD.getImageLength();
m.sizeZ = 1;
m.sizeC = isRGB() ? samples : 1;
m.sizeT = ifds.size();
m.pixelType = firstIFD.getPixelType();
m.indexed = false;
// now look for the EXIF IFD pointer
IFDList exifIFDs = tiffParser.getExifIFDs();
if (exifIFDs.size() > 0) {
IFD exifIFD = exifIFDs.get(0);
tiffParser.fillInIFD(exifIFD);
// put all the EXIF data in the metadata hashtable
for (Integer key : exifIFD.keySet()) {
int tag = key.intValue();
String name = IFD.getIFDTagName(tag);
if (tag == IFD.CFA_PATTERN) {
byte[] cfa = (byte[]) exifIFD.get(key);
int[] colorMap = new int[cfa.length];
for (int i=0; i<cfa.length; i++) colorMap[i] = (int) cfa[i];
addGlobalMeta(name, colorMap);
cfaPattern = colorMap;
}
else {
addGlobalMeta(name, exifIFD.get(key));
if (name.equals("MAKER_NOTE")) {
byte[] b = (byte[]) exifIFD.get(key);
int extra = new String(
b, 0, 10, Constants.ENCODING).startsWith("Nikon") ? 10 : 0;
byte[] buf = new byte[b.length];
System.arraycopy(b, extra, buf, 0, buf.length - extra);
RandomAccessInputStream makerNote =
new RandomAccessInputStream(buf);
TiffParser tp = new TiffParser(makerNote);
IFD note = null;
try {
note = tp.getFirstIFD();
}
catch (Exception e) {
LOGGER.debug("Failed to parse first IFD", e);
}
if (note != null) {
for (Integer nextKey : note.keySet()) {
int nextTag = nextKey.intValue();
addGlobalMeta(name, note.get(nextKey));
if (nextTag == 150) {
b = (byte[]) note.get(nextKey);
RandomAccessInputStream s = new RandomAccessInputStream(b);
byte check1 = s.readByte();
byte check2 = s.readByte();
lossyCompression = check1 != 0x46;
vPredictor = new int[4];
for (int q=0; q<vPredictor.length; q++) {
vPredictor[q] = s.readShort();
}
curve = new int[16385];
int bps = ifds.get(0).getBitsPerSample()[0];
int max = 1 << bps & 0x7fff;
int step = 0;
int csize = s.readShort();
if (csize > 1) {
step = max / (csize - 1);
}
if (check1 == 0x44 && check2 == 0x20 && step > 0) {
for (int i=0; i<csize; i++) {
curve[i * step] = s.readShort();
}
for (int i=0; i<max; i++) {
int n = i % step;
curve[i] = (curve[i - n] * (step - n) +
curve[i - n + step] * n) / step;
}
s.seek(562);
split = s.readShort();
}
else {
int maxValue = (int) Math.pow(2, bps) - 1;
Arrays.fill(curve, maxValue);
int nElements =
(int) (s.length() - s.getFilePointer()) / 2;
if (nElements < 100) {
for (int i=0; i<curve.length; i++) {
curve[i] = (short) i;
}
}
else {
for (int q=0; q<nElements; q++) {
curve[q] = s.readShort();
}
}
}
s.close();
}
else if (nextTag == WHITE_BALANCE_RGB_COEFFS) {
whiteBalance = (TiffRational[]) note.get(nextKey);
}
}
}
makerNote.close();
}
}
}
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
@Override
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
original = ifds.get(0);
if (cfaPattern != null) {
original.putIFDValue(IFD.COLOR_MAP, (int[]) cfaPattern);
}
ifds.set(0, original);
CoreMetadata m = core.get(0);
m.imageCount = 1;
m.sizeT = 1;
if (ifds.get(0).getSamplesPerPixel() == 1) {
m.interleaved = true;
}
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this);
}
// -- Helper methods --
private short adjustForWhiteBalance(short val, int index) {
if (whiteBalance != null && whiteBalance.length == 3) {
return (short) (val * whiteBalance[index].doubleValue());
}
return val;
}
}