You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ki...@apache.org on 2021/09/14 01:07:59 UTC

[commons-imaging] 01/02: [IMAGING-285] Correction for rational number computations

This is an automated email from the ASF dual-hosted git repository.

kinow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit 0fb5942c2144f53a0291b8da0184ea9f1520e863
Author: gwlucastrig <co...@gmail.com>
AuthorDate: Mon Sep 13 20:08:04 2021 -0400

    [IMAGING-285] Correction for rational number computations
---
 .../commons/imaging/common/ByteConversions.java    |  45 ++++++---
 .../commons/imaging/common/RationalNumber.java     | 107 +++++++++++++++++++--
 .../formats/tiff/fieldtypes/FieldTypeRational.java |   9 +-
 .../formats/tiff/taginfos/TagInfoRational.java     |   2 +-
 .../formats/tiff/taginfos/TagInfoRationals.java    |   2 +-
 .../formats/tiff/taginfos/TagInfoSRational.java    |   2 +-
 .../formats/tiff/taginfos/TagInfoSRationals.java   |   2 +-
 .../commons/imaging/common/RationalNumberTest.java |  15 +++
 .../formats/tiff/TiffReadWriteTagsTest.java        |  19 +++-
 9 files changed, 174 insertions(+), 29 deletions(-)

diff --git a/src/main/java/org/apache/commons/imaging/common/ByteConversions.java b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java
index 12d34e0..dd900d3 100644
--- a/src/main/java/org/apache/commons/imaging/common/ByteConversions.java
+++ b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java
@@ -348,11 +348,27 @@ public final class ByteConversions {
         return result;
     }
 
-    public static RationalNumber toRational(final byte[] bytes, final ByteOrder byteOrder) {
-        return toRational(bytes, 0, byteOrder);
-    }
-
-    private static RationalNumber toRational(final byte[] bytes, final int offset, final ByteOrder byteOrder) {
+    /**
+     * Interprets the content of a specified bytes array to create
+     * an instance of the RationalNumber class.
+     * @param bytes a valid array dimensioned to at least 8.
+     * @param byteOrder the byte order for integer conversion
+     * @param unsignedType indicates whether the extracted value is
+     * an unsigned type.
+     * @return a valid instance
+     */
+    public static RationalNumber toRational(
+            final byte[] bytes,
+            final ByteOrder byteOrder,
+            final boolean unsignedType) {
+        return toRational(bytes, 0, byteOrder, unsignedType);
+    }
+
+    private static RationalNumber toRational(
+            final byte[] bytes,
+            final int offset,
+            final ByteOrder byteOrder,
+            final boolean unsignedType) {
         final int byte0 = 0xff & bytes[offset + 0];
         final int byte1 = 0xff & bytes[offset + 1];
         final int byte2 = 0xff & bytes[offset + 2];
@@ -370,18 +386,25 @@ public final class ByteConversions {
             numerator = (byte3 << 24) | (byte2 << 16) | (byte1 << 8) | byte0;
             divisor = (byte7 << 24) | (byte6 << 16) | (byte5 << 8) | byte4;
         }
-        return new RationalNumber(numerator, divisor);
+        return new RationalNumber(numerator, divisor, unsignedType);
     }
 
-    public static RationalNumber[] toRationals(final byte[] bytes, final ByteOrder byteOrder) {
-        return toRationals(bytes, 0, bytes.length, byteOrder);
+    public static RationalNumber[] toRationals(
+            final byte[] bytes,
+            final ByteOrder byteOrder,
+            boolean unsignedType) {
+        return toRationals(bytes, 0, bytes.length, byteOrder, unsignedType);
     }
 
-    private static RationalNumber[] toRationals(final byte[] bytes,
-            final int offset, final int length, final ByteOrder byteOrder) {
+    private static RationalNumber[] toRationals(
+            final byte[] bytes,
+            final int offset,
+            final int length,
+            final ByteOrder byteOrder,
+            boolean unsignedType) {
         final RationalNumber[] result = new RationalNumber[length / 8];
         for (int i = 0; i < result.length; i++) {
-            result[i] = toRational(bytes, offset + 8 * i, byteOrder);
+            result[i] = toRational(bytes, offset + 8 * i, byteOrder, unsignedType);
         }
         return result;
     }
diff --git a/src/main/java/org/apache/commons/imaging/common/RationalNumber.java b/src/main/java/org/apache/commons/imaging/common/RationalNumber.java
index 89888c4..3ae186f 100644
--- a/src/main/java/org/apache/commons/imaging/common/RationalNumber.java
+++ b/src/main/java/org/apache/commons/imaging/common/RationalNumber.java
@@ -20,6 +20,14 @@ import java.text.NumberFormat;
 
 /**
  * Rational number, as used by the TIFF image format.
+ * <p>
+ * The TIFF format specifies two data types for rational numbers based on
+ * a pair of 32-bit integers.  Rational is based on unsigned 32-bit integers
+ * and SRational is based on signed 32-bit integers.  This treatment is
+ * problematic in Java because Java does not support unsigned types.
+ * To address this challenge, this class stores the numerator and divisor
+ * in long (64-bit) integers, applying masks as necessary for the unsigned
+ * type.
  */
 public class RationalNumber extends Number {
 
@@ -28,12 +36,55 @@ public class RationalNumber extends Number {
     // int-precision tolerance
     private static final double TOLERANCE = 1E-8;
 
-    public final int numerator;
-    public final int divisor;
+    // The TIFF and EXIF specifications call for the use
+    // of 32 bit unsigned integers.  Since Java does not have an
+    // unsigned type, this class widens the type to long in order
+    // to avoid unintended negative numbers.
+    public final long numerator;
+    public final long divisor;
+    public final boolean unsignedType;
 
+    /**
+     * Constructs an instance based on signed integers
+     * @param numerator a 32-bit signed integer
+     * @param divisor a non-zero 32-bit signed integer
+     */
     public RationalNumber(final int numerator, final int divisor) {
         this.numerator = numerator;
         this.divisor = divisor;
+        this.unsignedType = false;
+    }
+
+    /**
+     * Constructs an instance supports either signed or unsigned integers.
+     * @param numerator a numerator in the indicated form (signed or unsigned)
+     * @param divisor a non-zero divisor in the indicated form (signed or unsigned)
+     * @param unsignedType indicates whether the input values are to be treated
+     * as unsigned.
+     */
+    public RationalNumber(final int numerator, final int divisor, final boolean unsignedType) {
+        this.unsignedType = unsignedType;
+        if (unsignedType) {
+            this.numerator = numerator & 0xffffffffL;
+            this.divisor = divisor & 0xffffffffL;
+        } else {
+            this.numerator = numerator;
+            this.divisor = divisor;
+        }
+    }
+
+    /**
+     * A private constructor for methods such as negate() that create instances
+     * of this class using the content of the current instance.
+     * @param numerator a valid numerator
+     * @param divisor a valid denominator
+     * @param unsignedType indicates how numerator and divisor values
+     * are to be interpreted.
+     */
+    private RationalNumber(final long numerator, final long divisor, boolean unsignedType){
+        this.numerator = numerator;
+        this.divisor   = divisor;
+        this.unsignedType = unsignedType;
     }
 
     static RationalNumber factoryMethod(long n, long d) {
@@ -73,8 +124,44 @@ public class RationalNumber extends Number {
         return gcd(b, a % b);
     }
 
+    /**
+     * Negates the value of the RationalNumber. If the numerator of this
+     * instance has its high-order bit set, then its value is too large
+     * to be treated as a Java 32-bit signed integer. In such a case, the
+     * only way that a RationalNumber instance can be negated is to
+     * divide both terms by a common divisor, if a non-zero common divisor exists.
+     * However, if no such divisor exists, there is no numerically correct
+     * way to perform the negation. When a negation cannot be performed correctly,
+     * this method throws an unchecked exception.
+     * @return a valid instance with a negated value.
+     */
     public RationalNumber negate() {
-        return new RationalNumber(-numerator, divisor);
+        long n = numerator;
+        long d = divisor;
+        if (unsignedType) {
+            // An instance of an unsigned type can be negated if and only if
+            // its high-order bit (the sign bit) is clear. If the bit is set,
+            // the value will be too large to convert to a signed type.
+            // In such a case it is necessary to adjust the numerator and denominator
+            // by their greatest common divisor (gcd), if one exists.
+            // If no non-zero common divisor exists, an exception is thrown.
+            if ((n >> 31) == 1) {
+                // the unsigned value is so large that the high-order bit is set
+                // it cannot be converted to a negative number. Check to see
+                // whether there is an option to reduce its magnitude.
+                long g = gcd(numerator, divisor);
+                if (g != 0) {
+                    n /= g;
+                    d /= g;
+                }
+                if ((n >> 31) == 1) {
+                    throw new NumberFormatException(
+                            "Unsigned numerator is too large to negate "
+                            + numerator);
+                }
+            }
+        }
+        return new RationalNumber(-n, d, false);
     }
 
     @Override
@@ -84,17 +171,23 @@ public class RationalNumber extends Number {
 
     @Override
     public float floatValue() {
-        return (float) numerator / (float) divisor;
+        // The computation uses double value in order to preserve
+        // as much of the precision of the source numerator and denominator
+        // as possible.  Note that the expression
+        //    return (float)numerator/(float) denominator
+        // could lose precision since a Java float only carries 24 bits
+        // of precision while an integer carries 32.
+        return (float) doubleValue();
     }
 
     @Override
     public int intValue() {
-        return numerator / divisor;
+        return (int)(numerator / divisor);
     }
 
     @Override
     public long longValue() {
-        return (long) numerator / (long) divisor;
+        return numerator / divisor;
     }
 
     @Override
@@ -112,7 +205,7 @@ public class RationalNumber extends Number {
 
     public String toDisplayString() {
         if ((numerator % divisor) == 0) {
-            return Integer.toString(numerator / divisor);
+            return Long.toString(numerator / divisor);
         }
         final NumberFormat nf = NumberFormat.getInstance();
         nf.setMaximumFractionDigits(3);
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java
index a18ca7e..644964c 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java
@@ -31,11 +31,14 @@ public class FieldTypeRational extends FieldType {
     @Override
     public Object getValue(final TiffField entry) {
         final byte[] bytes = entry.getByteArrayValue();
+        boolean unsignedType = entry.getFieldType() != SRATIONAL;
         if (entry.getCount() == 1) {
-            return ByteConversions.toRational(bytes,
-                    entry.getByteOrder());
+            return ByteConversions.toRational(
+                    bytes,
+                    entry.getByteOrder(),
+                    unsignedType);
         }
-        return ByteConversions.toRationals(bytes, entry.getByteOrder());
+        return ByteConversions.toRationals(bytes, entry.getByteOrder(), unsignedType);
     }
 
     @Override
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java
index c113ff6..4370efa 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRational.java
@@ -29,7 +29,7 @@ public class TagInfoRational extends TagInfo {
     }
 
     public RationalNumber getValue(final ByteOrder byteOrder, final byte[] bytes) {
-        return ByteConversions.toRational(bytes, byteOrder);
+        return ByteConversions.toRational(bytes, byteOrder, true);
     }
 
     public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber value) {
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRationals.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRationals.java
index e4fde59..a23bbec 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRationals.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoRationals.java
@@ -29,7 +29,7 @@ public class TagInfoRationals extends TagInfo {
     }
 
     public RationalNumber[] getValue(final ByteOrder byteOrder, final byte[] bytes) {
-        return ByteConversions.toRationals(bytes, byteOrder);
+        return ByteConversions.toRationals(bytes, byteOrder, true);
     }
 
     public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) {
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java
index 8df641f..4f09114 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRational.java
@@ -29,7 +29,7 @@ public class TagInfoSRational extends TagInfo {
     }
 
     public RationalNumber getValue(final ByteOrder byteOrder, final byte[] bytes) {
-        return ByteConversions.toRational(bytes, byteOrder);
+        return ByteConversions.toRational(bytes, byteOrder, false);
     }
 
     public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber value) {
diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRationals.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRationals.java
index 350ed13..4200f19 100644
--- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRationals.java
+++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoSRationals.java
@@ -29,7 +29,7 @@ public class TagInfoSRationals extends TagInfo {
     }
 
     public RationalNumber[] getValue(final ByteOrder byteOrder, final byte[] bytes) {
-        return ByteConversions.toRationals(bytes, byteOrder);
+        return ByteConversions.toRationals(bytes, byteOrder, false);
     }
 
     public byte[] encodeValue(final ByteOrder byteOrder, final RationalNumber... values) {
diff --git a/src/test/java/org/apache/commons/imaging/common/RationalNumberTest.java b/src/test/java/org/apache/commons/imaging/common/RationalNumberTest.java
index c91fc4a..40cb1af 100644
--- a/src/test/java/org/apache/commons/imaging/common/RationalNumberTest.java
+++ b/src/test/java/org/apache/commons/imaging/common/RationalNumberTest.java
@@ -26,6 +26,10 @@ import org.apache.commons.imaging.internal.Debug;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
 public class RationalNumberTest extends ImagingTest {
 
     public static Stream<Double> data() {
@@ -120,4 +124,15 @@ public class RationalNumberTest extends ImagingTest {
         Debug.debug();
     }
 
+    @Test
+    public void testSpecialRationalNumber(){
+        RationalNumber test = new RationalNumber(0xF5937B1F, 70_000_000, true);
+        assertEquals(58.858331871428570, test.doubleValue(), 1.0e-14, "Unsigned integer support failed for double conversion");
+        assertEquals(58.858334f, test.floatValue(), 1.0e-6f, "Float conversion failed");
+        assertEquals(58L, test.longValue(), "Long value conversion failed");
+        assertEquals(58, test.intValue(), "Int value conversion failed");
+        assertThrows(NumberFormatException.class, () -> test.negate(), "Failed to detect negation of large unsigned value");
+
+    }
+
 }
diff --git a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadWriteTagsTest.java b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadWriteTagsTest.java
index c3f2ef8..0213afa 100644
--- a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadWriteTagsTest.java
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffReadWriteTagsTest.java
@@ -20,8 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.Map;
-import java.util.TreeMap;
 
 import org.apache.commons.imaging.FormatCompliance;
 import org.apache.commons.imaging.ImageReadException;
@@ -31,6 +29,8 @@ import org.apache.commons.imaging.common.bytesource.ByteSourceArray;
 import org.apache.commons.imaging.formats.tiff.constants.GeoTiffTagConstants;
 import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
 import org.apache.commons.imaging.formats.tiff.constants.MicrosoftHdPhotoTagConstants;
+import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
+import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
 import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
 import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
 import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
@@ -50,6 +50,11 @@ public class TiffReadWriteTagsTest extends TiffBaseTest {
         final String area = "A good area";
         final float widthRes = 2.2f;
         final double geoDoubleParams = -8.4;
+        RationalNumber exposureCompensation = new RationalNumber(-17, 10);
+        RationalNumber[] latitude = new RationalNumber[3];
+        latitude[0] = new RationalNumber(38, 1, true);
+        latitude[1] = new RationalNumber(36, 1, true);
+        latitude[2] = new RationalNumber(0xF5937B1E, 70_000_000, true);
 
         final TiffOutputSet set = new TiffOutputSet();
         final TiffOutputDirectory dir = set.getOrCreateRootDirectory();
@@ -62,15 +67,16 @@ public class TiffReadWriteTagsTest extends TiffBaseTest {
         dir.add(GpsTagConstants.GPS_TAG_GPS_AREA_INFORMATION, area);
         dir.add(MicrosoftHdPhotoTagConstants.EXIF_TAG_WIDTH_RESOLUTION, widthRes);
         dir.add(GeoTiffTagConstants.EXIF_TAG_GEO_DOUBLE_PARAMS_TAG, geoDoubleParams);
+        dir.add(ExifTagConstants.EXIF_TAG_EXPOSURE_COMPENSATION, exposureCompensation);
+        dir.add(GpsTagConstants.GPS_TAG_GPS_LATITUDE, latitude);
 
         final TiffImageWriterLossy writer = new TiffImageWriterLossy();
         final ByteArrayOutputStream tiff = new ByteArrayOutputStream();
         writer.write(tiff, set);
 
         final TiffReader reader = new TiffReader(true);
-        final Map<String, Object> params = new TreeMap<>();
         final FormatCompliance formatCompliance = new FormatCompliance("");
-        final TiffContents contents = reader.readFirstDirectory(new ByteSourceArray(tiff.toByteArray()), params, true, formatCompliance);
+        final TiffContents contents = reader.readDirectories(new ByteSourceArray(tiff.toByteArray()), true, formatCompliance);
         final TiffDirectory rootDir = contents.directories.get(0);
         assertEquals(description, rootDir.getSingleFieldValue(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION));
         assertEquals(page, rootDir.getFieldValue(TiffTagConstants.TIFF_TAG_PAGE_NUMBER, true)[0]);
@@ -83,5 +89,10 @@ public class TiffReadWriteTagsTest extends TiffBaseTest {
         assertEquals(area, rootDir.getFieldValue(GpsTagConstants.GPS_TAG_GPS_AREA_INFORMATION, true));
         assertEquals(widthRes, rootDir.getFieldValue(MicrosoftHdPhotoTagConstants.EXIF_TAG_WIDTH_RESOLUTION), 0.0);
         assertEquals(geoDoubleParams, rootDir.getFieldValue(GeoTiffTagConstants.EXIF_TAG_GEO_DOUBLE_PARAMS_TAG, true)[0], 0.0);
+        assertEquals(exposureCompensation.doubleValue(), rootDir.getFieldValue(ExifTagConstants.EXIF_TAG_EXPOSURE_COMPENSATION).doubleValue(), 0.0);
+        RationalNumber[] testLat = rootDir.getFieldValue(GpsTagConstants.GPS_TAG_GPS_LATITUDE, true);
+        for (int i = 0; i < 3; i++) {
+            assertEquals(latitude[i].doubleValue(), testLat[i].doubleValue(), 0.0);
+        }
     }
 }