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>