diff --git a/README.md b/README.md index c885241c..8b87f854 100644 --- a/README.md +++ b/README.md @@ -23,32 +23,32 @@ The goal is to create a set of efficient and robust ImageIO plug-ins, that can b | ------ | -------- | ----------- |:----:|:-----:| -------- | ----- | | Batik | **SVG** | Scalable Vector Graphics | ✔ | - | - | Requires [Batik](https://xmlgraphics.apache.org/batik/) | | WMF | MS Windows Metafile | ✔ | - | - | Requires [Batik](https://xmlgraphics.apache.org/batik/) -| [BMP](https://github.com/haraldk/TwelveMonkeys/wiki/BMP-Plugin) | **BMP** | MS Windows and IBM OS/2 Device Independent Bitmap | ✔ | ✔ | Native & Standard | +| [BMP](https://github.com/haraldk/TwelveMonkeys/wiki/BMP-Plugin) | **BMP** | MS Windows and IBM OS/2 Device Independent Bitmap | ✔ | ✔ | [Native](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/bmp_metadata.html) & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | | | CUR | MS Windows Cursor Format | ✔ | - | - | | | ICO | MS Windows Icon Format | ✔ | ✔ | - | -| [HDR](https://github.com/haraldk/TwelveMonkeys/wiki/HDR-Plugin) | HDR | Radiance High Dynamic Range RGBE Format | ✔ | - | Standard | +| [HDR](https://github.com/haraldk/TwelveMonkeys/wiki/HDR-Plugin) | HDR | Radiance High Dynamic Range RGBE Format | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | | [ICNS](https://github.com/haraldk/TwelveMonkeys/wiki/ICNS-Plugin) | ICNS | Apple Icon Image | ✔ | ✔ | - | -| [IFF](https://github.com/haraldk/TwelveMonkeys/wiki/IFF-Plugin) | IFF | Commodore Amiga/Electronic Arts Interchange File Format | ✔ | ✔ | Standard | -| [JPEG](https://github.com/haraldk/TwelveMonkeys/wiki/JPEG-Plugin) | **JPEG** | Joint Photographers Expert Group | ✔ | ✔ | Native & Standard | -| | JPEG Lossless | | ✔ | - | Native & Standard | -| [PCX](https://github.com/haraldk/TwelveMonkeys/wiki/PCX-Plugin) | PCX | ZSoft Paintbrush Format | ✔ | - | Standard | -| | DCX | Multi-page PCX fax document | ✔ | - | Standard | -| [PICT](https://github.com/haraldk/TwelveMonkeys/wiki/PICT-Plugin) | PICT | Apple QuickTime Picture Format | ✔ | ✔ | Standard | -| | PNTG | Apple MacPaint Picture Format | ✔ | | Standard | -| [PNM](https://github.com/haraldk/TwelveMonkeys/wiki/PNM-Plugin) | PAM | NetPBM Portable Any Map | ✔ | ✔ | Standard | -| | PBM | NetPBM Portable Bit Map | ✔ | - | Standard | -| | PGM | NetPBM Portable Grey Map | ✔ | - | Standard | -| | PPM | NetPBM Portable Pix Map | ✔ | ✔ | Standard | -| | PFM | Portable Float Map | ✔ | - | Standard | -| [PSD](https://github.com/haraldk/TwelveMonkeys/wiki/PSD-Plugin) | **PSD** | Adobe Photoshop Document | ✔ | - | Native & Standard | -| | PSB | Adobe Photoshop Large Document | ✔ | - | Native & Standard | -| [SGI](https://github.com/haraldk/TwelveMonkeys/wiki/SGI-Plugin) | SGI | Silicon Graphics Image Format | ✔ | - | Standard | -| [TGA](https://github.com/haraldk/TwelveMonkeys/wiki/TGA-Plugin) | TGA | Truevision TGA Image Format | ✔ | ✔ | Standard | +| [IFF](https://github.com/haraldk/TwelveMonkeys/wiki/IFF-Plugin) | IFF | Commodore Amiga/Electronic Arts Interchange File Format | ✔ | ✔ | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [JPEG](https://github.com/haraldk/TwelveMonkeys/wiki/JPEG-Plugin) | **JPEG** | Joint Photographers Expert Group | ✔ | ✔ | [Native](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/jpeg_metadata.html#image) & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | JPEG Lossless | | ✔ | - | [Native](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/jpeg_metadata.html#image) & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [PCX](https://github.com/haraldk/TwelveMonkeys/wiki/PCX-Plugin) | PCX | ZSoft Paintbrush Format | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | DCX | Multi-page PCX fax document | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [PICT](https://github.com/haraldk/TwelveMonkeys/wiki/PICT-Plugin) | PICT | Apple QuickTime Picture Format | ✔ | ✔ | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PNTG | Apple MacPaint Picture Format | ✔ | | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [PNM](https://github.com/haraldk/TwelveMonkeys/wiki/PNM-Plugin) | PAM | NetPBM Portable Any Map | ✔ | ✔ | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PBM | NetPBM Portable Bit Map | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PGM | NetPBM Portable Grey Map | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PPM | NetPBM Portable Pix Map | ✔ | ✔ | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PFM | Portable Float Map | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [PSD](https://github.com/haraldk/TwelveMonkeys/wiki/PSD-Plugin) | **PSD** | Adobe Photoshop Document | ✔ | - | Native & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | PSB | Adobe Photoshop Large Document | ✔ | - | Native & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [SGI](https://github.com/haraldk/TwelveMonkeys/wiki/SGI-Plugin) | SGI | Silicon Graphics Image Format | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [TGA](https://github.com/haraldk/TwelveMonkeys/wiki/TGA-Plugin) | TGA | Truevision TGA Image Format | ✔ | ✔ | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | |ThumbsDB| Thumbs.db| MS Windows Thumbs DB | ✔ | - | - | OLE2 Compound Document based format only -| [TIFF](https://github.com/haraldk/TwelveMonkeys/wiki/TIFF-Plugin) | **TIFF** | Aldus/Adobe Tagged Image File Format | ✔ | ✔ | Native & Standard | -| | BigTIFF | | ✔ | - | Native & Standard | -| [WebP](https://github.com/haraldk/TwelveMonkeys/wiki/WebP-Plugin) | **WebP** | Google WebP Format | ✔ | - | Standard | In progress -| XWD | XWD | X11 Window Dump Format | ✔ | - | Standard | +| [TIFF](https://github.com/haraldk/TwelveMonkeys/wiki/TIFF-Plugin) | **TIFF** | Aldus/Adobe Tagged Image File Format | ✔ | ✔ | [Native](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/tiff_metadata.html#ImageMetadata) & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| | BigTIFF | | ✔ | - | [Native](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/tiff_metadata.html#ImageMetadata) & [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | +| [WebP](https://github.com/haraldk/TwelveMonkeys/wiki/WebP-Plugin) | **WebP** | Google WebP Format | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | In progress +| XWD | XWD | X11 Window Dump Format | ✔ | - | [Standard](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/metadata/doc-files/standard_metadata.html) | **Important note on using Batik:** *Please read [The Apache™ XML Graphics Project - Security](http://xmlgraphics.apache.org/security.html), diff --git a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java index 23b4e057..863f04b7 100644 --- a/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java +++ b/common/common-io/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTest.java @@ -32,13 +32,13 @@ package com.twelvemonkeys.io.enc; import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.ObjectAbstractTest; + import org.junit.Test; import java.io.*; -import java.util.Arrays; import java.util.Random; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.fail; /** @@ -73,7 +73,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { } } - private byte[] createData(final int pLength) throws Exception { + private byte[] createData(final int pLength) { byte[] bytes = new byte[pLength]; RANDOM.nextBytes(bytes); return bytes; @@ -82,9 +82,8 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { private void runStreamTest(final int pLength) throws Exception { byte[] data = createData(pLength); ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); - OutputStream out = new EncoderStream(outBytes, createEncoder(), true); - try { + try (OutputStream out = new EncoderStream(outBytes, createEncoder(), true)) { // Provoke failure for encoders that doesn't take array offset properly into account int off = (data.length + 1) / 2; out.write(data, 0, off); @@ -92,9 +91,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { out.write(data, off, data.length - off); } } - finally { - out.close(); - } byte[] encoded = outBytes.toByteArray(); @@ -102,7 +98,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { // System.err.println("encoded: " + Arrays.toString(encoded)); byte[] decoded = FileUtil.read(new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder())); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(data, decoded); InputStream in = new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder()); outBytes = new ByteArrayOutputStream(); @@ -116,7 +112,7 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { } decoded = outBytes.toByteArray(); - assertTrue(Arrays.equals(data, decoded)); + assertArrayEquals(data, decoded); } @Test @@ -129,10 +125,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } for (int i = 100; i < 2000; i += 250) { @@ -143,10 +135,6 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } for (int i = 2000; i < 80000; i += 1000) { @@ -157,14 +145,8 @@ public abstract class EncoderAbstractTest extends ObjectAbstractTest { e.printStackTrace(); fail(e.getMessage() + ": " + i); } - catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage() + ": " + i); - } } } - // TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset. - - + // TODO: Test that the transition from byte[] to ByteBuffer didn't introduce bugs when writing to a wrapped array with offset. } diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java index a354be08..bb144842 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java @@ -330,7 +330,7 @@ abstract class AbstractDecoratedMap extends AbstractMap implements M } /** - * A simple Map.Entry implementaton. + * A simple Map.Entry implementation. */ static class BasicEntry implements Entry, Serializable { K mKey; diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java index b3199f56..50dd8839 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/TimeoutMap.java @@ -34,7 +34,7 @@ import java.io.Serializable; import java.util.*; /** - * A {@code Map} implementation that removes (exipres) its elements after + * A {@code Map} implementation that removes (expires) its elements after * a given period. The map is by default backed by a {@link java.util.HashMap}, * or can be instantiated with any given {@code Map} as backing. *

@@ -67,7 +67,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi protected long expiryTime = 60000L; // 1 minute ////////////////////// - private volatile long nextExpiryTime; + private volatile long nextExpiryTime = Long.MAX_VALUE; ////////////////////// /** @@ -178,7 +178,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @return {@code true} if this map contains no key-value mappings. */ public boolean isEmpty() { - return (size() <= 0); + return size() <= 0; } /** @@ -208,7 +208,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * @see #containsKey(java.lang.Object) */ public V get(Object pKey) { - TimedEntry entry = (TimedEntry) entries.get(pKey); + TimedEntry entry = (TimedEntry) entries.get(pKey); if (entry == null) { return null; @@ -236,7 +236,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * {@code null} values. */ public V put(K pKey, V pValue) { - TimedEntry entry = (TimedEntry) entries.get(pKey); + TimedEntry entry = (TimedEntry) entries.get(pKey); V oldValue; if (entry == null) { @@ -272,7 +272,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi * {@code null} values. */ public V remove(Object pKey) { - TimedEntry entry = (TimedEntry) entries.remove(pKey); + TimedEntry entry = (TimedEntry) entries.remove(pKey); return (entry != null) ? entry.getValue() : null; } @@ -284,13 +284,12 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi init(); } - /*protected*/ TimedEntry createEntry(K pKey, V pValue) { - return new TimedEntry(pKey, pValue); + /*protected*/ TimedEntry createEntry(K pKey, V pValue) { + return new TimedEntry(pKey, pValue); } /** * Removes any expired mappings. - * */ protected void removeExpiredEntries() { // Remove any expired elements @@ -312,7 +311,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi long next = Long.MAX_VALUE; nextExpiryTime = next; // Avoid multiple runs... for (Iterator> iterator = new EntryIterator(); iterator.hasNext();) { - TimedEntry entry = (TimedEntry) iterator.next(); + TimedEntry entry = (TimedEntry) iterator.next(); //// long expires = entry.expires(); if (expires < next) { @@ -376,7 +375,7 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi while (mNext == null && mIterator.hasNext()) { Entry> entry = mIterator.next(); - TimedEntry timed = (TimedEntry) entry.getValue(); + TimedEntry timed = (TimedEntry) entry.getValue(); if (timed.isExpiredBy(mNow)) { // Remove from map, and continue @@ -425,19 +424,28 @@ public class TimeoutMap extends AbstractDecoratedMap implements Expi /** * Keeps track of timed objects */ - private class TimedEntry extends BasicEntry { + private class TimedEntry extends BasicEntry { private long mTimestamp; TimedEntry(K pKey, V pValue) { super(pKey, pValue); - mTimestamp = System.currentTimeMillis(); + updateTimestamp(); } public V setValue(V pValue) { - mTimestamp = System.currentTimeMillis(); + updateTimestamp(); return super.setValue(pValue); } + private void updateTimestamp() { + mTimestamp = System.currentTimeMillis(); + + long expires = expires(); + if (expires < nextExpiryTime) { + nextExpiryTime = expires; + } + } + final boolean isExpired() { return isExpiredBy(System.currentTimeMillis()); } diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/TimeoutMapTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/TimeoutMapTest.java index 1ca479b5..dfd297e0 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/TimeoutMapTest.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/TimeoutMapTest.java @@ -557,7 +557,7 @@ public class TimeoutMapTest extends MapAbstractTest { // NOTE: Only wait fist time, to avoid slooow tests synchronized (this) { try { - wait(60l); + wait(60L); } catch (InterruptedException e) { } @@ -591,7 +591,7 @@ public class TimeoutMapTest extends MapAbstractTest { try { wait(60l); } - catch (InterruptedException e) { + catch (InterruptedException ignore) { } } } @@ -651,5 +651,24 @@ public class TimeoutMapTest extends MapAbstractTest { assertTrue("Wrong entry removed, keySet().iterator() is broken.", !map.containsKey(removedKey)); assertTrue("Wrong entry removed, keySet().iterator() is broken.", map.containsKey(otherKey)); } + + + @Test + public void testContainsKeyOnEmptyMap() { + // See #600 + Map timeoutMap = new TimeoutMap<>(30); + assertFalse(timeoutMap.containsKey("xyz")); + timeoutMap.put("xyz", "xyz"); + assertTrue(timeoutMap.containsKey("xyz")); + + try { + Thread.sleep(50); // Let the item expire + } + catch (InterruptedException ignore) { + } + + assertFalse(timeoutMap.containsKey("xyz")); + assertNull(timeoutMap.get("xyz")); + } } 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 97d8e748..78de748e 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 @@ -97,10 +97,6 @@ public abstract class ImageReaderAbstractTest { protected abstract List getMIMETypes(); - protected boolean allowsNullRawImageType() { - return false; - } - protected static void failBecause(String message, Throwable exception) { throw new AssertionError(message, exception); } @@ -221,6 +217,7 @@ public abstract class ImageReaderAbstractTest { image = reader.read(i); } catch (Exception e) { + e.printStackTrace(); failBecause(String.format("Image %s index %s could not be read: %s", data.getInput(), i, e), e); } @@ -1359,9 +1356,6 @@ public abstract class ImageReaderAbstractTest { reader.setInput(data.getInputStream()); ImageTypeSpecifier rawType = reader.getRawImageType(0); - if (rawType == null && allowsNullRawImageType()) { - continue; - } assertNotNull(rawType); Iterator types = reader.getImageTypes(0); @@ -1383,6 +1377,7 @@ public abstract class ImageReaderAbstractTest { assertTrue("ImageTypeSpecifier from getRawImageType should be in the iterator from getImageTypes", rawFound); } + reader.dispose(); } diff --git a/imageio/imageio-jpeg-jai-interop/pom.xml b/imageio/imageio-jpeg-jai-interop/pom.xml new file mode 100644 index 00000000..a87cc5f5 --- /dev/null +++ b/imageio/imageio-jpeg-jai-interop/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.7-SNAPSHOT + + imageio-jpeg-jai-interop + TwelveMonkeys :: ImageIO :: JPEG/JAI TIFF Interop + + Test JPEG plugin and JAI TIFF plugin interoperability + + + + + com.github.jai-imageio + jai-imageio-core + 1.3.0 + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + test-jar + test + + + com.twelvemonkeys.imageio + imageio-metadata + + + com.twelvemonkeys.imageio + imageio-jpeg + + + diff --git a/imageio/imageio-jpeg-jai-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jaiinterop/JAITIFFImageReaderInteroperabilityTest.java b/imageio/imageio-jpeg-jai-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jaiinterop/JAITIFFImageReaderInteroperabilityTest.java new file mode 100644 index 00000000..6401059a --- /dev/null +++ b/imageio/imageio-jpeg-jai-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jaiinterop/JAITIFFImageReaderInteroperabilityTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.jpeg.jaiinterop; + +import com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import org.junit.Ignore; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * Tests the JAI TIFFImageReader delegating to our JPEGImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: JAITIFFImageReaderInteroperabilityTest.java,v 1.0 08.05.12 15:25 haraldk Exp$ + */ +public class JAITIFFImageReaderInteroperabilityTest extends ImageReaderAbstractTest { + private static final String JAI_TIFF_PROVIDER_CLASS_NAME = "com.github.jaiimageio.impl.plugins.tiff.TIFFImageReaderSpi"; + + @Override + protected ImageReaderSpi createProvider() { + Iterator providers = IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() { + @Override + public boolean filter(final Object provider) { + return JAI_TIFF_PROVIDER_CLASS_NAME.equals(provider.getClass().getName()); + } + }, true); + + if (providers.hasNext()) { + return providers.next(); + } + + return null; + } + + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/tiff/foto_0001.tif"), new Dimension(1663, 2338)), // Little endian, Old JPEG + new TestData(getClassLoaderResource("/tiff/cmyk_jpeg.tif"), new Dimension(100, 100)), // CMYK, JPEG compressed, with ICC profile + new TestData(getClassLoaderResource("/tiff/jpeg-lossless-8bit-gray.tif"), new Dimension(512, 512)) // Lossless JPEG Gray, 8 bit/sample + ); + } + + @Override + protected List getFormatNames() { + return Collections.emptyList(); + } + + @Override + protected List getSuffixes() { + return Collections.emptyList(); + } + + @Override + protected List getMIMETypes() { + return Collections.emptyList(); + } + + @Ignore("Fails in TIFFImageReader") + @Override + public void testSetDestinationIllegal() { + } + + @Test + public void testReallyUsingOurJPEGImageReader() { + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + + if (readers.hasNext()) { + ImageReader reader = readers.next(); + + if ((reader.getOriginatingProvider() instanceof JPEGImageReaderSpi)) { + return; + } + } + + fail("Expected Spi not registered (dependency issue?): " + JPEGImageReaderSpi.class); + } +} diff --git a/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/cmyk_jpeg.tif b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/cmyk_jpeg.tif new file mode 100644 index 00000000..85d0bc92 Binary files /dev/null and b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/cmyk_jpeg.tif differ diff --git a/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/foto_0001.tif b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/foto_0001.tif new file mode 100644 index 00000000..a8f80d87 Binary files /dev/null and b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/foto_0001.tif differ diff --git a/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif new file mode 100755 index 00000000..dfa11295 Binary files /dev/null and b/imageio/imageio-jpeg-jai-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif differ diff --git a/imageio/imageio-jpeg-jep262-interop/pom.xml b/imageio/imageio-jpeg-jep262-interop/pom.xml new file mode 100644 index 00000000..9f1d0f5b --- /dev/null +++ b/imageio/imageio-jpeg-jep262-interop/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.7-SNAPSHOT + + imageio-jpeg-jep262-interop + TwelveMonkeys :: ImageIO :: JPEG/JEP-262 Interop + + Test JPEG plugin and JEP-262 (JDK TIFF plugin) interoperability + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + test-jar + test + + + com.twelvemonkeys.imageio + imageio-metadata + + + com.twelvemonkeys.imageio + imageio-jpeg + + + diff --git a/imageio/imageio-jpeg-jep262-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jep262interop/JEP262TIFFImageReaderInteroperabilityTest.java b/imageio/imageio-jpeg-jep262-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jep262interop/JEP262TIFFImageReaderInteroperabilityTest.java new file mode 100644 index 00000000..5b197393 --- /dev/null +++ b/imageio/imageio-jpeg-jep262-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jep262interop/JEP262TIFFImageReaderInteroperabilityTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.jpeg.jep262interop; + +import com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import org.junit.Ignore; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ServiceRegistry; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +/** + * Tests the JEP 262 TIFFImageReader delegating to our JPEGImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFImageReaderTest.java,v 1.0 08.05.12 15:25 haraldk Exp$ + */ +public class JEP262TIFFImageReaderInteroperabilityTest extends ImageReaderAbstractTest { + private static final String JEP_262_PROVIDER_CLASS_NAME = "com.sun.imageio.plugins.tiff.TIFFImageReaderSpi"; + + @Override + protected ImageReaderSpi createProvider() { + Iterator providers = IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, new ServiceRegistry.Filter() { + @Override + public boolean filter(final Object provider) { + return JEP_262_PROVIDER_CLASS_NAME.equals(provider.getClass().getName()) && ((ImageReaderSpi) provider).getVendorName().startsWith("Oracle"); + } + }, true); + + if (providers.hasNext()) { + return providers.next(); + } + + // Skip tests if we have no Spi (ie. pre JDK 9) + assumeTrue("Provider " + JEP_262_PROVIDER_CLASS_NAME + " not found", false); + + return null; + } + + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/tiff/foto_0001.tif"), new Dimension(1663, 2338)), // Little endian, Old JPEG + new TestData(getClassLoaderResource("/tiff/cmyk_jpeg.tif"), new Dimension(100, 100)), // CMYK, JPEG compressed, with ICC profile + new TestData(getClassLoaderResource("/tiff/jpeg-lossless-8bit-gray.tif"), new Dimension(512, 512)) // Lossless JPEG Gray, 8 bit/sample + ); + } + + @Override + protected List getFormatNames() { + return Collections.emptyList(); + } + + @Override + protected List getSuffixes() { + return Collections.emptyList(); + } + + @Override + protected List getMIMETypes() { + return Collections.emptyList(); + } + + @Ignore("Fails in TIFFImageReader") + @Override + public void testSetDestinationIllegal() { + } + + @Test + public void testReallyUsingOurJPEGImageReader() { + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + + if (readers.hasNext()) { + ImageReader reader = readers.next(); + + if ((reader.getOriginatingProvider() instanceof JPEGImageReaderSpi)) { + return; + } + } + + fail("Expected Spi not registered (dependency issue?): " + JPEGImageReaderSpi.class); + } +} diff --git a/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/cmyk_jpeg.tif b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/cmyk_jpeg.tif new file mode 100644 index 00000000..85d0bc92 Binary files /dev/null and b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/cmyk_jpeg.tif differ diff --git a/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/foto_0001.tif b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/foto_0001.tif new file mode 100644 index 00000000..a8f80d87 Binary files /dev/null and b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/foto_0001.tif differ diff --git a/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif new file mode 100755 index 00000000..dfa11295 Binary files /dev/null and b/imageio/imageio-jpeg-jep262-interop/src/test/resources/tiff/jpeg-lossless-8bit-gray.tif differ diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java index cecc9d01..a271a96b 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGB.java @@ -62,6 +62,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { * @return {@code dest}, or a new {@link WritableRaster} if {@code dest} is {@code null}. * @throws IllegalArgumentException if {@code src} and {@code dest} refer to the same object */ + @Override public WritableRaster filter(Raster src, WritableRaster dest) { Validate.notNull(src, "src may not be null"); // TODO: Why not allow same raster, if converting to 4 byte ABGR? @@ -142,10 +143,12 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { rgb[2] = (byte) (255 - (((cmyk[2] & 0xFF) * (255 - k) / 255) + k)); } + @Override public Rectangle2D getBounds2D(Raster src) { return src.getBounds(); } + @Override public WritableRaster createCompatibleDestRaster(final Raster src) { // WHAT?? This code no longer work for JRE 7u45+... JRE bug?! // Raster child = src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2}); @@ -157,6 +160,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0, 1, 2}); } + @Override public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { if (dstPt == null) { dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY()); @@ -168,6 +172,7 @@ class FastCMYKToRGB implements /*BufferedImageOp,*/ RasterOp { return dstPt; } + @Override public RenderingHints getRenderingHints() { return null; } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 2a152f92..53a89217 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -194,73 +194,38 @@ public final class JPEGImageReader extends ImageReaderBase { @Override public Iterator getImageTypes(int imageIndex) throws IOException { - checkBounds(imageIndex); - initHeader(imageIndex); + ImageTypeSpecifier rawImageType = getRawImageType(imageIndex); + ColorModel rawColorModel = rawImageType.getColorModel(); + JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); - Iterator types; - try { - types = delegate.getImageTypes(0); - } - catch (IndexOutOfBoundsException | NegativeArraySizeException ignore) { - types = null; - } + Set types = new LinkedHashSet<>(); - JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); - - if (types == null || !types.hasNext() || csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { - ArrayList typeList = new ArrayList<>(); - // Add the standard types, we can always convert to these - typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); - typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); - typeList.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)); - - // We also read and return CMYK if the source image is CMYK/YCCK + original color profile if present - ICC_Profile profile = getEmbeddedICCProfile(false); - - if (csType == JPEGColorSpace.CMYK || csType == JPEGColorSpace.YCCK) { - if (profile != null && profile.getNumComponents() == 4) { - typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); - } - - typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false)); - } - else if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.RGB) { - if (profile != null && profile.getNumComponents() == 3) { - typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false)); - } - } - else if (csType == JPEGColorSpace.YCbCrA || csType == JPEGColorSpace.RGBA) { - // Prepend ARGB types - typeList.addAll(0, Arrays.asList( - ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB), - ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR), - ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE), - ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE) - )); - - if (profile != null && profile.getNumComponents() == 3) { - typeList.add(ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[] {0, 1, 2, 3}, DataBuffer.TYPE_BYTE, true, false)); - } + if (rawColorModel.getColorSpace().getType() != ColorSpace.TYPE_GRAY) { + // Add the standard types, we can always convert to these, except for gray + if (rawColorModel.hasAlpha()) { + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR)); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE)); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE)); } - return typeList.iterator(); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR)); } - else if (csType == JPEGColorSpace.RGB) { - // Bug in com.sun...JPEGImageReader: returns gray as acceptable type, but refuses to convert - ArrayList typeList = new ArrayList<>(); - // Filter out the gray type - while (types.hasNext()) { - ImageTypeSpecifier type = types.next(); - if (type.getBufferedImageType() != BufferedImage.TYPE_BYTE_GRAY) { - typeList.add(type); - } + types.add(rawImageType); + + // If the source type has a luminance (Y) component, we can also convert to gray + if (sourceCSType != JPEGColorSpace.RGB && sourceCSType != JPEGColorSpace.RGBA && sourceCSType != JPEGColorSpace.CMYK) { + if (rawColorModel.hasAlpha()) { + types.add(ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE, false)); } - return typeList.iterator(); + types.add(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY)); } - return types; + return types.iterator(); } @Override @@ -268,34 +233,55 @@ public final class JPEGImageReader extends ImageReaderBase { checkBounds(imageIndex); initHeader(imageIndex); - // If delegate can determine the spec, we'll just go with that - try { - ImageTypeSpecifier rawType = delegate.getRawImageType(0); - - if (rawType != null) { - return rawType; - } - } - catch (IIOException | NullPointerException | ArrayIndexOutOfBoundsException | NegativeArraySizeException ignore) { - // Fall through - } - - // Otherwise, consult the image metadata + // Consult the image metadata JPEGColorSpace csType = getSourceCSType(getJFIF(), getAdobeDCT(), getSOF()); + ICC_Profile profile = getEmbeddedICCProfile(false); + + ColorSpace cs; + boolean hasAlpha = false; switch (csType) { - case CMYK: - // Create based on embedded profile if exists, or create from "Generic CMYK" - ICC_Profile profile = getEmbeddedICCProfile(false); + case GrayA: + hasAlpha = true; + case Gray: + // Create based on embedded profile if exists, otherwise create from Gray + cs = profile != null && profile.getNumComponents() == 1 + ? ColorSpaces.createColorSpace(profile) + : ColorSpaces.getColorSpace(ColorSpace.CS_GRAY); + return ImageTypeSpecifiers.createInterleaved(cs, hasAlpha ? new int[] {1, 0} : new int[] {0}, DataBuffer.TYPE_BYTE, hasAlpha, false); - if (profile != null && profile.getNumComponents() == 4) { - return ImageTypeSpecifiers.createInterleaved(ColorSpaces.createColorSpace(profile), new int[]{3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); + case YCbCrA: + case RGBA: + case PhotoYCCA: + hasAlpha = true; + case YCbCr: + case RGB: + case PhotoYCC: + // Create based on PhotoYCC profile... + if (csType == JPEGColorSpace.PhotoYCC || csType == JPEGColorSpace.PhotoYCCA) { + cs = ColorSpaces.getColorSpace(ColorSpace.CS_PYCC); + } + else { + // ...or create based on embedded profile if exists, otherwise create from sRGB + cs = profile != null && profile.getNumComponents() == 3 + ? ColorSpaces.createColorSpace(profile) + : ColorSpaces.getColorSpace(ColorSpace.CS_sRGB); } - return ImageTypeSpecifiers.createInterleaved(ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK), new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); + return ImageTypeSpecifiers.createInterleaved(cs, hasAlpha ? new int[] {3, 2, 1, 0} : new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, hasAlpha, false); + + case YCCK: + case CMYK: + // Create based on embedded profile if exists, otherwise create from "Generic CMYK" + cs = profile != null && profile.getNumComponents() == 4 + ? ColorSpaces.createColorSpace(profile) + : ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); + + return ImageTypeSpecifiers.createInterleaved(cs, new int[] {3, 2, 1, 0}, DataBuffer.TYPE_BYTE, false, false); + default: - // For other types, we probably can't give a proper type, return null - return null; + // For other types, we probably can't give a proper type + throw new IIOException("Could not determine JPEG source color space"); } } @@ -322,7 +308,8 @@ public final class JPEGImageReader extends ImageReaderBase { adobeDCT = null; } - JPEGColorSpace sourceCSType = getSourceCSType(getJFIF(), adobeDCT, sof); + JFIF jfif = getJFIF(); + JPEGColorSpace sourceCSType = getSourceCSType(jfif, adobeDCT, sof); if (sof.marker == JPEG.SOF3) { // Read image as lossless @@ -347,20 +334,15 @@ public final class JPEGImageReader extends ImageReaderBase { // We need to apply ICC profile unless the profile is sRGB/default gray (whatever that is) // - or only filter out the bad ICC profiles in the JPEGSegmentImageInputStream. - else if (delegate.canReadRaster() && ( - bogusAdobeDCT || - sourceCSType == JPEGColorSpace.CMYK || - sourceCSType == JPEGColorSpace.YCCK || - profile != null && !ColorSpaces.isCS_sRGB(profile) || - (long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE || - !delegate.getImageTypes(0).hasNext() || - sourceCSType == JPEGColorSpace.YCbCr && getRawImageType(imageIndex) != null)) { // TODO: Issue warning? + else if (bogusAdobeDCT + || profile != null && !ColorSpaces.isCS_sRGB(profile) + || (long) sof.lines * sof.samplesPerLine > Integer.MAX_VALUE + || delegateCSTypeMismatch(jfif, adobeDCT, sof, sourceCSType)) { if (DEBUG) { System.out.println("Reading using raster and extra conversion"); System.out.println("ICC color profile: " + profile); } - // TODO: Possible to optimize slightly, to avoid readAsRaster for non-CMYK and other good types? return readImageAsRasterAndReplaceColorProfile(imageIndex, param, sof, sourceCSType, profile); } @@ -371,6 +353,56 @@ public final class JPEGImageReader extends ImageReaderBase { return delegate.read(0, param); } + private boolean delegateCSTypeMismatch(final JFIF jfif, final AdobeDCT adobeDCT, final Frame startOfFrame, final JPEGColorSpace sourceCSType) throws IOException { + switch (sourceCSType) { + case GrayA: + case RGBA: + case YCbCrA: + case PhotoYCC: + case PhotoYCCA: + case CMYK: + case YCCK: + // These are no longer supported by the delegate, we'll handle ourselves + return true; + } + + try { + ImageTypeSpecifier rawImageType = delegate.getRawImageType(0); + + switch (sourceCSType) { + case Gray: + return rawImageType == null || rawImageType.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_GRAY; + case YCbCr: + // NOTE: For backwards compatibility, null is allowed for YCbCr + if (rawImageType == null) { + return false; + } + + // If We have a JFIF, but with non-standard component Ids, the standard reader mistakes it for RGB + if (jfif != null && (startOfFrame.components[0].id != 1 || startOfFrame.components[1].id != 2 || startOfFrame.components[2].id != 3)) { + return true; + } + // Else, if we have no Adobe marker and no subsampling, the standard reader mistakes it for RGB + else if (adobeDCT == null + && (startOfFrame.components[0].id != 1 || startOfFrame.components[1].id != 2 || startOfFrame.components[2].id != 3) + && (startOfFrame.components[0].hSub == 1 || startOfFrame.components[0].vSub == 1 + || startOfFrame.components[1].hSub == 1 || startOfFrame.components[1].vSub == 1 + || startOfFrame.components[2].hSub == 1 || startOfFrame.components[2].vSub == 1)) { + return true; + } + case RGB: + return rawImageType == null || rawImageType.getColorModel().getColorSpace().getType() != ColorSpace.TYPE_RGB; + default: + // Probably needs special handling, but we don't know what to do... + return false; + } + } + catch (IIOException | NullPointerException | ArrayIndexOutOfBoundsException | NegativeArraySizeException ignore) { + // An exception here is a clear indicator we need to handle conversion + return true; + } + } + private BufferedImage readImageAsRasterAndReplaceColorProfile(int imageIndex, ImageReadParam param, Frame startOfFrame, JPEGColorSpace csType, ICC_Profile profile) throws IOException { int origWidth = getWidth(imageIndex); int origHeight = getHeight(imageIndex); @@ -388,7 +420,10 @@ public final class JPEGImageReader extends ImageReaderBase { RasterOp convert = null; ICC_ColorSpace intendedCS = profile != null ? ColorSpaces.createColorSpace(profile) : null; - if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) { + if (destination.getNumBands() <= 2 && (csType != JPEGColorSpace.Gray && csType != JPEGColorSpace.GrayA)) { + convert = new LumaToGray(); + } + else if (profile != null && (csType == JPEGColorSpace.Gray || csType == JPEGColorSpace.GrayA)) { // com.sun. reader does not do ColorConvertOp for CS_GRAY, even if embedded ICC profile, // probably because IJG native part does it already...? If applied, color looks wrong (too dark)... // convert = new ColorConvertOp(intendedCS, image.getColorModel().getColorSpace(), null); @@ -397,8 +432,7 @@ public final class JPEGImageReader extends ImageReaderBase { // Handle inconsistencies if (startOfFrame.componentsInFrame() != intendedCS.getNumComponents()) { // If ICC profile number of components and startOfFrame does not match, ignore ICC profile - processWarningOccurred(String.format( - "Embedded ICC color profile is incompatible with image data. " + + processWarningOccurred(String.format("Embedded ICC color profile is incompatible with image data. " + "Profile indicates %d components, but SOF%d has %d color components. " + "Ignoring ICC profile, assuming source color space %s.", intendedCS.getNumComponents(), startOfFrame.marker & 0xf, startOfFrame.componentsInFrame(), csType @@ -422,10 +456,7 @@ public final class JPEGImageReader extends ImageReaderBase { ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK); if (cmykCS instanceof ICC_ColorSpace) { - processWarningOccurred( - "No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. " + - "Colors may look incorrect." - ); + processWarningOccurred("No embedded ICC color profile, defaulting to \"generic\" CMYK ICC profile. Colors may look incorrect."); // NOTE: Avoid using CCOp if same color space, as it's more compatible that way if (cmykCS != image.getColorModel().getColorSpace()) { @@ -434,17 +465,11 @@ public final class JPEGImageReader extends ImageReaderBase { } else { // ColorConvertOp using non-ICC CS is deadly slow, fall back to fast conversion instead - processWarningOccurred( - "No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. " + - "Colors may look incorrect." - ); + processWarningOccurred("No embedded ICC color profile, will convert using inaccurate CMYK to RGB conversion. Colors may look incorrect."); convert = new FastCMYKToRGB(); } } - else if (profile != null) { - processWarningOccurred("Embedded ICC color profile is incompatible with Java 2D, color profile will be ignored."); - } // We'll need a read param if (param == null) { @@ -523,10 +548,9 @@ public final class JPEGImageReader extends ImageReaderBase { switch (adobeDCT.transform) { case AdobeDCT.Unknown: return JPEGColorSpace.RGB; - case AdobeDCT.YCC: - return JPEGColorSpace.YCbCr; default: // TODO: Warning! + case AdobeDCT.YCC: return JPEGColorSpace.YCbCr; // assume it's YCbCr } } @@ -556,10 +580,9 @@ public final class JPEGImageReader extends ImageReaderBase { switch (adobeDCT.transform) { case AdobeDCT.Unknown: return JPEGColorSpace.CMYK; - case AdobeDCT.YCCK: - return JPEGColorSpace.YCCK; default: // TODO: Warning! + case AdobeDCT.YCCK: return JPEGColorSpace.YCCK; // assume it's YCCK } } @@ -636,6 +659,12 @@ public final class JPEGImageReader extends ImageReaderBase { try { if (imageInput != null) { + // Need to wrap stream to avoid messing with the byte order of the underlying stream + // in the case we are operating as a delegate for ie. TIFFImageReader. + if (!(imageInput instanceof SubImageInputStream)) { + imageInput = new SubImageInputStream(imageInput, Long.MAX_VALUE); + } + streamOffsets.add(imageInput.getStreamPosition()); } @@ -650,7 +679,7 @@ public final class JPEGImageReader extends ImageReaderBase { private void initDelegate(boolean seekForwardOnly, boolean ignoreMetadata) throws IOException { // JPEGSegmentImageInputStream that filters out/skips bad/unnecessary segments delegate.setInput(imageInput != null - ? new JPEGSegmentImageInputStream(new SubImageInputStream(imageInput, Long.MAX_VALUE), new JPEGSegmentWarningDelegate()) + ? new JPEGSegmentImageInputStream(imageInput, new JPEGSegmentWarningDelegate()) : null, seekForwardOnly, ignoreMetadata); } @@ -705,7 +734,6 @@ public final class JPEGImageReader extends ImageReaderBase { thumbnails = null; initDelegate(seekForwardOnly, ignoreMetadata); - initHeader(); } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java index 084fd3a5..8bb0a114 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGProviderInfo.java @@ -50,8 +50,8 @@ final class JPEGProviderInfo extends ReaderWriterProviderInfo { new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi"}, "com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriter", new String[] {"com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageWriterSpi"}, - false, null, null, null, null, - true, null, null, null, null + false, "javax_imageio_jpeg_stream_1.0", null, null, null, + true, "javax_imageio_jpeg_image_1.0", null, null, null ); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index f32f4d56..028071d7 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -67,7 +67,6 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { private int currentSegment = -1; private Segment segment; - JPEGSegmentImageInputStream(final ImageInputStream stream, final JPEGSegmentWarningListener warningListener) { this.stream = notNull(stream, "stream"); this.warningListener = notNull(warningListener, "warningListener"); @@ -333,7 +332,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { } private void streamInit() throws IOException { - stream.seek(0); + long position = stream.getStreamPosition(); try { int soi = stream.readUnsignedShort(); @@ -342,7 +341,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl { throw new IIOException(String.format("Not a JPEG stream (starts with: 0x%04x, expected SOI: 0x%04x)", soi, JPEG.SOI)); } - segment = new Segment(soi, 0, 0, 2); + segment = new Segment(soi, position, 0, 2); segments.add(segment); currentSegment = segments.size() - 1; // 0 diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/LumaToGray.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/LumaToGray.java new file mode 100644 index 00000000..29327289 --- /dev/null +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/LumaToGray.java @@ -0,0 +1,65 @@ +package com.twelvemonkeys.imageio.plugins.jpeg; + +import com.twelvemonkeys.lang.Validate; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.Raster; +import java.awt.image.RasterOp; +import java.awt.image.WritableRaster; + +/** + * LumaToGray. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LumaToGray.java,v 1.0 10/04/2021 haraldk Exp$ + */ +final class LumaToGray implements RasterOp { + + @Override + public WritableRaster filter(final Raster src, WritableRaster dest) { + Validate.notNull(src, "src may not be null"); + Validate.isTrue(src != dest, "src and dest raster may not be same"); + Validate.isTrue(src.getNumDataElements() >= 3, src.getNumDataElements(), "Luma raster must have at least 3 data elements: %s"); + + if (dest == null) { + dest = createCompatibleDestRaster(src); + } + + // If src and dest have alpha component, keep it, otherwise extract luma only + int[] bandList = src.getNumBands() > 3 && dest.getNumBands() > 1 ? new int[] {0, 3} : new int[] {0}; + dest.setRect(0, 0, src.createChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, bandList)); + + return dest; + } + + @Override + public Rectangle2D getBounds2D(final Raster src) { + return src.getBounds(); + } + + @Override + public WritableRaster createCompatibleDestRaster(final Raster src) { + WritableRaster raster = src.createCompatibleWritableRaster(); + return raster.createWritableChild(0, 0, src.getWidth(), src.getHeight(), 0, 0, new int[] {0}); + } + + @Override + public Point2D getPoint2D(final Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Double(srcPt.getX(), srcPt.getY()); + } + else { + dstPt.setLocation(srcPt); + } + + return dstPt; + } + + @Override + public RenderingHints getRenderingHints() { + return null; + } +} diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java index 10aeb808..c3658a72 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/FastCMYKToRGBTest.java @@ -63,7 +63,7 @@ public class FastCMYKToRGBTest { assertNotNull(pixel); assertEquals(3, pixel.length); byte[] expected = {(byte) 255, (byte) 255, (byte) 255}; - assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); } @Test @@ -95,7 +95,7 @@ public class FastCMYKToRGBTest { assertNotNull(pixel); assertEquals(3, pixel.length); byte[] expected = {(byte) 0, (byte) 0, (byte) 0}; - assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); } } @@ -134,7 +134,7 @@ public class FastCMYKToRGBTest { assertNotNull(pixel); assertEquals(3, pixel.length); byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; - assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); } } @@ -153,7 +153,7 @@ public class FastCMYKToRGBTest { assertNotNull(pixel); assertEquals(3, pixel.length); byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i)}; - assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); } } @@ -172,7 +172,7 @@ public class FastCMYKToRGBTest { assertNotNull(pixel); assertEquals(4, pixel.length); byte[] expected = {(byte) (255 - i), (byte) i, (byte) (127 - i), (byte) 0xff}; - assertTrue(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), Arrays.equals(expected, pixel)); + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); } } 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 3002c390..d4d5c62e 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 @@ -31,6 +31,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.lang.StringUtil; import org.hamcrest.core.IsInstanceOf; @@ -55,6 +56,7 @@ import java.awt.*; import java.awt.color.ColorSpace; import java.awt.color.ICC_Profile; import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.io.*; import java.util.List; @@ -140,11 +142,6 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest getFormatNames() { return Arrays.asList("JPEG", "jpeg", "JPG", "jpg", @@ -422,8 +419,8 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTest imageTypes = reader.getImageTypes(0); + + while (imageTypes.hasNext()) { + ImageTypeSpecifier specifier = imageTypes.next(); + assertNotEquals("RGB JPEGs can't be decoded as Gray as it has no luminance (Y) component", ColorSpace.TYPE_GRAY, specifier.getColorModel().getColorSpace().getType()); + } + + reader.dispose(); + } + + @Test(expected = Exception.class) + public void testRGBAsGray() throws IOException { + final JPEGImageReader reader = createReader(); + try { + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/adobe-unknown-rgb-ids.jpg"))); + + assertEquals(225, reader.getWidth(0)); + assertEquals(156, reader.getHeight(0)); + + final ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(new Rectangle(0, 0, 225, 8)); + param.setDestinationType(ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE)); + + // Should ideally throw IIOException due to destination type mismatch, but throws IllegalArgumentException... + reader.read(0, param); + } + finally { + reader.dispose(); + } + } + + @Test + public void testYCbCrAsGray() throws IOException { + JPEGImageReader reader = createReader(); + reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/jfif-ycbcr-no-subsampling-intel.jpg"))); + + ImageReadParam param = reader.getDefaultReadParam(); + param.setDestinationType(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY)); + + BufferedImage image = reader.read(0, param); + + assertNotNull(image); + assertEquals(BufferedImage.TYPE_BYTE_GRAY, image.getType()); + } + /** * Slightly fuzzy RGB equals method. Tolerance +/-5 steps. */ @@ -1818,7 +1865,7 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestHarald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: LumaToGrayTest.java,v 1.0 10/04/2021 haraldk Exp$ + */ +public class LumaToGrayTest { + @Test + public void testConvertByteYcc() { + LumaToGray convert = new LumaToGray(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 3, null); + WritableRaster result = null; + + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i)}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + byte[] expected = {(byte) i}; + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); + } + } + + @Test + public void testConvertByteYccK() { + LumaToGray convert = new LumaToGray(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = null; + + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) (255 - i), (byte) (127 + i), (byte) 255}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(1, pixel.length); + byte[] expected = {(byte) i}; + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); + } + } + + @Test + public void testConvertByteYccA() { + LumaToGray convert = new LumaToGray(); + + WritableRaster input = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 4, null); + WritableRaster result = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 1, 1, 2, null); + + byte[] pixel = null; + for (int i = 0; i < 255; i++) { + input.setDataElements(0, 0, new byte[] {(byte) i, (byte) 255, (byte) (127 + i), (byte) (255 - i)}); + result = convert.filter(input, result); + pixel = (byte[]) result.getDataElements(0, 0, pixel); + + assertNotNull(pixel); + assertEquals(2, pixel.length); + byte[] expected = {(byte) i, (byte) (255 - i)}; + assertArrayEquals(String.format("Was: %s, expected: %s", Arrays.toString(pixel), Arrays.toString(expected)), expected, pixel); + } + } +} \ No newline at end of file diff --git a/imageio/imageio-reference/pom.xml b/imageio/imageio-reference/pom.xml index c6004607..df36dafa 100644 --- a/imageio/imageio-reference/pom.xml +++ b/imageio/imageio-reference/pom.xml @@ -7,7 +7,7 @@ 3.7-SNAPSHOT imageio-reference - TwelveMonkeys :: ImageIO :: reference test cases + TwelveMonkeys :: ImageIO :: JDK Reference Tests Test cases for the JRE provided ImageReader implementations for reference. diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java index 6a1ee98e..dbfbda11 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoder.java @@ -63,7 +63,8 @@ final class RLEDecoder implements Decoder { buffer.put((byte) data); } - } else { + } + else { for (int b = 0; b < pixel.length; b++) { int data = stream.read(); if (data < 0) { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java new file mode 100755 index 00000000..6cc33585 --- /dev/null +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoder.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.tga; + +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +final class RLEEncoder implements Encoder { + + private final int pixelSize; + + RLEEncoder(final int pixelDepth) { + Validate.isTrue(pixelDepth % Byte.SIZE == 0, "Depth must be a multiple of bytes (8 bits)"); + pixelSize = pixelDepth / Byte.SIZE; + } + + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { + encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + buffer.position(buffer.remaining()); + } + + private void encode(final OutputStream stream, final byte[] buffer, final int pOffset, final int length) throws IOException { + // NOTE: It's best to encode a 2 byte repeat + // run as a replicate run except when preceded and followed by a + // literal run, in which case it's best to merge the three into one + // literal run. Always encode 3 byte repeats as replicate runs. + // Worst case: output = input + (input + 127) / 128 + + int offset = pOffset; + final int max = pOffset + length - pixelSize; + final int maxMinus1 = max - pixelSize; + + while (offset <= max) { + // Compressed run + int run = 1; + while (run < 127 && offset < max && equalPixel(buffer, offset, offset + pixelSize)) { + offset += pixelSize; + run++; + } + + if (run > 1) { + stream.write(0x80 | (run - 1)); + stream.write(buffer, offset, pixelSize); + offset += pixelSize; + } + + // Literal run + int runStart = offset; + run = 0; + while ((run < 127 && ((offset < max && !(equalPixel(buffer, offset, offset + pixelSize))) + || (offset < maxMinus1 && !(equalPixel(buffer, offset, offset + 2 * pixelSize)))))) { + offset += pixelSize; + run++; + } + + // If last pixel, include it in literal run, if space + if (offset == max && run > 0 && run < 127) { + offset += pixelSize; + run++; + } + + if (run > 0) { + stream.write(run - 1); + stream.write(buffer, runStart, run * pixelSize); + } + + // If last pixel, and not space, start new literal run + if (offset == max && (run <= 0 || run >= 127)) { + stream.write(0); + stream.write(buffer, offset, pixelSize); + offset += pixelSize; + } + } + } + + private boolean equalPixel(final byte[] buffer, final int offset, int compareOffset) { + for (int i = 0; i < pixelSize; i++) { + if (buffer[offset + i] != buffer[compareOffset + i]) { + return false; + } + } + + return true; + } +} diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java index 39ee721c..0b3048c9 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAExtensions.java @@ -47,26 +47,26 @@ import static com.twelvemonkeys.imageio.plugins.tga.TGA.EXT_AREA_SIZE; */ final class TGAExtensions { - private String authorName; - private String authorComments; + String authorName; + String authorComments; - private Calendar creationDate; - private String jobId; + Calendar creationDate; + String jobId; - private String softwareId; - private String softwareVersion; + String softwareId; + String softwareVersion; - private int backgroundColor; - private double pixelAspectRatio; - private double gamma; + int backgroundColor; + double pixelAspectRatio; + double gamma; - private long colorCorrectionOffset; - private long postageStampOffset; - private long scanLineOffset; + long colorCorrectionOffset; + long postageStampOffset; + long scanLineOffset; - private int attributeType; + int attributeType; - private TGAExtensions() { + TGAExtensions() { } static TGAExtensions read(final ImageInputStream stream) throws IOException { @@ -142,6 +142,7 @@ final class TGAExtensions { return null; } + //noinspection MagicConstant calendar.set(year, month - 1, date, hourOfDay, minute, second); return calendar; @@ -176,6 +177,7 @@ final class TGAExtensions { } } + @SuppressWarnings("SwitchStatementWithTooFewBranches") public boolean isAlphaPremultiplied() { switch (attributeType) { case 4: diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java index 8b58fb23..e40e6fc6 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAHeader.java @@ -31,7 +31,6 @@ package com.twelvemonkeys.imageio.plugins.tga; import javax.imageio.IIOException; -import javax.imageio.ImageWriteParam; import javax.imageio.stream.ImageInputStream; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; @@ -58,9 +57,9 @@ final class TGAHeader { private int height; private int pixelDepth; private int attributeBits; - private int origin; + int origin; private int interleave; - private String identification; + String identification; private IndexColorModel colorMap; int getImageType() { @@ -119,7 +118,7 @@ final class TGAHeader { '}'; } - static TGAHeader from(final RenderedImage image, final ImageWriteParam param) { + static TGAHeader from(final RenderedImage image, final boolean compressed) { notNull(image, "image"); ColorModel colorModel = image.getColorModel(); @@ -128,7 +127,7 @@ final class TGAHeader { TGAHeader header = new TGAHeader(); header.colorMapType = colorMap != null ? 1 : 0; - header.imageType = getImageType(colorModel, param); + header.imageType = getImageType(colorModel, compressed); header.colorMapStart = 0; header.colorMapSize = colorMap != null ? colorMap.getMapSize() : 0; header.colorMapDepth = colorMap != null ? (colorMap.hasAlpha() ? 32 : 24) : 0; @@ -149,7 +148,7 @@ final class TGAHeader { return header; } - private static int getImageType(final ColorModel colorModel, final ImageWriteParam param) { + private static int getImageType(final ColorModel colorModel, final boolean compressed) { int uncompressedType; if (colorModel instanceof IndexColorModel) { @@ -169,7 +168,7 @@ final class TGAHeader { } } - return uncompressedType | (TGAImageWriteParam.isRLE(param) ? 8 : 0); + return uncompressedType | (compressed ? 8 : 0); } void write(final DataOutput stream) throws IOException { diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java index 8e4e528e..7c9bb0be 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParam.java @@ -30,7 +30,13 @@ package com.twelvemonkeys.imageio.plugins.tga; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + import javax.imageio.ImageWriteParam; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; import java.util.Locale; /** @@ -42,14 +48,29 @@ public final class TGAImageWriteParam extends ImageWriteParam { this(null); } - @SuppressWarnings("WeakerAccess") public TGAImageWriteParam(final Locale locale) { super(locale); + canWriteCompressed = true; compressionTypes = new String[]{"None", "RLE"}; } - static boolean isRLE(final ImageWriteParam param) { - return param != null && param.getCompressionMode() == MODE_EXPLICIT && "RLE".equals(param.getCompressionType()); + static boolean isRLE(final ImageWriteParam param, final IIOMetadata metadata) { + return (param == null || param.canWriteCompressed() && param.getCompressionMode() == MODE_COPY_FROM_METADATA) && "RLE".equals(compressionTypeFromMetadata(metadata)) + || param != null && param.canWriteCompressed() && param.getCompressionMode() == MODE_EXPLICIT && "RLE".equals(param.getCompressionType()); + } + + private static String compressionTypeFromMetadata(final IIOMetadata metadata) { + if (metadata != null) { + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList compressionTypeName = root.getElementsByTagName("CompressionTypeName"); + + if (compressionTypeName.getLength() > 0) { + Node value = compressionTypeName.item(0).getAttributes().getNamedItem("value"); + return value != null ? value.getNodeValue() : null; + } + } + + return null; } } diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java index b5696876..9dbedc87 100644 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriter.java @@ -31,18 +31,26 @@ package com.twelvemonkeys.imageio.plugins.tga; import com.twelvemonkeys.imageio.ImageWriterBase; +import com.twelvemonkeys.imageio.util.IIOUtil; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; +import com.twelvemonkeys.io.LittleEndianDataOutputStream; +import com.twelvemonkeys.io.enc.EncoderStream; +import com.twelvemonkeys.lang.Validate; import javax.imageio.*; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.image.*; +import java.io.DataOutput; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; +import static com.twelvemonkeys.imageio.plugins.tga.TGAImageWriteParam.isRLE; import static com.twelvemonkeys.lang.Validate.notNull; /** @@ -55,13 +63,23 @@ final class TGAImageWriter extends ImageWriterBase { @Override public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { - TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), param); + Validate.notNull(imageType, "imageType"); + + TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), isRLE(param, null)); return new TGAMetadata(header, null); } @Override public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) { - return null; + Validate.notNull(inData, "inData"); + Validate.notNull(imageType, "imageType"); + + if (inData instanceof TGAMetadata) { + return inData; + } + + // TODO: Make metadata mutable, and do actual merge + return getDefaultImageMetadata(imageType, param); } @Override @@ -73,16 +91,23 @@ final class TGAImageWriter extends ImageWriterBase { } } + @Override + public ImageWriteParam getDefaultWriteParam() { + return new TGAImageWriteParam(getLocale()); + } + @Override public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException { assertOutput(); + Validate.notNull(image, "image"); if (image.hasRaster()) { throw new UnsupportedOperationException("Raster not supported"); } + final boolean compressed = isRLE(param, image.getMetadata()); RenderedImage renderedImage = image.getRenderedImage(); - TGAHeader header = TGAHeader.from(renderedImage, param); + TGAHeader header = TGAHeader.from(renderedImage, compressed); header.write(imageOutput); @@ -94,7 +119,7 @@ final class TGAImageWriter extends ImageWriterBase { ? ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false).createBufferedImage(renderedImage.getWidth(), 1).getRaster() : ImageTypeSpecifier.createFromRenderedImage(renderedImage).createBufferedImage(renderedImage.getWidth(), 1).getRaster(); - DataBuffer buffer = rowRaster.getDataBuffer(); + final DataBuffer buffer = rowRaster.getDataBuffer(); for (int tileY = 0; tileY < renderedImage.getNumYTiles(); tileY++) { for (int tileX = 0; tileX < renderedImage.getNumXTiles(); tileX++) { @@ -110,6 +135,8 @@ final class TGAImageWriter extends ImageWriterBase { break; } + DataOutput imageOutput = compressed ? createRLEStream(header, this.imageOutput) : this.imageOutput; + switch (buffer.getDataType()) { case DataBuffer.TYPE_BYTE: rowRaster.setDataElements(0, 0, raster.createChild(0, y, raster.getWidth(), 1, 0, 0, null)); @@ -118,22 +145,37 @@ final class TGAImageWriter extends ImageWriterBase { case DataBuffer.TYPE_USHORT: rowRaster.setDataElements(0, 0, raster.createChild(0, y, raster.getWidth(), 1, 0, 0, null)); short[] shorts = ((DataBufferUShort) buffer).getData(); - imageOutput.writeShorts(shorts, 0, shorts.length); + + // TODO: Get rid of this, due to stupid design in EncoderStream... + ByteBuffer bb = ByteBuffer.allocate(shorts.length * 2); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.asShortBuffer().put(shorts); + imageOutput.write(bb.array()); + // TODO: The below should work just as good +// for (short value : shorts) { +// imageOutput.writeShort(value); +// } break; default: - throw new IIOException("Unsupported data"); + throw new IIOException("Unsupported data type"); } - processImageProgress(tileY * 100f / renderedImage.getNumYTiles()); + if (compressed) { + ((LittleEndianDataOutputStream) imageOutput).close(); + } } + processImageProgress(tileY * 100f / renderedImage.getNumYTiles()); } } // TODO: If we have thumbnails, we need to write extension too. processImageComplete(); + } + private static LittleEndianDataOutputStream createRLEStream(final TGAHeader header, final ImageOutputStream stream) { + return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(header.getPixelDepth()))); } // TODO: Refactor to common util diff --git a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java index 241a800a..f3755c9c 100755 --- a/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java +++ b/imageio/imageio-tga/src/main/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadata.java @@ -78,7 +78,12 @@ final class TGAMetadata extends AbstractMetadata { chroma.appendChild(numChannels); switch (header.getPixelDepth()) { case 8: - numChannels.setAttribute("value", Integer.toString(1)); + if (header.getImageType() == TGA.IMAGETYPE_MONOCHROME || header.getImageType() == TGA.IMAGETYPE_MONOCHROME_RLE) { + numChannels.setAttribute("value", Integer.toString(1)); + } + else { + numChannels.setAttribute("value", Integer.toString(3)); + } break; case 16: if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) { @@ -146,7 +151,7 @@ final class TGAMetadata extends AbstractMetadata { IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); node.appendChild(compressionTypeName); String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE - ? "Uknown" : "RLE"; + ? "Unknown" : "RLE"; compressionTypeName.setAttribute("value", value); IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); @@ -155,7 +160,7 @@ final class TGAMetadata extends AbstractMetadata { return node; default: - // No compreesion + // No compression return null; } } @@ -199,10 +204,10 @@ final class TGAMetadata extends AbstractMetadata { } break; case 24: - bitsPerSample.setAttribute("value", createListValue(3, Integer.toString(8))); + bitsPerSample.setAttribute("value", createListValue(3, "8")); break; case 32: - bitsPerSample.setAttribute("value", createListValue(4, Integer.toString(8))); + bitsPerSample.setAttribute("value", createListValue(4, "8")); break; } diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java new file mode 100644 index 00000000..8ce2e9b1 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEDecoderTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.tga; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderAbstractTest; +import com.twelvemonkeys.io.enc.Encoder; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * RLEDecoderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RLEDecoderTest.java,v 1.0 07/04/2021 haraldk Exp$ + */ +public class RLEDecoderTest extends DecoderAbstractTest { + public Decoder createDecoder() { + return new RLEDecoder(8); + } + + public Encoder createCompatibleEncoder() { + return new RLEEncoder(8); + } + + @Test + public void testRLE8() throws IOException { + RLEDecoder decoder = new RLEDecoder(8); + + ByteBuffer buffer = ByteBuffer.allocate(256); + // Literal run, 2 bytes, compressed run, 8 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0xFF, (byte) 0xF1, (byte) 0x87, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(10, decoded); + assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xF1, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}, Arrays.copyOf(buffer.array(), 10)); + } + + @Test + public void testRLE16() throws IOException { + RLEDecoder decoder = new RLEDecoder(16); + + ByteBuffer buffer = ByteBuffer.allocate(512); + // Literal run, 2 * 2 bytes, compressed run, 8 * 2 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(20, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 20)); + } + + @Test + public void testRLE24() throws IOException { + RLEDecoder decoder = new RLEDecoder(24); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + // Literal run, 2 * 3 bytes, compressed run, 8 * 3 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(30, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 30)); + } + + @Test + public void testRLE32() throws IOException { + RLEDecoder decoder = new RLEDecoder(32); + + ByteBuffer buffer = ByteBuffer.allocate(1024); + // Literal run, 2 * 4 bytes, compressed run, 8 * 4 bytes + ByteArrayInputStream stream = new ByteArrayInputStream(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}); + + int decoded = decoder.decode(stream, buffer); + + assertEquals(40, decoded); + assertArrayEquals(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }, + Arrays.copyOf(buffer.array(), 40)); + } + +} \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java new file mode 100644 index 00000000..be8a4d8c --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/RLEEncoderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.tga; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.EncoderAbstractTest; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertArrayEquals; + +/** + * RLEEncoderTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: RLEEncoderTest.java,v 1.0 07/04/2021 haraldk Exp$ + */ + +public class RLEEncoderTest extends EncoderAbstractTest { + @Override + protected Encoder createEncoder() { + return new RLEEncoder(8); + } + + @Override + protected Decoder createCompatibleDecoder() { + return new RLEDecoder(8); + } + + @Test + public void testRLE8() throws IOException { + RLEEncoder encoder = new RLEEncoder(8); + + // Literal run, 2 bytes, compressed run, 8 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] {(byte) 0xFF, (byte) 0xF1, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}); + ByteArrayOutputStream stream = new ByteArrayOutputStream(10); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] {1, (byte) 0xFF, (byte) 0xF1, (byte) 0x87, (byte) 0xFE}, stream.toByteArray()); + } + + @Test + public void testRLE16() throws IOException { + RLEEncoder encoder = new RLEEncoder(16); + + // Literal run, 2 * 2 bytes, compressed run, 8 * 2 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] {(byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0x00, (byte) 0xFE}); + ByteArrayOutputStream stream = new ByteArrayOutputStream(20); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] {1, (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0x87, (byte) 0x00, (byte) 0xFE}, stream.toByteArray()); + } + + @Test + public void testRLE24() throws IOException { + RLEEncoder encoder = new RLEEncoder(24); + + // Literal run, 2 * 3 bytes, compressed run, 8 * 3 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }); + ByteArrayOutputStream stream = new ByteArrayOutputStream(30); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] { + 1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, + (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE + }, stream.toByteArray()); + } + + @Test + public void testRLE32() throws IOException { + RLEEncoder encoder = new RLEEncoder(32); + + // Literal run, 2 * 4 bytes, compressed run, 8 * 4 bytes + ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, + (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }); + ByteArrayOutputStream stream = new ByteArrayOutputStream(40); + + encoder.encode(stream, buffer); + + assertArrayEquals(new byte[] { + 1, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xF1, (byte) 0xF1, (byte) 0xF1, + (byte) 0x87, (byte) 0x00, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE + }, stream.toByteArray()); + } +} \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java new file mode 100644 index 00000000..d8ff3059 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriteParamTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.tga; + +import org.junit.Test; + +import javax.imageio.ImageWriteParam; +import java.awt.image.BufferedImage; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeFalse; + +/** + * TGAImageWriteParamTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAImageWriteParamTest.java,v 1.0 08/04/2021 haraldk Exp$ + */ +public class TGAImageWriteParamTest { + @Test + public void testDefaultCopyFromMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + assertTrue(param.canWriteCompressed()); + assertEquals(ImageWriteParam.MODE_COPY_FROM_METADATA, param.getCompressionMode()); + } + + @Test + public void testIsRLENoParamNoMetadata() { + assertFalse(TGAImageWriteParam.isRLE(null, null)); + } + + @Test + public void testIsRLEParamCantWriteCompressedNoMetadata() { + // Base class has canWriteCompressed == false, need to test + ImageWriteParam param = new ImageWriteParam(null); + assumeFalse(param.canWriteCompressed()); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamDefaultNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DEFAULT); + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamExplicitNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + + param.setCompressionType("RLE"); + assertTrue(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamDisabledNoMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_DISABLED); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamCopyNoMetadata() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertFalse(TGAImageWriteParam.isRLE(param, null)); + } + + @Test + public void testIsRLEParamCantWriteCompressedAndMetadata() { + // Base class has canWriteCompressed == false, need to test + ImageWriteParam param = new ImageWriteParam(null); + assumeFalse(param.canWriteCompressed()); + + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamCopyAndMetadataNoCompression() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamCopyAndMetadataRLE() { + ImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + + @Test + public void testIsRLEParamExplicitAndMetadata() { + TGAImageWriteParam param = new TGAImageWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + + param.setCompressionType("RLE"); + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null))); + assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null))); + } + +} \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java index 8d5e5b45..8d7e4f37 100644 --- a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAImageWriterTest.java @@ -30,15 +30,18 @@ package com.twelvemonkeys.imageio.plugins.tga; +import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest; +import com.twelvemonkeys.io.FastByteArrayOutputStream; import org.junit.Test; +import org.w3c.dom.NodeList; -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriter; +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; @@ -53,7 +56,7 @@ import java.util.Arrays; import java.util.List; import static com.twelvemonkeys.imageio.util.ImageReaderAbstractTest.assertImageDataEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import static org.junit.Assume.assumeNotNull; /** @@ -84,6 +87,13 @@ public class TGAImageWriterTest extends ImageWriterAbstractTest ); } + @Test + public void testDefaultParamIsTGA() throws IOException { + ImageWriter writer = createWriter(); + assertEquals(writer.getDefaultWriteParam().getClass(), TGAImageWriteParam.class); + writer.dispose(); + } + @Test public void testWriteRead() throws IOException { ImageWriter writer = createWriter(); @@ -108,5 +118,120 @@ public class TGAImageWriterTest extends ImageWriterAbstractTest assertImageDataEquals("Images differ for " + testData, (BufferedImage) testData, image); } } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testWriteReadRLE() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("RLE"); + + for (RenderedImage testData : getTestData()) { + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(4096); + + try (ImageOutputStream stream = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(stream); + writer.write(null, new IIOImage(drawSomething((BufferedImage) testData), null, null), param); + } + + try (ImageInputStream stream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(stream); + + BufferedImage image = reader.read(0); + + assertNotNull(image); + assertImageDataEquals("Images differ for " + testData, (BufferedImage) testData, image); + } + } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testRewriteCompressionCopyFromMetadataUncompressed() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tga/UTC24.TGA"))) { + reader.setInput(input); + IIOImage image = reader.readAll(0, null); + assertNull(findCompressionType(image.getMetadata())); // Sanity + + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(65536); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(output); + + // Copy from metadata should be default, we'll validate here + ImageWriteParam param = writer.getDefaultWriteParam(); + assertEquals(ImageWriteParam.MODE_COPY_FROM_METADATA, param.getCompressionMode()); + + writer.write(null, image, param); + } + + try (ImageInputStream stream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(stream); + IIOMetadata metadata = reader.getImageMetadata(0); + + assertNull(findCompressionType(metadata)); + } + } + + writer.dispose(); + reader.dispose(); + } + + @Test + public void testRewriteCompressionCopyFromMetadataRLE() throws IOException { + ImageWriter writer = createWriter(); + ImageReader reader = ImageIO.getImageReader(writer); + + assumeNotNull(reader); + + try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/tga/CTC24.TGA"))) { + reader.setInput(input); + IIOImage image = reader.readAll(0, null); + assertEquals("RLE", findCompressionType(image.getMetadata())); // Sanity + + FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768); + + try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) { + writer.setOutput(output); + + // Copy from metadata should be default, we'll just go with no param here + writer.write(null, image, null); + } + + try (ImageInputStream inputStream = new ByteArrayImageInputStream(buffer.toByteArray())) { + reader.setInput(inputStream); + IIOMetadata metadata = reader.getImageMetadata(0); + + assertEquals("RLE", findCompressionType(metadata)); + } + } + + writer.dispose(); + reader.dispose(); + } + + private String findCompressionType(IIOMetadata metadata) { + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + NodeList compressionTypeName = root.getElementsByTagName("CompressionTypeName"); + if (compressionTypeName.getLength() > 0) { + return compressionTypeName.item(0).getAttributes().getNamedItem("value").getNodeValue(); + } + + return null; } } \ No newline at end of file diff --git a/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java new file mode 100644 index 00000000..c1bd18e1 --- /dev/null +++ b/imageio/imageio-tga/src/test/java/com/twelvemonkeys/imageio/plugins/tga/TGAMetadataTest.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.tga; + +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.w3c.dom.Node; + +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.util.Calendar; + +import static org.junit.Assert.*; + +/** + * TGAMetadataTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAMetadataTest.java,v 1.0 08/04/2021 haraldk Exp$ + */ +public class TGAMetadataTest { + @Test + public void testStandardFeatures() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + final TGAMetadata metadata = new TGAMetadata(header, null); + + // Standard metadata format + assertTrue(metadata.isStandardMetadataFormatSupported()); + Node root = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName); + assertNotNull(root); + assertTrue(root instanceof IIOMetadataNode); + + // Other formats + assertNull(metadata.getNativeMetadataFormatName()); + assertNull(metadata.getExtraMetadataFormatNames()); + assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { + @Override + public void run() { + metadata.getAsTree("com_foo_bar_1.0"); + } + }); + + // Read-only + assertTrue(metadata.isReadOnly()); + assertThrows(IllegalStateException.class, new ThrowingRunnable() { + @Override + public void run() throws Throwable { + metadata.mergeTree(IIOMetadataFormatImpl.standardMetadataFormatName, new IIOMetadataNode(IIOMetadataFormatImpl.standardMetadataFormatName)); + } + }); + } + + @Test + public void testStandardChromaGray() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(3, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("GRAY", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("1", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + assertNull(blackIsZero.getNextSibling()); // No more children + } + + @Test + public void testStandardChromaRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(3, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("RGB", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + assertNull(blackIsZero.getNextSibling()); // No more children + } + + @Test + public void testStandardChromaPalette() { + byte[] bw = {0, (byte) 0xff}; + IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, -1); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode chroma = metadata.getStandardChromaNode(); + assertNotNull(chroma); + assertEquals("Chroma", chroma.getNodeName()); + assertEquals(4, chroma.getLength()); + + IIOMetadataNode colorSpaceType = (IIOMetadataNode) chroma.getFirstChild(); + assertEquals("ColorSpaceType", colorSpaceType.getNodeName()); + assertEquals("RGB", colorSpaceType.getAttribute("name")); + + IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); + assertEquals("NumChannels", numChannels.getNodeName()); + assertEquals("3", numChannels.getAttribute("value")); + + IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); + assertEquals("BlackIsZero", blackIsZero.getNodeName()); + assertEquals("TRUE", blackIsZero.getAttribute("value")); + + IIOMetadataNode palette = (IIOMetadataNode) blackIsZero.getNextSibling(); + assertEquals("Palette", palette.getNodeName()); + assertEquals(bw.length, palette.getLength()); + + for (int i = 0; i < palette.getLength(); i++) { + IIOMetadataNode item0 = (IIOMetadataNode) palette.item(i); + assertEquals("PaletteEntry", item0.getNodeName()); + assertEquals(String.valueOf(i), item0.getAttribute("index")); + String rgb = String.valueOf(bw[i] & 0xff); + assertEquals(rgb, item0.getAttribute("red")); + assertEquals(rgb, item0.getAttribute("green")); + assertEquals(rgb, item0.getAttribute("blue")); + } + + // TODO: BackgroundIndex == 1?? + } + + @Test + public void testStandardCompressionRLE() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode compression = metadata.getStandardCompressionNode(); + assertNotNull(compression); + assertEquals("Compression", compression.getNodeName()); + assertEquals(2, compression.getLength()); + + IIOMetadataNode compressionTypeName = (IIOMetadataNode) compression.getFirstChild(); + assertEquals("CompressionTypeName", compressionTypeName.getNodeName()); + assertEquals("RLE", compressionTypeName.getAttribute("value")); + + IIOMetadataNode lossless = (IIOMetadataNode) compressionTypeName.getNextSibling(); + assertEquals("Lossless", lossless.getNodeName()); + assertEquals("TRUE", lossless.getAttribute("value")); + + assertNull(lossless.getNextSibling()); // No more children + } + + @Test + public void testStandardCompressionNone() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false); + TGAMetadata metadata = new TGAMetadata(header, null); + + assertNull(metadata.getStandardCompressionNode()); // No compression, all default... + } + + @Test + public void testStandardDataGray() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataRGBA() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("UnsignedIntegral", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8 8 8 8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDataPalette() { + byte[] rgb = new byte[1 << 8]; // Colors doesn't really matter here + IndexColorModel indexColorModel = new IndexColorModel(8, rgb.length, rgb, rgb, rgb, 0); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode data = metadata.getStandardDataNode(); + assertNotNull(data); + assertEquals("Data", data.getNodeName()); + assertEquals(3, data.getLength()); + + IIOMetadataNode planarConfiguration = (IIOMetadataNode) data.getFirstChild(); + assertEquals("PlanarConfiguration", planarConfiguration.getNodeName()); + assertEquals("PixelInterleaved", planarConfiguration.getAttribute("value")); + + IIOMetadataNode sampleFomat = (IIOMetadataNode) planarConfiguration.getNextSibling(); + assertEquals("SampleFormat", sampleFomat.getNodeName()); + assertEquals("Index", sampleFomat.getAttribute("value")); + + IIOMetadataNode bitsPerSample = (IIOMetadataNode) sampleFomat.getNextSibling(); + assertEquals("BitsPerSample", bitsPerSample.getNodeName()); + assertEquals("8", bitsPerSample.getAttribute("value")); + + assertNull(bitsPerSample.getNextSibling()); // No more children + } + + @Test + public void testStandardDimensionNormal() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode dimension = metadata.getStandardDimensionNode(); + assertNotNull(dimension); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(2, dimension.getLength()); + + IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild(); + assertEquals("ImageOrientation", imageOrientation.getNodeName()); + assertEquals("Normal", imageOrientation.getAttribute("value")); + + IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", pixelAspectRatio.getAttribute("value")); + + assertNull(pixelAspectRatio.getNextSibling()); // No more children + } + + @Test + public void testStandardDimensionFlipH() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + header.origin = TGA.ORIGIN_LOWER_LEFT; + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode dimension = metadata.getStandardDimensionNode(); + assertNotNull(dimension); + assertEquals("Dimension", dimension.getNodeName()); + assertEquals(2, dimension.getLength()); + + IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild(); + assertEquals("ImageOrientation", imageOrientation.getNodeName()); + assertEquals("FlipH", imageOrientation.getAttribute("value")); + + IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling(); + assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); + assertEquals("1.0", pixelAspectRatio.getAttribute("value")); + + assertNull(pixelAspectRatio.getNextSibling()); // No more children + } + + @Test + public void testStandardDocument() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode document = metadata.getStandardDocumentNode(); + assertNotNull(document); + assertEquals("Document", document.getNodeName()); + assertEquals(1, document.getLength()); + + IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("1.0", formatVersion.getAttribute("value")); + + assertNull(formatVersion.getNextSibling()); // No more children + } + + @Test + public void testStandardDocumentExtensions() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + TGAExtensions extensions = new TGAExtensions(); + extensions.creationDate = Calendar.getInstance(); + extensions.creationDate.set(2021, Calendar.APRIL, 8, 18, 55, 0); + TGAMetadata metadata = new TGAMetadata(header, extensions); + + IIOMetadataNode document = metadata.getStandardDocumentNode(); + assertNotNull(document); + assertEquals("Document", document.getNodeName()); + assertEquals(2, document.getLength()); + + IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild(); + assertEquals("FormatVersion", formatVersion.getNodeName()); + assertEquals("2.0", formatVersion.getAttribute("value")); + + IIOMetadataNode imageCreationTime = (IIOMetadataNode) formatVersion.getNextSibling(); + assertEquals("ImageCreationTime", imageCreationTime.getNodeName()); + assertEquals("2021", imageCreationTime.getAttribute("year")); + assertEquals("4", imageCreationTime.getAttribute("month")); + assertEquals("8", imageCreationTime.getAttribute("day")); + assertEquals("18", imageCreationTime.getAttribute("hour")); + assertEquals("55", imageCreationTime.getAttribute("minute")); + assertEquals("0", imageCreationTime.getAttribute("second")); + + assertNull(imageCreationTime.getNextSibling()); // No more children + } + + @Test + public void testStandardText() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true); + header.identification = "MY_FILE.TGA"; + + TGAExtensions extensions = new TGAExtensions(); + extensions.softwareId = "TwelveMonkeys"; + extensions.authorName = "Harald K"; + extensions.authorComments = "Comments, comments... "; + + TGAMetadata metadata = new TGAMetadata(header, extensions); + + IIOMetadataNode text = metadata.getStandardTextNode(); + assertNotNull(text); + assertEquals("Text", text.getNodeName()); + assertEquals(4, text.getLength()); + + IIOMetadataNode textEntry = (IIOMetadataNode) text.item(0); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("DocumentName", textEntry.getAttribute("keyword")); + assertEquals(header.getIdentification(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(1); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Software", textEntry.getAttribute("keyword")); + assertEquals(extensions.getSoftware(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(2); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("Artist", textEntry.getAttribute("keyword")); + assertEquals(extensions.getAuthorName(), textEntry.getAttribute("value")); + + textEntry = (IIOMetadataNode) text.item(3); + assertEquals("TextEntry", textEntry.getNodeName()); + assertEquals("UserComment", textEntry.getAttribute("keyword")); + assertEquals(extensions.getAuthorComments(), textEntry.getAttribute("value")); + } + + @Test + public void testStandardTransparencyRGB() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("none", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + + @Test + public void testStandardTransparencyRGBA() { + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + + @Test + public void testStandardTransparencyPalette() { + byte[] bw = {0, (byte) 0xff}; + IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, 1); + TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true); + TGAMetadata metadata = new TGAMetadata(header, null); + + IIOMetadataNode transparency = metadata.getStandardTransparencyNode(); + assertNotNull(transparency); + assertEquals("Transparency", transparency.getNodeName()); + assertEquals(1, transparency.getLength()); + + IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); + assertEquals("Alpha", alpha.getNodeName()); + assertEquals("nonpremultiplied", alpha.getAttribute("value")); + + assertNull(alpha.getNextSibling()); // No more children + } + +} \ No newline at end of file diff --git a/imageio/imageio-tiff-jdk-interop/pom.xml b/imageio/imageio-tiff-jdk-interop/pom.xml new file mode 100644 index 00000000..6eb42888 --- /dev/null +++ b/imageio/imageio-tiff-jdk-interop/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.7-SNAPSHOT + + imageio-tiff-jdk-interop + TwelveMonkeys :: ImageIO :: TIFF/JDK JPEG Interop + + Test TIFF plugin and JDK JPEG plugin interoperability + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + test-jar + test + + + com.twelvemonkeys.imageio + imageio-metadata + + + com.twelvemonkeys.imageio + imageio-tiff + + + diff --git a/imageio/imageio-tiff-jdk-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jdkinterop/TIFFImageReaderJDKJPEGInteroperabilityTest.java b/imageio/imageio-tiff-jdk-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jdkinterop/TIFFImageReaderJDKJPEGInteroperabilityTest.java new file mode 100644 index 00000000..cce05aa5 --- /dev/null +++ b/imageio/imageio-tiff-jdk-interop/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/jdkinterop/TIFFImageReaderJDKJPEGInteroperabilityTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021, 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 of the copyright holder 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 HOLDER 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.jpeg.jdkinterop; + +import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReader; +import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi; +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import org.junit.Ignore; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.fail; + +/** + * Tests our TIFFImageReader delegating to the JDK JPEGImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TIFFImageReaderJDKJPEGInteroperabilityTest.java,v 1.0 08.05.12 15:25 haraldk Exp$ + */ +public class TIFFImageReaderJDKJPEGInteroperabilityTest extends ImageReaderAbstractTest { + private static final String JDK_JPEG_PROVIDER_CLASS_NAME = "com.sun.imageio.plugins.jpeg.JPEGImageReaderSpi"; + + @Override + protected ImageReaderSpi createProvider() { + return new TIFFImageReaderSpi(); + } + + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/tiff/foto_0001.tif"), new Dimension(1663, 2338)), // Little endian, Old JPEG + new TestData(getClassLoaderResource("/tiff/quad-jpeg.tif"), new Dimension(512, 384)), // YCbCr, JPEG compressed, striped + new TestData(getClassLoaderResource("/tiff/cmyk_jpeg.tif"), new Dimension(100, 100)) // CMYK, JPEG compressed, with ICC profile + ); + } + + @Override + protected List getFormatNames() { + return Collections.emptyList(); + } + + @Override + protected List getSuffixes() { + return Collections.emptyList(); + } + + @Override + protected List getMIMETypes() { + return Collections.emptyList(); + } + + @Ignore("Fails in TIFFImageReader") + @Override + public void testSetDestinationIllegal() { + } + + @Test + public void testReallyUsingJDKJPEGImageReader() { + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + + if (readers.hasNext()) { + ImageReader reader = readers.next(); + + if (JDK_JPEG_PROVIDER_CLASS_NAME.equals(reader.getOriginatingProvider().getClass().getName())) { + return; + } + } + + fail("Expected Spi not in use (dependency issue?): " + JDK_JPEG_PROVIDER_CLASS_NAME); + } +} diff --git a/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/cmyk_jpeg.tif b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/cmyk_jpeg.tif new file mode 100644 index 00000000..85d0bc92 Binary files /dev/null and b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/cmyk_jpeg.tif differ diff --git a/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/foto_0001.tif b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/foto_0001.tif new file mode 100644 index 00000000..a8f80d87 Binary files /dev/null and b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/foto_0001.tif differ diff --git a/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/quad-jpeg.tif b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/quad-jpeg.tif new file mode 100755 index 00000000..e14f4c22 Binary files /dev/null and b/imageio/imageio-tiff-jdk-interop/src/test/resources/tiff/quad-jpeg.tif differ 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 e1c51753..2a9b2ffe 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; @@ -67,7 +68,6 @@ import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.plugins.jpeg.JPEGImageReadParam; -import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import java.awt.*; @@ -84,7 +84,8 @@ import java.util.*; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -import static com.twelvemonkeys.imageio.util.IIOUtil.*; +import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter; +import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow; import static java.util.Arrays.asList; /** @@ -131,7 +132,6 @@ public final class TIFFImageReader extends ImageReaderBase { // TODO: Thumbnail support (what is a TIFF thumbnail anyway? Photoshop way? Or use subfiletype?) // TODOs ImageIO advanced functionality: - // TODO: Tiling support (readTile, readTileRaster) // TODO: Implement readAsRenderedImage to allow tiled RenderedImage? // For some layouts, we could do reads super-fast with a memory mapped buffer. // TODO: Implement readRaster directly (100% correctly) @@ -781,11 +781,11 @@ public final class TIFFImageReader extends ImageReaderBase { throw new IIOException("Unsupported BitsPerSample for SampleFormat 2/Signed Integer (expected 8/16/32): " + bitsPerSample); case TIFFExtension.SAMPLEFORMAT_FP: - if (bitsPerSample == 32) { + if (bitsPerSample == 16 || bitsPerSample == 32) { return DataBuffer.TYPE_FLOAT; } - throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 32): " + bitsPerSample); + throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 16/32): " + bitsPerSample); default: throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat); } @@ -1969,7 +1969,9 @@ public final class TIFFImageReader extends ImageReaderBase { case DataBuffer.TYPE_FLOAT: /*for (int band = 0; band < bands; band++)*/ { + boolean needsWidening = getBitsPerSample() == 16; float[] rowDataFloat = ((DataBufferFloat) tileRowRaster.getDataBuffer()).getData(band); + short[] rowDataShort = needsWidening ? new short[rowDataFloat.length] : null; WritableRaster destChannel = banded ? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band}) @@ -1978,12 +1980,19 @@ public final class TIFFImageReader extends ImageReaderBase { ? tileRowRaster.createChild(tileRowRaster.getMinX(), 0, tileRowRaster.getWidth(), 1, 0, 0, new int[] {band}) : tileRowRaster; + for (int row = startRow; row < startRow + rowsInTile; row++) { if (row >= srcRegion.y + srcRegion.height) { break; // We're done with this tile } - readFully(input, rowDataFloat); + if (needsWidening) { + readFully(input, rowDataShort); + toFloat(rowDataFloat, rowDataShort); + } + else { + readFully(input, rowDataFloat); + } if (row >= srcRegion.y) { normalizeColor(interpretation, rowDataFloat); @@ -2008,7 +2017,52 @@ public final class TIFFImageReader extends ImageReaderBase { } } - private void clamp(float[] rowDataFloat) { + private void toFloat(final float[] rowDataFloat, final short[] rowDataShort) { + for (int i = 0; i < rowDataFloat.length; i++) { + rowDataFloat[i] = toFloat(rowDataShort[i]); + } + } + + /** + * Converts an IEEE 754 half-precision data type to single-precision. + * + * @param shortValue a 16 bit half precision value + * @return an IEE 754 single precision float + * + * @see Stack Overflow answer by x4u + * @see >> 8) + || magic[0] == 'M' && magic[1] == 'M' && magic[2] == (versionMagic >>> 8) && magic[3] == (versionMagic & 0xFF); + } + catch (EOFException ignore) { + return false; } finally { stream.reset(); 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 a6573673..799fff2e 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 @@ -102,6 +102,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest imageio-reference + imageio-jpeg-jep262-interop + imageio-jpeg-jai-interop + imageio-tiff-jdk-interop @@ -144,6 +147,12 @@ ${project.version} + + ${project.groupId} + imageio-tiff + ${project.version} + + ${project.groupId} imageio-core diff --git a/pom.xml b/pom.xml index 1daa6990..197bb7fb 100755 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,7 @@ TwelveMonkeys ${project.version} ${project.url} + twelvemonkeys-${project.artifactId}