Author: damjan Date: Thu Dec 22 12:12:59 2011 New Revision: 1222196 URL: http://svn.apache.org/viewvc?rev=1222196&view=rev Log: Added support for writing all CCITT Group 3 and Group 4 compression types for TIFF, and added exhaustive tests for this.
Fixed some row aligned bugs when writing Modified Huffman TIFFs, fixed parsing with TIFFs that use tiles intead of strips, and generally brought the CCITT compression implementation to production quality standards. Added: commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java (with props) Modified: commons/proper/sanselan/trunk/RELEASE_NOTES commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/BitArrayOutputStream.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/constants/TiffConstants.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReader.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderStrips.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderTiled.java commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java commons/proper/sanselan/trunk/src/site/xdoc/formatsupport.xml Modified: commons/proper/sanselan/trunk/RELEASE_NOTES URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/RELEASE_NOTES?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/RELEASE_NOTES (original) +++ commons/proper/sanselan/trunk/RELEASE_NOTES Thu Dec 22 12:12:59 2011 @@ -42,8 +42,7 @@ Release 0.98 * SANSELAN-46 - allowed all Sanselan tests to pass. * SANSELAN-59 - deleted confusing redefinition of some constants. * Altered TIFF tag searching to do an exact directory match when possible. - * SANSELAN-48 - added support for reading CCITT Modified Huffman, T.4 and T.6 images, - and writing CCITT Modified Huffman images. + * SANSELAN-48 - added support for reading and writing CCITT Modified Huffman, Group 3 and Group 4 images. Release 0.97 ------------ Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/BitArrayOutputStream.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/BitArrayOutputStream.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/BitArrayOutputStream.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/BitArrayOutputStream.java Thu Dec 22 12:12:59 2011 @@ -71,6 +71,14 @@ public class BitArrayOutputStream { } } + public int getBitsAvailableInCurrentByte() { + int count = 0; + for (int mask = cacheMask; mask != 0; mask >>>= 1) { + ++count; + } + return count; + } + private void writeByte(int b) { if (bytesWritten >= buffer.length) { byte[] bigger = new byte[buffer.length * 2]; Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java Thu Dec 22 12:12:59 2011 @@ -18,6 +18,7 @@ package org.apache.commons.sanselan.comm import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.Arrays; import org.apache.commons.sanselan.ImageReadException; import org.apache.commons.sanselan.ImageWriteException; @@ -77,6 +78,30 @@ public class T4AndT6Compression { } } + private static void compress1DLine(BitInputStreamFlexible inputStream, BitArrayOutputStream outputStream, + int[] referenceLine, int width) throws ImageWriteException { + int color = WHITE; + int runLength = 0; + for (int x = 0; x < width; x++) { + try { + int nextColor = inputStream.readBits(1); + if (referenceLine != null) { + referenceLine[x] = nextColor; + } + if (color == nextColor) { + ++runLength; + } else { + writeRunLength(outputStream, runLength, color); + color = nextColor; + runLength = 1; + } + } catch (IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + writeRunLength(outputStream, runLength, color); + } + /** * Compressed with the "Modified Huffman" encoding of section 10 in the TIFF6 specification. * No EOLs, no RTC, rows are padded to end on a byte boundary. @@ -91,23 +116,8 @@ public class T4AndT6Compression { new ByteArrayInputStream(uncompressed)); BitArrayOutputStream outputStream = new BitArrayOutputStream(); for (int y = 0; y < height; y++) { - int color = WHITE; - int runLength = 0; - for (int x = 0; x < width; x++) { - try { - int nextColor = inputStream.readBits(1); - if (color == nextColor) { - ++runLength; - } else { - writeRunLength(outputStream, runLength, color); - color = nextColor; - runLength = 1; - } - } catch (IOException ioException) { - throw new ImageWriteException("Error reading image to compress", ioException); - } - } - writeRunLength(outputStream, runLength, color); + compress1DLine(inputStream, outputStream, null, width); + inputStream.flushCache(); outputStream.flush(); } return outputStream.toByteArray(); @@ -148,6 +158,33 @@ public class T4AndT6Compression { return outputStream.toByteArray(); } + public static byte[] compressT4_1D(byte[] uncompressed, int width, int height, boolean hasFill) throws ImageWriteException { + BitInputStreamFlexible inputStream = new BitInputStreamFlexible( + new ByteArrayInputStream(uncompressed)); + BitArrayOutputStream outputStream = new BitArrayOutputStream(); + if (hasFill) { + T4_T6_Tables.EOL16.writeBits(outputStream); + } else { + T4_T6_Tables.EOL.writeBits(outputStream); + } + for (int y = 0; y < height; y++) { + compress1DLine(inputStream, outputStream, null, width); + if (hasFill) { + int bitsAvailable = outputStream.getBitsAvailableInCurrentByte(); + if (bitsAvailable < 4) { + outputStream.flush(); + bitsAvailable = 8; + } + for (; bitsAvailable > 4; bitsAvailable--) { + outputStream.writeBit(0); + } + } + T4_T6_Tables.EOL.writeBits(outputStream); + inputStream.flushCache(); + } + return outputStream.toByteArray(); + } + /** * Decompresses T.4 1D encoded data. EOL at the beginning and after each row, * can be preceded by fill bits to fit on a byte boundary, no RTC. @@ -166,14 +203,7 @@ public class T4AndT6Compression { int rowLength; try { entry = (T4_T6_Tables.Entry) controlCodes.decode(inputStream); - if (entry != T4_T6_Tables.EOL && - entry != T4_T6_Tables.EOL13 && - entry != T4_T6_Tables.EOL14 && - entry != T4_T6_Tables.EOL15 && - entry != T4_T6_Tables.EOL16 && - entry != T4_T6_Tables.EOL17 && - entry != T4_T6_Tables.EOL18 && - entry != T4_T6_Tables.EOL19) { + if (!isEOL(entry, hasFill)) { throw new ImageReadException("Expected EOL not found"); } int color = WHITE; @@ -198,6 +228,108 @@ public class T4AndT6Compression { return outputStream.toByteArray(); } + public static byte[] compressT4_2D(byte[] uncompressed, int width, int height, boolean hasFill, int parameterK) throws ImageWriteException { + BitInputStreamFlexible inputStream = new BitInputStreamFlexible( + new ByteArrayInputStream(uncompressed)); + BitArrayOutputStream outputStream = new BitArrayOutputStream(); + int[] referenceLine = new int[width]; + int[] codingLine = new int[width]; + int kCounter = 0; + if (hasFill) { + T4_T6_Tables.EOL16.writeBits(outputStream); + } else { + T4_T6_Tables.EOL.writeBits(outputStream); + } + for (int y = 0; y < height; y++) { + if (kCounter > 0) { + // 2D + outputStream.writeBit(0); + for (int i = 0; i < width; i++) { + try { + codingLine[i] = inputStream.readBits(1); + } catch (IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int a1 = nextChangingElement(codingLine, codingA0Color, 0); + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width; ) { + if (b2 < a1) { + T4_T6_Tables.P.writeBits(outputStream); + a0 = b2; + } else { + int a1b1 = a1 - b1; + if (-3 <= a1b1 && a1b1 <= 3) { + T4_T6_Tables.Entry entry; + if (a1b1 == -3) { + entry = T4_T6_Tables.VL3; + } else if (a1b1 == -2) { + entry = T4_T6_Tables.VL2; + } else if (a1b1 == -1) { + entry = T4_T6_Tables.VL1; + } else if (a1b1 == 0) { + entry = T4_T6_Tables.V0; + } else if (a1b1 == 1) { + entry = T4_T6_Tables.VR1; + } else if (a1b1 == 2) { + entry = T4_T6_Tables.VR2; + } else { + entry = T4_T6_Tables.VR3; + } + entry.writeBits(outputStream); + codingA0Color = 1 - codingA0Color; + a0 = a1; + } else { + int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1); + int a0a1 = a1 - a0; + int a1a2 = a2 - a1; + T4_T6_Tables.H.writeBits(outputStream); + writeRunLength(outputStream, a0a1, codingA0Color); + writeRunLength(outputStream, a1a2, 1 - codingA0Color); + a0 = a2; + } + } + referenceA0Color = changingElementAt(referenceLine, a0); + a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + } + int[] swap = referenceLine; + referenceLine = codingLine; + codingLine = swap; + } else { + // 1D + outputStream.writeBit(1); + compress1DLine(inputStream, outputStream, referenceLine, width); + } + if (hasFill) { + int bitsAvailable = outputStream.getBitsAvailableInCurrentByte(); + if (bitsAvailable < 4) { + outputStream.flush(); + bitsAvailable = 8; + } + for (; bitsAvailable > 4; bitsAvailable--) { + outputStream.writeBit(0); + } + } + T4_T6_Tables.EOL.writeBits(outputStream); + kCounter++; + if (kCounter == parameterK) { + kCounter = 0; + } + inputStream.flushCache(); + } + return outputStream.toByteArray(); + } + /** * Decompressed T.4 2D encoded data. EOL at the beginning and after each row, * can be preceded by fill bits to fit on a byte boundary, and is succeeded @@ -219,14 +351,7 @@ public class T4AndT6Compression { int rowLength = 0; try { entry = (T4_T6_Tables.Entry) controlCodes.decode(inputStream); - if (entry != T4_T6_Tables.EOL && - entry != T4_T6_Tables.EOL13 && - entry != T4_T6_Tables.EOL14 && - entry != T4_T6_Tables.EOL15 && - entry != T4_T6_Tables.EOL16 && - entry != T4_T6_Tables.EOL17 && - entry != T4_T6_Tables.EOL18 && - entry != T4_T6_Tables.EOL19) { + if (!isEOL(entry, hasFill)) { throw new ImageReadException("Expected EOL not found"); } int tagBit = inputStream.readBits(1); @@ -312,6 +437,82 @@ public class T4AndT6Compression { return outputStream.toByteArray(); } + public static byte[] compressT6(byte[] uncompressed, int width, int height) throws ImageWriteException { + BitInputStreamFlexible inputStream = new BitInputStreamFlexible( + new ByteArrayInputStream(uncompressed)); + BitArrayOutputStream outputStream = new BitArrayOutputStream(); + int[] referenceLine = new int[width]; + int[] codingLine = new int[width]; + for (int y = 0; y < height; y++) { + for (int i = 0; i < width; i++) { + try { + codingLine[i] = inputStream.readBits(1); + } catch (IOException ioException) { + throw new ImageWriteException("Error reading image to compress", ioException); + } + } + int codingA0Color = WHITE; + int referenceA0Color = WHITE; + int a1 = nextChangingElement(codingLine, codingA0Color, 0); + int b1 = nextChangingElement(referenceLine, referenceA0Color, 0); + int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + for (int a0 = 0; a0 < width; ) { + if (b2 < a1) { + T4_T6_Tables.P.writeBits(outputStream); + a0 = b2; + } else { + int a1b1 = a1 - b1; + if (-3 <= a1b1 && a1b1 <= 3) { + T4_T6_Tables.Entry entry; + if (a1b1 == -3) { + entry = T4_T6_Tables.VL3; + } else if (a1b1 == -2) { + entry = T4_T6_Tables.VL2; + } else if (a1b1 == -1) { + entry = T4_T6_Tables.VL1; + } else if (a1b1 == 0) { + entry = T4_T6_Tables.V0; + } else if (a1b1 == 1) { + entry = T4_T6_Tables.VR1; + } else if (a1b1 == 2) { + entry = T4_T6_Tables.VR2; + } else { + entry = T4_T6_Tables.VR3; + } + entry.writeBits(outputStream); + codingA0Color = 1 - codingA0Color; + a0 = a1; + } else { + int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1); + int a0a1 = a1 - a0; + int a1a2 = a2 - a1; + T4_T6_Tables.H.writeBits(outputStream); + writeRunLength(outputStream, a0a1, codingA0Color); + writeRunLength(outputStream, a1a2, 1 - codingA0Color); + a0 = a2; + } + } + referenceA0Color = changingElementAt(referenceLine, a0); + a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1); + if (codingA0Color == referenceA0Color) { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + } else { + b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1); + b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1); + } + b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1); + } + int[] swap = referenceLine; + referenceLine = codingLine; + codingLine = swap; + inputStream.flushCache(); + } + // EOFB + T4_T6_Tables.EOL.writeBits(outputStream); + T4_T6_Tables.EOL.writeBits(outputStream); + return outputStream.toByteArray(); + } + /** * Decompress T.6 encoded data. No EOLs, except for 2 consecutive ones at the end * (the EOFB, end of fax block). No RTC. No fill bits anywhere. All data is 2D encoded. @@ -395,6 +596,23 @@ public class T4AndT6Compression { return outputStream.toByteArray(); } + private static boolean isEOL(T4_T6_Tables.Entry entry, boolean hasFill) { + if (entry == T4_T6_Tables.EOL) { + return true; + } + if (hasFill) { + return entry == T4_T6_Tables.EOL13 || + entry == T4_T6_Tables.EOL14 || + entry == T4_T6_Tables.EOL15 || + entry == T4_T6_Tables.EOL16 || + entry == T4_T6_Tables.EOL17 || + entry == T4_T6_Tables.EOL18 || + entry == T4_T6_Tables.EOL19; + } else { + return false; + } + } + private static void writeRunLength(BitArrayOutputStream bitStream, int runLength, int color) { final T4_T6_Tables.Entry[] makeUpCodes; final T4_T6_Tables.Entry[] terminatingCodes; Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java Thu Dec 22 12:12:59 2011 @@ -254,7 +254,6 @@ class T4_T6_Tables { public static final Entry EOL14 = new Entry("00000000000001", Integer.valueOf(0)); public static final Entry EOL15 = new Entry("000000000000001", Integer.valueOf(0)); public static final Entry EOL16 = new Entry("0000000000000001", Integer.valueOf(0)); - // FIXME: why are there EOLs with more than 16 bits? public static final Entry EOL17 = new Entry("00000000000000001", Integer.valueOf(0)); public static final Entry EOL18 = new Entry("000000000000000001", Integer.valueOf(0)); public static final Entry EOL19 = new Entry("0000000000000000001", Integer.valueOf(0)); Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/constants/TiffConstants.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/constants/TiffConstants.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/constants/TiffConstants.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/constants/TiffConstants.java Thu Dec 22 12:12:59 2011 @@ -45,4 +45,27 @@ public interface TiffConstants public static final int TIFF_COMPRESSION_UNCOMPRESSED_2 = 32771; public static final int TIFF_COMPRESSION_PACKBITS = 32773; + /** + * Parameter key. Used in write operations to indicate the desired + * T.4 options to use when using TIFF_COMPRESSION_CCITT_GROUP_3. + * <p> + * Valid values: any Integer containing a mixture of the + * TIFF_FLAG_T4_OPTIONS_2D, TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE, + * and TIFF_FLAG_T4_OPTIONS_FILL flags. + */ + public static final String PARAM_KEY_T4_OPTIONS = "T4_OPTIONS"; + + /** + * Parameter key. Used in write operations to indicate the desired + * T.6 options to use when using TIFF_COMPRESSION_CCITT_GROUP_4. + * <p> + * Valid values: any Integer containing either zero or + * TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE. + */ + public static final String PARAM_KEY_T6_OPTIONS = "T6_OPTIONS"; + + public static final int TIFF_FLAG_T4_OPTIONS_2D = 1; + public static final int TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE = 2; + public static final int TIFF_FLAG_T4_OPTIONS_FILL = 4; + public static final int TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE = 2; } \ No newline at end of file Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReader.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReader.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReader.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReader.java Thu Dec 22 12:12:59 2011 @@ -107,14 +107,14 @@ public abstract class DataReader impleme private int count = 0; protected byte[] decompress(byte compressed[], int compression, - int expected_size) throws ImageReadException, IOException + int expected_size, int tileWidth, int tileHeight) throws ImageReadException, IOException { switch (compression) { case TIFF_COMPRESSION_UNCOMPRESSED : // None; return compressed; case TIFF_COMPRESSION_CCITT_1D : // CCITT Group 3 1-Dimensional Modified Huffman run-length encoding. - return T4AndT6Compression.decompressModifiedHuffman(compressed, width, height); + return T4AndT6Compression.decompressModifiedHuffman(compressed, tileWidth, tileHeight); case TIFF_COMPRESSION_CCITT_GROUP_3 : { int t4Options = 0; @@ -122,16 +122,16 @@ public abstract class DataReader impleme if (field != null) { t4Options = field.getIntValue(); } - boolean is2D = (t4Options & 1) != 0; - boolean usesUncompressedMode = (t4Options & 2) != 0; + boolean is2D = (t4Options & TIFF_FLAG_T4_OPTIONS_2D) != 0; + boolean usesUncompressedMode = (t4Options & TIFF_FLAG_T4_OPTIONS_UNCOMPRESSED_MODE) != 0; if (usesUncompressedMode) { throw new ImageReadException("T.4 compression with the uncompressed mode extension is not yet supported"); } - boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0; + boolean hasFillBitsBeforeEOL = (t4Options & TIFF_FLAG_T4_OPTIONS_FILL) != 0; if (is2D) { - return T4AndT6Compression.decompressT4_2D(compressed, width, height, hasFillBitsBeforeEOL); + return T4AndT6Compression.decompressT4_2D(compressed, tileWidth, tileHeight, hasFillBitsBeforeEOL); } else { - return T4AndT6Compression.decompressT4_1D(compressed, width, height, hasFillBitsBeforeEOL); + return T4AndT6Compression.decompressT4_1D(compressed, tileWidth, tileHeight, hasFillBitsBeforeEOL); } } case TIFF_COMPRESSION_CCITT_GROUP_4 : @@ -141,11 +141,11 @@ public abstract class DataReader impleme if (field != null) { t6Options = field.getIntValue(); } - boolean usesUncompressedMode = (t6Options & 2) != 0; + boolean usesUncompressedMode = (t6Options & TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0; if (usesUncompressedMode) { throw new ImageReadException("T.6 compression with the uncompressed mode extension is not yet supported"); } - return T4AndT6Compression.decompressT6(compressed, width, height); + return T4AndT6Compression.decompressT6(compressed, tileWidth, tileHeight); } case TIFF_COMPRESSION_LZW : // LZW { Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderStrips.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderStrips.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderStrips.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderStrips.java Thu Dec 22 12:12:59 2011 @@ -93,7 +93,7 @@ public final class DataReaderStrips exte byte compressed[] = imageData.strips[strip].data; byte decompressed[] = decompress(compressed, compression, - bytesPerStrip); + bytesPerStrip, width, rowsInThisStrip); interpretStrip(bi, decompressed, pixelsPerStrip); Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderTiled.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderTiled.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderTiled.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/datareaders/DataReaderTiled.java Thu Dec 22 12:12:59 2011 @@ -106,7 +106,7 @@ public final class DataReaderTiled exten byte compressed[] = imageData.tiles[tile].data; byte decompressed[] = decompress(compressed, compression, - bytesPerTile); + bytesPerTile, tileWidth, tileLength); interpretTile(bi, decompressed, x, y); Modified: commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java (original) +++ commons/proper/sanselan/trunk/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java Thu Dec 22 12:12:59 2011 @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import org.apache.commons.sanselan.ImageReadException; import org.apache.commons.sanselan.ImageWriteException; import org.apache.commons.sanselan.common.BinaryConstants; import org.apache.commons.sanselan.common.BinaryOutputStream; @@ -263,7 +264,7 @@ public abstract class TiffImageWriterBas // clear format key. if (params.containsKey(PARAM_KEY_FORMAT)) params.remove(PARAM_KEY_FORMAT); - + String xmpXml = null; if (params.containsKey(PARAM_KEY_XMP_XML)) { @@ -294,6 +295,9 @@ public abstract class TiffImageWriterBas } params.remove(PARAM_KEY_COMPRESSION); } + HashMap rawParams = new HashMap(params); + params.remove(PARAM_KEY_T4_OPTIONS); + params.remove(PARAM_KEY_T6_OPTIONS); if (params.size() > 0) { Object firstKey = params.keySet().iterator().next(); @@ -303,7 +307,8 @@ public abstract class TiffImageWriterBas int samplesPerPixel; int bitsPerSample; int photometricInterpretation; - if (compression == TIFF_COMPRESSION_CCITT_1D) { + if (compression == TIFF_COMPRESSION_CCITT_1D || compression == TIFF_COMPRESSION_CCITT_GROUP_3 || + compression == TIFF_COMPRESSION_CCITT_GROUP_4) { samplesPerPixel = 1; bitsPerSample = 1; photometricInterpretation = 0; @@ -326,11 +331,48 @@ public abstract class TiffImageWriterBas // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel); // System.out.println("stripCount: " + stripCount); + int t4Options = 0; + int t6Options = 0; if (compression == TIFF_COMPRESSION_CCITT_1D) { for (int i = 0; i < strips.length; i++) strips[i] = T4AndT6Compression.compressModifiedHuffman(strips[i], width, strips[i].length / ((width + 7) / 8)); + } else if (compression == TIFF_COMPRESSION_CCITT_GROUP_3) { + Integer t4Parameter = (Integer) rawParams.get(PARAM_KEY_T4_OPTIONS); + if (t4Parameter != null) { + t4Options = t4Parameter.intValue(); + } + t4Options &= 0x7; + boolean is2D = (t4Options & 1) != 0; + boolean usesUncompressedMode = (t4Options & 2) != 0; + if (usesUncompressedMode) { + throw new ImageWriteException("T.4 compression with the uncompressed mode extension is not yet supported"); + } + boolean hasFillBitsBeforeEOL = (t4Options & 4) != 0; + for (int i = 0; i < strips.length; i++) { + if (is2D) { + strips[i] = T4AndT6Compression.compressT4_2D(strips[i], width, + strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL, rowsPerStrip); + } else { + strips[i] = T4AndT6Compression.compressT4_1D(strips[i], width, + strips[i].length / ((width + 7) / 8), hasFillBitsBeforeEOL); + } + } + } else if (compression == TIFF_COMPRESSION_CCITT_GROUP_4) + { + Integer t6Parameter = (Integer) rawParams.get(PARAM_KEY_T6_OPTIONS); + if (t6Parameter != null) { + t6Options = t6Parameter.intValue(); + } + t6Options &= 0x4; + boolean usesUncompressedMode = (t6Options & TIFF_FLAG_T6_OPTIONS_UNCOMPRESSED_MODE) != 0; + if (usesUncompressedMode) { + throw new ImageWriteException("T.6 compression with the uncompressed mode extension is not yet supported"); + } + for (int i = 0; i < strips.length; i++) + strips[i] = T4AndT6Compression.compressT6(strips[i], width, + strips[i].length / ((width + 7) / 8)); } else if (compression == TIFF_COMPRESSION_PACKBITS) { for (int i = 0; i < strips.length; i++) @@ -354,7 +396,7 @@ public abstract class TiffImageWriterBas // do nothing. } else throw new ImageWriteException( - "Invalid compression parameter (Only CCITT 1D, LZW, Packbits and uncompressed supported)."); + "Invalid compression parameter (Only CCITT 1D/Group 3/Group 4, LZW, Packbits and uncompressed supported)."); TiffElement.DataElement imageData[] = new TiffElement.DataElement[strips.length]; for (int i = 0; i < strips.length; i++) @@ -464,6 +506,22 @@ public abstract class TiffImageWriterBas .writeData(yResolution.intValue(), 1, byteOrder)); directory.add(field); } + + if (t4Options != 0) { + TiffOutputField field = new TiffOutputField( + TIFF_TAG_T4_OPTIONS, FIELD_TYPE_LONG, 1, + FIELD_TYPE_LONG + .writeData(Integer.valueOf(t4Options), byteOrder)); + directory.add(field); + } + if (t6Options != 0) { + TiffOutputField field = new TiffOutputField( + TIFF_TAG_T6_OPTIONS, FIELD_TYPE_LONG, 1, + FIELD_TYPE_LONG + .writeData(Integer.valueOf(t6Options), byteOrder)); + directory.add(field); + } + if (null != xmpXml) { Modified: commons/proper/sanselan/trunk/src/site/xdoc/formatsupport.xml URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/site/xdoc/formatsupport.xml?rev=1222196&r1=1222195&r2=1222196&view=diff ============================================================================== --- commons/proper/sanselan/trunk/src/site/xdoc/formatsupport.xml (original) +++ commons/proper/sanselan/trunk/src/site/xdoc/formatsupport.xml Thu Dec 22 12:12:59 2011 @@ -160,10 +160,9 @@ limitations under the License. Supported through version 6.0. TIFFs is a open-ended container format, so it's not possible to support every possibly variation. Supports Bi-Level, Palette/Indexed, RGB, CMYK, YCbCr, CIELab and LOGLUV images. - Supports reading LZW, CCITT Modified Huffman/T.4/T.6 and Packbits/RLE compression, - and writing LZW, CCITT Modified Huffman, and Packbits/RLE compression. - Notably missing other forms of compression though, including JPEG. - Supports Tiled images. + Supports reading and writing LZW, CCITT Modified Huffman/Group 3/Group 4, + and Packbits/RLE compression. Notably missing other forms of compression though, + including JPEG. Supports reading Tiled images. </td> <td> <a href="http://partners.adobe.com/public/developer/tiff/index.html">Adobe</a> Added: commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java URL: http://svn.apache.org/viewvc/commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java?rev=1222196&view=auto ============================================================================== --- commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java (added) +++ commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java Thu Dec 22 12:12:59 2011 @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.sanselan.formats.tiff; + +import java.awt.image.BufferedImage; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; + +import org.apache.commons.sanselan.ImageFormat; +import org.apache.commons.sanselan.ImageReadException; +import org.apache.commons.sanselan.ImageWriteException; +import org.apache.commons.sanselan.Sanselan; +import org.apache.commons.sanselan.SanselanConstants; +import org.apache.commons.sanselan.common.itu_t4.T4AndT6Compression; +import org.apache.commons.sanselan.formats.tiff.constants.TiffConstants; +import org.apache.commons.sanselan.util.Debug; + +public class TiffCcittTest extends TiffBaseTest { + public void testAll5x2Compressions() { + byte[] uncompressed = new byte[2]; + int[] combinations = new int[10]; + do { + for (int x = 0; x < 5; x++) { + if (combinations[x] != 0) { + uncompressed[0] |= (0x80 >>> x); + } + } + for (int x = 0; x < 5; x++) { + if (combinations[5 + x] != 0) { + uncompressed[1] |= (0x80 >>> x); + } + } + + try { + byte[] compressed = T4AndT6Compression.compressModifiedHuffman(uncompressed, 5, 2); + byte[] result = T4AndT6Compression.decompressModifiedHuffman(compressed, 5, 2); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + byte[] compressed = T4AndT6Compression.compressT4_1D(uncompressed, 5, 2, true); + byte[] result = T4AndT6Compression.decompressT4_1D(compressed, 5, 2, true); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + byte[] compressed = T4AndT6Compression.compressT4_1D(uncompressed, 5, 2, false); + byte[] result = T4AndT6Compression.decompressT4_1D(compressed, 5, 2, false); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + byte[] compressed = T4AndT6Compression.compressT4_2D(uncompressed, 5, 2, true, 2); + byte[] result = T4AndT6Compression.decompressT4_2D(compressed, 5, 2, true); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + byte[] compressed = T4AndT6Compression.compressT4_2D(uncompressed, 5, 2, false, 2); + byte[] result = T4AndT6Compression.decompressT4_2D(compressed, 5, 2, false); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + byte[] compressed = T4AndT6Compression.compressT6(uncompressed, 5, 2); + byte[] result = T4AndT6Compression.decompressT6(compressed, 5, 2); + assertEquals(uncompressed.length, result.length); + for (int i = 0; i < uncompressed.length; i++) { + assertEquals(uncompressed[i], result[i]); + } + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } + } while (nextCombination(combinations, 1)); + } + + public void testAll5x2Images() { + int[] combinations = new int[10]; + BufferedImage image = new BufferedImage(5, 2, BufferedImage.TYPE_INT_RGB); + do { + for (int x = 0; x < 5; x++) { + if (combinations[x] == 0) { + image.setRGB(x, 0, 0xFFFFFF); + } else { + image.setRGB(x, 0, 0); + } + } + for (int x = 0; x < 5; x++) { + if (combinations[5 + x] == 0) { + image.setRGB(x, 1, 0xFFFFFF); + } else { + image.setRGB(x, 1, 0); + } + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_1D); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3); + params.put(TiffConstants.PARAM_KEY_T4_OPTIONS, 0); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3); + params.put(TiffConstants.PARAM_KEY_T4_OPTIONS, 4); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + FileOutputStream fos = new FileOutputStream("/tmp/test.tiff"); + fos.write(compressed); + fos.close(); + + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3); + params.put(TiffConstants.PARAM_KEY_T4_OPTIONS, 1); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_3); + params.put(TiffConstants.PARAM_KEY_T4_OPTIONS, 5); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + + try { + HashMap params = new HashMap(); + params.put(SanselanConstants.PARAM_KEY_COMPRESSION, TiffConstants.TIFF_COMPRESSION_CCITT_GROUP_4); + byte[] compressed = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_TIFF, params); + BufferedImage result = Sanselan.getBufferedImage(compressed); + compareImages(image, result); + } catch (ImageWriteException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (ImageReadException ex) { + Debug.debug(ex); + assertFalse(true); + } catch (IOException ex) { + Debug.debug(ex); + assertFalse(true); + } + } while (nextCombination(combinations, 1)); + } + + /** + * Generates the next combination of elements in the sequence array, + * with each element having a maximum value of max. + * Initially, the sequence should be set to minimum values + * of each element. + * @param sequence the array of elements to update + * @param max the maximum value of each element in the sequence + * @return false if there is no more combinations (ie. nothing was done), true otherwise + */ + private static boolean nextCombination(int[] sequence, int max) { + int i; + for (i = 0; i < sequence.length; i++) { + if (sequence[i] == max) { + sequence[i] = 0; + } else { + sequence[i]++; + break; + } + } + return i < sequence.length; + } + + private void compareImages(BufferedImage a, BufferedImage b) + { + assertEquals(a.getWidth(), b.getWidth()); + assertEquals(a.getHeight(), b.getHeight()); + + for (int x = 0; x < a.getWidth(); x++) + for (int y = 0; y < a.getHeight(); y++) + { + int a_argb = a.getRGB(x, y); + int b_argb = b.getRGB(x, y); + if (a_argb != b_argb) + { + Debug.debug("width", a.getWidth()); + Debug.debug("height", a.getHeight()); + Debug.debug("x", x); + Debug.debug("y", y); + Debug.debug("a_argb", a_argb + " (0x" + + Integer.toHexString(a_argb) + ")"); + Debug.debug("b_argb", b_argb + " (0x" + + Integer.toHexString(b_argb) + ")"); + } + assertEquals(a_argb, b_argb); + } + } +} Propchange: commons/proper/sanselan/trunk/src/test/java/org/apache/commons/sanselan/formats/tiff/TiffCcittTest.java ------------------------------------------------------------------------------ svn:eol-style = native