diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java index bd53c739..2e079d74 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/HuffmanTable.java @@ -33,41 +33,27 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import javax.imageio.IIOException; -import javax.imageio.stream.ImageInputStream; import java.io.DataInput; import java.io.IOException; final class HuffmanTable extends Segment { - final int l[][][] = new int[4][2][16]; - final int th[] = new int[4]; // 1: this table is presented + private final int l[][][] = new int[4][2][16]; + private final int th[] = new int[4]; // 1: this table is present final int v[][][][] = new int[4][2][16][200]; // tables - final int[][] tc = new int[4][2]; // 1: this table is presented + final int[][] tc = new int[4][2]; // 1: this table is present - public static final int MSB = 0x80000000; + static final int MSB = 0x80000000; private HuffmanTable() { super(JPEG.DHT); + } - tc[0][0] = 0; - tc[1][0] = 0; - tc[2][0] = 0; - tc[3][0] = 0; - tc[0][1] = 0; - tc[1][1] = 0; - tc[2][1] = 0; - tc[3][1] = 0; - th[0] = 0; - th[1] = 0; - th[2] = 0; - th[3] = 0; - } - - protected void buildHuffTables(final int[][][] HuffTab) throws IOException { - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 2; j++) { - if (tc[i][j] != 0) { - buildHuffTable(HuffTab[i][j], l[i][j], v[i][j]); + void buildHuffTables(final int[][][] HuffTab) throws IOException { + for (int t = 0; t < 4; t++) { + for (int c = 0; c < 2; c++) { + if (tc[t][c] != 0) { + buildHuffTable(HuffTab[t][c], l[t][c], v[t][c]); } } } @@ -81,10 +67,8 @@ final class HuffmanTable extends Segment { // Effect: // build up HuffTab[t][c] using L and V. private void buildHuffTable(final int tab[], final int L[], final int V[][]) throws IOException { - int currentTable, temp; - int k; - temp = 256; - k = 0; + int temp = 256; + int k = 0; for (int i = 0; i < 8; i++) { // i+1 is Code length for (int j = 0; j < L[i]; j++) { @@ -99,7 +83,7 @@ final class HuffmanTable extends Segment { tab[k] = i | MSB; } - currentTable = 1; + int currentTable = 1; k = 0; for (int i = 8; i < 16; i++) { // i+1 is Code length @@ -122,8 +106,27 @@ final class HuffmanTable extends Segment { @Override public String toString() { - // TODO: Id and class for tables - return "DHT[]"; + StringBuilder builder = new StringBuilder("DHT["); + + for (int t = 0; t < tc.length; t++) { + for (int c = 0; c < tc[t].length; c++) { + if (tc[t][c] != 0) { + if (builder.length() > 4) { + builder.append(", "); + } + + builder.append("id: "); + builder.append(t); + + builder.append(", class: "); + builder.append(c); + } + } + } + + builder.append(']'); + + return builder.toString(); } public static Segment read(final DataInput data, final int length) throws IOException { diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index abebea43..a6131d9a 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -109,37 +109,7 @@ public final class JPEGImageReader extends ImageReaderBase { static final int ALL_APP_MARKERS = -1; /** Segment identifiers for the JPEG segments we care about reading. */ - private static final Map> SEGMENT_IDENTIFIERS = JPEGSegmentUtil.ALL_SEGMENTS; //createSegmentIds(); - - private static Map> createSegmentIds() { - Map> map = new LinkedHashMap<>(); - - // Need all APP markers to be able to re-generate proper metadata later - for (int appMarker = JPEG.APP0; appMarker <= JPEG.APP15; appMarker++) { - map.put(appMarker, JPEGSegmentUtil.ALL_IDS); - } - - // SOFn markers - map.put(JPEG.SOF0, null); - map.put(JPEG.SOF1, null); - map.put(JPEG.SOF2, null); - map.put(JPEG.SOF3, null); - map.put(JPEG.SOF5, null); - map.put(JPEG.SOF6, null); - map.put(JPEG.SOF7, null); - map.put(JPEG.SOF9, null); - map.put(JPEG.SOF10, null); - map.put(JPEG.SOF11, null); - map.put(JPEG.SOF13, null); - map.put(JPEG.SOF14, null); - map.put(JPEG.SOF15, null); - - map.put(JPEG.DQT, null); - map.put(JPEG.DHT, null); - map.put(JPEG.SOS, null); - - return Collections.unmodifiableMap(map); - } + private static final Map> SEGMENT_IDENTIFIERS = JPEGSegmentUtil.ALL_SEGMENTS; /** Our JPEG reading delegate */ private final ImageReader delegate; @@ -385,7 +355,7 @@ public final class JPEGImageReader extends ImageReaderBase { // TODO: What about stream position? // TODO: Param handling: Source region, offset, subsampling, destination, destination type, etc.... // Read image as lossless - return new JPEGLosslessDecoderWrapper().readImage(segments, imageInput); + return new JPEGLosslessDecoderWrapper(this).readImage(segments, imageInput); } // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) @@ -944,7 +914,7 @@ public final class JPEGImageReader extends ImageReaderBase { if (isLossless()) { // TODO: What about stream position? // TODO: Param handling: Reading as raster should support source region, subsampling etc. - return new JPEGLosslessDecoderWrapper().readRaster(segments, imageInput); + return new JPEGLosslessDecoderWrapper(this).readRaster(segments, imageInput); } return delegate.readRaster(imageIndex, param); diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java index 776c6aef..312f9b33 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoder.java @@ -36,14 +36,16 @@ import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOException; import javax.imageio.stream.ImageInputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.List; final class JPEGLosslessDecoder { private final ImageInputStream input; + private final JPEGImageReader listenerDelegate; private final Frame frame; - private final HuffmanTable huffTable; + private final List huffTables; private final QuantizationTable quantTable; private Scan scan; @@ -103,7 +105,7 @@ final class JPEGLosslessDecoder { return yDim; } - JPEGLosslessDecoder(final List segments, final ImageInputStream data) { + JPEGLosslessDecoder(final List segments, final ImageInputStream data, final JPEGImageReader listenerDelegate) { Validate.notNull(segments); frame = get(segments, Frame.class); @@ -111,11 +113,25 @@ final class JPEGLosslessDecoder { QuantizationTable qt = get(segments, QuantizationTable.class); quantTable = qt != null ? qt : new QuantizationTable(); // For lossless there are no DQTs - huffTable = get(segments, HuffmanTable.class); // For non-lossless there can be multiple of DHTs + huffTables = getAll(segments, HuffmanTable.class); // For lossless there's usually only one, and only DC tables + RestartInterval dri = get(segments, RestartInterval.class); restartInterval = dri != null ? dri.interval : 0; input = data; + this.listenerDelegate = listenerDelegate; + } + + private List getAll(final List segments, final Class type) { + ArrayList list = new ArrayList<>(); + + for (Segment segment : segments) { + if (type.isInstance(segment)) { + list.add(type.cast(segment)); + } + } + + return list; } private T get(final List segments, final Class type) { @@ -138,10 +154,13 @@ final class JPEGLosslessDecoder { current = input.readUnsignedShort(); if (current != JPEG.SOI) { // SOI - throw new IIOException("Not a JPEG file"); + throw new IIOException("Not a JPEG file, does not start with 0xFFD8"); + } + + for (HuffmanTable huffTable : huffTables) { + huffTable.buildHuffTables(HuffTab); } - huffTable.buildHuffTables(HuffTab); quantTable.enhanceTables(TABLE); current = input.readUnsignedShort(); @@ -175,8 +194,23 @@ final class JPEGLosslessDecoder { Frame.Component component = getComponentSpec(components, scanComps[i].scanCompSel); qTab[i] = quantTables[component.qtSel]; nBlock[i] = component.vSub * component.hSub; - dcTab[i] = HuffTab[scanComps[i].dcTabSel][0]; - acTab[i] = HuffTab[scanComps[i].acTabSel][1]; + + int dcTabSel = scanComps[i].dcTabSel; + int acTabSel = scanComps[i].acTabSel; + + // NOTE: If we don't find any DC tables for lossless operation, this file isn't any good. + // However, we have seen files with AC tables only, we'll treat these as if the AC was DC + if (useACForDC(dcTabSel)) { + processWarningOccured("Lossless JPEG with no DC tables encountered. Assuming only tables present to be DC tables."); + + dcTab[i] = HuffTab[dcTabSel][1]; + acTab[i] = HuffTab[acTabSel][0]; + } + else { + // All good + dcTab[i] = HuffTab[dcTabSel][0]; + acTab[i] = HuffTab[acTabSel][1]; + } } xDim = frame.samplesPerLine; @@ -202,10 +236,8 @@ final class JPEGLosslessDecoder { scanNum++; while (true) { // Decode one scan - final int temp[] = new int[1]; // to store remainder bits - final int index[] = new int[1]; - temp[0] = 0; - index[0] = 0; + int temp[] = new int[1]; // to store remainder bits + int index[] = new int[1]; for (int i = 0; i < 10; i++) { pred[i] = (1 << (precision - 1)); @@ -242,10 +274,7 @@ final class JPEGLosslessDecoder { } } - if ((current >= RESTART_MARKER_BEGIN) && (current <= RESTART_MARKER_END)) { - //empty - } - else { + if ((current < RESTART_MARKER_BEGIN) || (current > RESTART_MARKER_END)) { break; //current=MARKER } } @@ -259,6 +288,34 @@ final class JPEGLosslessDecoder { return outputRef; } + private void processWarningOccured(String warning) { + listenerDelegate.processWarningOccurred(warning); + } + + private boolean useACForDC(final int dcTabSel) { + if (isLossless()) { + for (HuffmanTable huffTable : huffTables) { + if (huffTable.tc[dcTabSel][0] == 0 && huffTable.tc[dcTabSel][1] != 0) { + return true; + } + } + } + + return false; + } + + private boolean isLossless() { + switch (frame.marker) { + case JPEG.SOF3: + case JPEG.SOF7: + case JPEG.SOF11: + case JPEG.SOF15: + return true; + default: + return false; + } + } + private Frame.Component getComponentSpec(Frame.Component[] components, int sel) { for (Frame.Component component : components) { if (component.id == sel) { @@ -320,15 +377,15 @@ final class JPEGLosslessDecoder { } for (int i = 0; i < nBlock[0]; i++) { - final int value = getHuffmanValue(dcTab[0], temp, index); + int value = getHuffmanValue(dcTab[0], temp, index); if (value >= 0xFF00) { return value; } - final int n = getn(prev, value, temp, index); + int n = getn(prev, value, temp, index); - final int nRestart = (n >> 8); + int nRestart = (n >> 8); if ((nRestart >= RESTART_MARKER_BEGIN) && (nRestart <= RESTART_MARKER_END)) { return nRestart; } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java index 9b58532e..ccea1e14 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGLosslessDecoderWrapper.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; 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; @@ -56,6 +57,12 @@ import java.util.List; */ final class JPEGLosslessDecoderWrapper { + private final JPEGImageReader listenerDelegate; + + JPEGLosslessDecoderWrapper(final JPEGImageReader listenerDelegate) { + this.listenerDelegate = listenerDelegate; + } + /** * Decodes a JPEG Lossless stream to a {@code BufferedImage}. * Currently the following conversions are supported: @@ -63,14 +70,19 @@ final class JPEGLosslessDecoderWrapper { * - 8Bit, Grayscale -> BufferedImage.TYPE_BYTE_GRAY * - 16Bit, Grayscale -> BufferedImage.TYPE_USHORT_GRAY * - * @param segments + * @param segments segments * @param input input stream which contains a jpeg lossless data * @return if successfully a BufferedImage is returned * @throws IOException is thrown if the decoder failed or a conversion is not supported */ BufferedImage readImage(final List segments, final ImageInputStream input) throws IOException { - JPEGLosslessDecoder decoder = new JPEGLosslessDecoder(segments, createBufferedInput(input)); + JPEGLosslessDecoder decoder = new JPEGLosslessDecoder(segments, createBufferedInput(input), listenerDelegate); + // TODO: Allow 10/12/14 bit (using a ComponentColorModel with correct bits, as in TIFF) + // TODO: Rewrite this to pass a pre-allocated buffer of correct type (byte/short)/correct bands + // TODO: Progress callbacks + // TODO: Warning callbacks + // Callback can then do subsampling etc. int[][] decoded = decoder.decode(); int width = decoder.getDimX(); int height = decoder.getDimY(); @@ -82,23 +94,17 @@ final class JPEGLosslessDecoderWrapper { return to8Bit1ComponentGrayScale(decoded, width, height); case 16: return to16Bit1ComponentGrayScale(decoded, width, height); - default: - throw new IOException("JPEG Lossless with " + decoder.getPrecision() + " bit precision and 1 component cannot be decoded"); } } - // 3 components, assumed to be RGB - if (decoder.getNumComponents() == 3) { + else if (decoder.getNumComponents() == 3) { switch (decoder.getPrecision()) { case 8: return to24Bit3ComponentRGB(decoded, width, height); - - default: - throw new IOException("JPEG Lossless with " + decoder.getPrecision() + " bit precision and 3 components cannot be decoded"); } } - throw new IOException("JPEG Lossless with " + decoder.getPrecision() + " bit precision and " + decoder.getNumComponents() + " component(s) cannot be decoded"); + throw new IIOException("JPEG Lossless with " + decoder.getPrecision() + " bit precision and " + decoder.getNumComponents() + " component(s) not supported"); } private ImageInputStream createBufferedInput(final ImageInputStream input) throws IOException { diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index dbf722e5..fe2eeb02 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -100,6 +100,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest