#304 TIFF JPEG Lossless support

This commit is contained in:
Harald Kuhr 2017-01-18 18:59:38 +01:00
parent 2e90e4b897
commit 3b76d9fcfd
11 changed files with 214 additions and 49 deletions

View File

@ -148,7 +148,7 @@ class JPEGImage10Metadata extends AbstractMetadata {
if (segment instanceof Frame) {
Frame sofSegment = (Frame) segment;
IIOMetadataNode colorSpaceType = new IIOMetadataNode("ColorSpaceType");
colorSpaceType.setAttribute("name", sofSegment.componentsInFrame() == 1 ? "Gray" : "RGB"); // TODO YCC, YCCK, CMYK etc
colorSpaceType.setAttribute("name", sofSegment.componentsInFrame() == 1 ? "GRAY" : "RGB"); // TODO YCC, YCCK, CMYK etc
chroma.appendChild(colorSpaceType);
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");

View File

@ -352,10 +352,24 @@ public final class JPEGImageReader extends ImageReaderBase {
JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), adobeDCT, sof);
if (sof.marker == JPEG.SOF3) {
// Read image as lossless
if (DEBUG) {
System.out.println("Reading using Lossless decoder");
}
// TODO: What about stream position?
// TODO: Param handling: Source region, offset, subsampling, destination, destination type, etc....
// Read image as lossless
return new JPEGLosslessDecoderWrapper(this).readImage(segments, imageInput);
BufferedImage bufferedImage = new JPEGLosslessDecoderWrapper(this).readImage(segments, imageInput);
// TODO: This is QnD, move param handling to lossless wrapper
// TODO: Create test!
BufferedImage destination = param != null ? param.getDestination() : null;
if (destination != null) {
destination.getRaster().setDataElements(0, 0, bufferedImage.getRaster());
return destination;
}
return bufferedImage;
}
// We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is)
@ -382,6 +396,18 @@ public final class JPEGImageReader extends ImageReaderBase {
return delegate.read(imageIndex, param);
}
static void drawOnto(final BufferedImage pDestination, final Image pSource) {
Graphics2D g = pDestination.createGraphics();
try {
g.setComposite(AlphaComposite.Src);
g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
g.drawImage(pSource, 0, 0, null);
}
finally {
g.dispose();
}
}
private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException {
int origWidth = getWidth(imageIndex);
int origHeight = getHeight(imageIndex);

View File

@ -33,10 +33,9 @@ import com.twelvemonkeys.imageio.stream.BufferedImageInputStream;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.Raster;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.IOException;
import java.util.List;
@ -71,7 +70,7 @@ final class JPEGLosslessDecoderWrapper {
* - 16Bit, Grayscale -> BufferedImage.TYPE_USHORT_GRAY
*
* @param segments segments
* @param input input stream which contains a jpeg lossless data
* @param input input stream which contains JPEG Lossless data
* @return if successfully a BufferedImage is returned
* @throws IOException is thrown if the decoder failed or a conversion is not supported
*/
@ -92,8 +91,11 @@ final class JPEGLosslessDecoderWrapper {
switch (decoder.getPrecision()) {
case 8:
return to8Bit1ComponentGrayScale(decoded, width, height);
case 10:
case 12:
case 14:
case 16:
return to16Bit1ComponentGrayScale(decoded, width, height);
return to16Bit1ComponentGrayScale(decoded, decoder.getPrecision(), width, height);
}
}
// 3 components, assumed to be RGB
@ -121,12 +123,20 @@ final class JPEGLosslessDecoderWrapper {
* precision: 16 bit, componentCount = 1
*
* @param decoded data buffer
* @param precision
* @param width of the image
* @param height of the image
* @return a BufferedImage.TYPE_USHORT_GRAY
* @param height of the image @return a BufferedImage.TYPE_USHORT_GRAY
*/
private BufferedImage to16Bit1ComponentGrayScale(int[][] decoded, int width, int height) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
private BufferedImage to16Bit1ComponentGrayScale(int[][] decoded, int precision, int width, int height) {
BufferedImage image;
if (precision == 16) {
image = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_GRAY);
}
else {
ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), new int[] {precision}, false, false, Transparency.OPAQUE, DataBuffer.TYPE_USHORT);
image = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(width, height), colorModel.isAlphaPremultiplied(), null);
}
short[] imageBuffer = ((DataBufferUShort) image.getRaster().getDataBuffer()).getData();
for (int i = 0; i < imageBuffer.length; i++) {

View File

@ -21,6 +21,11 @@
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-metadata</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>

View File

@ -38,6 +38,7 @@ import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.imageio.metadata.psd.PSD;
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
@ -51,6 +52,7 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.lang.StringUtil;
import org.w3c.dom.NodeList;
import javax.imageio.*;
@ -239,9 +241,97 @@ public final class TIFFImageReader extends ImageReaderBase {
if (Arrays.equals(foo.getBytes(StandardCharsets.US_ASCII), Arrays.copyOf(value, foo.length()))) {
System.err.println("foo: " + foo);
// int offset = foo.length() + 1;
// ImageInputStream input = new ByteArrayImageInputStream(value, offset, value.length - offset);
int offset = foo.length() + 1;
ImageInputStream input = new ByteArrayImageInputStream(value, offset, value.length - offset);
// input.setByteOrder(ByteOrder.LITTLE_ENDIAN); // TODO: WHY???!
while (input.getStreamPosition() < value.length - offset) {
int resourceId = input.readInt();
if (resourceId != PSD.RESOURCE_TYPE) {
System.err.println("Not a PSD resource: " + resourceId);
break;
}
int resourceKey = input.readInt();
System.err.println("resourceKey: " + intToStr(resourceKey));
long resourceLength = input.readUnsignedInt();
System.err.println("resourceLength: " + resourceLength);
long pad = (4 - (resourceLength % 4)) % 4;
long resourceLengthPadded = resourceLength + pad; // Padded to 32 bit boundary, possibly 64 bit for 8B64 resources
long streamPosition = input.getStreamPosition();
if (resourceKey == ('L' << 24 | 'a' << 16 | 'y' << 8 | 'r')) {
short count = input.readShort();
System.err.println("layer count: " + count);
for (int layer = 0; layer < count; layer++) {
int top = input.readInt();
int left = input.readInt();
int bottom = input.readInt();
int right = input.readInt();
System.err.printf("%d, %d, %d, %d\n", top, left, bottom, right);
short channels = input.readShort();
System.err.println("channels: " + channels);
for (int channel = 0; channel < channels; channel++) {
short channelId = input.readShort();
System.err.println("channelId: " + channelId);
long channelLength = input.readUnsignedInt();
System.err.println("channelLength: " + channelLength);
}
System.err.println("8BIM: " + intToStr(input.readInt()));
int blendMode = input.readInt();
System.err.println("blend mode key: " + intToStr(blendMode));
int opacity = input.readUnsignedByte();
System.err.println("opacity: " + opacity);
int clipping = input.readUnsignedByte();
System.err.println("clipping: " + clipping);
byte flags = input.readByte();
System.err.printf("flags: 0x%02x\n", flags);
input.readByte(); // Pad
long layerExtraDataLength = input.readUnsignedInt();
long pos = input.getStreamPosition();
System.err.println("length: " + layerExtraDataLength);
long layerMaskSize = input.readUnsignedInt();
input.skipBytes(layerMaskSize);
long layerBlendingRangesSize = input.readUnsignedInt();
input.skipBytes(layerBlendingRangesSize);
String layerName = readPascalString(input);
System.err.println("layerName: " + layerName);
int mod = (layerName.length() + 1) % 4; // len + 1 for null-term
System.err.println("mod: " + mod);
if (mod != 0) {
input.skipBytes(4 - mod);
}
System.err.println("input.getStreamPosition(): " + input.getStreamPosition());
// TODO: More data here
System.err.println(TIFFReader.HexDump.dump(0, value, (int) (offset + input.getStreamPosition()), 64));;
input.seek(pos + layerExtraDataLength);
}
// long len = input.readUnsignedInt();
// System.err.println("len: " + len);
//
// int count = input.readUnsignedShort();
// System.err.println("count: " + count);
System.err.println(TIFFReader.HexDump.dump(0, value, (int) (offset + input.getStreamPosition()), 64));;
}
input.seek(streamPosition + resourceLengthPadded);
System.out.println("input.getStreamPosition(): " + input.getStreamPosition());
}
// Directory psd2 = new PSDReader().read(input);
// System.err.println("-----------------------------------------------------------------------------");
// System.err.println("psd2: " + psd2);
@ -251,6 +341,30 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
static String readPascalString(final DataInput pInput) throws IOException {
int length = pInput.readUnsignedByte();
if (length == 0) {
return "";
}
byte[] bytes = new byte[length];
pInput.readFully(bytes);
return StringUtil.decode(bytes, 0, bytes.length, "ASCII");
}
static String intToStr(int value) {
return new String(
new byte[]{
(byte) ((value & 0xff000000) >>> 24),
(byte) ((value & 0x00ff0000) >> 16),
(byte) ((value & 0x0000ff00) >> 8),
(byte) ((value & 0x000000ff))
}
);
}
private void readIFD(final int imageIndex) throws IOException {
readMetadata();
checkBounds(imageIndex);
@ -1048,7 +1162,18 @@ public final class TIFFImageReader extends ImageReaderBase {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
// TODO: Refactor + duplicate this for all JPEG-in-TIFF cases
switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
break;
case DataBuffer.TYPE_USHORT:
normalizeColor(interpretation, ((DataBufferUShort) raster.getDataBuffer()).getData());
break;
default:
throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType());
}
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
}
@ -1081,9 +1206,8 @@ public final class TIFFImageReader extends ImageReaderBase {
int mode = getValueAsIntWithDefault(TIFF.TAG_OLD_JPEG_PROC, TIFFExtension.JPEG_PROC_BASELINE);
switch (mode) {
case TIFFExtension.JPEG_PROC_BASELINE:
break; // Supported
case TIFFExtension.JPEG_PROC_LOSSLESS:
throw new IIOException("Unsupported TIFF JPEGProcessingMode: Lossless (14)");
break; // Supported
default:
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
}
@ -1350,7 +1474,7 @@ public final class TIFFImageReader extends ImageReaderBase {
return jpegReader.getImageMetadata(0);
}
catch (IIOException e) {
processWarningOccurred("Could not read metadata metadata JPEG compressed TIFF: " + e.getMessage() + " colors may look incorrect");
processWarningOccurred("Could not read metadata for JPEG compressed TIFF (" + e.getMessage() + "): Colors may look incorrect");
return null;
}
@ -1391,20 +1515,8 @@ public final class TIFFImageReader extends ImageReaderBase {
}
private ImageReader createJPEGDelegate() throws IOException {
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
ImageReaderSpi jpegProvider = lookupProviderByName(IIORegistry.getDefaultInstance(), "com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi");
if (jpegProvider != null) {
return jpegProvider.createReaderInstance();
}
// Fall back to default reader below
if (DEBUG) {
System.err.println("Could not create " + "com.sun.imageio.plugins.jpeg.JPEGImageReader"
+ ", falling back to default JPEG capable ImageReader");
}
// If we can't get the standard reader, fall back to the default (first) reader
// We'll just use the default (first) reader
// If it's the TwelveMonkeys one, we will be able to read JPEG Lossless etc.
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("JPEG");
if (!readers.hasNext()) {
throw new IIOException("Could not instantiate JPEGImageReader");
@ -1415,28 +1527,17 @@ public final class TIFFImageReader extends ImageReaderBase {
private static InputStream createJFIFStream(int bands, int stripTileWidth, int stripTileHeight, byte[][] qTables, byte[][] dcTables, byte[][] acTables) throws IOException {
FastByteArrayOutputStream stream = new FastByteArrayOutputStream(
2 + 2 + 2 + 6 + 3 * bands +
2 +
5 * qTables.length + qTables.length * qTables[0].length +
5 * dcTables.length + dcTables.length * dcTables[0].length +
5 * acTables.length + acTables.length * acTables[0].length +
2 + 2 + 6 + 3 * bands +
8 + 2 * bands
);
DataOutputStream out = new DataOutputStream(stream);
out.writeShort(JPEG.SOI);
out.writeShort(JPEG.SOF0);
out.writeShort(2 + 6 + 3 * bands); // SOF0 len
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
out.writeShort(stripTileHeight); // height
out.writeShort(stripTileWidth); // width
out.writeByte(bands); // Number of components
for (int comp = 0; comp < bands; comp++) {
out.writeByte(comp); // Component id
out.writeByte(comp == 0 ? 0x22 : 0x11); // h/v subsampling TODO: FixMe, consult YCbCrSubsampling
out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal
}
// TODO: Consider merging if tables are equal
for (int tableIndex = 0; tableIndex < qTables.length; tableIndex++) {
@ -1465,6 +1566,19 @@ public final class TIFFImageReader extends ImageReaderBase {
out.write(table); // Table data
}
out.writeShort(JPEG.SOF0);
out.writeShort(2 + 6 + 3 * bands); // SOF0 len
out.writeByte(8); // bits TODO: Consult raster/transfer type or BitsPerSample for 12/16 bits support
out.writeShort(stripTileHeight); // height
out.writeShort(stripTileWidth); // width
out.writeByte(bands); // Number of components
for (int comp = 0; comp < bands; comp++) {
out.writeByte(comp); // Component id
out.writeByte(comp == 0 ? 0x22 : 0x11); // h/v subsampling TODO: FixMe, consult YCbCrSubsampling
out.writeByte(comp); // Q table selector TODO: Consider merging if tables are equal
}
out.writeShort(JPEG.SOS);
out.writeShort(6 + 2 * bands); // SOS length
out.writeByte(bands); // Num comp

View File

@ -149,11 +149,15 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-contig-16.tif"), new Dimension(73, 43)), // CMYK 16 bit/sample
// Separated (CMYK) Planar (PlanarConfiguration: 2)
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-08.tif"), new Dimension(73, 43)), // CMYK 8 bit/sample
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)) // CMYK 16 bit/sample
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)), // CMYK 16 bit/sample
new TestData(getClassLoaderResource("/tiff/jpeg-lossless-8bit-gray.tif"), new Dimension(512, 512)), // Lossless JPEG Gray, 8 bit/sample
new TestData(getClassLoaderResource("/tiff/jpeg-lossless-12bit-gray.tif"), new Dimension(512, 512)), // Lossless JPEG Gray, 12 bit/sample
new TestData(getClassLoaderResource("/tiff/jpeg-lossless-16bit-gray.tif"), new Dimension(512, 512)), // Lossless JPEG Gray, 16 bit/sample
new TestData(getClassLoaderResource("/tiff/jpeg-lossless-24bit-rgb"), new Dimension(512, 512)) // Lossless JPEG RGB, 8 bit/sample
);
}
protected List<TestData> getUnsupportedTestData() {
private List<TestData> getUnsupportedTestData() {
return Arrays.asList(
// RGB Interleaved (PlanarConfiguration: 1)
new TestData(getClassLoaderResource("/tiff/depth/flower-rgb-contig-02.tif"), new Dimension(73, 43)), // RGB 2 bit/sample
@ -265,7 +269,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
assertNotNull(image);
assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight()));
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), contains("metadata"));
verify(warningListener, atLeastOnce()).warningOccurred(eq(reader), contains("JPEG"));
}
}

View File

@ -135,6 +135,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>imageio-core</artifactId>