#228: TIFFImageWriter now correctly writes images with sample model translation.

This commit is contained in:
Harald Kuhr 2016-07-07 15:27:08 +02:00
parent 04a39158e5
commit c18893184b
5 changed files with 145 additions and 52 deletions

View File

@ -1606,10 +1606,15 @@ public abstract class ImageReaderAbstractTest<T extends ImageReader> {
* Slightly fuzzy RGB equals method. Variable tolerance. * Slightly fuzzy RGB equals method. Variable tolerance.
*/ */
public static void assertRGBEquals(String message, int expectedRGB, int actualRGB, int tolerance) { public static void assertRGBEquals(String message, int expectedRGB, int actualRGB, int tolerance) {
assertEquals(message, (expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 0); try {
assertEquals(message, (expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, tolerance); assertEquals((expectedRGB >>> 24) & 0xff, (actualRGB >>> 24) & 0xff, 0);
assertEquals(message, (expectedRGB >> 8) & 0xff, (actualRGB >> 8) & 0xff, tolerance); assertEquals((expectedRGB >> 16) & 0xff, (actualRGB >> 16) & 0xff, tolerance);
assertEquals(message, (expectedRGB ) & 0xff, (actualRGB ) & 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 { static final protected class TestData {

View File

@ -956,7 +956,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest<JPEGImageReader
int rgb = imageRGB.getRGB(x, y); int rgb = imageRGB.getRGB(x, y);
if (rgb != cmykAsRGB) { if (rgb != cmykAsRGB) {
assertRGBEquals(String.format("Diff at [%d, %d]: #%04x != #%04x", x, y, cmykAsRGB, rgb), rgb, cmykAsRGB, 2); assertRGBEquals(String.format("Diff at [%d, %d]", x, y), rgb, cmykAsRGB, 2);
} }
} }
} }

View File

@ -447,7 +447,7 @@ public class PSDImageReaderTest extends ImageReaderAbstractTest<PSDImageReader>
// NOTE: Allow some slack, as Java 1.7 and 1.8 color management differs slightly // NOTE: Allow some slack, as Java 1.7 and 1.8 color management differs slightly
int rgb = image.getRGB(0, 0); 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);
} }
} }
} }

View File

@ -669,37 +669,55 @@ public final class TIFFImageWriter extends ImageWriterBase {
final int tileWidth = renderedImage.getTileWidth(); final int tileWidth = renderedImage.getTileWidth();
// TODO: SampleSize may differ between bands/banks // TODO: SampleSize may differ between bands/banks
int sampleSize = renderedImage.getSampleModel().getSampleSize(0); final int sampleSize = renderedImage.getSampleModel().getSampleSize(0);
final ByteBuffer buffer; final int numBands = renderedImage.getSampleModel().getNumBands();
if (sampleSize == 1) {
buffer = ByteBuffer.allocate((tileWidth + 7) / 8); final ByteBuffer buffer = ByteBuffer.allocate((tileWidth * numBands * sampleSize + 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 yTile = minTileY; yTile < maxYTiles; yTile++) {
for (int xTile = minTileX; xTile < maxXTiles; xTile++) { for (int xTile = minTileX; xTile < maxXTiles; xTile++) {
final Raster tile = renderedImage.getTile(xTile, yTile); 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 DataBuffer dataBuffer = tile.getDataBuffer();
final int numBands = tile.getNumBands();
switch (dataBuffer.getDataType()) { switch (dataBuffer.getDataType()) {
case DataBuffer.TYPE_BYTE: case DataBuffer.TYPE_BYTE:
// System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE"); // System.err.println("Writing " + numBands + "BYTE -> " + numBands + "BYTE");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) { int steps = (tileWidth * sampleSize + 7) / 8;
for (int y = 0; y < tileHeight; y++) { // Shift needed for "packed" samples with "odd" offset
int steps = sampleSize == 1 ? (tileWidth + 7) / 8 : tileWidth; int shift = offsetX % 8;
final int yOff = y * steps * numBands;
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; final int xOff = yOff + x * numBands;
for (int s = 0; s < numBands; s++) { for (int s = 0; s < numBands; s++) {
if (sampleSize == 8 || shift == 0) {
// Normal interleaved/planar case
buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff)); buffer.put((byte) (dataBuffer.getElem(b, xOff + bandOffsets[s]) & 0xff));
} }
else {
// "Packed" case
buffer.put((byte) (rowBuffer.getElem(b, x - offsetX + bandOffsets[s]) & 0xff));
}
}
} }
flushBuffer(buffer, stream); flushBuffer(buffer, stream);
@ -716,13 +734,13 @@ public final class TIFFImageWriter extends ImageWriterBase {
case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT: case DataBuffer.TYPE_SHORT:
if (numComponents == 1) { if (numComponents == 1) {
// TODO: This is foobar...
// System.err.println("Writing USHORT -> " + numBands * 2 + "_BYTES"); // 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; final int xOff = yOff + x;
buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff)); buffer.putShort((short) (dataBuffer.getElem(b, xOff) & 0xffff));
@ -765,7 +783,31 @@ public final class TIFFImageWriter extends ImageWriterBase {
case DataBuffer.TYPE_INT: case DataBuffer.TYPE_INT:
// TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB) // TODO: This is incorrect for 32 bits/sample, only works for packed (INT_(A)RGB)
if (1 == numComponents) {
// System.err.println("Writing INT -> " + numBands * 4 + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = offsetY; y < tileHeight + offsetY; y++) {
int yOff = y * stride / 4;
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"); // System.err.println("Writing INT -> " + numBands + "_BYTES");
for (int b = 0; b < dataBuffer.getNumBanks(); b++) { for (int b = 0; b < dataBuffer.getNumBanks(); b++) {
for (int y = 0; y < tileHeight; y++) { for (int y = 0; y < tileHeight; y++) {
final int yOff = y * tileWidth; final int yOff = y * tileWidth;
@ -786,6 +828,7 @@ public final class TIFFImageWriter extends ImageWriterBase {
} }
} }
} }
}
break; break;
default: default:

View File

@ -34,11 +34,9 @@ import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
import com.twelvemonkeys.imageio.metadata.exif.Rational; import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF; import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTestCase;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import org.junit.Test; import org.junit.Test;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.imageio.*; import javax.imageio.*;
@ -50,9 +48,8 @@ import javax.imageio.stream.ImageOutputStream;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage; import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.ByteArrayOutputStream; import java.net.URL;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; 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_4BYTE_ABGR),
new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY), new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_GRAY),
new BufferedImage(300, 200, BufferedImage.TYPE_USHORT_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) new BufferedImage(300, 200, BufferedImage.TYPE_BYTE_INDEXED)
); );
} }
@ -429,7 +426,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF
IIOImage original; 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(); ImageReader reader = ImageIO.getImageReaders(input).next();
reader.setInput(input); reader.setInput(input);
@ -493,7 +490,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF
IIOImage original; 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(); ImageReader reader = ImageIO.getImageReaders(input).next();
reader.setInput(input); reader.setInput(input);
@ -558,7 +555,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF
IIOImage original; 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(); ImageReader reader = ImageIO.getImageReaders(input).next();
reader.setInput(input); reader.setInput(input);
@ -618,7 +615,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF
IIOImage original; 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(); ImageReader reader = ImageIO.getImageReaders(input).next();
reader.setInput(input); reader.setInput(input);
@ -682,7 +679,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
// Read original LZW compressed TIFF // Read original LZW compressed TIFF
IIOImage original; 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(); ImageReader reader = ImageIO.getImageReaders(input).next();
reader.setInput(input); reader.setInput(input);
@ -744,4 +741,52 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTestCase {
assertTrue("Software metadata not found", softwareFound); assertTrue("Software metadata not found", softwareFound);
} }
} }
@Test
public void testWriteCropped() throws IOException {
List<URL> 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);
}
}
}
} }