From c18893184b284202c92c8e4d326e997d9840271f Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 7 Jul 2016 15:27:08 +0200 Subject: [PATCH] #228: TIFFImageWriter now correctly writes images with sample model translation. --- .../imageio/util/ImageReaderAbstractTest.java | 13 +- .../plugins/jpeg/JPEGImageReaderTest.java | 2 +- .../plugins/psd/PSDImageReaderTest.java | 2 +- .../imageio/plugins/tiff/TIFFImageWriter.java | 113 ++++++++++++------ .../plugins/tiff/TIFFImageWriterTest.java | 67 +++++++++-- 5 files changed, 145 insertions(+), 52 deletions(-) diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java index bccf599c..2864f844 100644 --- a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java +++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/util/ImageReaderAbstractTest.java @@ -1606,10 +1606,15 @@ public abstract class ImageReaderAbstractTest { * Slightly fuzzy RGB equals method. Variable tolerance. */ public static void assertRGBEquals(String message, int expectedRGB, int actualRGB, int tolerance) { - assertEquals(message, (expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 0); - assertEquals(message, (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, tolerance); - assertEquals(message, (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, tolerance); - assertEquals(message, (expectedRGB ) & 0xff, (actualRGB ) & 0xff, tolerance); + try { + assertEquals((expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 0); + assertEquals((expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, tolerance); + assertEquals((expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, tolerance); + assertEquals((expectedRGB ) & 0xff, (actualRGB ) & 0xff, tolerance); + } + catch (AssertionError e) { + assertEquals(message, String.format("#%08x", expectedRGB), String.format("#%08x", actualRGB)); + } } static final protected class TestData { diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index c55eae6a..de654cd1 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -956,7 +956,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest // NOTE: Allow some slack, as Java 1.7 and 1.8 color management differs slightly int rgb = image.getRGB(0, 0); - assertRGBEquals(String.format("#%04x != #%04x", colors[i], rgb), colors[i], rgb, 1); + assertRGBEquals("Colors differ", colors[i], rgb, 1); } } } 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 17e75864..68e21163 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 @@ -669,36 +669,54 @@ public final class TIFFImageWriter extends ImageWriterBase { final int tileWidth = renderedImage.getTileWidth(); // TODO: SampleSize may differ between bands/banks - int sampleSize = renderedImage.getSampleModel().getSampleSize(0); - 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); + final int sampleSize = renderedImage.getSampleModel().getSampleSize(0); + final int numBands = renderedImage.getSampleModel().getNumBands(); + + final ByteBuffer buffer = ByteBuffer.allocate((tileWidth * numBands * sampleSize + 7) / 8); for (int yTile = minTileY; yTile < maxYTiles; yTile++) { for (int xTile = minTileX; xTile < maxXTiles; xTile++) { final Raster tile = renderedImage.getTile(xTile, yTile); + + // Model translation + final int offsetX = tile.getMinX() - tile.getSampleModelTranslateX(); + final int offsetY = tile.getMinY() - tile.getSampleModelTranslateY(); + + // Scanline stride, not accounting for model translation + final int stride = (tile.getSampleModel().getWidth() * sampleSize + 7) / 8; final DataBuffer dataBuffer = tile.getDataBuffer(); - final int numBands = tile.getNumBands(); switch (dataBuffer.getDataType()) { case DataBuffer.TYPE_BYTE: - // System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE"); - for (int b = 0; b < dataBuffer.getNumBanks(); b++) { - for (int y = 0; y < tileHeight; y++) { - int steps = sampleSize == 1 ? (tileWidth + 7) / 8 : tileWidth; - final int yOff = y * steps * numBands; + int steps = (tileWidth * sampleSize + 7) / 8; + // Shift needed for "packed" samples with "odd" offset + int shift = offsetX % 8; - for (int x = 0; x < steps; x++) { + // TODO: Generalize this code, to always use row raster + final WritableRaster rowRaster = shift != 0 ? tile.createCompatibleWritableRaster(tile.getWidth(), 1) : null; + final DataBuffer rowBuffer = shift != 0 ? rowRaster.getDataBuffer() : null; + + for (int b = 0; b < dataBuffer.getNumBanks(); b++) { + for (int y = offsetY; y < tileHeight + offsetY; y++) { + final int yOff = y * stride * numBands; + + if (shift != 0) { + rowRaster.setDataElements(0, 0, tile.createChild(0, y - offsetY, tile.getWidth(), 1, 0, 0, null)); + } + + for (int x = offsetX; x < steps + offsetX; x++) { final int xOff = yOff + x * numBands; for (int s = 0; s < numBands; s++) { - buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff)); + if (sampleSize == 8 || shift == 0) { + // Normal interleaved/planar case + buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff)); + } + else { + // "Packed" case + buffer.put((byte) (rowBuffer.getElem(b, x - offsetX + bandOffsets[s]) & 0xff)); + } } } @@ -716,13 +734,13 @@ public final class TIFFImageWriter extends ImageWriterBase { case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_SHORT: if (numComponents == 1) { - // TODO: This is foobar... // System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES"); - for (int b = 0; b < dataBuffer.getNumBanks(); b++) { - for (int y = 0; y < tileHeight; y++) { - final int yOff = y * tileWidth; - for (int x = 0; x < tileWidth; x++) { + for (int b = 0; b < dataBuffer.getNumBanks(); b++) { + for (int y = offsetY; y < tileHeight + offsetY; y++) { + int yOff = y * stride / 2; + + for (int x = offsetX; x < tileWidth + offsetX; x++) { final int xOff = yOff + x; buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff)); @@ -765,24 +783,49 @@ public final class TIFFImageWriter extends ImageWriterBase { case DataBuffer.TYPE_INT: // TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB) -// System.err.println("Writing INT -> " + numBands + "_BYTES"); - for (int b = 0; b < dataBuffer.getNumBanks(); b++) { - for (int y = 0; y < tileHeight; y++) { - final int yOff = y * tileWidth; + if (1 == numComponents) { +// System.err.println("Writing INT -> " + numBands * 4 + "_BYTES"); - for (int x = 0; x < tileWidth; x++) { - final int xOff = yOff + x; - int element = dataBuffer.getElem(b, xOff); + for (int b = 0; b < dataBuffer.getNumBanks(); b++) { + for (int y = offsetY; y < tileHeight + offsetY; y++) { + int yOff = y * stride / 4; - for (int s = 0; s < numBands; s++) { - buffer.put((byte) ((element >> bitOffsets[s]) & 0xff)); + for (int x = offsetX; x < tileWidth + offsetX; x++) { + final int xOff = yOff + x; + + buffer.putInt(dataBuffer.getElem(b, xOff)); + } + + flushBuffer(buffer, stream); + + if (stream instanceof DataOutputStream) { + DataOutputStream dataOutputStream = (DataOutputStream) stream; + dataOutputStream.flush(); } } + } + } + else { +// System.err.println("Writing INT -> " + numBands + "_BYTES"); - flushBuffer(buffer, stream); - if (stream instanceof DataOutputStream) { - DataOutputStream dataOutputStream = (DataOutputStream) stream; - dataOutputStream.flush(); + for (int b = 0; b < dataBuffer.getNumBanks(); b++) { + for (int y = 0; y < tileHeight; y++) { + final int yOff = y * tileWidth; + + for (int x = 0; x < tileWidth; x++) { + final int xOff = yOff + x; + int element = dataBuffer.getElem(b, xOff); + + for (int s = 0; s < numBands; s++) { + buffer.put((byte) ((element >> bitOffsets[s]) & 0xff)); + } + } + + flushBuffer(buffer, stream); + if (stream instanceof DataOutputStream) { + DataOutputStream dataOutputStream = (DataOutputStream) stream; + dataOutputStream.flush(); + } } } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index fdd925a0..df2fc798 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -34,11 +34,9 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader; import com.twelvemonkeys.imageio.metadata.exif.Rational; import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; -import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; -import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.imageio.*; @@ -50,9 +48,8 @@ import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; +import java.io.*; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -88,7 +85,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { new BufferedImage(300, 200, BufferedImage.TYPE_4BYTE_ABGR), new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY), new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_GRAY), -// new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY), // TODO! + new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_BINARY), new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED) ); } @@ -429,7 +426,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { // Read original LZW compressed TIFF IIOImage original; - try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/a33.tif"))) { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/a33.tif"))) { ImageReader reader = ImageIO.getImageReaders(input).next(); reader.setInput(input); @@ -493,7 +490,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { // Read original LZW compressed TIFF IIOImage original; - try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/a33.tif"))) { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/a33.tif"))) { ImageReader reader = ImageIO.getImageReaders(input).next(); reader.setInput(input); @@ -558,7 +555,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { // Read original LZW compressed TIFF IIOImage original; - try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) { ImageReader reader = ImageIO.getImageReaders(input).next(); reader.setInput(input); @@ -618,7 +615,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { // Read original LZW compressed TIFF IIOImage original; - try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) { ImageReader reader = ImageIO.getImageReaders(input).next(); reader.setInput(input); @@ -682,7 +679,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { // Read original LZW compressed TIFF IIOImage original; - try (ImageInputStream input = ImageIO.createImageInputStream(getClass().getResource("/tiff/quad-lzw.tif"))) { + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tiff/quad-lzw.tif"))) { ImageReader reader = ImageIO.getImageReaders(input).next(); reader.setInput(input); @@ -744,4 +741,52 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase { assertTrue("Software metadata not found", softwareFound); } } + + @Test + public void testWriteCropped() throws IOException { + List testData = Arrays.asList( + getClassLoaderResource("/tiff/quad-lzw.tif"), + getClassLoaderResource("/tiff/grayscale-alpha.tiff"), + getClassLoaderResource("/tiff/ccitt/group3_1d.tif"), + getClassLoaderResource("/tiff/depth/flower-palette-02.tif"), + getClassLoaderResource("/tiff/depth/flower-palette-04.tif"), + getClassLoaderResource("/tiff/depth/flower-minisblack-16.tif"), + getClassLoaderResource("/tiff/depth/flower-minisblack-32.tif") + ); + + for (URL resource : testData) { + // Read it + BufferedImage original = ImageIO.read(resource); + + // Crop it + BufferedImage subimage = original.getSubimage(original.getWidth() / 4, original.getHeight() / 4, original.getWidth() / 2, original.getHeight() / 2); + + // Store cropped + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ImageOutputStream output = ImageIO.createImageOutputStream(bytes)) { + ImageWriter imageWriter = createImageWriter(); + imageWriter.setOutput(output); + imageWriter.write(subimage); + } + + // Re-read cropped + BufferedImage cropped = ImageIO.read(new ByteArrayImageInputStream(bytes.toByteArray())); + + // Compare + assertImageEquals(String.format("Cropped output differs: %s", resource.getFile()), subimage, cropped, 0); + } + } + + private void assertImageEquals(final String message, final BufferedImage expected, final BufferedImage actual, final int tolerance) { + assertNotNull(message, expected); + assertNotNull(message, actual); + assertEquals(message + ", widths differ", expected.getWidth(), actual.getWidth()); + assertEquals(message + ", heights differ", expected.getHeight(), actual.getHeight()); + + for (int y = 0; y < expected.getHeight(); y++) { + for (int x = 0; x < expected.getWidth(); x++) { + assertRGBEquals(String.format("%s, ARGB differs at (%s,%s)", message, x, y), expected.getRGB(x, y), actual.getRGB(x, y), tolerance); + } + } + } }