diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Half.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Half.java new file mode 100644 index 00000000..459c7f2e --- /dev/null +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Half.java @@ -0,0 +1,160 @@ +package com.twelvemonkeys.imageio.metadata.tiff; + +/** + * IEEE 754 half-precision floating point data type. + * + * @see Stack Overflow answer by x4u + * @see Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: Half.java,v 1.0 10/04/2021 haraldk Exp$ + */ +public final class Half extends Number implements Comparable { + // Short, Int, Long + // Half, Float, Double :-) + + public static final int SIZE = 16; + + private final short shortBits; + private final transient float floatValue; + + public Half(short shortBits) { + this.shortBits = shortBits; + this.floatValue = shortBitsToFloat(shortBits); + } + + @Override + public int intValue() { + return (int) floatValue; + } + + @Override + public long longValue() { + return (long) floatValue; + } + + @Override + public float floatValue() { + return floatValue; + } + + @Override + public double doubleValue() { + return floatValue; + } + + public int hashCode() { + return shortBits; + } + + public boolean equals(Object other) { + return (other instanceof Half) + && ((Half) other).shortBits == shortBits; + } + + @Override + public int compareTo(final Half other) { + return Float.compare(floatValue, other.floatValue); + } + + @Override + public String toString() { + return Float.toString(floatValue); + } + + public static Half valueOf(String value) throws NumberFormatException { + return new Half(parseHalf(value)); + } + + public static short parseHalf(String value) throws NumberFormatException { + return floatToShortBits(Float.parseFloat(value)); + } + + /** + * Converts an IEEE 754 half-precision data type to single-precision. + * + * @param shortBits a 16 bit half precision value + * @return an IEE 754 single precision float + * + */ + public static float shortBitsToFloat(final short shortBits) { + int mantissa = shortBits & 0x03ff; // 10 bits mantissa + int exponent = shortBits & 0x7c00; // 5 bits exponent + + if (exponent == 0x7c00) { // NaN/Inf + exponent = 0x3fc00; // -> NaN/Inf + } + else if (exponent != 0) { // Normalized value + exponent += 0x1c000; // exp - 15 + 127 + + // Smooth transition + if (mantissa == 0 && exponent > 0x1c400) { + return Float.intBitsToFloat((shortBits & 0x8000) << 16 | exponent << 13 | 0x3ff); + } + } + else if (mantissa != 0) { // && exp == 0 -> subnormal + exponent = 0x1c400; // Make it normal + + do { + mantissa <<= 1; // mantissa * 2 + exponent -= 0x400; // Decrease exp by 1 + } while ((mantissa & 0x400) == 0); // while not normal + + mantissa &= 0x3ff; // Discard subnormal bit + } // else +/-0 -> +/-0 + + // Combine all parts, sign << (31 - 15), value << (23 - 10) + return Float.intBitsToFloat((shortBits & 0x8000) << 16 | (exponent | mantissa) << 13); + } + + /** + * Converts a float value to IEEE 754 half-precision bits. + * + * @param floatValue a float value + * @return the IEE 754 single precision 16 bits value + * + */ + public static short floatToShortBits(final float floatValue) { + // TODO: Is this okay? Need test + return (short) floatTo16Bits(floatValue); + } + + private static int floatTo16Bits(final float floatValue) { + int fbits = Float.floatToIntBits(floatValue); + int sign = fbits >>> 16 & 0x8000; // sign only + int val = (fbits & 0x7fffffff) + 0x1000; // rounded value + + if (val >= 0x47800000) { // might be or become NaN/Inf, avoid Inf due to rounding + if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf + if (val < 0x7f800000) { // was value but too large + return sign | 0x7c00; // make it +/-Inf + } + + return sign | 0x7c00 | // remains +/-Inf or NaN + (fbits & 0x007fffff) >>> 13;// keep NaN (and Inf) bits + } + + return sign | 0x7bff; // unrounded not quite Inf + } + + if (val >= 0x38800000) { // remains normalized value + return sign | val - 0x38000000 >>> 13; // exp - 127 + 15 + } + + if (val < 0x33000000) { // too small for subnormal + return sign; // becomes +/-0 + } + + val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc + + return sign | ((fbits & 0x7fffff | 0x800000)// add subnormal bit + + (0x800000 >>> val - 102) // round depending on cut off + >>> 126 - val); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0 + } + + // Restores the floatValue on de-serialization + private Object readResolve() { + return new Half(shortBits); + } +} diff --git a/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/HalfTest.java b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/HalfTest.java new file mode 100644 index 00000000..fa27cd51 --- /dev/null +++ b/imageio/imageio-metadata/src/test/java/com/twelvemonkeys/imageio/metadata/tiff/HalfTest.java @@ -0,0 +1,181 @@ +package com.twelvemonkeys.imageio.metadata.tiff; + +import com.twelvemonkeys.io.FastByteArrayOutputStream; + +import org.junit.Test; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * HalfTest. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: HalfTest.java,v 1.0 10/04/2021 haraldk Exp$ + */ +public class HalfTest { + Random random = new Random(8374698541237L); + + @Test + public void testSize() { + assertEquals(16, Half.SIZE); + } + + @Test + public void testRoundTrip() { + for (int i = 0; i < 1024; i++) { + short half = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF); + float floatValue = Half.shortBitsToFloat(half); + assertEquals(half, Half.floatToShortBits(floatValue)); + } + } + + @Test + public void testRoundTripBack() { + for (int i = 0; i < 1024; i++) { + float floatValue = random.nextFloat(); + short half = Half.floatToShortBits(floatValue); + assertEquals(floatValue, Half.shortBitsToFloat(half), 0.0003); // Might lose some precision 32 -> 16 bit + } + } + + @Test + public void testHashCode() { + for (int i = 0; i < 1024; i++) { + short halfBits = (short) random.nextInt(Short.MAX_VALUE); + Half half = new Half(halfBits); + assertEquals(halfBits, half.hashCode()); + } + } + + @Test + public void testEquals() { + for (int i = 0; i < 1024; i++) { + short halfBits = (short) random.nextInt(Short.MAX_VALUE); + Half half = new Half(halfBits); + assertEquals(new Half(halfBits), half); + } + } + + @Test + public void testCompareEquals() { + for (int i = 0; i < 1024; i++) { + short halfBits = (short) random.nextInt(Short.MAX_VALUE); + Half half = new Half(halfBits); + assertEquals(0, new Half(halfBits).compareTo(half)); + } + } + + @Test + public void testCompareLess() { + for (int i = 0; i < 1024; i++) { + short halfBits = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF); + Half half = new Half(halfBits ); + assertEquals(-1, new Half((short) (halfBits - 2)).compareTo(half)); + } + } + + @Test + public void testCompareGreater() { + for (int i = 0; i < 1024; i++) { + short halfBits = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF); + Half half = new Half(halfBits); + assertEquals(1, new Half((short) (halfBits + 2)).compareTo(half)); + } + } + + @Test + public void testToString() { + assertEquals("0.0", new Half((short) 0).toString()); + // TODO: More... But we just delegate to Float.toString, so no worries... :-) + } + + @Test(expected = NullPointerException.class) + public void testParseHAlfNull() { + Half.parseHalf(null); + } + + @Test(expected = NumberFormatException.class) + public void testParseHalfBad() { + Half.parseHalf("foo"); + } + + @Test + public void testParseHalf() { + short half = Half.parseHalf("9876.5432"); + assertEquals(Half.floatToShortBits(9876.5432f), half); + // TODO: More... But we just delegate to Float.valueOf, so no worries... :-) + } + + @Test(expected = NullPointerException.class) + public void testValueOfNull() { + Half.valueOf(null); + } + + @Test(expected = NumberFormatException.class) + public void testValueOfBad() { + Half.valueOf("foo"); + } + + @Test + public void testValueOf() { + Half half = Half.valueOf("12.3456"); + assertEquals(new Half(Half.floatToShortBits(12.3456f)), half); + // TODO: More... But we just delegate to Float.valueOf, so no worries... :-) + } + + @Test + public void testIntValue() { + for (int i = 0; i < 1024; i++) { + int intValue = i << 1; + Half half = new Half(Half.floatToShortBits((float) intValue)); + assertEquals(intValue, half.intValue()); + } + } + + @Test + public void testLongValue() { + for (int i = 0; i < 1024; i++) { + long longValue = i << 2; + Half half = new Half(Half.floatToShortBits((float) longValue)); + assertEquals(longValue, half.longValue()); + } + } + + @Test + public void testFloatValue() { + for (int i = 0; i < 1024; i++) { + float floatValue = random.nextFloat(); + Half half = new Half(Half.floatToShortBits(floatValue)); + assertEquals(floatValue, half.floatValue(), 0.0003); + } + } + + @Test + public void testDoubleValue() { + for (int i = 0; i < 1024; i++) { + double doubleValue = random.nextDouble(); + Half half = new Half(Half.floatToShortBits((float) doubleValue)); + assertEquals(doubleValue, half.doubleValue(), 0.0003); + } + } + + @Test + public void testSerializationRoundTrip() throws IOException, ClassNotFoundException { + Half original = new Half((short) 0x3D75); + FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(64); + new ObjectOutputStream(bytes).writeObject(original); + + Object restored = new ObjectInputStream(bytes.createInputStream()).readObject(); + assertTrue(restored instanceof Half); + assertEquals(original, restored); // Only tests bits, not transient float value + + assertEquals(original.floatValue(), ((Half) restored).floatValue(), 0); + } +} \ No newline at end of file diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 2a9b2ffe..986dc906 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 @@ -42,6 +42,7 @@ import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader; import com.twelvemonkeys.imageio.metadata.jpeg.JPEG; import com.twelvemonkeys.imageio.metadata.psd.PSD; import com.twelvemonkeys.imageio.metadata.psd.PSDReader; +import com.twelvemonkeys.imageio.metadata.tiff.Half; import com.twelvemonkeys.imageio.metadata.tiff.Rational; import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader; @@ -2019,49 +2020,10 @@ public final class TIFFImageReader extends ImageReaderBase { private void toFloat(final float[] rowDataFloat, final short[] rowDataShort) { for (int i = 0; i < rowDataFloat.length; i++) { - rowDataFloat[i] = toFloat(rowDataShort[i]); + rowDataFloat[i] = Half.shortBitsToFloat(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