diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java index d444b799..87c328c3 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java @@ -108,6 +108,10 @@ abstract class LZWDecoder implements Decoder { break; } + if (table[code] == null) { + throw new DecodeException(String.format("Corrupted TIFF LZW: code %d (table size: %d)", code, tableLength)); + } + table[code].writeTo(buffer); } else { @@ -184,8 +188,9 @@ abstract class LZWDecoder implements Decoder { } } - public static LZWDecoder create(boolean oldBitReversedStream) { + public static Decoder create(boolean oldBitReversedStream) { return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWSpecDecoder(); +// return oldBitReversedStream ? new LZWCompatibilityDecoder() : new LZWTreeDecoder(); } static final class LZWSpecDecoder extends LZWDecoder { @@ -282,7 +287,9 @@ abstract class LZWDecoder implements Decoder { } } - static final class LZWString { + static final class LZWString implements Comparable { + static final LZWString EMPTY = new LZWString((byte) 0, (byte) 0, 0, null); + final LZWString previous; final int length; @@ -301,6 +308,10 @@ abstract class LZWDecoder implements Decoder { } public final LZWString concatenate(final byte value) { + if (this == EMPTY) { + return new LZWString(value); + } + return new LZWString(value, this.firstChar, length + 1, this); } @@ -364,6 +375,35 @@ abstract class LZWDecoder implements Decoder { result = 31 * result + (int) firstChar; return result; } + + @Override + public int compareTo(final LZWString other) { + if (other == this) { + return 0; + } + + if (length != other.length) { + return other.length - length; + } + + if (firstChar != other.firstChar) { + return other.firstChar - firstChar; + } + + LZWString t = this; + LZWString o = other; + + for (int i = length - 1; i > 0; i--) { + if (t.value != o.value) { + return o.value - t.value; + } + + t = t.previous; + o = o.previous; + } + + return 0; + } } } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java new file mode 100644 index 00000000..ee3cdf17 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2014, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.io.enc.Encoder; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.TreeMap; + +import static com.twelvemonkeys.imageio.plugins.tiff.LZWDecoder.LZWString; + +/** + * LZWEncoder + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LZWEncoder.java,v 1.0 02.12.13 14:13 haraldk Exp$ + */ +final class LZWEncoder implements Encoder { + // TODO: Consider extracting LZWStringTable from LZWDecoder + + /** Clear: Re-initialize tables. */ + static final int CLEAR_CODE = 256; + /** End of Information. */ + static final int EOI_CODE = 257; + + private static final int MIN_BITS = 9; + private static final int MAX_BITS = 12; + + private static final int TABLE_SIZE = 1 << MAX_BITS; + + private int remaining; + + private final LZWString[] table = new LZWString[TABLE_SIZE]; +// private final Map reverseTable = new HashMap<>(TABLE_SIZE - 256); // This is foobar + private final Map reverseTable = new TreeMap<>(); // This is foobar + private int tableLength; + LZWString omega = LZWString.EMPTY; + + int bitsPerCode; + private int oldCode = CLEAR_CODE; + private int maxCode; + int bitMask; + + int bits; + int bitPos; + + protected LZWEncoder(final int length) { + this.remaining = length; + + // First 258 entries of table is always fixed + for (int i = 0; i < 256; i++) { + table[i] = new LZWString((byte) i); + } + + init(); + } + + private static int bitmaskFor(final int bits) { + return (1 << bits) - 1; + } + + private void init() { + tableLength = 258; + bitsPerCode = MIN_BITS; + bitMask = bitmaskFor(bitsPerCode); + maxCode = maxCode(); +// omega = LZWString.EMPTY; + reverseTable.clear(); + } + + protected int maxCode() { + return bitMask; + } + + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { +// InitializeStringTable(); +// WriteCode(ClearCode); +// Ω = the empty string; +// for each character in the strip { +// K = GetNextCharacter(); +// if Ω+K is in the string table { +// Ω = Ω+K;/* string concatenation */ +// } +// else{ +// WriteCode (CodeFromString( Ω)); +// AddTableEntry(Ω+K); +// Ω=K; +// } }/*end of for loop*/ +// WriteCode (CodeFromString(Ω)); +// WriteCode (EndOfInformation); + + if (remaining < 0) { + throw new IOException("Write past end of stream"); + } + + // TODO: Write 9 bit clear code ONLY first time! + if (oldCode == CLEAR_CODE) { + writeCode(stream, CLEAR_CODE); + } + + int len = buffer.remaining(); + + while (buffer.hasRemaining()) { + byte k = buffer.get(); + + LZWString string = omega.concatenate(k); + + int tableIndex = isInTable(string); + if (tableIndex >= 0) { + omega = string; + oldCode = tableIndex; + } + else { + writeCode(stream, oldCode); + addStringToTable(string); + oldCode = k & 0xff; + omega = table[k & 0xff]; + + // Handle table (almost) full + if (tableLength >= TABLE_SIZE - 2) { + writeCode(stream, CLEAR_CODE); + init(); + } + } + } + + remaining -= len; + + // Write EOI when er are done (the API isn't very supportive of this) + if (remaining <= 0) { + writeCode(stream, oldCode); + writeCode(stream, EOI_CODE); + if (bitPos > 0) { + writeCode(stream, 0); + } + } + } + + private int isInTable(final LZWString string) { + if (string.length == 1) { + return string.value & 0xff; + } + + Integer index = reverseTable.get(string); + return index != null ? index : -1; + + // TODO: Needs optimization :-) +// for (int i = 258; i < tableLength; i++) { +// if (table[i].equals(string)) { +// return i; +// } +// } + +// return -1; + } + + private int addStringToTable(final LZWString string) { +// System.err.println("LZWEncoder.addStringToTable: " + string); + final int index = tableLength++; + table[index] = string; + reverseTable.put(string, index); + + if (tableLength > maxCode) { + bitsPerCode++; + + if (bitsPerCode > MAX_BITS) { + throw new IllegalStateException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS)); + } + + bitMask = bitmaskFor(bitsPerCode); + maxCode = maxCode(); + } + +// if (string.length > maxString) { +// maxString = string.length; +// } + + return index; + } + + private void writeCode(final OutputStream stream, final int code) throws IOException { +// System.err.printf("LZWEncoder.writeCode: 0x%04x\n", code); + bits = (bits << bitsPerCode) | (code & bitMask); + bitPos += bitsPerCode; + + while (bitPos >= 8) { + int b = (bits >> (bitPos - 8)) & 0xff; +// System.err.printf("write: 0x%02x\n", b); + stream.write(b); + bitPos -= 8; + } + + bits &= bitmaskFor(bitPos); + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index 9697c157..93eac986 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -46,6 +46,7 @@ import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; import java.awt.image.*; import java.io.*; import java.nio.ByteBuffer; @@ -177,6 +178,12 @@ public final class TIFFImageWriter extends ImageWriterBase { } else { entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents)); + + // TODO: What is the default TIFF color space? + ColorSpace colorSpace = colorModel.getColorSpace(); + if (colorSpace instanceof ICC_ColorSpace) { + entries.add(new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData())); + } } if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) { @@ -340,11 +347,11 @@ public final class TIFFImageWriter extends ImageWriterBase { return new DataOutputStream(stream); case TIFFExtension.COMPRESSION_LZW: -// stream = IIOUtil.createStreamAdapter(imageOutput); -// stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8)); -// stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); -// -// return new DataOutputStream(stream); + stream = IIOUtil.createStreamAdapter(imageOutput); + stream = new EncoderStream(stream, new LZWEncoder((image.getTileWidth() * image.getTileHeight() * image.getTile(0, 0).getNumBands() * image.getColorModel().getComponentSize(0) + 7) / 8)); + stream = new HorizontalDifferencingStream(stream, image.getTileWidth(), image.getTile(0, 0).getNumBands(), image.getColorModel().getComponentSize(0), imageOutput.getByteOrder()); + + return new DataOutputStream(stream); } throw new IllegalArgumentException(String.format("Unsupported TIFF compression: %d", compression)); @@ -459,6 +466,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } flushBuffer(buffer, stream); + if (stream instanceof DataOutputStream) { DataOutputStream dataOutputStream = (DataOutputStream) stream; dataOutputStream.flush(); @@ -484,6 +492,7 @@ public final class TIFFImageWriter extends ImageWriterBase { } flushBuffer(buffer, stream); + if (stream instanceof DataOutputStream) { DataOutputStream dataOutputStream = (DataOutputStream) stream; dataOutputStream.flush(); diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java index f826ec82..4cb9538a 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoderTest.java @@ -26,15 +26,18 @@ package com.twelvemonkeys.imageio.plugins.tiff;/* * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.io.enc.Decoder; import com.twelvemonkeys.io.enc.DecoderAbstractTestCase; import com.twelvemonkeys.io.enc.DecoderStream; import com.twelvemonkeys.io.enc.Encoder; +import org.junit.Ignore; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import static org.junit.Assert.*; @@ -47,6 +50,8 @@ import static org.junit.Assert.*; */ public class LZWDecoderTest extends DecoderAbstractTestCase { + public static final int SPEED_TEST_ITERATIONS = 1024; + @Test public void testIsOldBitReversedStreamTrue() throws IOException { assertTrue(LZWDecoder.isOldBitReversedStream(getClass().getResourceAsStream("/lzw/lzw-short.bin"))); @@ -78,13 +83,6 @@ public class LZWDecoderTest extends DecoderAbstractTestCase { int data; try { -// long toSkip = 3800; -// while ((toSkip -= expected.skip(toSkip)) > 0) { -// } -// toSkip = 3800; -// while ((toSkip -= actual.skip(toSkip)) > 0) { -// } - while ((data = actual.read()) != -1) { count++; @@ -106,7 +104,28 @@ public class LZWDecoderTest extends DecoderAbstractTestCase { @Override public Encoder createCompatibleEncoder() { - // Don't have an encoder yet + // TODO: Need to know length of data to compress in advance... return null; } + + @Ignore + @Test(timeout = 3000) + public void testSpeed() throws IOException { + byte[] bytes = FileUtil.read(getClass().getResourceAsStream("/lzw/lzw-long.bin")); + + + for (int i = 0; i < SPEED_TEST_ITERATIONS; i++) { + ByteBuffer buffer = ByteBuffer.allocate(1024); + ByteArrayInputStream input = new ByteArrayInputStream(bytes); + LZWDecoder decoder = new LZWDecoder.LZWSpecDecoder(); + + int read, total = 0; + while((read = decoder.decode(input, buffer)) > 0) { + buffer.clear(); + total += read; + } + + assertEquals(49152, total); + } + } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoderTest.java new file mode 100644 index 00000000..ced15e6d --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoderTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2014, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.io.FastByteArrayOutputStream; +import com.twelvemonkeys.io.enc.Decoder; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Random; + +import static org.junit.Assert.assertEquals; + +/** + * LZWEncoderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LZWEncoderTest.java,v 1.0 06.12.13 13:48 haraldk Exp$ + */ +public class LZWEncoderTest { + + static final int SPEED_TEST_RUNS = 1024; + static final int LENGTH = 1024; + static final int ITERATIONS = 4; + + private final Random random = new Random(2451348571893475l); + + @Test + public void testExample() throws IOException { + byte[] bytes = new byte[] {7, 7, 7, 8, 8, 7, 7, 6, 6}; + LZWEncoder encoder = new LZWEncoder(bytes.length); + + OutputStream stream = new FastByteArrayOutputStream(10); + encoder.encode(stream, ByteBuffer.wrap(bytes)); + } + + @Test + public void testExampleEncodeDecode() throws IOException { + byte[] bytes = new byte[] {7, 7, 7, 8, 8, 7, 7, 6, 6}; + LZWEncoder encoder = new LZWEncoder(bytes.length); + + FastByteArrayOutputStream stream = new FastByteArrayOutputStream(10); + encoder.encode(stream, ByteBuffer.wrap(bytes)); + + ByteArrayInputStream inputStream = stream.createInputStream(); + Decoder decoder = LZWDecoder.create(false); + ByteBuffer buffer = ByteBuffer.allocate(bytes.length); + int index = 0; + + while (decoder.decode(inputStream, buffer) > 0) { + buffer.flip(); + + while (buffer.hasRemaining()) { + assertEquals(String.format("Diff at index %s", index), bytes[index], buffer.get()); + index++; + } + + buffer.clear(); + } + + assertEquals(9, index); + assertEquals(-1, inputStream.read()); + } + + @Test + public void testEncodeDecode() throws IOException { + byte[] bytes = new byte[LENGTH]; + LZWEncoder encoder = new LZWEncoder(bytes.length); + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + + FastByteArrayOutputStream stream = new FastByteArrayOutputStream((LENGTH * 3) / 4); + + for (int i = 0; i < ITERATIONS; i++) { + encoder.encode(stream, ByteBuffer.wrap(bytes, i * LENGTH / ITERATIONS, LENGTH / ITERATIONS)); + } + + ByteArrayInputStream inputStream = stream.createInputStream(); + LZWDecoder decoder = new LZWDecoder.LZWSpecDecoder(); // Strict mode + ByteBuffer buffer = ByteBuffer.allocate(LENGTH / ITERATIONS); + + int index = 0; + + for (int i = 0; i < ITERATIONS; i++) { + while (decoder.decode(inputStream, buffer) > 0) { + buffer.flip(); + + while (buffer.hasRemaining()) { + byte expected = bytes[index]; + byte actual = buffer.get(); + assertEquals(String.format("Diff at index %s: 0x%02x != 0x%02x", index, expected, actual), expected, actual); + index++; + } + + buffer.clear(); + } + } + + assertEquals(LENGTH, index); + assertEquals(-1, inputStream.read()); + } + + @Test + public void testEncodeDecodeRandom() throws IOException { + byte[] bytes = new byte[LENGTH]; + LZWEncoder encoder = new LZWEncoder(bytes.length); + + random.nextBytes(bytes); + + FastByteArrayOutputStream stream = new FastByteArrayOutputStream((LENGTH * 3) / 4); + + for (int i = 0; i < ITERATIONS; i++) { + encoder.encode(stream, ByteBuffer.wrap(bytes, i * LENGTH / ITERATIONS, LENGTH / ITERATIONS)); + } + + ByteArrayInputStream inputStream = stream.createInputStream(); + LZWDecoder decoder = new LZWDecoder.LZWSpecDecoder(); // Strict mode + ByteBuffer buffer = ByteBuffer.allocate(LENGTH / ITERATIONS); + + int index = 0; + + for (int i = 0; i < ITERATIONS; i++) { + while (decoder.decode(inputStream, buffer) > 0) { + buffer.flip(); + + while (buffer.hasRemaining()) { + byte expected = bytes[index]; + byte actual = buffer.get(); + assertEquals(String.format("Diff at index %s: 0x%02x != 0x%02x", index, expected, actual), expected, actual); +// System.err.println(String.format("Equal at index %s: 0x%02x (%d)", index, expected & 0xff, expected)); + index++; + } + + buffer.clear(); + } + } + + assertEquals(LENGTH, index); + assertEquals(-1, inputStream.read()); + } + + @Ignore + @Test(timeout = 10000) + public void testSpeed() throws IOException { + for (int run = 0; run < SPEED_TEST_RUNS; run++) { + byte[] bytes = new byte[LENGTH]; + LZWEncoder encoder = new LZWEncoder(bytes.length); + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + + FastByteArrayOutputStream stream = new FastByteArrayOutputStream((LENGTH * 3) / 4); + + for (int i = 0; i < ITERATIONS; i++) { + encoder.encode(stream, ByteBuffer.wrap(bytes, i * LENGTH / ITERATIONS, LENGTH / ITERATIONS)); + } + + assertEquals(719, stream.size()); + } + } +}