From 250c58cc2e13fc7d5ff580f853976d2a7ffc76df Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 29 Aug 2019 19:06:05 +0200 Subject: [PATCH] #289, #493: Finally implemented subsampling for < 8 bit samples (cherry picked from commit 0c2433dc9f0618fe744e51ea2c9b42a0c4d828b0) --- .../twelvemonkeys/imageio/util/IIOUtil.java | 87 ++++++++++- .../imageio/util/IIOUtilTest.java | 145 ++++++++++++++++++ .../imageio/plugins/tiff/TIFFImageReader.java | 28 ++-- .../plugins/tiff/TIFFImageReaderTest.java | 23 +++ 4 files changed, 263 insertions(+), 20 deletions(-) mode change 100755 => 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java create mode 100644 imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java old mode 100755 new mode 100644 index fa4dbe20..0fe52052 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/util/IIOUtil.java @@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio.util; import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.lang.Validate; import javax.imageio.IIOParam; import javax.imageio.ImageIO; @@ -148,10 +149,9 @@ public final class IIOUtil { return null; } - if (pSourceRegion != null) { - if (pSourceRegion.x != 0 || pSourceRegion.y != 0 || pSourceRegion.width != pImage.getWidth() || pSourceRegion.height != pImage.getHeight()) { - return pImage.getSubimage(pSourceRegion.x, pSourceRegion.y, pSourceRegion.width, pSourceRegion.height); - } + if (pSourceRegion != null + && (pSourceRegion.x != 0 || pSourceRegion.y != 0 || pSourceRegion.width != pImage.getWidth() || pSourceRegion.height != pImage.getHeight())) { + return pImage.getSubimage(pSourceRegion.x, pSourceRegion.y, pSourceRegion.width, pSourceRegion.height); } return pImage; @@ -192,7 +192,7 @@ public final class IIOUtil { * The names are all upper-case, and contains no duplicates. * * @return a normalized array of {@code String}s. - * @see javax.imageio.ImageIO#getReaderFormatNames() + * @see ImageIO#getReaderFormatNames() */ public static String[] getNormalizedReaderFormatNames() { return normalizeNames(ImageIO.getReaderFormatNames()); @@ -203,7 +203,7 @@ public final class IIOUtil { * The names are all upper-case, and contains no duplicates. * * @return a normalized array of {@code String}s. - * @see javax.imageio.ImageIO#getWriterFormatNames() + * @see ImageIO#getWriterFormatNames() */ public static String[] getNormalizedWriterFormatNames() { return normalizeNames(ImageIO.getWriterFormatNames()); @@ -216,6 +216,79 @@ public final class IIOUtil { normalizedNames.add(name.toUpperCase()); } - return normalizedNames.toArray(new String[normalizedNames.size()]); + return normalizedNames.toArray(new String[0]); + } + + // TODO: RasterUtils? Subsampler? + public static void subsampleRow(byte[] srcRow, int srcPos, int srcWidth, + byte[] destRow, int destPos, + int samplesPerPixel, int bitsPerSample, int samplePeriod) { + Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op... + Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 8 && (bitsPerSample == 1 || bitsPerSample % 2 == 0), + "bitsPerSample must be > 0 and <= 8 and a power of 2"); + Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); + Validate.isTrue(samplesPerPixel * bitsPerSample <= 8 || samplesPerPixel * bitsPerSample % 8 == 0, + "samplesPerPixel * bitsPerSample must be < 8 or a multiple of 8 "); + + if (bitsPerSample * samplesPerPixel % 8 == 0) { + int pixelStride = bitsPerSample * samplesPerPixel / 8; + for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { + // System.arraycopy should be intrinsic, but consider using direct array access for pixelStride == 1 + System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); + } + } + else { + // Start bit fiddling... + int pixelStride = bitsPerSample * samplesPerPixel; + int mask = (1 << pixelStride) - 1; + + for (int x = 0; x < srcWidth; x += samplePeriod) { + int dstOff = (destPos + x / samplePeriod) * pixelStride / 8; + int srcOff = (srcPos + x) * pixelStride / 8; + + int srcBitPos = 8 - pixelStride - (x * pixelStride) % 8; + int srcMask = mask << srcBitPos; + + int dstBitPos = 8 - pixelStride - (x * pixelStride / samplePeriod) % 8; + int dstMask = ~(mask << dstBitPos); + + int val = ((srcRow[srcOff] & srcMask) >> srcBitPos); + destRow[dstOff] = (byte) ((destRow[dstOff] & dstMask) | val << dstBitPos); + } + } + } + + public static void subsampleRow(short[] srcRow, int srcPos, int srcWidth, + short[] destRow, int destPos, + int samplesPerPixel, int bitsPerSample, int samplePeriod) { + Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op... + Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 16 && (bitsPerSample == 1 || bitsPerSample % 2 == 0), + "bitsPerSample must be > 0 and <= 16 and a power of 2"); + Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); + Validate.isTrue(samplesPerPixel * bitsPerSample <= 16 || samplesPerPixel * bitsPerSample % 16 == 0, + "samplesPerPixel * bitsPerSample must be < 16 or a multiple of 16 "); + + int pixelStride = bitsPerSample * samplesPerPixel / 16; + for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { + // System.arraycopy should be intrinsic, but consider using direct array access for pixelStride == 1 + System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); + } + } + + public static void subsampleRow(int[] srcRow, int srcPos, int srcWidth, + int[] destRow, int destPos, + int samplesPerPixel, int bitsPerSample, int samplePeriod) { + Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op... + Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 32 && (bitsPerSample == 1 || bitsPerSample % 2 == 0), + "bitsPerSample must be > 0 and <= 32 and a power of 2"); + Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0"); + Validate.isTrue(samplesPerPixel * bitsPerSample <= 32 || samplesPerPixel * bitsPerSample % 32 == 0, + "samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32 "); + + int pixelStride = bitsPerSample * samplesPerPixel / 32; + for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) { + // System.arraycopy should be intrinsic, but consider using direct array access for pixelStride == 1 + System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride); + } } } \ No newline at end of file diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java new file mode 100644 index 00000000..05b9fa64 --- /dev/null +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/IIOUtilTest.java @@ -0,0 +1,145 @@ +package com.twelvemonkeys.imageio.util; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * IIOUtilTest + */ +public class IIOUtilTest { + + @Test + public void subsampleRowPeriod2Byte() { + int period = 2; + + byte[] input = {-1, 0, (byte) 0xAA, 0, -1}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {-1, (byte) 0xAA, -1}; + + IIOUtil.subsampleRow(input, 0, input.length, output, 0, 1, 8, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2ByteStride3() { + int period = 2; + + byte[] input = {-1, -1, -1, 0, 0, 0, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, 0, 0, 0, -1, -1, -1}; + byte[] output = new byte[9]; + byte[] expected = {-1, -1, -1, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, -1, -1, -1}; + + IIOUtil.subsampleRow(input, 0, input.length / 3, output, 0, 3, 8, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2Byte1() { + int period = 2; + + byte[] input = {(byte) 0xaa, (byte) 0xaa, (byte) 0xaa}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 0, input.length * 8, output, 0, 1, 1, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod3_1Bit() { + int period = 3; + + byte[] input = {(byte) 0x92, (byte) 0x49, (byte) 0x24}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff}; + + IIOUtil.subsampleRow(input, 0, input.length * 8, output, 0, 1, 1, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_2Bit() { + int period = 2; + + byte[] input = {(byte) 0xcc, (byte) 0xcc, (byte) 0xcc}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 0, input.length * 4, output, 0, 1, 2, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_4Bit() { + int period = 2; + + byte[] input = {(byte) 0xf0, (byte) 0xf0, (byte) 0xf0}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 0, input.length * 2, output, 0, 1, 4, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_1Bit2Samples() { + int period = 2; + + byte[] input = {(byte) 0xcc, (byte) 0xcc, (byte) 0xcc}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 0, input.length * 4, output, 0, 2, 1, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_2Bit2Samples() { + int period = 2; + + byte[] input = {(byte) 0xf0, (byte) 0xf0, (byte) 0xf0}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 0, input.length * 2, output, 0, 2, 2, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_4Bit2Samples() { + int period = 2; + + byte[] input = {-1, 0, (byte) 0xAA, 0, -1}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {-1, (byte) 0xAA, -1}; + + IIOUtil.subsampleRow(input, 0, input.length, output, 0, 2, 4, period); + + assertArrayEquals(expected, output); + } + + @Test + public void subsampleRowPeriod2_1BitOffset1() { + int period = 2; + + byte[] input = {(byte) 0xaa, (byte) 0xaa, (byte) 0xaa}; + byte[] output = new byte[divCeil(input.length, period)]; + byte[] expected = {(byte) 0xff, (byte) 0xf0}; + + IIOUtil.subsampleRow(input, 1, input.length * 8, output, 0, 1, 1, period); + + assertArrayEquals(expected, output); + } + + private int divCeil(int numerator, int denominator) { + return (numerator + denominator - 1) / denominator; + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 5f053b05..83f0a6a2 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -48,6 +48,7 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; import com.twelvemonkeys.imageio.metadata.xmp.XMPReader; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.SubImageInputStream; +import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.io.FastByteArrayOutputStream; @@ -82,8 +83,7 @@ import java.util.*; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter; -import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName; +import static com.twelvemonkeys.imageio.util.IIOUtil.*; import static java.util.Arrays.asList; /** @@ -502,7 +502,7 @@ public final class TIFFImageReader extends ImageReaderBase { if (cs == ColorSpace.getInstance(ColorSpace.CS_GRAY) && (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 || bitsPerSample == 8 || bitsPerSample == 16 || bitsPerSample == 32)) { return ImageTypeSpecifiers.createGrayscale(bitsPerSample, dataType); } - else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4 ) { + else if (bitsPerSample == 1 || bitsPerSample == 2 || bitsPerSample == 4) { // Use packed format for 1/2/4 bits return ImageTypeSpecifiers.createPackedGrayscale(cs, bitsPerSample, dataType); } @@ -1841,6 +1841,7 @@ public final class TIFFImageReader extends ImageReaderBase { DataBuffer dataBuffer = tileRowRaster.getDataBuffer(); int bands = dataBuffer.getNumBanks(); boolean banded = bands > 1; + int bitsPerSample = getBitsPerSample(); switch (tileRowRaster.getTransferType()) { case DataBuffer.TYPE_BYTE: @@ -1870,9 +1871,8 @@ public final class TIFFImageReader extends ImageReaderBase { // Subsample horizontal if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands); - } + IIOUtil.subsampleRow(rowDataByte, srcRegion.x * numBands, colsInTile, + rowDataByte, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub); } destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); @@ -1913,9 +1913,8 @@ public final class TIFFImageReader extends ImageReaderBase { // Subsample horizontal if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands); - } + subsampleRow(rowDataShort, srcRegion.x * numBands, colsInTile, + rowDataShort, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub); } destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); @@ -1950,9 +1949,8 @@ public final class TIFFImageReader extends ImageReaderBase { // Subsample horizontal if (xSub != 1) { - for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) { - System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands); - } + subsampleRow(rowDataInt, srcRegion.x * numBands, colsInTile, + rowDataInt, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub); } destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel); @@ -1998,6 +1996,9 @@ public final class TIFFImageReader extends ImageReaderBase { } break; + + default: + throw new AssertionError("Unsupported data type: " + tileRowRaster.getTransferType()); } } @@ -2294,7 +2295,7 @@ public final class TIFFImageReader extends ImageReaderBase { case TIFFBaseline.COMPRESSION_NONE: return stream; case TIFFBaseline.COMPRESSION_PACKBITS: - return new DecoderStream(createFillOrderStream(fillOrder, stream), new PackBitsDecoder(), 1024); + return new DecoderStream(createFillOrderStream(fillOrder, stream), new PackBitsDecoder(), 256); case TIFFExtension.COMPRESSION_LZW: // NOTE: Needs large buffer for compatibility with certain encoders return new DecoderStream(createFillOrderStream(fillOrder, stream), LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 4096)); @@ -2469,6 +2470,7 @@ public final class TIFFImageReader extends ImageReaderBase { return null; } + @Override public boolean canReadRaster() { return true; diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 292daa12..883519e6 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -708,6 +708,29 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest