package Pixels; import static Pixels.BufferConverter.*; import Utils.*; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.zip.Inflater; public class TiffReader { static final int TAG_WIDTH = 256; static final int TAG_HEIGHT = 257; static final int TAG_BITS_PER_SAMPLE = 258; static final int TAG_COMPRESSION = 259; static final int TAG_PHOTOMETRIC_INTERPRETATION = 262; static final int TAG_STRIP_OFFSETS = 273; static final int TAG_SAMPLES_PER_PIXEL = 277; static final int TAG_STRIP_COUNTS = 279; static final int TAG_PLANAR_CONFIGURATION = 284; static final int TAG_PREDICTOR = 317; static final int TAG_SAMPLE_FORMAT = 339; static final int TAG_MIN_VALUE = 340; static final int TAG_MAX_VALUE = 341; static final int COMPRESSION_LZW = 5; static final int COMPRESSION_ADOBE_DEFLATE = 8; static final int COMPRESSION_PACKBITS = 32773; static final int COMPRESSION_DEFLATE = 32946; static final int DC_NONE = 0; static final int DC_LZW = 1; static final int DC_PACKBITS = 2; static final int DC_DEFLATE = 3; public static RefVector acquireArgbImages(FileChannel fc, int maxImages) throws IOException { byte[] buf = new byte[4096]; ByteBuffer bb = ByteBuffer.wrap(buf); fc.position(0); bb.limit(8); fc.read(bb); boolean isLittleEndian; if (buf[0] == 'I' && buf[1] == 'I') isLittleEndian = true; else if (buf[1] == 'M' && buf[1] == 'M') isLittleEndian = false; else return null; RefVector images = new RefVector<>(int[].class); int ifdOffset = getInt(buf, 4, isLittleEndian); for (int i = 0; i < maxImages; i++) { ifdOffset = readImage(images, ifdOffset, isLittleEndian, fc, bb, buf); if (ifdOffset == 0) break; } return images; } static int readImage( RefVector images, int ifdOffset, boolean isLittleEndian, FileChannel fc, ByteBuffer bb, byte[] buf ) throws IOException { fc.position(ifdOffset); bb.position(0); bb.limit(2); fc.read(bb); final int extraOff = buf.length / 2; int trueTagCount = getShort(buf, 0, isLittleEndian); //System.out.println("trueTagCount = " + trueTagCount); int width = 0; int height = 0; int sampleType = TYPE_NONE; int sampleLen = 0; int channels = 0; int compression = 0; int firstStripOffset = 0; int[] stripOffsets = null; int firstStripCount = 0; int[] stripCounts = null; float minValue = 0f; float maxValue = 0f; boolean isFloat = false; boolean isPlanar = false; boolean shouldDiffOrInvert = false; int tagIndex = 0; while (tagIndex < trueTagCount) { int nTags = Math.min(trueTagCount - tagIndex, extraOff / 12); bb.position(0); bb.limit(nTags * 12); fc.read(bb); for (int i = 0, t = 0; i < nTags; i++, t += 12) { int attr = getShort(buf, t, isLittleEndian); int type = getShort(buf, t+2, isLittleEndian); int count = getInt(buf, t+4, isLittleEndian); //System.out.println("attr = " + attr + ", type = " + type + ", count = " + count); if (count == 1) { int value = type == 3 ? getShort(buf, t+8, isLittleEndian) : getInt(buf, t+8, isLittleEndian); if (attr == TAG_WIDTH) width = value; else if (attr == TAG_HEIGHT) height = value; else if (attr == TAG_BITS_PER_SAMPLE) sampleLen = value; else if (attr == TAG_SAMPLES_PER_PIXEL) channels = value; else if (attr == TAG_PLANAR_CONFIGURATION) isPlanar = value == 2; else if (attr == TAG_PHOTOMETRIC_INTERPRETATION) shouldDiffOrInvert = value == 0; else if (attr == TAG_COMPRESSION) compression = value; else if (attr == TAG_PREDICTOR) shouldDiffOrInvert = value == 2; else if (attr == TAG_SAMPLE_FORMAT) isFloat = value == 3; else if (attr == TAG_MIN_VALUE) minValue = Float.intBitsToFloat(value); else if (attr == TAG_MAX_VALUE) maxValue = Float.intBitsToFloat(value); else if (attr == TAG_STRIP_OFFSETS) firstStripOffset = value; else if (attr == TAG_STRIP_COUNTS) firstStripCount = value; } else if ( attr == TAG_BITS_PER_SAMPLE || attr == TAG_STRIP_OFFSETS || attr == TAG_STRIP_COUNTS // TODO: add TAG_MIN_VALUE and TAG_MAX_VALUE, since there can be multiple mins and maxes ) { int offset = getInt(buf, t+8, isLittleEndian); int bytesPerElem = (type == 3 ? 2 : 4); int size = count * bytesPerElem; if (attr == TAG_STRIP_OFFSETS) stripOffsets = null; if (attr == TAG_STRIP_COUNTS) stripCounts = null; long oldPos = fc.position(); try { fc.position(offset); int bytesTotal = 0; while (bytesTotal < size) { bb.limit(Math.min(extraOff + size - bytesTotal, buf.length)); bb.position(extraOff); long res = fc.read(bb); if (res <= 0) break; int available = (int)res / bytesPerElem; if (attr == TAG_BITS_PER_SAMPLE) { int firstSampleLen = type == 3 ? getShort(buf, extraOff, isLittleEndian) : getInt(buf, extraOff, isLittleEndian); sampleLen = firstSampleLen; channels = count; break; } else if (attr == TAG_STRIP_OFFSETS) { if (stripOffsets == null) stripOffsets = new int[count]; getArray(stripOffsets, bytesTotal / bytesPerElem, buf, extraOff, available, type, isLittleEndian); } else if (attr == TAG_STRIP_COUNTS) { if (stripCounts == null) stripCounts = new int[count]; getArray(stripCounts, bytesTotal / bytesPerElem, buf, extraOff, available, type, isLittleEndian); } bytesTotal += (int)res; } } catch (IOException ex) { continue; } finally { fc.position(oldPos); } } } tagIndex += nTags; } /* System.out.println("TiffReader: " + "width = " + width + ", height = " + height + ", sampleType = " + sampleType + ", sampleLen = " + sampleLen + ", channels = " + channels + ", compression = " + compression + ", firstStripOffset = " + firstStripOffset + ", firstStripCount = " + firstStripCount + ", minValue = " + minValue + ", maxValue = " + maxValue + ", isFloat = " + isFloat + ", isPlanar = " + isPlanar + ", shouldDiffOrInvert = " + shouldDiffOrInvert ); System.out.println("stripOffsets: " + Misc.formatIntArray(stripOffsets, "" + firstStripOffset)); System.out.println("stripCounts: " + Misc.formatIntArray(stripCounts, "" + firstStripCount)); */ fc.position(ifdOffset + trueTagCount * 12 + 2); bb.position(0); bb.limit(4); fc.read(bb); int nextIfdOffset = getInt(buf, 0, isLittleEndian); if (width <= 0 || height <= 0) return nextIfdOffset; int dcMode = DC_NONE; switch (compression) { case 0: case 1: break; case COMPRESSION_LZW: dcMode = DC_LZW; break; case COMPRESSION_PACKBITS: dcMode = DC_PACKBITS; break; case COMPRESSION_DEFLATE: case COMPRESSION_ADOBE_DEFLATE: dcMode = DC_DEFLATE; break; default: throw new IOException("TiffReader: unsupported compression type (" + compression + ")"); } if (sampleLen == 1) sampleType = TYPE_BIT; else if (sampleLen == 8) sampleType = TYPE_BYTE; else if (sampleLen == 16) sampleType = TYPE_SHORT; else sampleType = isFloat ? TYPE_FLOAT : TYPE_BYTE; int area = width * height; int size = area; if (sampleType == TYPE_BIT) size = (size + 7) / 8; else if (sampleType == TYPE_SHORT) size *= 2; else if (sampleType == TYPE_FLOAT) size *= 4; channels = Math.max(channels, 1); size *= channels; byte[] imageData = ByteBufferPool.acquireAsIs(size); if (dcMode == DC_NONE) { fc.position(firstStripOffset); fc.read(ByteBuffer.wrap(imageData, 0, size)); } else { decompress(dcMode, fc, imageData, size, stripOffsets, stripCounts); } int[] pixels = IntBufferPool.acquireAsIs(area + 2); int pixelsFilled = convertToPackedArgb( sampleType, isLittleEndian, isPlanar, shouldDiffOrInvert, pixels, imageData, size, width, height, channels, maxValue ); if (pixelsFilled < area) Arrays.fill(pixels, pixelsFilled, area, 0); ByteBufferPool.release(imageData); // When obtaining this buffer for the first time, we don't know the width or height, // which means we don't know how long the buffer is supposed to be. // That's why we write width and height to [length - 2] and [length - 1] instead of [area] and [area + 1] pixels[pixels.length - 2] = width; pixels[pixels.length - 1] = height; images.add(pixels); return nextIfdOffset; } static void decompress( int mode, FileChannel fc, byte[] outData, int outSize, int[] offsets, int[] lengths ) throws IOException { int nStrips = Math.min(offsets.length, lengths.length); int inputSize = getLargestElement(lengths, nStrips); byte[] strip = ByteBufferPool.acquireAsIs(inputSize); ByteBuffer bb = ByteBuffer.wrap(strip); BitReader br = new BitReader(); IntVector lzwTableAndHeap = new IntVector(); int outOffset = 0; for (int i = 0; i < nStrips; i++) { bb.position(0); bb.limit(lengths[i]); fc.position(offsets[i]); fc.read(bb); if (mode == DC_LZW) outOffset = lzwDecompressStrip(outData, outOffset, outSize, strip, lengths[i], br, lzwTableAndHeap); if (mode == DC_PACKBITS) outOffset = packbitsDecompressStrip(outData, outOffset, outSize, strip, lengths[i]); if (mode == DC_DEFLATE) outOffset = deflateDecompressStrip(outData, outOffset, outSize, strip, lengths[i]); } ByteBufferPool.release(strip); } static int lzwDecompressStrip(byte[] outData, int outOffset, int outSize, byte[] strip, int length, BitReader br, IntVector lzwTableAndHeap) { br.reset(strip, 0, length); final int lzwTableSize = 2 * 16384; int lzwHeapSize = 256; lzwTableAndHeap.resize(lzwTableSize + lzwHeapSize); Arrays.fill(lzwTableAndHeap.buf, 0, lzwTableSize + lzwHeapSize, 0); int oldCode = 0; int bitsToRead = 9; int nextSymbol = 258; while (true) { int code = br.getBits(bitsToRead); //System.out.println("LZW code: " + code); if (code == 257 || code == -1) break; boolean isClear = code == 256; if (isClear) { for (int i = 0; i < 512; i += 2) { lzwTableAndHeap.buf[i] = lzwTableSize + (i >> 1); lzwTableAndHeap.buf[i+1] = 1; } for (int i = 0; i < 256; i++) lzwTableAndHeap.buf[lzwTableSize + i] = i; nextSymbol = 258; bitsToRead = 9; oldCode = code = br.getBits(bitsToRead); if (code == 257 || code == -1) break; } int toAddOff = 0; int toAddLen = 0; if (isClear) { toAddOff = lzwTableAndHeap.buf[2*code]; toAddLen = lzwTableAndHeap.buf[2*code+1]; } else { int oldOff = lzwTableAndHeap.buf[2*oldCode]; int oldLen = lzwTableAndHeap.buf[2*oldCode+1]; int firstOffset; if (code < nextSymbol) { toAddOff = lzwTableAndHeap.buf[2*code]; toAddLen = lzwTableAndHeap.buf[2*code+1]; firstOffset = toAddOff; } else { firstOffset = oldOff; } int nextSymbolOffset = lzwTableAndHeap.addFromSelf(oldOff, (oldLen + 3) >> 2); int firstByte = lzwTableAndHeap.buf[firstOffset] & 0xff; if ((oldLen & 3) == 0) lzwTableAndHeap.add(firstByte); else lzwTableAndHeap.buf[nextSymbolOffset + (oldLen >> 2)] |= firstByte << (8*(oldLen & 3)); lzwTableAndHeap.buf[2*nextSymbol] = nextSymbolOffset; lzwTableAndHeap.buf[2*nextSymbol+1] = oldLen + 1; if (code >= nextSymbol) { toAddOff = nextSymbolOffset; toAddLen = oldLen + 1; } } int[] th = lzwTableAndHeap.buf; for (int i = 0; i < toAddLen && outOffset+i < outSize; i++) outData[outOffset+i] = (byte)(th[toAddOff + (i>>2)] >>> ((i&3)*8)); outOffset += toAddLen; if (outOffset >= outSize) break; if (!isClear) { nextSymbol++; bitsToRead = 32 - Integer.numberOfLeadingZeros(nextSymbol + 1); oldCode = code; } } return outOffset; } static int packbitsDecompressStrip(byte[] outData, int outOffset, int outSize, byte[] strip, int length) { boolean isRepeat = false; int left = 0; for (int i = 0; i < length && outOffset < outSize; i++) { if (left == 0) { int mode = strip[i]; isRepeat = mode < 0; left = Math.abs(mode) + 1; continue; } if (isRepeat) { byte b = strip[i]; for (int j = 0; j < left && outOffset+j < outSize; j++) outData[outOffset+j] = b; outOffset += left; left = 0; } else { outData[outOffset++] = strip[i]; left--; } } return outOffset; } static int deflateDecompressStrip(byte[] outData, int outOffset, int outSize, byte[] strip, int length) { Inflater inflater = new Inflater(); inflater.setInput(strip, 0, length); try { while (!inflater.finished() && outOffset < outSize) { int res = inflater.inflate(outData, outOffset, outSize - outOffset); if (res <= 0) break; outOffset += res; } } catch (Exception ex) { ex.printStackTrace(); } inflater.end(); return outOffset; } static int getLargestElement(int[] array, int count) { int result = 0; for (int i = 0; i < count; i++) result = Math.max(result, array[i]); return result; } static int getShort(byte[] buf, int off, boolean isLittleEndian) { if (isLittleEndian) { return (buf[off] & 0xff) | (buf[off+1] & 0xff) << 8; } return (buf[off] & 0xff) << 8 | (buf[off+1] & 0xff); } static int getInt(byte[] buf, int off, boolean isLittleEndian) { if (isLittleEndian) { return (buf[off] & 0xff) | (buf[off+1] & 0xff) << 8 | (buf[off+2] & 0xff) << 16 | (buf[off+3] & 0xff) << 24; } return (buf[off] & 0xff) << 24 | (buf[off+1] & 0xff) << 16 | (buf[off+2] & 0xff) << 8 | (buf[off+3] & 0xff); } static void getArray(int[] out, int outOff, byte[] in, int inOff, int count, int type, boolean isLittleEndian) { int s0, s1, s2, s3; if (isLittleEndian) { s0 = 0; s1 = 8; s2 = 16; s3 = 24; } else { s0 = 24; s1 = 16; s2 = 8; s3 = 0; } if (type == 3) { for (int i = 0; i < count; i++) out[outOff+i] = (in[inOff + 2*i] & 0xff) << s0 | (in[inOff + 2*i + 1] & 0xff) << s1; } else { for (int i = 0; i < count; i++) out[outOff+i] = (in[inOff + 4*i] & 0xff) << s0 | (in[inOff + 4*i + 1] & 0xff) << s1 | (in[inOff + 4*i + 2] & 0xff) << s2 | (in[inOff + 4*i + 3] & 0xff) << s3; } } }