TIFFUtilities: Enhanced support for old-style to new-style JPEG transformation

This commit is contained in:
Oliver Schmidtmer 2018-08-21 14:55:36 +02:00
parent 7af0cfaa78
commit 632e5e6664
6 changed files with 221 additions and 65 deletions

View File

@ -45,11 +45,9 @@ import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@ -358,13 +356,21 @@ public final class TIFFUtilities {
}
}
int compression = -1;
Entry compressionEntry = IFD.getEntryById(TIFF.TAG_COMPRESSION);
if (compressionEntry != null && compressionEntry.getValue() instanceof Number) {
compression = ((Number) compressionEntry.getValue()).shortValue();
}
boolean rearrangedByteStrips = false;
Entry oldJpegData = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
Entry oldJpegDataLength = IFD.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
long[] jpegByteCounts = null;
long[] jpegOffsets = null;
if (oldJpegData != null && oldJpegData.valueCount() > 0) {
// convert JPEGInterchangeFormat to new-style-JPEG
long[] jpegByteCounts = new long[0];
long[] jpegOffsets = getValueAsLongArray(oldJpegData);
jpegByteCounts = new long[0];
jpegOffsets = getValueAsLongArray(oldJpegData);
if (oldJpegDataLength != null && oldJpegDataLength.valueCount() > 0) {
jpegByteCounts = getValueAsLongArray(oldJpegDataLength);
}
@ -388,6 +394,20 @@ public final class TIFFUtilities {
newIFD.remove(oldJpegDataLength);
rearrangedByteStrips = true;
}
else if (offsets.length == 1 && oldJpegDataLength != null && (jpegOffsets[0] < offsets[0]) && (jpegOffsets[0] + jpegByteCounts[0]) > (offsets[0] + byteCounts[0])) {
// ByteStrip contains only a part of JPEGInterchangeFormat
newOffsets = writeData(jpegOffsets, jpegByteCounts, outputStream);
newIFD.remove(stripOffsetsEntry);
newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_OFFSETS : TIFF.TAG_STRIP_OFFSETS, newOffsets));
newIFD.remove(stripByteCountsEntry);
newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_BYTE_COUNTS : TIFF.TAG_STRIP_BYTE_COUNTS, new int[]{(int) (jpegByteCounts[0])}));
newIFD.remove(oldJpegData);
newIFD.remove(oldJpegDataLength);
rearrangedByteStrips = true;
}
else if (oldJpegDataLength != null) {
// multiple bytestrips
// search for SOF on first strip and copy to each if needed
@ -423,7 +443,7 @@ public final class TIFFUtilities {
byte[] buffer = new byte[(int) byteCounts[i]];
newByteCounts[i] = (int) (jpegInterchangeData.length + byteCounts[i]);
stream.readFully(buffer);
if (buffer[0] != 0xff && buffer[1] != 0xda) {
if (buffer[0] != ((byte) 0xff) || buffer[1] != ((byte) 0xda)) {
outputStream.write(sosMarker);
newByteCounts[i] += sosMarker.length;
}
@ -440,7 +460,58 @@ public final class TIFFUtilities {
rearrangedByteStrips = true;
}
}
else if (compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
// old-style but no JPEGInterchangeFormat
long[] yCbCrSubSampling = getValueAsLongArray(IFD.getEntryById(TIFF.TAG_YCBCR_SUB_SAMPLING));
int subsampling = yCbCrSubSampling != null
? (int) ((yCbCrSubSampling[0] & 0xf) << 4 | yCbCrSubSampling[1] & 0xf)
: 0x22;
int bands = ((Number) IFD.getEntryById(TIFF.TAG_SAMPLES_PER_PIXEL).getValue()).intValue();
int w = ((Number) IFD.getEntryById(TIFF.TAG_IMAGE_WIDTH).getValue()).intValue();
int h = ((Number) IFD.getEntryById(TIFF.TAG_IMAGE_HEIGHT).getValue()).intValue();
int r = ((Number) (useTiles ? IFD.getEntryById(TIFF.TAG_TILE_HEIGTH) : IFD.getEntryById(TIFF.TAG_ROWS_PER_STRIP)).getValue()).intValue();
int c = useTiles ? ((Number) IFD.getEntryById(TIFF.TAG_TILE_WIDTH).getValue()).intValue() : w;
newOffsets = new int[offsets.length];
int[] newByteCounts = new int[byteCounts.length];
// No JPEGInterchangeFormat
for (int i = 0; i < offsets.length; i++) {
byte[] start = new byte[2];
stream.seek(offsets[i]);
stream.readFully(start);
newOffsets[i] = (int) outputStream.getStreamPosition();
if (start[0] == ((byte) 0xff) && start[1] == ((byte) 0xd8)) {
// full image stream, nothing to do
writeData(stream, outputStream, offsets[i], byteCounts[i]);
}
else if (start[0] == ((byte) 0xff) && start[1] == ((byte) 0xda)) {
// starts with SOS
outputStream.writeShort(JPEG.SOI);
writeSOF0(outputStream, bands, c, r, subsampling);
writeData(stream, outputStream, offsets[i], byteCounts[i]);
outputStream.writeShort(JPEG.EOI);
}
else {
// raw data
outputStream.writeShort(JPEG.SOI);
writeSOF0(outputStream, bands, c, r, subsampling);
writeSOS(outputStream, bands);
writeData(stream, outputStream, offsets[i], byteCounts[i]);
outputStream.writeShort(JPEG.EOI);
}
newByteCounts[i] = ((int) outputStream.getStreamPosition()) - newOffsets[i];
}
newIFD.remove(stripOffsetsEntry);
newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_OFFSETS : TIFF.TAG_STRIP_OFFSETS, newOffsets));
newIFD.remove(stripByteCountsEntry);
newIFD.add(new TIFFEntry(useTiles ? TIFF.TAG_TILE_BYTE_COUNTS : TIFF.TAG_STRIP_BYTE_COUNTS, newByteCounts));
rearrangedByteStrips = true;
}
if (!rearrangedByteStrips && stripOffsetsEntry != null && stripByteCountsEntry != null) {
newOffsets = writeData(offsets, byteCounts, outputStream);
@ -457,12 +528,21 @@ public final class TIFFUtilities {
oldJpegTableQ = IFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES);
oldJpegTableDC = IFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES);
oldJpegTableAC = IFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES);
if (oldJpegTableQ != null || oldJpegTableDC != null || oldJpegTableAC != null) {
if ((oldJpegTableQ != null) || (oldJpegTableDC != null) || (oldJpegTableAC != null)) {
if (IFD.getEntryById(TIFF.TAG_JPEG_TABLES) != null) {
throw new IOException("Found old-style and new-style JPEGTables");
}
newIFD.add(mergeTables(oldJpegTableQ, oldJpegTableDC, oldJpegTableAC));
boolean tablesInStream = jfifContainsTables(oldJpegTableQ, jpegOffsets, jpegByteCounts);
tablesInStream &= jfifContainsTables(oldJpegTableDC, jpegOffsets, jpegByteCounts);
tablesInStream &= jfifContainsTables(oldJpegTableAC, jpegOffsets, jpegByteCounts);
if (!tablesInStream) {
// merge them only to JPEGTables if they are not already contained within the stream
Entry jpegTables = mergeTables(oldJpegTableQ, oldJpegTableDC, oldJpegTableAC);
if (jpegTables != null) {
newIFD.add(jpegTables);
}
}
if (oldJpegTableQ != null) {
newIFD.remove(oldJpegTableQ);
}
@ -474,17 +554,66 @@ public final class TIFFUtilities {
}
}
Entry compressionEntry = IFD.getEntryById(TIFF.TAG_COMPRESSION);
if(compressionEntry != null) {
Number compression = (Number) compressionEntry.getValue();
if (compression.shortValue() == TIFFExtension.COMPRESSION_OLD_JPEG) {
newIFD.remove(compressionEntry);
newIFD.add(new TIFFEntry(TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, TIFFExtension.COMPRESSION_JPEG));
}
if (compressionEntry != null && compression == TIFFExtension.COMPRESSION_OLD_JPEG) {
newIFD.remove(compressionEntry);
newIFD.add(new TIFFEntry(TIFF.TAG_COMPRESSION, TIFF.TYPE_SHORT, TIFFExtension.COMPRESSION_JPEG));
}
return newIFD;
}
//TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction
private void writeSOF0(ImageOutputStream outputStream, int bands, int width, int height, int subsampling) throws IOException {
outputStream.writeShort(JPEG.SOF0); // TODO: Use correct process for data
outputStream.writeShort(2 + 6 + 3 * bands); // SOF0 len
outputStream.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
outputStream.writeShort(height); // height
outputStream.writeShort(width); // width
outputStream.writeByte(bands); // Number of components
for (int comp = 0; comp < bands; comp++) {
outputStream.writeByte(comp); // Component id
outputStream.writeByte(comp == 0 ? subsampling : 0x11); // h/v subsampling
outputStream.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal, correct selection if only 1 or 2 valid tables are contained
}
}
//TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction
private void writeSOS(ImageOutputStream outputStream, int bands) throws IOException {
outputStream.writeShort(JPEG.SOS);
outputStream.writeShort(6 + 2 * bands); // SOS length
outputStream.writeByte(bands); // Num comp
for (int component = 0; component < bands; component++) {
outputStream.writeByte(component); // Comp id
outputStream.writeByte(component == 0 ? component : 0x10 + (component & 0xf)); // dc/ac selector TODO: correct selection if only 1 or 2 valid tables are contained
}
outputStream.writeByte(0); // Spectral selection start
outputStream.writeByte(0); // Spectral selection end
outputStream.writeByte(0); // Approx high & low
}
private void writeData(ImageInputStream input, ImageOutputStream output, long offset, long length) throws IOException {
input.seek(offset);
byte[] buffer = new byte[(int) length];
stream.readFully(buffer);
output.write(buffer);
}
private boolean jfifContainsTables(Entry tableEntry, long[] jpegOffsets, long[] jpegLengths) throws IOException {
if (jpegLengths == null || jpegOffsets == null || jpegLengths.length == 0) return false;
if (tableEntry != null) {
long[] tableOffsets = getValueAsLongArray(tableEntry);
for (long offset : tableOffsets) {
if (offset < jpegOffsets[0] || offset > (jpegOffsets[0] + jpegLengths[0])) {
return false;
}
}
}
return true;
}
//TODO merge/extract from TIFFReader Jpeg/6 stream reconstruction
private Entry mergeTables(Entry qEntry, Entry dcEntry, Entry acEntry) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
@ -494,12 +623,16 @@ public final class TIFFUtilities {
long[] off = getValueAsLongArray(qEntry);
byte[] table = new byte[64];
for (int tableId = 0; tableId < off.length; tableId++) {
stream.seek(off[tableId]);
stream.readFully(table);
dos.writeShort(JPEG.DQT);
dos.writeShort(3 + 64);
dos.writeByte(tableId);
dos.write(table);
try {
stream.seek(off[tableId]);
stream.readFully(table);
dos.writeShort(JPEG.DQT);
dos.writeShort(3 + 64);
dos.writeByte(tableId);
dos.write(table);
} catch (EOFException e) {
// invalid table pointer, ignore
}
}
}
@ -507,30 +640,50 @@ public final class TIFFUtilities {
if (dcEntry != null && dcEntry.valueCount() > 0) {
long[] off = getValueAsLongArray(dcEntry);
for (int tableId = 0; tableId < off.length; tableId++) {
stream.seek(off[tableId]);
byte[] table = readHUFFTable();
dos.writeShort(JPEG.DHT);
dos.writeShort(3 + table.length);
dos.writeByte(tableId);
dos.write(table);
try {
stream.seek(off[tableId]);
byte[] table = readHUFFTable();
if (table.length > (16 + 17)) {
// to long, table is invalid, just ignoe
continue;
}
dos.writeShort(JPEG.DHT);
dos.writeShort(3 + table.length);
dos.writeByte(tableId);
dos.write(table);
} catch (EOFException e) {
// invalid table pointer, ignore
}
}
}
if (acEntry != null && acEntry.valueCount() > 0) {
long[] off = getValueAsLongArray(acEntry);
for (int tableId = 0; tableId < off.length; tableId++) {
stream.seek(off[tableId]);
byte[] table = readHUFFTable();
dos.writeShort(JPEG.DHT);
dos.writeShort(3 + table.length);
dos.writeByte(16 | tableId);
dos.write(table);
try {
stream.seek(off[tableId]);
byte[] table = readHUFFTable();
if (table.length > (16 + 256)) {
// to long, table is invalid, just ignoe
continue;
}
dos.writeShort(JPEG.DHT);
dos.writeShort(3 + table.length);
dos.writeByte(16 | tableId);
dos.write(table);
} catch (EOFException e) {
// invalid table pointer, ignore
}
}
}
dos.writeShort(JPEG.EOI);
bos.close();
if (bos.size() == 4) {
// no valid tables, don't add
return null;
}
return new TIFFEntry(TIFF.TAG_JPEG_TABLES, TIFF.TYPE_UNDEFINED, bos.toByteArray());
}
@ -540,15 +693,11 @@ public final class TIFFUtilities {
stream.readFully(lengths);
int numCodes = 0;
for (int i = 0; i < lengths.length; i++) {
numCodes += lengths[i];
numCodes += ((int) lengths[i]) & 0xff;
}
byte table[] = new byte[16 + numCodes];
System.arraycopy(lengths, 0, table, 0, 16);
int off = 16;
for (int i = 0; i < lengths.length; i++) {
stream.read(table, off, lengths[i]);
off += lengths[i];
}
stream.readFully(table, 16, numCodes);
return table;
}
@ -559,7 +708,11 @@ public final class TIFFUtilities {
stream.seek(offsets[i]);
byte[] buffer = new byte[(int) byteCounts[i]];
stream.readFully(buffer);
try {
stream.readFully(buffer);
} catch (EOFException e) {
// invalid strip length
}
outputStream.write(buffer);
}
return newOffsets;
@ -571,7 +724,7 @@ public final class TIFFUtilities {
if (entry.valueCount() == 1) {
// For single entries, this will be a boxed type
value = new long[] {((Number) entry.getValue()).longValue()};
value = new long[]{((Number) entry.getValue()).longValue()};
}
else if (entry.getValue() instanceof short[]) {
short[] shorts = (short[]) entry.getValue();

View File

@ -202,36 +202,39 @@ public class TIFFUtilitiesTest {
}
@Test
public void testMergeBogusInterchangeFormatLength() throws IOException {
String[] testFiles = new String[] {
public void testOldStyleJPEGTransform() throws IOException {
String[] testFiles = new String[]{
"/tiff/old-style-jpeg-bogus-jpeginterchangeformatlength.tif", // InterchangeFormat before StripOffset, length not including StripOffset
"/tiff/old-style-jpeg-no-jpeginterchangeformatlength.tif", // missing JPEGInterChangeFormatLength and JPEGInterchangeFormat == StipOffset
"/tiff/old-style-jpeg-multiple-strips.tif" // InterchangeFormat with multiple strips
"/tiff/old-style-jpeg-multiple-strips.tif", // InterchangeFormat with multiple strips
"/contrib/tiff/old-style-jpeg-invalid-tables.tif", // AC/DC Tables are invalid (to long) and lie within the JPEGInterchangeFormat stream
"/contrib/tiff/smallliz.tif", // InterchangeFormat contains whole JPEG, ByteStrip only raw ImageData after SOS
"/contrib/tiff/WangJPEG.tif", // multiple strips, first strip contains SOS
"/contrib/tiff/zackthecat.tif" // No JPEGInterchangeFormat, ByteStrip contains only raw image data
};
for (String testFile : testFiles) {
File output = File.createTempFile("imageiotest", ".tif");
ImageOutputStream outputStream = ImageIO.createImageOutputStream(output);
InputStream inputStream1 = getClassLoaderResource(testFile).openStream();
ImageInputStream imageInput1 = ImageIO.createImageInputStream(inputStream1);
InputStream inputStream2 = getClassLoaderResource(testFile).openStream();
ImageInputStream imageInput2 = ImageIO.createImageInputStream(inputStream2);
ArrayList<TIFFUtilities.TIFFPage> pages = new ArrayList<>();
pages.addAll(TIFFUtilities.getPages(imageInput1));
pages.addAll(TIFFUtilities.getPages(imageInput2));
TIFFUtilities.writePages(outputStream, pages);
try {
File output = File.createTempFile("imageiotest", ".tif");
ImageOutputStream outputStream = ImageIO.createImageOutputStream(output);
InputStream inputStream = getClassLoaderResource(testFile).openStream();
ImageInputStream imageInput = ImageIO.createImageInputStream(inputStream);
List<TIFFUtilities.TIFFPage> pages = TIFFUtilities.getPages(imageInput);
TIFFUtilities.writePages(outputStream, pages);
ImageInputStream testOutput = ImageIO.createImageInputStream(output);
ImageReader reader = ImageIO.getImageReaders(testOutput).next();
reader.setInput(testOutput);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
reader.read(i);
ImageInputStream testOutput = ImageIO.createImageInputStream(output);
ImageReader reader = ImageIO.getImageReaders(testOutput).next();
reader.setInput(testOutput);
int numImages = reader.getNumImages(true);
for (int i = 0; i < numImages; i++) {
reader.read(i);
}
imageInput.close();
outputStream.close();
} catch (Exception exc) {
throw new IOException(testFile, exc);
}
imageInput1.close();
imageInput2.close();
outputStream.close();
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.