diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java new file mode 100644 index 00000000..05d40bc4 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStream.java @@ -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 Oliver Schmidtmer + * @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); + } + } + } + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java index 637ea6f3..4ed13ee6 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriteParam.java @@ -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; // } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index b4012e0b..a5cfa189 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -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); @@ -208,7 +209,7 @@ public final class TIFFImageWriter extends ImageWriterBase { int[] bitOffsets; if (sampleModel instanceof ComponentSampleModel) { bandOffsets = ((ComponentSampleModel) sampleModel).getBandOffsets(); - bitOffsets = null; + bitOffsets = null; } else if (sampleModel instanceof SinglePixelPackedSampleModel) { bitOffsets = ((SinglePixelPackedSampleModel) sampleModel).getBitOffsets(); @@ -222,26 +223,34 @@ public final class TIFFImageWriter extends ImageWriterBase { throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel); } - Set entries = new LinkedHashSet<>(); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth())); - entries.add(new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight())); + HashMap 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 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 + colorMap.length / 3] = (short) upScale((color >> 8) & 0xff); - colorMap[i + 2 * colorMap.length / 3] = (short) upScale((color ) & 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); } 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++) { @@ -786,7 +843,7 @@ public final class TIFFImageWriter extends ImageWriterBase { int argIdx = 0; // TODO: Proper argument parsing: -t -c - int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; + int type = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : -1; int compression = args.length > argIdx + 1 ? Integer.parseInt(args[argIdx++]) : 0; if (args.length <= argIdx) { @@ -911,7 +968,6 @@ public final class TIFFImageWriter extends ImageWriterBase { // } // writer.dispose(); - image = null; BufferedImage read = ImageIO.read(output); diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java new file mode 100644 index 00000000..9a6f12a2 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/CCITTFaxEncoderStreamTest.java @@ -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 Oliver Schmidtmer + * @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); + } +}