TMI-TIFF: Fixed bug in YCbCr reading. Implemented "old-style" JPEG reading for two test images. More work needed.

This commit is contained in:
Harald Kuhr 2013-02-06 11:20:42 +01:00
parent fcd15a9e36
commit c394f8a4bc
10 changed files with 273 additions and 21 deletions

View File

@ -163,4 +163,10 @@ public interface TIFF {
int TAG_TILE_BYTE_COUNTS = 325;
int TAG_JPEG_TABLES = 347;
// "Old-style" JPEG (Obsolete)
int TAG_JPEG_PROC = 512;
int TAG_JPEG_QTABLES = 519;
int TAG_JPEG_DCTABLES = 520;
int TAG_JPEG_ACTABLES = 521;
}

View File

@ -112,7 +112,7 @@ class JPEGTables {
// Read lengths as short array
short[] lengths = new short[DHT_LENGTH];
for (int i = 0, lengthsLength = lengths.length; i < lengthsLength; i++) {
for (int i = 0; i < DHT_LENGTH; i++) {
lengths[i] = (short) data.readUnsignedByte();
}
read += lengths.length;

View File

@ -65,4 +65,8 @@ interface TIFFExtension {
int SAMPLEFORMAT_INT = 2;
int SAMPLEFORMAT_FP = 3;
int SAMPLEFORMAT_UNDEFINED = 4;
// "Old-style" JPEG (obsolete)
int JPEG_PROC_BASELINE = 1;
int JPEG_PROC_LOSSLESS = 14;
}

View File

@ -59,9 +59,7 @@ import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.*;
import java.util.List;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
@ -485,9 +483,6 @@ public class TIFFImageReader extends ImageReaderBase {
WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
int row = 0;
// Read data
processImageStarted(imageIndex);
switch (compression) {
// TIFF Baseline
case TIFFBaseline.COMPRESSION_NONE:
@ -547,6 +542,9 @@ public class TIFFImageReader extends ImageReaderBase {
}
}
// Read data
processImageStarted(imageIndex);
// TODO: Read only tiles that lies within region
// General uncompressed/compressed reading
for (int y = 0; y < tilesDown; y++) {
@ -606,6 +604,7 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFExtension.COMPRESSION_JPEG:
// JPEG ('new-style' JPEG)
// TODO: Refactor all JPEG reading out to separate JPEG support class?
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider());
@ -680,6 +679,9 @@ public class TIFFImageReader extends ImageReaderBase {
// ...and the JPEG reader will probably choke on missing tables...
}
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
@ -689,14 +691,14 @@ public class TIFFImageReader extends ImageReaderBase {
int colsInTile = Math.min(stripTileWidth, width - col);
imageInput.seek(stripTileOffsets[i]);
SubImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
try {
jpegReader.setInput(subStream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setDestinationOffset(new Point(col, row));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
@ -722,6 +724,240 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2)
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
// TODO: Issue warning?
int mode = getValueAsIntWithDefault(TIFF.TAG_JPEG_PROC, 1);
if (mode == TIFFExtension.JPEG_PROC_LOSSLESS) {
throw new IIOException("Unsupported TIFF JPEGProcessingMode: Lossless (14)");
}
else if (mode != TIFFExtension.JPEG_PROC_BASELINE) {
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
}
// May use normal tiling??
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
// 513/JPEGInterchangeFormat (may be absent...)
// 514/JPEGInterchangeFormatLength (may be absent...)
// 515/JPEGRestartInterval (may be absent)
// 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms
// 519/JPEGQTables
// 520/JPEGDCTables
// 521/JPEGACTables
// This field was originally intended to point to a list of offsets to the quantization tables, one per
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
// usually found in a JPEG stream DQT marker.
// The original specification strongly recommended that, within the TIFF file, each component be
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
// a DCT-based process.
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
jpegReader = new JPEGImageReader(getOriginatingProvider());
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
int jpegLenght = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
ImageInputStream subStream;
if (jpegOffset != -1) {
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
imageInput.seek(jpegOffset);
subStream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
jpegReader.setInput(subStream);
// Read data
processImageStarted(imageIndex);
try {
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
subStream.close();
}
processImageProgress(100f * row / (float) height);
if (abortRequested()) {
processReadAborted();
}
}
else {
// The hard way: Read tables and re-create a full JFIF stream
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
// use only the first occurrence, and update selectors in SOF0 and SOS
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_QTABLES, "JPEGQTables", true);
byte[][] qTables = new byte[3][(int) (qTablesOffsets[1] - qTablesOffsets[0])]; // TODO: Using the offsets seems fragile.. Use fixed length??
for (int j = 0; j < 3; j++) {
imageInput.seek(qTablesOffsets[j]);
imageInput.readFully(qTables[j]);
}
// System.err.println("qTables: " + qTables[0].length);
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_DCTABLES, "JPEGDCTables", true);
byte[][] dcTables = new byte[3][(int) (dcTablesOffsets[1] - dcTablesOffsets[0])];
for (int j = 0; j < 3; j++) {
imageInput.seek(dcTablesOffsets[j]);
imageInput.readFully(dcTables[j]);
}
// System.err.println("dcTables: " + dcTables[0].length);
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_ACTABLES, "JPEGACTables", true);
byte[][] acTables = new byte[3][(int) (acTablesOffsets[1] - acTablesOffsets[0])];
for (int j = 0; j < 3; j++) {
imageInput.seek(acTablesOffsets[j]);
imageInput.readFully(acTables[j]);
}
// System.err.println("acTables: " + acTables[0].length);
// Read data
processImageStarted(imageIndex);
for (int y = 0; y < tilesDown; y++) {
int col = 0;
int rowsInTile = Math.min(stripTileHeight, height - row);
for (int x = 0; x < tilesAcross; x++) {
int colsInTile = Math.min(stripTileWidth, width - col);
int i = y * tilesAcross + x;
imageInput.seek(stripTileOffsets[i]);
subStream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
Arrays.asList(
// TODO; Get rid of hardcoded data + extract method/class...
// TODO:
// - Create a BAIS with size large enough to keep JFIF structure incl tables and SOS,
// - Wrap in DataInput,
// - Insert width/height, component ids etc at correct place
// - Insert tables at correct place
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd8, // SOI
// SOF0 (short), length (short)
(byte) 0xff, (byte) 0xc0, 0x00, 0x11, // SOF0, 17 bytes
// bits (byte), width (short), height (short)
0x08, 0x00, (byte) 0xe0, 0x00, (byte) 0xf0,
// num comp (byte), (id (byte) h/vsub (byte), qtsel (byte) * num comp)
0x03, 0x00, 0x22, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x02,
// 0x03, 0x00, 0x22, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x01,
// DQT
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x00,
}),
// ... table data
new ByteArrayInputStream(qTables[0]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x01,
}),
// ... table data
new ByteArrayInputStream(qTables[1]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x02,
}),
// ... table data
new ByteArrayInputStream(qTables[2]),
// DHT (DC)
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x00,
}),
// ... table data
new ByteArrayInputStream(dcTables[0]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x01,
}),
// ... table data
new ByteArrayInputStream(dcTables[1]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x02,
}),
// ... table data
new ByteArrayInputStream(dcTables[2]),
// DHT (AC)
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x10,
}),
// ... table data
new ByteArrayInputStream(acTables[0]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x11,
}),
// ... table data
new ByteArrayInputStream(acTables[1]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x12,
}),
// ... table data
new ByteArrayInputStream(acTables[2]),
new ByteArrayInputStream(new byte[] {
(byte) 0xff, (byte) 0xda, // SOS
// TODO: Figure out what the last 3 bytes are...
// Length: 12 (short), num comp (byte), (id (byte), dc/ac sel (byte) * num comp), ?? byte, ?? byte ?? byte
0x00, 0x0C, 0x03, 0x00, 0x00, 0x01, 0x11, 0x02, 0x11, 0x00, 0x00, 0x00
// 0x00, 0x0C, 0x03, 0x00, 0x00, 0x01, 0x11, 0x02, 0x12, 0x00, 0x63, 0x00
}),
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)));
jpegReader.setInput(subStream);
try {
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setDestinationOffset(new Point(col, row));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
subStream.close();
}
if (abortRequested()) {
break;
}
col += colsInTile;
}
processImageProgress(100f * row / (float) height);
if (abortRequested()) {
processReadAborted();
break;
}
row += rowsInTile;
}
}
break;
case TIFFBaseline.COMPRESSION_CCITT_HUFFMAN:
// CCITT modified Huffman
// Additionally, the specification defines these values as part of the TIFF extensions:
@ -729,8 +965,6 @@ public class TIFFImageReader extends ImageReaderBase {
// CCITT Group 3 fax encoding
case TIFFExtension.COMPRESSION_CCITT_T6:
// CCITT Group 4 fax encoding
case TIFFExtension.COMPRESSION_OLD_JPEG:
// JPEG ('old-style' JPEG, later overridden in Technote2)
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:

View File

@ -216,18 +216,21 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
}
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
// TODO: FixMe: This is bogus...
double y = yCbCr[offset ] & 0xff;
double cb = yCbCr[offset + 1] & 0xff;
double cr = yCbCr[offset + 2] & 0xff;
double y = (yCbCr[offset ] & 0xff);
double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values?
double cr = (yCbCr[offset + 2] & 0xff) - 128;
double lumaRed = coefficients[0];
double lumaGreen = coefficients[1];
double lumaBlue = coefficients[2];
rgb[offset ] = clamp((int) Math.round(cr * (2 - 2 * lumaRed) + y));
rgb[offset + 2] = clamp((int) Math.round(cb * (2 - 2 * lumaBlue) + y));
rgb[offset + 1] = clamp((int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen));
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen);
rgb[offset ] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
private static byte clamp(int val) {

View File

@ -52,11 +52,14 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled
new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled
new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, OLD LZW compressed, tiled
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, Old spec (reversed) LZW compressed, tiled
new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed
new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed
new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)), // CMYK, uncompressed
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)) // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
);
}
@ -89,4 +92,6 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
protected List<String> getMIMETypes() {
return Arrays.asList("image/tiff");
}
// TODO: Test YCbCr colors
}

View File

@ -43,7 +43,7 @@ import java.io.InputStream;
*/
@Ignore
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
// TODO: Implement
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
@Override
protected InputStream makeInputStream(byte[] pBytes) {
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, pBytes.length / 4, null);

Binary file not shown.