blob: 704840dedb317281eb01a35c2daeb39059ac29ab [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.internal.image;
import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import java.io.*;
final class TIFFDirectory {
TIFFRandomFileAccess file;
boolean isLittleEndian;
ImageLoader loader;
int depth;
/* Directory fields */
int imageWidth;
int imageLength;
int[] bitsPerSample;
int compression;
int photometricInterpretation;
int[] stripOffsets;
int samplesPerPixel;
int rowsPerStrip;
int[] stripByteCounts;
int t4Options;
int colorMapOffset;
/* Encoder fields */
ImageData image;
LEDataOutputStream out;
static final int NO_VALUE = -1;
static final short TAG_ImageWidth = 256;
static final short TAG_ImageLength = 257;
static final short TAG_BitsPerSample = 258;
static final short TAG_Compression = 259;
static final short TAG_PhotometricInterpretation = 262;
static final short TAG_StripOffsets = 273;
static final short TAG_SamplesPerPixel = 277;
static final short TAG_RowsPerStrip = 278;
static final short TAG_StripByteCounts = 279;
static final short TAG_XResolution = 282;
static final short TAG_YResolution = 283;
static final short TAG_T4Options = 292;
static final short TAG_ResolutionUnit = 296;
static final short TAG_ColorMap = 320;
static final int TYPE_BYTE = 1;
static final int TYPE_ASCII = 2;
static final int TYPE_SHORT = 3;
static final int TYPE_LONG = 4;
static final int TYPE_RATIONAL = 5;
/* Different compression schemes */
static final int COMPRESSION_NONE = 1;
static final int COMPRESSION_CCITT_3_1 = 2;
static final int COMPRESSION_PACKBITS = 32773;
static final int IFD_ENTRY_SIZE = 12;
public TIFFDirectory(TIFFRandomFileAccess file, boolean isLittleEndian, ImageLoader loader) {
this.file = file;
this.isLittleEndian = isLittleEndian;
this.loader = loader;
}
public TIFFDirectory(ImageData image) {
this.image = image;
}
/* PackBits decoder */
int decodePackBits(byte[] src, byte[] dest, int offsetDest) {
int destIndex = offsetDest;
int srcIndex = 0;
while (srcIndex < src.length) {
byte n = src[srcIndex];
if (0 <= n && n <= 127) {
/* Copy next n+1 bytes literally */
System.arraycopy(src, ++srcIndex, dest, destIndex, n + 1);
srcIndex += n + 1;
destIndex += n + 1;
} else if (-127 <= n && n <= -1) {
/* Copy next byte -n+1 times */
byte value = src[++srcIndex];
for (int j = 0; j < -n + 1; j++) {
dest[destIndex++] = value;
}
srcIndex++;
} else {
/* Noop when n == -128 */
srcIndex++;
}
}
/* Number of bytes copied */
return destIndex - offsetDest;
}
int getEntryValue(int type, byte[] buffer, int index) {
return toInt(buffer, index + 8, type);
}
void getEntryValue(int type, byte[] buffer, int index, int[] values) throws IOException {
int start = index + 8;
int size;
int offset = toInt(buffer, start, TYPE_LONG);
switch (type) {
case TYPE_SHORT: size = 2; break;
case TYPE_LONG: size = 4; break;
case TYPE_RATIONAL: size = 8; break;
case TYPE_ASCII:
case TYPE_BYTE: size = 1; break;
default: SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT); return;
}
if (values.length * size > 4) {
buffer = new byte[values.length * size];
file.seek(offset);
file.read(buffer);
start = 0;
}
for (int i = 0; i < values.length; i++) {
values[i] = toInt(buffer, start + i * size, type);
}
}
void decodePixels(ImageData image) throws IOException {
/* Each row is byte aligned */
byte[] imageData = new byte[(imageWidth * depth + 7) / 8 * imageLength];
image.data = imageData;
int destIndex = 0;
int length = stripOffsets.length;
for (int i = 0; i < length; i++) {
/* Read a strip */
byte[] data = new byte[stripByteCounts[i]];
file.seek(stripOffsets[i]);
file.read(data);
if (compression == COMPRESSION_NONE) {
System.arraycopy(data, 0, imageData, destIndex, data.length);
destIndex += data.length;
} else if (compression == COMPRESSION_PACKBITS) {
destIndex += decodePackBits(data, imageData, destIndex);
} else if (compression == COMPRESSION_CCITT_3_1 || compression == 3) {
TIFFModifiedHuffmanCodec codec = new TIFFModifiedHuffmanCodec();
int nRows = rowsPerStrip;
if (i == length -1) {
int n = imageLength % rowsPerStrip;
if (n != 0) nRows = n;
}
destIndex += codec.decode(data, imageData, destIndex, imageWidth, nRows);
}
if (loader.hasListeners()) {
loader.notifyListeners(new ImageLoaderEvent(loader, image, i, i == length - 1));
}
}
}
PaletteData getColorMap() throws IOException {
int numColors = 1 << bitsPerSample[0];
/* R, G, B entries are 16 bit wide (2 bytes) */
int numBytes = 3 * 2 * numColors;
byte[] buffer = new byte[numBytes];
file.seek(colorMapOffset);
file.read(buffer);
RGB[] colors = new RGB[numColors];
/**
* SWT does not support 16-bit depth color formats.
* Convert the 16-bit data to 8-bit data.
* The correct way to do this is to multiply each
* 16 bit value by the value:
* (2^8 - 1) / (2^16 - 1).
* The fast way to do this is just to drop the low
* byte of the 16-bit value.
*/
int offset = isLittleEndian ? 1 : 0;
int startG = 2 * numColors;
int startB = startG + 2 * numColors;
for (int i = 0; i < numColors; i++) {
int r = buffer[offset] & 0xFF;
int g = buffer[startG + offset] & 0xFF;
int b = buffer[startB + offset] & 0xFF;
colors[i] = new RGB(r, g, b);
offset += 2;
}
return new PaletteData(colors);
}
PaletteData getGrayPalette() {
int numColors = 1 << bitsPerSample[0];
RGB[] rgbs = new RGB[numColors];
for (int i = 0; i < numColors; i++) {
int value = i * 0xFF / (numColors - 1);
if (photometricInterpretation == 0) value = 0xFF - value;
rgbs[i] = new RGB(value, value, value);
}
return new PaletteData(rgbs);
}
PaletteData getRGBPalette(int bitsR, int bitsG, int bitsB) {
int blueMask = 0;
for (int i = 0; i < bitsB; i++) {
blueMask |= 1 << i;
}
int greenMask = 0;
for (int i = bitsB; i < bitsB + bitsG; i++) {
greenMask |= 1 << i;
}
int redMask = 0;
for (int i = bitsB + bitsG; i < bitsB + bitsG + bitsR; i++) {
redMask |= 1 << i;
}
return new PaletteData(redMask, greenMask, blueMask);
}
int formatStrips(int rowByteSize, int nbrRows, byte[] data, int maxStripByteSize, int offsetPostIFD, int extraBytes, int[][] strips) {
/*
* Calculate the nbr of required strips given the following requirements:
* - each strip should, if possible, not be greater than maxStripByteSize
* - each strip should contain 1 or more entire rows
*
* Format the strip fields arrays so that the image data is stored in one
* contiguous block. This block is stored after the IFD and after any tag
* info described in the IFD.
*/
int n, nbrRowsPerStrip;
if (rowByteSize > maxStripByteSize) {
/* Each strip contains 1 row */
n = data.length / rowByteSize;
nbrRowsPerStrip = 1;
} else {
int nbr = (data.length + maxStripByteSize - 1) / maxStripByteSize;
nbrRowsPerStrip = nbrRows / nbr;
n = (nbrRows + nbrRowsPerStrip - 1) / nbrRowsPerStrip;
}
int stripByteSize = rowByteSize * nbrRowsPerStrip;
int[] offsets = new int[n];
int[] counts = new int[n];
/*
* Nbr of bytes between the end of the IFD directory and the start of
* the image data. Keep space for at least the offsets and counts
* data, each field being TYPE_LONG (4 bytes). If other tags require
* space between the IFD and the image block, use the extraBytes
* parameter.
* If there is only one strip, the offsets and counts data is stored
* directly in the IFD and we need not reserve space for it.
*/
int postIFDData = n == 1 ? 0 : n * 2 * 4;
int startOffset = offsetPostIFD + extraBytes + postIFDData; /* offset of image data */
int offset = startOffset;
for (int i = 0; i < n; i++) {
/*
* Store all strips sequentially to allow us
* to copy all pixels in one contiguous area.
*/
offsets[i] = offset;
counts[i] = stripByteSize;
offset += stripByteSize;
}
/* The last strip may contain fewer rows */
int mod = data.length % stripByteSize;
if (mod != 0) counts[counts.length - 1] = mod;
strips[0] = offsets;
strips[1] = counts;
return nbrRowsPerStrip;
}
int[] formatColorMap(RGB[] rgbs) {
/*
* In a TIFF ColorMap, all red come first, followed by
* green and blue. All values must be converted from
* 8 bit to 16 bit.
*/
int[] colorMap = new int[rgbs.length * 3];
int offsetGreen = rgbs.length;
int offsetBlue = rgbs.length * 2;
for (int i = 0; i < rgbs.length; i++) {
colorMap[i] = rgbs[i].red << 8 | rgbs[i].red;
colorMap[i + offsetGreen] = rgbs[i].green << 8 | rgbs[i].green;
colorMap[i + offsetBlue] = rgbs[i].blue << 8 | rgbs[i].blue;
}
return colorMap;
}
void parseEntries(byte[] buffer) throws IOException {
for (int offset = 0; offset < buffer.length; offset += IFD_ENTRY_SIZE) {
int tag = toInt(buffer, offset, TYPE_SHORT);
int type = toInt(buffer, offset + 2, TYPE_SHORT);
int count = toInt(buffer, offset + 4, TYPE_LONG);
switch (tag) {
case TAG_ImageWidth: {
imageWidth = getEntryValue(type, buffer, offset);
break;
}
case TAG_ImageLength: {
imageLength = getEntryValue(type, buffer, offset);
break;
}
case TAG_BitsPerSample: {
if (type != TYPE_SHORT) SWT.error(SWT.ERROR_INVALID_IMAGE);
bitsPerSample = new int[count];
getEntryValue(type, buffer, offset, bitsPerSample);
break;
}
case TAG_Compression: {
compression = getEntryValue(type, buffer, offset);
break;
}
case TAG_PhotometricInterpretation: {
photometricInterpretation = getEntryValue(type, buffer, offset);
break;
}
case TAG_StripOffsets: {
if (type != TYPE_LONG && type != TYPE_SHORT) SWT.error(SWT.ERROR_INVALID_IMAGE);
stripOffsets = new int[count];
getEntryValue(type, buffer, offset, stripOffsets);
break;
}
case TAG_SamplesPerPixel: {
if (type != TYPE_SHORT) SWT.error(SWT.ERROR_INVALID_IMAGE);
samplesPerPixel = getEntryValue(type, buffer, offset);
/* Only the basic 1 and 3 values are supported */
if (samplesPerPixel != 1 && samplesPerPixel != 3) SWT.error(SWT.ERROR_UNSUPPORTED_DEPTH);
break;
}
case TAG_RowsPerStrip: {
rowsPerStrip = getEntryValue(type, buffer, offset);
break;
}
case TAG_StripByteCounts: {
stripByteCounts = new int[count];
getEntryValue(type, buffer, offset, stripByteCounts);
break;
}
case TAG_XResolution: {
/* Ignored */
break;
}
case TAG_YResolution: {
/* Ignored */
break;
}
case TAG_T4Options: {
if (type != TYPE_LONG) SWT.error(SWT.ERROR_INVALID_IMAGE);
t4Options = getEntryValue(type, buffer, offset);
if ((t4Options & 0x1) == 1) {
/* 2-dimensional coding is not supported */
SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
}
break;
}
case TAG_ResolutionUnit: {
/* Ignored */
break;
}
case TAG_ColorMap: {
if (type != TYPE_SHORT) SWT.error(SWT.ERROR_INVALID_IMAGE);
/* Get the offset of the colorMap (use TYPE_LONG) */
colorMapOffset = getEntryValue(TYPE_LONG, buffer, offset);
break;
}
}
}
}
public ImageData read() throws IOException {
/* Set TIFF default values */
bitsPerSample = new int[] {1};
colorMapOffset = NO_VALUE;
compression = 1;
imageLength = NO_VALUE;
imageWidth = NO_VALUE;
photometricInterpretation = NO_VALUE;
rowsPerStrip = Integer.MAX_VALUE;
samplesPerPixel = 1;
stripByteCounts = null;
stripOffsets = null;
byte[] buffer = new byte[2];
file.read(buffer);
int numberEntries = toInt(buffer, 0, TYPE_SHORT);
buffer = new byte[IFD_ENTRY_SIZE * numberEntries];
file.read(buffer);
parseEntries(buffer);
PaletteData palette = null;
depth = 0;
switch (photometricInterpretation) {
case 0:
case 1: {
/* Bilevel or Grayscale image */
palette = getGrayPalette();
depth = bitsPerSample[0];
break;
}
case 2: {
/* RGB image */
if (colorMapOffset != NO_VALUE) SWT.error(SWT.ERROR_INVALID_IMAGE);
/* SamplesPerPixel 3 is the only value supported */
palette = getRGBPalette(bitsPerSample[0], bitsPerSample[1], bitsPerSample[2]);
depth = bitsPerSample[0] + bitsPerSample[1] + bitsPerSample[2];
break;
}
case 3: {
/* Palette Color image */
if (colorMapOffset == NO_VALUE) SWT.error(SWT.ERROR_INVALID_IMAGE);
palette = getColorMap();
depth = bitsPerSample[0];
break;
}
default: {
SWT.error(SWT.ERROR_INVALID_IMAGE);
}
}
ImageData image = ImageData.internal_new(
imageWidth,
imageLength,
depth,
palette,
1,
null,
0,
null,
null,
-1,
-1,
SWT.IMAGE_TIFF,
0,
0,
0,
0);
decodePixels(image);
return image;
}
int toInt(byte[] buffer, int i, int type) {
if (type == TYPE_LONG) {
return isLittleEndian ?
(buffer[i] & 0xFF) | ((buffer[i + 1] & 0xFF) << 8) | ((buffer[i + 2] & 0xFF) << 16) | ((buffer[i + 3] & 0xFF) << 24) :
(buffer[i + 3] & 0xFF) | ((buffer[i + 2] & 0xFF) << 8) | ((buffer[i + 1] & 0xFF) << 16) | ((buffer[i] & 0xFF) << 24);
}
if (type == TYPE_SHORT) {
return isLittleEndian ?
(buffer[i] & 0xFF) | ((buffer[i + 1] & 0xFF) << 8) :
(buffer[i + 1] & 0xFF) | ((buffer[i] & 0xFF) << 8);
}
/* Invalid type */
SWT.error(SWT.ERROR_INVALID_IMAGE);
return -1;
}
void write(int photometricInterpretation) throws IOException {
boolean isRGB = photometricInterpretation == 2;
boolean isColorMap = photometricInterpretation == 3;
boolean isBiLevel = photometricInterpretation == 0 || photometricInterpretation == 1;
int imageWidth = image.width;
int imageLength = image.height;
int rowByteSize = image.bytesPerLine;
int numberEntries = isBiLevel ? 9 : 11;
int lengthDirectory = 2 + 12 * numberEntries + 4;
/* Offset following the header and the directory */
int nextOffset = 8 + lengthDirectory;
/* Extra space used by XResolution and YResolution values */
int extraBytes = 16;
int[] colorMap = null;
if (isColorMap) {
PaletteData palette = image.palette;
RGB[] rgbs = palette.getRGBs();
colorMap = formatColorMap(rgbs);
/* The number of entries of the Color Map must match the bitsPerSample field */
if (colorMap.length != 3 * 1 << image.depth) SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
/* Extra space used by ColorMap values */
extraBytes += colorMap.length * 2;
}
if (isRGB) {
/* Extra space used by BitsPerSample values */
extraBytes += 6;
}
/* TIFF recommends storing the data in strips of no more than 8 Ko */
byte[] data = image.data;
int[][] strips = new int[2][];
int nbrRowsPerStrip = formatStrips(rowByteSize, imageLength, data, 8192, nextOffset, extraBytes, strips);
int[] stripOffsets = strips[0];
int[] stripByteCounts = strips[1];
int bitsPerSampleOffset = NO_VALUE;
if (isRGB) {
bitsPerSampleOffset = nextOffset;
nextOffset += 6;
}
int stripOffsetsOffset = NO_VALUE, stripByteCountsOffset = NO_VALUE;
int xResolutionOffset, yResolutionOffset, colorMapOffset = NO_VALUE;
int cnt = stripOffsets.length;
if (cnt > 1) {
stripOffsetsOffset = nextOffset;
nextOffset += 4 * cnt;
stripByteCountsOffset = nextOffset;
nextOffset += 4 * cnt;
}
xResolutionOffset = nextOffset;
nextOffset += 8;
yResolutionOffset = nextOffset;
nextOffset += 8;
if (isColorMap) {
colorMapOffset = nextOffset;
nextOffset += colorMap.length * 2;
}
/* TIFF header */
writeHeader();
/* Image File Directory */
out.writeShort(numberEntries);
writeEntry(TAG_ImageWidth, TYPE_LONG, 1, imageWidth);
writeEntry(TAG_ImageLength, TYPE_LONG, 1, imageLength);
if (isColorMap) writeEntry(TAG_BitsPerSample, TYPE_SHORT, 1, image.depth);
if (isRGB) writeEntry(TAG_BitsPerSample, TYPE_SHORT, 3, bitsPerSampleOffset);
writeEntry(TAG_Compression, TYPE_SHORT, 1, COMPRESSION_NONE);
writeEntry(TAG_PhotometricInterpretation, TYPE_SHORT, 1, photometricInterpretation);
writeEntry(TAG_StripOffsets, TYPE_LONG, cnt, cnt > 1 ? stripOffsetsOffset : stripOffsets[0]);
if (isRGB) writeEntry(TAG_SamplesPerPixel, TYPE_SHORT, 1, 3);
writeEntry(TAG_RowsPerStrip, TYPE_LONG, 1, nbrRowsPerStrip);
writeEntry(TAG_StripByteCounts, TYPE_LONG, cnt, cnt > 1 ? stripByteCountsOffset : stripByteCounts[0]);
writeEntry(TAG_XResolution, TYPE_RATIONAL, 1, xResolutionOffset);
writeEntry(TAG_YResolution, TYPE_RATIONAL, 1, yResolutionOffset);
if (isColorMap) writeEntry(TAG_ColorMap, TYPE_SHORT, colorMap.length, colorMapOffset);
/* Offset of next IFD (0 for last IFD) */
out.writeInt(0);
/* Values longer than 4 bytes Section */
/* BitsPerSample 8,8,8 */
if (isRGB) for (int i = 0; i < 3; i++) out.writeShort(8);
if (cnt > 1) {
for (int i = 0; i < cnt; i++) out.writeInt(stripOffsets[i]);
for (int i = 0; i < cnt; i++) out.writeInt(stripByteCounts[i]);
}
/* XResolution and YResolution set to 300 dpi */
for (int i = 0; i < 2; i++) {
out.writeInt(300);
out.writeInt(1);
}
/* ColorMap */
if (isColorMap) for (int i = 0; i < colorMap.length; i++) out.writeShort(colorMap[i]);
/* Image Data */
out.write(data);
}
void writeEntry(short tag, int type, int count, int value) throws IOException {
out.writeShort(tag);
out.writeShort(type);
out.writeInt(count);
out.writeInt(value);
}
void writeHeader() throws IOException {
/* little endian */
out.write(0x49);
out.write(0x49);
/* TIFF identifier */
out.writeShort(42);
/*
* Offset of the first IFD is chosen to be 8.
* It is word aligned and immediately after this header.
*/
out.writeInt(8);
}
void writeToStream(LEDataOutputStream byteStream) throws IOException {
out = byteStream;
int photometricInterpretation = -1;
/* Scanline pad must be 1 */
if (image.scanlinePad != 1) SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
switch (image.depth) {
case 1: {
/* Palette must be black and white or white and black */
PaletteData palette = image.palette;
RGB[] rgbs = palette.colors;
if (palette.isDirect || rgbs == null || rgbs.length != 2) SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
RGB rgb0 = rgbs[0];
RGB rgb1 = rgbs[1];
if (!(rgb0.red == rgb0.green && rgb0.green == rgb0.blue &&
rgb1.red == rgb1.green && rgb1.green == rgb1.blue &&
((rgb0.red == 0x0 && rgb1.red == 0xFF) || (rgb0.red == 0xFF && rgb1.red == 0x0)))) {
SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
}
/* 0 means a color index of 0 is imaged as white */
photometricInterpretation = image.palette.colors[0].red == 0xFF ? 0 : 1;
break;
}
case 4:
case 8: {
photometricInterpretation = 3;
break;
}
case 24: {
photometricInterpretation = 2;
break;
}
default: {
SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
}
}
write(photometricInterpretation);
}
}