Added support for writing CCITT Modified Huffman compressed TIFF files,
and cleaned up TiffImageWriterBase a little.



git-svn-id: https://svn.apache.org/repos/asf/commons/proper/sanselan/trunk@1221614 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 6c61d1a..3cf118b 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -42,6 +42,8 @@
  * 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.
 
 Release 0.97
 ------------
diff --git a/src/main/java/org/apache/commons/sanselan/SanselanConstants.java b/src/main/java/org/apache/commons/sanselan/SanselanConstants.java
index 06ef32d..0c808a3 100644
--- a/src/main/java/org/apache/commons/sanselan/SanselanConstants.java
+++ b/src/main/java/org/apache/commons/sanselan/SanselanConstants.java
@@ -61,6 +61,7 @@
      * Currently only applies to writing TIFF image files.
      * <p>
      * Valid values: TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED,
+     * TiffConstants.TIFF_COMPRESSION_CCITT_1D,
      * TiffConstants.TIFF_COMPRESSION_LZW,
      * TiffConstants.TIFF_COMPRESSION_PACKBITS.
      * <p>
diff --git a/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java b/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java
index 6d01968..4d649ab 100644
--- a/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java
+++ b/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4AndT6Compression.java
@@ -20,6 +20,7 @@
 import java.io.IOException;
 
 import org.apache.commons.sanselan.ImageReadException;
+import org.apache.commons.sanselan.ImageWriteException;
 import org.apache.commons.sanselan.common.BitArrayOutputStream;
 import org.apache.commons.sanselan.common.BitInputStreamFlexible;
 
@@ -77,6 +78,42 @@
     }
     
     /**
+     * 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.
+     * @param uncompressed
+     * @param width
+     * @param height
+     * @return the compressed data
+     * @throws ImageReadException
+     */
+    public static byte[] compressModifiedHuffman(byte[] uncompressed, int width, int height) throws ImageWriteException {
+        BitInputStreamFlexible inputStream = new BitInputStreamFlexible(
+                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);
+            outputStream.flush();
+        }
+        return outputStream.toByteArray();
+    }
+    
+    /**
      * Decompresses the "Modified Huffman" encoding of section 10 in the TIFF6 specification.
      * No EOLs, no RTC, rows are padded to end on a byte boundary.
      * @param compressed
@@ -358,6 +395,47 @@
         return outputStream.toByteArray();
     }
     
+    private static void writeRunLength(BitArrayOutputStream bitStream, int runLength, int color) {
+        final T4_T6_Tables.Entry[] makeUpCodes;
+        final T4_T6_Tables.Entry[] terminatingCodes;
+        if (color == WHITE) {
+            makeUpCodes = T4_T6_Tables.whiteMakeUpCodes;
+            terminatingCodes = T4_T6_Tables.whiteTerminatingCodes;
+        } else {
+            makeUpCodes = T4_T6_Tables.blackMakeUpCodes;
+            terminatingCodes = T4_T6_Tables.blackTerminatingCodes;
+        }
+        while (runLength >= 1792) {
+            T4_T6_Tables.Entry entry = lowerBound(T4_T6_Tables.additionalMakeUpCodes, runLength);
+            entry.writeBits(bitStream);
+            runLength -= entry.value.intValue();
+        }
+        while (runLength >= 64) {
+            T4_T6_Tables.Entry entry = lowerBound(makeUpCodes, runLength);
+            entry.writeBits(bitStream);
+            runLength -= entry.value.intValue();
+        }
+        T4_T6_Tables.Entry terminatingEntry = terminatingCodes[runLength];
+        terminatingEntry.writeBits(bitStream);
+    }
+    
+    private static T4_T6_Tables.Entry lowerBound(T4_T6_Tables.Entry[] entries, int value) {
+        int first = 0;
+        int last = entries.length - 1;
+        do {
+            int middle = (first + last) / 2;
+            if (entries[middle].value.intValue() <= value &&
+                    ((middle + 1) >= entries.length || value < entries[middle + 1].value.intValue())) {
+                return entries[middle];
+            } else if (entries[middle].value.intValue() > value) {
+                last = middle - 1;
+            } else {
+                first = middle + 1;
+            }
+        } while (first < last);
+        return entries[first];
+    }
+    
     private static int readTotalRunLength(BitInputStreamFlexible bitStream, int color) throws ImageReadException {
         try {
             int totalLength = 0;
diff --git a/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java b/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java
index 323afd1..a6d9d16 100644
--- a/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java
+++ b/src/main/java/org/apache/commons/sanselan/common/itu_t4/T4_T6_Tables.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.sanselan.common.itu_t4;
 
+import org.apache.commons.sanselan.common.BitArrayOutputStream;
+
 class T4_T6_Tables {
     public static class Entry {
         String bitString;
@@ -25,6 +27,16 @@
             this.bitString = bitString;
             this.value = value;
         }
+        
+        public void writeBits(BitArrayOutputStream outputStream) {
+            for (int i = 0; i < bitString.length(); i++) {
+                if (bitString.charAt(i) == '0') {
+                    outputStream.writeBit(0);
+                } else {
+                    outputStream.writeBit(1);
+                }
+            }
+        }
     }
     
     public static final Entry[] whiteTerminatingCodes = {
diff --git a/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java b/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java
index c4c9c5a..fa89e2a 100644
--- a/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java
+++ b/src/main/java/org/apache/commons/sanselan/formats/tiff/write/TiffImageWriterBase.java
@@ -30,6 +30,7 @@
 import org.apache.commons.sanselan.common.BinaryConstants;
 import org.apache.commons.sanselan.common.BinaryOutputStream;
 import org.apache.commons.sanselan.common.PackBits;
+import org.apache.commons.sanselan.common.itu_t4.T4AndT6Compression;
 import org.apache.commons.sanselan.common.mylzw.MyLzwCompressor;
 import org.apache.commons.sanselan.formats.tiff.TiffElement;
 import org.apache.commons.sanselan.formats.tiff.TiffImageData;
@@ -256,14 +257,6 @@
     public void writeImage(BufferedImage src, OutputStream os, Map params)
             throws ImageWriteException, IOException
     {
-        // writeImageNew(src, os, params);
-        // }
-        //
-        // public void writeImageNew(BufferedImage src, OutputStream os, Map
-        // params)
-        // throws ImageWriteException, IOException
-        // {
-
         // make copy of params; we'll clear keys as we consume them.
         params = new HashMap(params);
 
@@ -281,15 +274,6 @@
         int width = src.getWidth();
         int height = src.getHeight();
 
-        // BinaryOutputStream bos = new BinaryOutputStream(os,
-        // WRITE_BYTE_ORDER);
-        //
-        // writeImageFileHeader(bos, WRITE_BYTE_ORDER);
-
-        // List directoryFields = new ArrayList();
-
-        final int photometricInterpretation = 2; // TODO:
-
         int compression = TIFF_COMPRESSION_LZW; // LZW is default
         if (params.containsKey(PARAM_KEY_COMPRESSION))
         {
@@ -303,33 +287,44 @@
             }
             params.remove(PARAM_KEY_COMPRESSION);
         }
-
-        final int samplesPerPixel = 3; // TODO:
-        final int bitsPerSample = 8; // TODO:
-
-        // int fRowsPerStrip; // TODO:
-        int rowsPerStrip = 8000 / (width * samplesPerPixel); // TODO:
-        rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one.
-
-        byte strips[][] = getStrips(src, samplesPerPixel, bitsPerSample,
-                rowsPerStrip);
-
-        // int stripCount = (height + fRowsPerStrip - 1) / fRowsPerStrip;
-        // int stripCount = strips.length;
-
         if (params.size() > 0)
         {
             Object firstKey = params.keySet().iterator().next();
             throw new ImageWriteException("Unknown parameter: " + firstKey);
         }
 
+        int samplesPerPixel;
+        int bitsPerSample;
+        int photometricInterpretation;
+        if (compression == TIFF_COMPRESSION_CCITT_1D) {
+            samplesPerPixel = 1;
+            bitsPerSample = 1;
+            photometricInterpretation = 0;
+        } else {
+            samplesPerPixel = 3;
+            bitsPerSample = 8;
+            photometricInterpretation = 2;
+        }
+
+
+        int rowsPerStrip = 64000 / (width * bitsPerSample * samplesPerPixel); // TODO:
+        rowsPerStrip = Math.max(1, rowsPerStrip); // must have at least one.
+
+        byte strips[][] = getStrips(src, samplesPerPixel, bitsPerSample,
+                rowsPerStrip);
+
         // System.out.println("width: " + width);
         // System.out.println("height: " + height);
         // System.out.println("fRowsPerStrip: " + fRowsPerStrip);
         // System.out.println("fSamplesPerPixel: " + fSamplesPerPixel);
         // System.out.println("stripCount: " + stripCount);
 
-        if (compression == TIFF_COMPRESSION_PACKBITS)
+        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_PACKBITS)
         {
             for (int i = 0; i < strips.length; i++)
                 strips[i] = new PackBits().compress(strips[i]);
@@ -352,19 +347,13 @@
             // do nothing.
         } else
             throw new ImageWriteException(
-                    "Invalid compression parameter (Only LZW, Packbits and uncompressed supported).");
+                    "Invalid compression parameter (Only CCITT 1D, LZW, Packbits and uncompressed supported).");
 
         TiffElement.DataElement imageData[] = new TiffElement.DataElement[strips.length];
         for (int i = 0; i < strips.length; i++)
             imageData[i] = new TiffImageData.Data(0, strips[i].length,
                     strips[i]);
 
-        // int stripOffsets[] = new int[stripCount];
-        // int stripByteCounts[] = new int[stripCount];
-        //
-        // for (int i = 0; i < strips.length; i++)
-        // stripByteCounts[i] = strips[i].length;
-
         TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
         TiffOutputDirectory directory = outputSet.addRootDirectory();
 
@@ -407,12 +396,21 @@
                                 new int[] { samplesPerPixel, }, byteOrder));
                 directory.add(field);
             }
+            
+            if (samplesPerPixel == 3)
             {
                 TiffOutputField field = new TiffOutputField(
                         TIFF_TAG_BITS_PER_SAMPLE, FIELD_TYPE_SHORT, 3,
                         FIELD_TYPE_SHORT.writeData(new int[] { bitsPerSample,
                                 bitsPerSample, bitsPerSample, }, byteOrder));
                 directory.add(field);
+            } else if (samplesPerPixel == 1)
+            {
+                TiffOutputField field = new TiffOutputField(
+                        TIFF_TAG_BITS_PER_SAMPLE, FIELD_TYPE_SHORT, 1,
+                        FIELD_TYPE_SHORT.writeData(
+                                new int[] { bitsPerSample, }, byteOrder));
+                directory.add(field);
             }
             // {
             // stripOffsetsField = new WriteField(TIFF_TAG_STRIP_OFFSETS,
@@ -499,9 +497,9 @@
                 int rowsInStrip = Math.min(rowsPerStrip, remaining_rows);
                 remaining_rows -= rowsInStrip;
 
-                int bitsInStrip = bitsPerSample * rowsInStrip * width
-                        * samplesPerPixel;
-                int bytesInStrip = (bitsInStrip + 7) / 8;
+                int bitsInRow = bitsPerSample * samplesPerPixel * width;
+                int bytesPerRow = (bitsInRow + 7) / 8;
+                int bytesInStrip = rowsInStrip * bytesPerRow;
 
                 byte uncompressed[] = new byte[bytesInStrip];
 
@@ -511,6 +509,8 @@
 
                 for (; (y < height) && (y < stop); y++)
                 {
+                    int bitCache = 0;
+                    int bitsInCache = 0;
                     for (int x = 0; x < width; x++)
                     {
                         int rgb = src.getRGB(x, y);
@@ -518,9 +518,34 @@
                         int green = 0xff & (rgb >> 8);
                         int blue = 0xff & (rgb >> 0);
 
-                        uncompressed[counter++] = (byte) red;
-                        uncompressed[counter++] = (byte) green;
-                        uncompressed[counter++] = (byte) blue;
+                        if (bitsPerSample == 1)
+                        {
+                            int sample = (red + green + blue) / 3;
+                            if (sample > 127)
+                                sample = 0;
+                            else
+                                sample = 1;
+                            bitCache <<= 1;
+                            bitCache |= sample;
+                            bitsInCache++;
+                            if (bitsInCache == 8)
+                            {
+                                uncompressed[counter++] = (byte) bitCache;
+                                bitCache = 0;
+                                bitsInCache = 0;
+                            }
+                        }
+                        else
+                        {
+                            uncompressed[counter++] = (byte) red;
+                            uncompressed[counter++] = (byte) green;
+                            uncompressed[counter++] = (byte) blue;
+                        }
+                    }
+                    if (bitsInCache > 0)
+                    {
+                        bitCache <<= (8 - bitsInCache);
+                        uncompressed[counter++] = (byte) bitCache;
                     }
                 }
 
diff --git a/src/site/xdoc/formatsupport.xml b/src/site/xdoc/formatsupport.xml
index 841c84e..338a16b 100644
--- a/src/site/xdoc/formatsupport.xml
+++ b/src/site/xdoc/formatsupport.xml
@@ -160,8 +160,9 @@
         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 LZW, CCITT Modified Huffman/T.4/T.6 and Packbits/RLE compression.
-        Notably missing other forms of compression, though, including JPEG.
+        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.
     </td>
     <td>