Merge pull request #183 from Schmidor/CCITTWriter

CCITTFaxEncoderStream for TiffImageWriter
This commit is contained in:
Harald Kuhr 2015-10-15 22:16:46 +02:00
commit c33cfea02c
4 changed files with 690 additions and 52 deletions

View File

@ -0,0 +1,396 @@
/*
* Copyright (c) 2013, 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.lang.Validate;
import java.io.IOException;
import java.io.OutputStream;
/**
* CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression.
*
* @author <a href="mailto:mail@schmidor.de">Oliver Schmidtmer</a>
* @author last modified by $Author$
* @version $Id$
*/
public class CCITTFaxEncoderStream extends OutputStream {
private int currentBufferLength = 0;
private final byte[] inputBuffer;
private final int inputBufferLength;
private int columns;
private int rows;
private int[] changesCurrentRow;
private int[] changesReferenceRow;
private int currentRow = 0;
private int changesCurrentRowLength = 0;
private int changesReferenceRowLength = 0;
private byte outputBuffer = 0;
private byte outputBufferBitLength = 0;
private int type;
private int fillOrder;
private boolean optionG32D;
private boolean optionG3Fill;
private boolean optionUncompressed;
private OutputStream stream;
public CCITTFaxEncoderStream(final OutputStream stream, final int columns, final int rows, final int type, final int fillOrder,
final long options) {
this.stream = stream;
this.type = type;
this.columns = columns;
this.rows = rows;
this.fillOrder = fillOrder;
this.changesReferenceRow = new int[columns];
this.changesCurrentRow = new int[columns];
switch (type) {
case TIFFExtension.COMPRESSION_CCITT_T4:
optionG32D = (options & TIFFExtension.GROUP3OPT_2DENCODING) != 0;
optionG3Fill = (options & TIFFExtension.GROUP3OPT_FILLBITS) != 0;
optionUncompressed = (options & TIFFExtension.GROUP3OPT_UNCOMPRESSED) != 0;
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
optionUncompressed = (options & TIFFExtension.GROUP4OPT_UNCOMPRESSED) != 0;
break;
}
inputBufferLength = (columns + 7) / 8;
inputBuffer = new byte[inputBufferLength];
Validate.isTrue(!optionUncompressed, optionUncompressed,
"CCITT GROUP 3/4 OPTION UNCOMPRESSED is not supported");
}
@Override
public void write(int b) throws IOException {
inputBuffer[currentBufferLength] = (byte) b;
currentBufferLength++;
if (currentBufferLength == inputBufferLength) {
encodeRow();
currentBufferLength = 0;
}
}
@Override
public void flush() throws IOException {
stream.flush();
}
@Override
public void close() throws IOException {
stream.close();
}
private void encodeRow() throws IOException {
currentRow++;
int[] tmp = changesReferenceRow;
changesReferenceRow = changesCurrentRow;
changesCurrentRow = tmp;
changesReferenceRowLength = changesCurrentRowLength;
changesCurrentRowLength = 0;
int index = 0;
boolean white = true;
while (index < columns) {
int byteIndex = index / 8;
int bit = index % 8;
if ((((inputBuffer[byteIndex] >> (7 - bit)) & 1) == 1) == (white)) {
changesCurrentRow[changesCurrentRowLength] = index;
changesCurrentRowLength++;
white = !white;
}
index++;
}
switch (type) {
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
encodeRowType2();
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
encodeRowType4();
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
encodeRowType6();
break;
}
if (currentRow == rows) {
if (type == TIFFExtension.COMPRESSION_CCITT_T6) {
writeEOL();
writeEOL();
}
fill();
}
}
private void encodeRowType2() throws IOException {
encode1D();
fill();
}
private void encodeRowType4() throws IOException {
writeEOL();
if (optionG32D) {
// do k=1 only on first line. Detect first line by missing reference
// line.
if (changesReferenceRowLength == 0) {
write(1, 1);
encode1D();
}
else {
write(0, 1);
encode2D();
}
}
else {
encode1D();
}
if (optionG3Fill) {
fill();
}
}
private void encodeRowType6() throws IOException {
encode2D();
}
private void encode1D() throws IOException {
int index = 0;
boolean white = true;
while (index < columns) {
int[] nextChanges = getNextChanges(index, white);
int runLength = nextChanges[0] - index;
writeRun(runLength, white);
index += runLength;
white = !white;
}
}
private int[] getNextChanges(int pos, boolean white) {
int[] result = new int[] {columns, columns};
for (int i = 0; i < changesCurrentRowLength; i++) {
if (pos < changesCurrentRow[i] || (pos == 0 && white)) {
result[0] = changesCurrentRow[i];
if ((i + 1) < changesCurrentRowLength) {
result[1] = changesCurrentRow[i + 1];
}
break;
}
}
return result;
}
private void writeRun(int runLength, boolean white) throws IOException {
int nonterm = runLength / 64;
Code[] codes = white ? WHITE_NONTERMINATING_CODES : BLACK_NONTERMINATING_CODES;
while (nonterm > 0) {
if (nonterm >= codes.length) {
write(codes[codes.length - 1].code, codes[codes.length - 1].length);
nonterm -= codes.length - 1;
}
else {
write(codes[nonterm - 1].code, codes[nonterm - 1].length);
nonterm = 0;
}
}
Code c = white ? WHITE_TERMINATING_CODES[runLength % 64] : BLACK_TERMINATING_CODES[runLength % 64];
write(c.code, c.length);
}
private void encode2D() throws IOException {
boolean white = true;
int index = 0; // a0
while (index < columns) {
int[] nextChanges = getNextChanges(index, white); // a1, a2
int[] nextRefs = getNextRefChanges(index, white); // b1, b2
int difference = nextChanges[0] - nextRefs[0];
if (nextChanges[0] > nextRefs[1]) {
// PMODE
write(1, 4);
index = nextRefs[1];
}
else if (difference > 3 || difference < -3) {
// HMODE
write(1, 3);
writeRun(nextChanges[0] - index, white);
writeRun(nextChanges[1] - nextChanges[0], !white);
index = nextChanges[1];
}
else {
// VMODE
switch (difference) {
case 0:
write(1, 1);
break;
case 1:
write(3, 3);
break;
case 2:
write(3, 6);
break;
case 3:
write(3, 7);
break;
case -1:
write(2, 3);
break;
case -2:
write(2, 6);
break;
case -3:
write(2, 7);
break;
}
white = !white;
index = nextRefs[0] + difference;
}
}
}
private int[] getNextRefChanges(int a0, boolean white) {
int[] result = new int[] {columns, columns};
for (int i = (white ? 0 : 1); i < changesReferenceRowLength; i += 2) {
if (changesReferenceRow[i] > a0 || (a0 == 0 && i == 0)) {
result[0] = changesReferenceRow[i];
if ((i + 1) < changesReferenceRowLength) {
result[1] = changesReferenceRow[i + 1];
}
break;
}
}
return result;
}
private void write(int code, int codeLength) throws IOException {
for (int i = 0; i < codeLength; i++) {
boolean codeBit = ((code >> (codeLength - i - 1)) & 1) == 1;
if (fillOrder == TIFFBaseline.FILL_LEFT_TO_RIGHT) {
outputBuffer |= (codeBit ? 1 << (7 - ((outputBufferBitLength) % 8)) : 0);
}
else {
outputBuffer |= (codeBit ? 1 << (((outputBufferBitLength) % 8)) : 0);
}
outputBufferBitLength++;
if (outputBufferBitLength == 8) {
stream.write(outputBuffer);
clearOutputBuffer();
}
}
}
private void writeEOL() throws IOException {
if (optionG3Fill) {
// Fill up so EOL ends on a byte-boundary
while (outputBufferBitLength != 4) {
write(0, 1);
}
}
write(1, 12);
}
private void fill() throws IOException {
if (outputBufferBitLength != 0) {
stream.write(outputBuffer);
}
clearOutputBuffer();
}
private void clearOutputBuffer() {
outputBuffer = 0;
outputBufferBitLength = 0;
}
public static class Code {
private Code(int code, int length) {
this.code = code;
this.length = length;
}
final int code;
final int length;
}
public static final Code[] WHITE_TERMINATING_CODES;
public static final Code[] WHITE_NONTERMINATING_CODES;
public static final Code[] BLACK_TERMINATING_CODES;
public static final Code[] BLACK_NONTERMINATING_CODES;
static {
// Setup HUFFMAN Codes
WHITE_TERMINATING_CODES = new Code[64];
WHITE_NONTERMINATING_CODES = new Code[40];
for (int i = 0; i < CCITTFaxDecoderStream.WHITE_CODES.length; i++) {
int bitLength = i + 4;
for (int j = 0; j < CCITTFaxDecoderStream.WHITE_CODES[i].length; j++) {
int value = CCITTFaxDecoderStream.WHITE_RUN_LENGTHS[i][j];
int code = CCITTFaxDecoderStream.WHITE_CODES[i][j];
if (value < 64) {
WHITE_TERMINATING_CODES[value] = new Code(code, bitLength);
}
else {
WHITE_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength);
}
}
}
BLACK_TERMINATING_CODES = new Code[64];
BLACK_NONTERMINATING_CODES = new Code[40];
for (int i = 0; i < CCITTFaxDecoderStream.BLACK_CODES.length; i++) {
int bitLength = i + 2;
for (int j = 0; j < CCITTFaxDecoderStream.BLACK_CODES[i].length; j++) {
int value = CCITTFaxDecoderStream.BLACK_RUN_LENGTHS[i][j];
int code = CCITTFaxDecoderStream.BLACK_CODES[i][j];
if (value < 64) {
BLACK_TERMINATING_CODES[value] = new Code(code, bitLength);
}
else {
BLACK_NONTERMINATING_CODES[(value / 64) - 1] = new Code(code, bitLength);
}
}
}
}
}

View File

@ -65,7 +65,7 @@ public final class TIFFImageWriteParam extends ImageWriteParam {
// See: http://download.java.net/media/jai-imageio/javadoc/1.1/com/sun/media/imageio/plugins/tiff/TIFFImageWriteParam.html
compressionTypes = new String[] {
"None",
null, null, null,/* "CCITT RLE", "CCITT T.4", "CCITT T.6", */
"CCITT RLE", "CCITT T.4", "CCITT T.6",
"LZW", "JPEG", "ZLib", "PackBits", "Deflate",
null/* "EXIF JPEG" */ // A well-defined form of "Old-style JPEG", no tables/process, only 513 (offset) and 514 (length)
};
@ -111,6 +111,15 @@ public final class TIFFImageWriteParam extends ImageWriteParam {
else if (param.getCompressionType().equals("JPEG")) {
return TIFFExtension.COMPRESSION_JPEG;
}
else if (param.getCompressionType().equals("CCITT RLE")) {
return TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE;
}
else if (param.getCompressionType().equals("CCITT T.4")) {
return TIFFExtension.COMPRESSION_CCITT_T4;
}
else if (param.getCompressionType().equals("CCITT T.6")) {
return TIFFExtension.COMPRESSION_CCITT_T6;
}
// else if (param.getCompressionType().equals("EXIF JPEG")) {
// return TIFFExtension.COMPRESSION_OLD_JPEG;
// }

View File

@ -194,6 +194,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
ColorModel colorModel = renderedImage.getColorModel();
int numComponents = colorModel.getNumComponents();
//TODO: streamMetadata?
TIFFImageMetadata metadata;
if (image.getMetadata() != null) {
metadata = convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
@ -222,26 +223,34 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
Set<Entry> entries = new LinkedHashSet<>();
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
HashMap<Integer, Entry> entries = new LinkedHashMap<>();
entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
// entries.add(new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
entries.add(new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// If numComponents > numColorComponents, write ExtraSamples
if (numComponents > colorModel.getNumColorComponents()) {
// TODO: Write per component > numColorComponents
if (colorModel.hasAlpha()) {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied()
? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA
: TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
// Write compression field from param or metadata
// TODO: Support COPY_FROM_METADATA
int compression = TIFFImageWriteParam.getCompressionType(param);
entries.add(new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
int compression;
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
&& image.getMetadata() != null) {
compression = (int) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue();
}
else {
compression = TIFFImageWriteParam.getCompressionType(param);
}
entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
// TODO: Let param/metadata control predictor
// TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
@ -249,7 +258,22 @@ public final class TIFFImageWriter extends ImageWriterBase {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
entries.add(new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS);
if (group3options == null) {
group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
}
entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS);
if (group4options == null) {
group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
}
entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
break;
default:
}
@ -257,49 +281,56 @@ public final class TIFFImageWriter extends ImageWriterBase {
int photometric = compression == TIFFExtension.COMPRESSION_JPEG ?
TIFFExtension.PHOTOMETRIC_YCBCR :
getPhotometricInterpretation(colorModel);
entries.add(new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
entries.add(new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel)));
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents));
if (colorModel.getPixelSize() == 1) {
numComponents = 1;
}
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numComponents));
// Note: Assuming sRGB to be the default RGB interpretation
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
entries.add(new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
// Default sample format SAMPLEFORMAT_UINT need not be written
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT /* TODO: if (isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.add(new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
// TODO: Float values!
// Get Software from metadata, or use default
Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE);
entries.add(software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
entries.put(TIFF.TAG_SOFTWARE, software != null
? software
: new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
// Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults
// TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent.
Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION);
entries.add(xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION);
entries.add(yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT);
entries.add(resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
entries.put(TIFF.TAG_RESOLUTION_UNIT,
resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.add(new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, Integer.MAX_VALUE)); // TODO: Allowed but not recommended
// - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
TIFFEntry dummyStripByteCounts = new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1);
entries.add(dummyStripByteCounts); // Updated later
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, dummyStripByteCounts); // Updated later
// - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
TIFFEntry dummyStripOffsets = new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1);
entries.add(dummyStripOffsets); // Updated later
entries.put(TIFF.TAG_STRIP_OFFSETS, dummyStripOffsets); // Updated later
// TODO: If tiled, write tile indexes etc
// Depending on param.getTilingMode
@ -308,14 +339,15 @@ public final class TIFFImageWriter extends ImageWriterBase {
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamOffset = exifWriter.computeIFDSize(entries) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF
long streamOffset = exifWriter.computeIFDSize(entries.values()) + 12; // 12 == 4 byte magic, 4 byte IDD 0 pointer, 4 byte EOF
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, renderedImage.getWidth() * renderedImage.getHeight() * numComponents));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS,
renderedImage.getWidth() * renderedImage.getHeight() * numComponents));
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset));
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, streamOffset));
exifWriter.write(entries, imageOutput); // NOTE: Writer takes case of ordering tags
exifWriter.write(entries.values(), imageOutput); // NOTE: Writer takes case of ordering tags
imageOutput.flush();
}
else {
@ -344,7 +376,8 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
else {
// Write image data
writeImageData(createCompressorStream(renderedImage, param), renderedImage, numComponents, bandOffsets, bitOffsets);
writeImageData(createCompressorStream(renderedImage, param, entries), renderedImage, numComponents, bandOffsets,
bitOffsets);
}
// Update IFD0-pointer, and write IFD
@ -352,11 +385,11 @@ public final class TIFFImageWriter extends ImageWriterBase {
long streamPosition = imageOutput.getStreamPosition();
entries.remove(dummyStripOffsets);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8));
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, 8));
entries.remove(dummyStripByteCounts);
entries.add(new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8));
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, streamPosition - 8));
long ifdOffset = exifWriter.writeIFD(entries, imageOutput);
long ifdOffset = exifWriter.writeIFD(entries.values(), imageOutput);
imageOutput.writeInt(0); // Next IFD (none)
streamPosition = imageOutput.getStreamPosition();
@ -368,7 +401,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
}
private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param) {
private DataOutput createCompressorStream(RenderedImage image, ImageWriteParam param, HashMap<Integer, Entry> entries) {
/*
36 MB test data:
@ -422,7 +455,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
// Use predictor by default for LZW and ZLib/Deflate
// TODO: Unless explicitly disabled in TIFFImageWriteParam
int compression = TIFFImageWriteParam.getCompressionType(param);
int compression = (int) entries.get(TIFF.TAG_COMPRESSION).getValue();
OutputStream stream;
switch (compression) {
@ -465,8 +498,27 @@ public final class TIFFImageWriter extends ImageWriterBase {
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());
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);
case TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE:
case TIFFExtension.COMPRESSION_CCITT_T4:
case TIFFExtension.COMPRESSION_CCITT_T6:
long option = 0L;
if (compression != TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE) {
option = (long) entries.get(compression == TIFFExtension.COMPRESSION_CCITT_T4
? TIFF.TAG_GROUP3OPTIONS
: TIFF.TAG_GROUP4OPTIONS).getValue();
}
Entry fillOrderEntry = entries.get(TIFF.TAG_FILL_ORDER);
int fillOrder = (int) (fillOrderEntry != null
? fillOrderEntry.getValue()
: TIFFBaseline.FILL_LEFT_TO_RIGHT);
stream = IIOUtil.createStreamAdapter(imageOutput);
stream = new CCITTFaxEncoderStream(stream, image.getTileWidth(), image.getTileHeight(), compression, fillOrder, option);
return new DataOutputStream(stream);
}
@ -475,12 +527,12 @@ public final class TIFFImageWriter extends ImageWriterBase {
}
private int getPhotometricInterpretation(final ColorModel colorModel) {
if (colorModel.getNumComponents() == 1 && colorModel.getComponentSize(0) == 1) {
if (colorModel.getPixelSize() == 1) {
if (colorModel instanceof IndexColorModel) {
if (colorModel.getRGB(0) == 0xFFFFFF && colorModel.getRGB(1) == 0x000000) {
if (colorModel.getRGB(0) == 0xFFFFFFFF && colorModel.getRGB(1) == 0xFF000000) {
return TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO;
}
else if (colorModel.getRGB(0) != 0x000000 || colorModel.getRGB(1) != 0xFFFFFF) {
else if (colorModel.getRGB(0) != 0xFF000000 || colorModel.getRGB(1) != 0xFFFFFFFF) {
return TIFFBaseline.PHOTOMETRIC_PALETTE;
}
// Else, fall through to default, BLACK_IS_ZERO
@ -513,9 +565,9 @@ public final class TIFFImageWriter extends ImageWriterBase {
for (int i = 0; i < colorModel.getMapSize(); i++) {
int color = colorModel.getRGB(i);
colorMap[i ] = (short) upScale((color >> 16) & 0xff);
colorMap[i] = (short) upScale((color >> 16) & 0xff);
colorMap[i + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff);
colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 0xff);
colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color) & 0xff);
}
return colorMap;
@ -555,16 +607,20 @@ public final class TIFFImageWriter extends ImageWriterBase {
// TODO: SampleSize may differ between bands/banks
int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
final ByteBuffer buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8);
// System.err.println("tileWidth: " + tileWidth);
final ByteBuffer buffer;
if (sampleSize == 1) {
buffer = ByteBuffer.allocate((tileWidth + 7) / 8);
}
else {
buffer = ByteBuffer.allocate(tileWidth * renderedImage.getSampleModel().getNumBands() * sampleSize / 8);
}
// System.err.println("tileWidth: " + tileWidth);
for (int yTile = minTileY; yTile < maxYTiles; yTile++) {
for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
final Raster tile = renderedImage.getTile(xTile, yTile);
final DataBuffer dataBuffer = tile.getDataBuffer();
final int numBands = tile.getNumBands();
// final SampleModel sampleModel = tile.getSampleModel();
switch (dataBuffer.getDataType()) {
case DataBuffer.TYPE_BYTE:
@ -572,9 +628,10 @@ public final class TIFFImageWriter extends ImageWriterBase {
// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth * numBands;
int steps = sampleSize == 1 ? (tileWidth + 7) / 8 : tileWidth;
final int yOff = y * steps * numBands;
for (int x = 0; x < tileWidth; x++) {
for (int x = 0; x < steps; x++) {
final int xOff = yOff + x * numBands;
for (int s = 0; s < numBands; s++) {
@ -911,7 +968,6 @@ public final class TIFFImageWriter extends ImageWriterBase {
// }
// writer.dispose();
image = null;
BufferedImage read = ImageIO.read(output);

View File

@ -0,0 +1,177 @@
/*
* Copyright (c) 2013, 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.imageio.plugins.tiff.CCITTFaxEncoderStream.Code;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.*;
import java.net.URL;
import static org.junit.Assert.*;
/**
* CCITTFaxEncoderStreamTest
*
* @author <a href="mailto:mail@schmidor.de">Oliver Schmidtmer</a>
* @author last modified by $Author$
* @version $Id$
*/
public class CCITTFaxEncoderStreamTest {
// Image should be (6 x 4):
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 1 0 1 1 x x
// 1 1 0 0 1 1 x x
BufferedImage image;
@Before
public void init() {
image = new BufferedImage(6, 4, BufferedImage.TYPE_BYTE_BINARY);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 6; x++) {
image.setRGB(x, y, x != 3 ? 0xff000000 : 0xffffffff);
}
}
image.setRGB(2, 3, 0xffffffff);
}
@Test
public void testBuildCodes() throws IOException {
assertTrue(CCITTFaxEncoderStream.WHITE_TERMINATING_CODES.length == 64);
for (Code code : CCITTFaxEncoderStream.WHITE_TERMINATING_CODES) {
assertNotNull(code);
}
assertTrue(CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES.length == 40);
for (Code code : CCITTFaxEncoderStream.WHITE_NONTERMINATING_CODES) {
assertNotNull(code);
}
assertTrue(CCITTFaxEncoderStream.BLACK_TERMINATING_CODES.length == 64);
for (Code code : CCITTFaxEncoderStream.BLACK_TERMINATING_CODES) {
assertNotNull(code);
}
assertTrue(CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES.length == 40);
for (Code code : CCITTFaxEncoderStream.BLACK_NONTERMINATING_CODES) {
assertNotNull(code);
}
}
@Test
public void testType2() throws IOException {
testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 1, 0L);
}
@Test
public void testType4() throws IOException {
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, 0L);
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_FILLBITS);
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1, TIFFExtension.GROUP3OPT_2DENCODING);
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T4, 1,
TIFFExtension.GROUP3OPT_FILLBITS | TIFFExtension.GROUP3OPT_2DENCODING);
}
@Test
public void testType6() throws IOException {
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 1, 0L);
}
@Test
public void testReversedFillOrder() throws IOException {
testStreamEncodeDecode(TIFFBaseline.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE, 2, 0L);
testStreamEncodeDecode(TIFFExtension.COMPRESSION_CCITT_T6, 2, 0L);
}
@Test
public void testReencodeImages() throws IOException {
testImage(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif"));
// testImage(getClassLoaderResource("/tiff/test-single-gray-compression-type-2.tiff"));
}
protected URL getClassLoaderResource(final String pName) {
return getClass().getResource(pName);
}
private void testStreamEncodeDecode(int type, int fillOrder, long options) throws IOException {
byte[] imageData = ((DataBufferByte) image.getData().getDataBuffer()).getData();
byte[] redecodedData = new byte[imageData.length];
ByteArrayOutputStream imageOutput = new ByteArrayOutputStream();
OutputStream outputSteam = new CCITTFaxEncoderStream(imageOutput, 6, 4, type, fillOrder, options);
outputSteam.write(imageData);
outputSteam.close();
byte[] encodedData = imageOutput.toByteArray();
CCITTFaxDecoderStream inputStream = new CCITTFaxDecoderStream(new ByteArrayInputStream(encodedData), 6, type,
fillOrder, options);
new DataInputStream(inputStream).readFully(redecodedData);
inputStream.close();
assertArrayEquals(imageData, redecodedData);
}
private void testImage(URL imageUrl) throws IOException {
ImageInputStream iis = ImageIO.createImageInputStream(imageUrl.openStream());
ImageReader reader = ImageIO.getImageReadersByFormatName("TIFF").next();
reader.setInput(iis, true);
ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next();
ImageOutputStream output = ImageIO.createImageOutputStream(outputBuffer);
writer.setOutput(output);
BufferedImage originalImage = reader.read(0);
IIOImage outputImage = new IIOImage(originalImage, null, reader.getImageMetadata(0));
writer.write(outputImage);
FileOutputStream stream = new FileOutputStream("H:\\tmp\\test.tif");
try {
stream.write(outputBuffer.toByteArray());
}
finally {
stream.close();
}
BufferedImage reencodedImage = ImageIO.read(new ByteArrayInputStream(outputBuffer.toByteArray()));
byte[] reencodedData = ((DataBufferByte) reencodedImage.getData().getDataBuffer()).getData();
Assert.assertArrayEquals(((DataBufferByte) originalImage.getData().getDataBuffer()).getData(),
reencodedData);
}
}