Index: trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- trunk/src/com/drew/lang/BufferBoundsException.java	(revision 15217)
+++ 	(revision )
@@ -1,59 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import java.io.IOException;
-
-/**
- * A checked replacement for {@link IndexOutOfBoundsException}.  Used by {@link RandomAccessReader}.
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public final class BufferBoundsException extends IOException
-{
-    private static final long serialVersionUID = 2911102837808946396L;
-
-    public BufferBoundsException(int index, int bytesRequested, long bufferLength)
-    {
-        super(getMessage(index, bytesRequested, bufferLength));
-    }
-
-    public BufferBoundsException(final String message)
-    {
-        super(message);
-    }
-
-    private static String getMessage(int index, int bytesRequested, long bufferLength)
-    {
-        if (index < 0)
-            return String.format("Attempt to read from buffer using a negative index (%d)", index);
-
-        if (bytesRequested < 0)
-            return String.format("Number of requested bytes cannot be negative (%d)", bytesRequested);
-
-        if ((long)index + (long)bytesRequested - 1L > (long)Integer.MAX_VALUE)
-            return String.format("Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: %d, requested count: %d)", index, bytesRequested);
-
-        return String.format("Attempt to read from beyond end of underlying data source (requested index: %d, requested count: %d, max index: %d)",
-                index, bytesRequested, bufferLength - 1);
-    }
-}
Index: trunk/src/com/drew/lang/ByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/ByteArrayReader.java	(revision 15217)
+++ 	(revision )
@@ -1,107 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-
-import java.io.IOException;
-
-/**
- * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for
- * issues.
- * <p>
- * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
- * <code>setMotorolaByteOrder(boolean)</code>.
- *
- * @author Drew Noakes https://drewnoakes.com
- * */
-public class ByteArrayReader extends RandomAccessReader
-{
-    @NotNull
-    private final byte[] _buffer;
-    private final int _baseOffset;
-
-    @SuppressWarnings({ "ConstantConditions" })
-    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
-    public ByteArrayReader(@NotNull byte[] buffer)
-    {
-        this(buffer, 0);
-    }
-
-    @SuppressWarnings({ "ConstantConditions" })
-    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
-    public ByteArrayReader(@NotNull byte[] buffer, int baseOffset)
-    {
-        if (buffer == null)
-            throw new NullPointerException();
-        if (baseOffset < 0)
-            throw new IllegalArgumentException("Must be zero or greater");
-
-        _buffer = buffer;
-        _baseOffset = baseOffset;
-    }
-
-    @Override
-    public int toUnshiftedOffset(int localOffset)
-    {
-        return localOffset + _baseOffset;
-    }
-
-    @Override
-    public long getLength()
-    {
-        return _buffer.length - _baseOffset;
-    }
-
-    @Override
-    public byte getByte(int index) throws IOException
-    {
-        validateIndex(index, 1);
-        return _buffer[index + _baseOffset];
-    }
-
-    @Override
-    protected void validateIndex(int index, int bytesRequested) throws IOException
-    {
-        if (!isValidIndex(index, bytesRequested))
-            throw new BufferBoundsException(toUnshiftedOffset(index), bytesRequested, _buffer.length);
-    }
-
-    @Override
-    protected boolean isValidIndex(int index, int bytesRequested) throws IOException
-    {
-        return bytesRequested >= 0
-            && index >= 0
-            && (long)index + (long)bytesRequested - 1L < getLength();
-    }
-
-    @Override
-    @NotNull
-    public byte[] getBytes(int index, int count) throws IOException
-    {
-        validateIndex(index, count);
-
-        byte[] bytes = new byte[count];
-        System.arraycopy(_buffer, index + _baseOffset, bytes, 0, count);
-        return bytes;
-    }
-}
Index: trunk/src/com/drew/lang/Charsets.java
===================================================================
--- trunk/src/com/drew/lang/Charsets.java	(revision 15217)
+++ 	(revision )
@@ -1,41 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-package com.drew.lang;
-
-import java.nio.charset.Charset;
-
-/**
- * Holds a set of commonly used character encodings.
- *
- * Newer JDKs include java.nio.charset.StandardCharsets, but we cannot use that in this library.
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public final class Charsets
-{
-    public static final Charset UTF_8 = Charset.forName("UTF-8");
-    public static final Charset UTF_16 = Charset.forName("UTF-16");
-    public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
-    public static final Charset ASCII = Charset.forName("US-ASCII");
-    public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
-    public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
-    public static final Charset WINDOWS_1252 = Charset.forName("Cp1252");
-}
Index: trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- trunk/src/com/drew/lang/CompoundException.java	(revision 15217)
+++ 	(revision )
@@ -1,109 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-
-import java.io.PrintStream;
-import java.io.PrintWriter;
-
-/**
- * Represents a compound exception, as modelled in JDK 1.4, but
- * unavailable in previous versions.  This class allows support
- * of these previous JDK versions.
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public class CompoundException extends Exception
-{
-    private static final long serialVersionUID = -9207883813472069925L;
-
-    @Nullable
-    private final Throwable _innerException;
-
-    public CompoundException(@Nullable String msg)
-    {
-        this(msg, null);
-    }
-
-    public CompoundException(@Nullable Throwable exception)
-    {
-        this(null, exception);
-    }
-
-    public CompoundException(@Nullable String msg, @Nullable Throwable innerException)
-    {
-        super(msg);
-        _innerException = innerException;
-    }
-
-    @Nullable
-    public Throwable getInnerException()
-    {
-        return _innerException;
-    }
-
-    @Override
-    @NotNull
-    public String toString()
-    {
-        StringBuilder string = new StringBuilder();
-        string.append(super.toString());
-        if (_innerException != null) {
-            string.append("\n");
-            string.append("--- inner exception ---");
-            string.append("\n");
-            string.append(_innerException.toString());
-        }
-        return string.toString();
-    }
-
-    @Override
-    public void printStackTrace(@NotNull PrintStream s)
-    {
-        super.printStackTrace(s);
-        if (_innerException != null) {
-            s.println("--- inner exception ---");
-            _innerException.printStackTrace(s);
-        }
-    }
-
-    @Override
-    public void printStackTrace(@NotNull PrintWriter s)
-    {
-        super.printStackTrace(s);
-        if (_innerException != null) {
-            s.println("--- inner exception ---");
-            _innerException.printStackTrace(s);
-        }
-    }
-
-    @Override
-    public void printStackTrace()
-    {
-        super.printStackTrace();
-        if (_innerException != null) {
-            System.err.println("--- inner exception ---");
-            _innerException.printStackTrace();
-        }
-    }
-}
Index: trunk/src/com/drew/lang/DateUtil.java
===================================================================
--- trunk/src/com/drew/lang/DateUtil.java	(revision 15217)
+++ 	(revision )
@@ -1,32 +1,0 @@
-package com.drew.lang;
-
-/**
- * @author Drew Noakes http://drewnoakes.com
- */
-public class DateUtil
-{
-    private static int[] _daysInMonth365 = new int[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
-
-    public static boolean isValidDate(int year, int month, int day)
-    {
-        if (year < 1 || year > 9999 || month < 0 || month > 11)
-            return false;
-
-        int daysInMonth = _daysInMonth365[month];
-        if (month == 1)
-        {
-            boolean isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
-            if (isLeapYear)
-                daysInMonth++;
-        }
-
-        return day >= 1 && day <= daysInMonth;
-    }
-
-    public static boolean isValidTime(int hours, int minutes, int seconds)
-    {
-        return hours >= 0 && hours < 24
-            && minutes >= 0 && minutes < 60
-            && seconds >= 0 && seconds < 60;
-    }
-}
Index: trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- trunk/src/com/drew/lang/GeoLocation.java	(revision 15217)
+++ 	(revision )
@@ -1,163 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-
-import java.text.DecimalFormat;
-
-/**
- * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates.
- * <p>
- * Values of latitude and longitude are given in degrees.
- * <p>
- * This type is immutable.
- */
-public final class GeoLocation
-{
-    private final double _latitude;
-    private final double _longitude;
-
-    /**
-     * Instantiates a new instance of {@link GeoLocation}.
-     *
-     * @param latitude the latitude, in degrees
-     * @param longitude the longitude, in degrees
-     */
-    public GeoLocation(double latitude, double longitude)
-    {
-        _latitude = latitude;
-        _longitude = longitude;
-    }
-
-    /**
-     * @return the latitudinal angle of this location, in degrees.
-     */
-    public double getLatitude()
-    {
-        return _latitude;
-    }
-
-    /**
-     * @return the longitudinal angle of this location, in degrees.
-     */
-    public double getLongitude()
-    {
-        return _longitude;
-    }
-
-    /**
-     * @return true, if both latitude and longitude are equal to zero
-     */
-    public boolean isZero()
-    {
-        return _latitude == 0 && _longitude == 0;
-    }
-
-    /**
-     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) representation as a string,
-     * of format: {@code -1° 23' 4.56"}
-     */
-    @NotNull
-    public static String decimalToDegreesMinutesSecondsString(double decimal)
-    {
-        double[] dms = decimalToDegreesMinutesSeconds(decimal);
-        DecimalFormat format = new DecimalFormat("0.##");
-        return String.format("%s\u00B0 %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2]));
-    }
-
-    /**
-     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) component values, as
-     * a double array.
-     */
-    @NotNull
-    public static double[] decimalToDegreesMinutesSeconds(double decimal)
-    {
-        int d = (int)decimal;
-        double m = Math.abs((decimal % 1) * 60);
-        double s = (m % 1) * 60;
-        return new double[] { d, (int)m, s};
-    }
-
-    /**
-     * Converts DMS (degrees-minutes-seconds) rational values, as given in {@link com.drew.metadata.exif.GpsDirectory},
-     * into a single value in degrees, as a double.
-     */
-    @Nullable
-    public static Double degreesMinutesSecondsToDecimal(@NotNull final Rational degs, @NotNull final Rational mins, @NotNull final Rational secs, final boolean isNegative)
-    {
-        double decimal = Math.abs(degs.doubleValue())
-                + mins.doubleValue() / 60.0d
-                + secs.doubleValue() / 3600.0d;
-
-        if (Double.isNaN(decimal))
-            return null;
-
-        if (isNegative)
-            decimal *= -1;
-
-        return decimal;
-    }
-
-    @Override
-    public boolean equals(final Object o)
-    {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        GeoLocation that = (GeoLocation) o;
-        if (Double.compare(that._latitude, _latitude) != 0) return false;
-        if (Double.compare(that._longitude, _longitude) != 0) return false;
-        return true;
-    }
-
-    @Override
-    public int hashCode()
-    {
-        int result;
-        long temp;
-        temp = _latitude != +0.0d ? Double.doubleToLongBits(_latitude) : 0L;
-        result = (int) (temp ^ (temp >>> 32));
-        temp = _longitude != +0.0d ? Double.doubleToLongBits(_longitude) : 0L;
-        result = 31 * result + (int) (temp ^ (temp >>> 32));
-        return result;
-    }
-
-    /**
-     * @return a string representation of this location, of format: {@code 1.23, 4.56}
-     */
-    @Override
-    @NotNull
-    public String toString()
-    {
-        return _latitude + ", " + _longitude;
-    }
-
-    /**
-     * @return a string representation of this location, of format: {@code -1° 23' 4.56", 54° 32' 1.92"}
-     */
-    @NotNull
-    public String toDMSString()
-    {
-        return decimalToDegreesMinutesSecondsString(_latitude) + ", " + decimalToDegreesMinutesSecondsString(_longitude);
-    }
-}
Index: trunk/src/com/drew/lang/RandomAccessReader.java
===================================================================
--- trunk/src/com/drew/lang/RandomAccessReader.java	(revision 15217)
+++ 	(revision )
@@ -1,444 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.StringValue;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-
-/**
- * Base class for random access data reading operations of common data types.
- * <p>
- * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
- * {@link com.drew.lang.RandomAccessReader#setMotorolaByteOrder(boolean)}.
- * <p>
- * Concrete implementations include:
- * <ul>
- *     <li>{@link ByteArrayReader}</li>
- * </ul>
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public abstract class RandomAccessReader
-{
-    private boolean _isMotorolaByteOrder = true;
-
-    public abstract int toUnshiftedOffset(int localOffset);
-
-    /**
-     * Gets the byte value at the specified byte <code>index</code>.
-     * <p>
-     * Implementations should not perform any bounds checking in this method. That should be performed
-     * in <code>validateIndex</code> and <code>isValidIndex</code>.
-     *
-     * @param index The index from which to read the byte
-     * @return The read byte value
-     * @throws IllegalArgumentException <code>index</code> is negative
-     * @throws BufferBoundsException if the requested byte is beyond the end of the underlying data source
-     * @throws IOException if the byte is unable to be read
-     */
-    public abstract byte getByte(int index) throws IOException;
-
-    /**
-     * Returns the required number of bytes from the specified index from the underlying source.
-     *
-     * @param index The index from which the bytes begins in the underlying source
-     * @param count The number of bytes to be returned
-     * @return The requested bytes
-     * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative
-     * @throws BufferBoundsException if the requested bytes extend beyond the end of the underlying data source
-     * @throws IOException if the byte is unable to be read
-     */
-    @NotNull
-    public abstract byte[] getBytes(int index, int count) throws IOException;
-
-    /**
-     * Ensures that the buffered bytes extend to cover the specified index. If not, an attempt is made
-     * to read to that point.
-     * <p>
-     * If the stream ends before the point is reached, a {@link BufferBoundsException} is raised.
-     *
-     * @param index the index from which the required bytes start
-     * @param bytesRequested the number of bytes which are required
-     * @throws IOException if the stream ends before the required number of bytes are acquired
-     */
-    protected abstract void validateIndex(int index, int bytesRequested) throws IOException;
-
-    protected abstract boolean isValidIndex(int index, int bytesRequested) throws IOException;
-
-    /**
-     * Returns the length of the data source in bytes.
-     * <p>
-     * This is a simple operation for implementations (such as
-     * {@link ByteArrayReader}) that have the entire data source available.
-     * <p>
-     * Users of this method must be aware that sequentially accessed implementations
-     * will have to read and buffer the entire data source in
-     * order to determine the length.
-     *
-     * @return the length of the data source, in bytes.
-     */
-    public abstract long getLength() throws IOException;
-
-    /**
-     * Sets the endianness of this reader.
-     * <ul>
-     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
-     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
-     * </ul>
-     *
-     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
-     */
-    public void setMotorolaByteOrder(boolean motorolaByteOrder)
-    {
-        _isMotorolaByteOrder = motorolaByteOrder;
-    }
-
-    /**
-     * Gets the endianness of this reader.
-     * <ul>
-     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
-     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
-     * </ul>
-     */
-    public boolean isMotorolaByteOrder()
-    {
-        return _isMotorolaByteOrder;
-    }
-
-    /**
-     * Gets whether a bit at a specific index is set or not.
-     *
-     * @param index the number of bits at which to test
-     * @return true if the bit is set, otherwise false
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public boolean getBit(int index) throws IOException
-    {
-        int byteIndex = index / 8;
-        int bitIndex = index % 8;
-
-        validateIndex(byteIndex, 1);
-
-        byte b = getByte(byteIndex);
-        return ((b >> bitIndex) & 1) == 1;
-    }
-
-    /**
-     * Returns an unsigned 8-bit int calculated from one byte of data at the specified index.
-     *
-     * @param index position within the data buffer to read byte
-     * @return the 8 bit int value, between 0 and 255
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public short getUInt8(int index) throws IOException
-    {
-        validateIndex(index, 1);
-
-        return (short) (getByte(index) & 0xFF);
-    }
-
-    /**
-     * Returns a signed 8-bit int calculated from one byte of data at the specified index.
-     *
-     * @param index position within the data buffer to read byte
-     * @return the 8 bit int value, between 0x00 and 0xFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public byte getInt8(int index) throws IOException
-    {
-        validateIndex(index, 1);
-
-        return getByte(index);
-    }
-
-    /**
-     * Returns an unsigned 16-bit int calculated from two bytes of data at the specified index.
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the 16 bit int value, between 0x0000 and 0xFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public int getUInt16(int index) throws IOException
-    {
-        validateIndex(index, 2);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return (getByte(index    ) << 8 & 0xFF00) |
-                   (getByte(index + 1)      & 0xFF);
-        } else {
-            // Intel ordering - LSB first
-            return (getByte(index + 1) << 8 & 0xFF00) |
-                   (getByte(index    )      & 0xFF);
-        }
-    }
-
-    /**
-     * Returns a signed 16-bit int calculated from two bytes of data at the specified index (MSB, LSB).
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the 16 bit int value, between 0x0000 and 0xFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public short getInt16(int index) throws IOException
-    {
-        validateIndex(index, 2);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return (short) ((getByte(index    ) << 8 & (short)0xFF00) |
-                            (getByte(index + 1)      & (short)0xFF));
-        } else {
-            // Intel ordering - LSB first
-            return (short) ((getByte(index + 1) << 8 & (short)0xFF00) |
-                            (getByte(index    )      & (short)0xFF));
-        }
-    }
-
-    /**
-     * Get a 24-bit unsigned integer from the buffer, returning it as an int.
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the unsigned 24-bit int value as a long, between 0x00000000 and 0x00FFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public int getInt24(int index) throws IOException
-    {
-        validateIndex(index, 3);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first (big endian)
-            return ((getByte(index    )) << 16 & 0xFF0000) |
-                   ((getByte(index + 1)) << 8  & 0xFF00) |
-                   ((getByte(index + 2))       & 0xFF);
-        } else {
-            // Intel ordering - LSB first (little endian)
-            return ((getByte(index + 2)) << 16 & 0xFF0000) |
-                   ((getByte(index + 1)) << 8  & 0xFF00) |
-                   ((getByte(index    ))       & 0xFF);
-        }
-    }
-
-    /**
-     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public long getUInt32(int index) throws IOException
-    {
-        validateIndex(index, 4);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first (big endian)
-            return (((long)getByte(index    )) << 24 & 0xFF000000L) |
-                   (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
-                   (((long)getByte(index + 2)) << 8  & 0xFF00L) |
-                   ((getByte(index + 3))       & 0xFFL);
-        } else {
-            // Intel ordering - LSB first (little endian)
-            return (((long)getByte(index + 3)) << 24 & 0xFF000000L) |
-                   (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
-                   (((long)getByte(index + 1)) << 8  & 0xFF00L) |
-                   ((getByte(index    ))       & 0xFFL);
-        }
-    }
-
-    /**
-     * Returns a signed 32-bit integer from four bytes of data at the specified index the buffer.
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public int getInt32(int index) throws IOException
-    {
-        validateIndex(index, 4);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first (big endian)
-            return (getByte(index    ) << 24 & 0xFF000000) |
-                   (getByte(index + 1) << 16 & 0xFF0000) |
-                   (getByte(index + 2) << 8  & 0xFF00) |
-                   (getByte(index + 3)       & 0xFF);
-        } else {
-            // Intel ordering - LSB first (little endian)
-            return (getByte(index + 3) << 24 & 0xFF000000) |
-                   (getByte(index + 2) << 16 & 0xFF0000) |
-                   (getByte(index + 1) << 8  & 0xFF00) |
-                   (getByte(index    )       & 0xFF);
-        }
-    }
-
-    /**
-     * Get a signed 64-bit integer from the buffer.
-     *
-     * @param index position within the data buffer to read first byte
-     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public long getInt64(int index) throws IOException
-    {
-        validateIndex(index, 8);
-
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return ((long)getByte(index    ) << 56 & 0xFF00000000000000L) |
-                   ((long)getByte(index + 1) << 48 & 0xFF000000000000L) |
-                   ((long)getByte(index + 2) << 40 & 0xFF0000000000L) |
-                   ((long)getByte(index + 3) << 32 & 0xFF00000000L) |
-                   ((long)getByte(index + 4) << 24 & 0xFF000000L) |
-                   ((long)getByte(index + 5) << 16 & 0xFF0000L) |
-                   ((long)getByte(index + 6) << 8  & 0xFF00L) |
-                   (getByte(index + 7)       & 0xFFL);
-        } else {
-            // Intel ordering - LSB first
-            return ((long)getByte(index + 7) << 56 & 0xFF00000000000000L) |
-                   ((long)getByte(index + 6) << 48 & 0xFF000000000000L) |
-                   ((long)getByte(index + 5) << 40 & 0xFF0000000000L) |
-                   ((long)getByte(index + 4) << 32 & 0xFF00000000L) |
-                   ((long)getByte(index + 3) << 24 & 0xFF000000L) |
-                   ((long)getByte(index + 2) << 16 & 0xFF0000L) |
-                   ((long)getByte(index + 1) << 8  & 0xFF00L) |
-                   (getByte(index    )       & 0xFFL);
-        }
-    }
-
-    /**
-     * Gets a s15.16 fixed point float from the buffer.
-     * <p>
-     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
-     *
-     * @return the floating point value
-     * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative
-     */
-    public float getS15Fixed16(int index) throws IOException
-    {
-        validateIndex(index, 4);
-
-        if (_isMotorolaByteOrder) {
-            float res = (getByte(index    ) & 0xFF) << 8 |
-                        (getByte(index + 1) & 0xFF);
-            int d =     (getByte(index + 2) & 0xFF) << 8 |
-                        (getByte(index + 3) & 0xFF);
-            return (float)(res + d/65536.0);
-        } else {
-            // this particular branch is untested
-            float res = (getByte(index + 3) & 0xFF) << 8 |
-                        (getByte(index + 2) & 0xFF);
-            int d =     (getByte(index + 1) & 0xFF) << 8 |
-                        (getByte(index    ) & 0xFF);
-            return (float)(res + d/65536.0);
-        }
-    }
-
-    public float getFloat32(int index) throws IOException
-    {
-        return Float.intBitsToFloat(getInt32(index));
-    }
-
-    public double getDouble64(int index) throws IOException
-    {
-        return Double.longBitsToDouble(getInt64(index));
-    }
-
-    @NotNull
-    public StringValue getStringValue(int index, int bytesRequested, @Nullable Charset charset) throws IOException
-    {
-        return new StringValue(getBytes(index, bytesRequested), charset);
-    }
-
-    @NotNull
-    public String getString(int index, int bytesRequested, @NotNull Charset charset) throws IOException
-    {
-        return new String(getBytes(index, bytesRequested), charset.name());
-    }
-
-    @NotNull
-    public String getString(int index, int bytesRequested, @NotNull String charset) throws IOException
-    {
-        byte[] bytes = getBytes(index, bytesRequested);
-        try {
-            return new String(bytes, charset);
-        } catch (UnsupportedEncodingException e) {
-            return new String(bytes);
-        }
-    }
-
-    /**
-     * Creates a String from the _data buffer starting at the specified index,
-     * and ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
-     *
-     * @param index          The index within the buffer at which to start reading the string.
-     * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
-     *                       reading will stop and the string will be truncated to this length.
-     * @return The read string.
-     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
-     */
-    @NotNull
-    public String getNullTerminatedString(int index, int maxLengthBytes, @NotNull Charset charset) throws IOException
-    {
-        return new String(getNullTerminatedBytes(index, maxLengthBytes), charset.name());
-    }
-
-    @NotNull
-    public StringValue getNullTerminatedStringValue(int index, int maxLengthBytes, @Nullable Charset charset) throws IOException
-    {
-        byte[] bytes = getNullTerminatedBytes(index, maxLengthBytes);
-
-        return new StringValue(bytes, charset);
-    }
-
-    /**
-     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
-     *
-     * @param index The index within the buffer at which to start reading the string.
-     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
-     * the returned array will be <code>maxLengthBytes</code> long.
-     * @return The read byte array, excluding the null terminator.
-     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
-     */
-    @NotNull
-    public byte[] getNullTerminatedBytes(int index, int maxLengthBytes) throws IOException
-    {
-        byte[] buffer = getBytes(index, maxLengthBytes);
-
-        // Count the number of non-null bytes
-        int length = 0;
-        while (length < buffer.length && buffer[length] != 0)
-            length++;
-
-        if (length == maxLengthBytes)
-            return buffer;
-
-        byte[] bytes = new byte[length];
-        if (length > 0)
-            System.arraycopy(buffer, 0, bytes, 0, length);
-        return bytes;
-    }
-}
Index: trunk/src/com/drew/lang/Rational.java
===================================================================
--- trunk/src/com/drew/lang/Rational.java	(revision 15217)
+++ 	(revision )
@@ -1,323 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-
-import java.io.Serializable;
-
-/**
- * Immutable class for holding a rational number without loss of precision.  Provides
- * a familiar representation via {@link Rational#toString} in form <code>numerator/denominator</code>.
- *
- * Note that any value with a numerator of zero will be treated as zero, even if the
- * denominator is also zero.
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-@SuppressWarnings("WeakerAccess")
-public class Rational extends java.lang.Number implements Comparable<Rational>, Serializable
-{
-    private static final long serialVersionUID = 510688928138848770L;
-
-    /** Holds the numerator. */
-    private final long _numerator;
-
-    /** Holds the denominator. */
-    private final long _denominator;
-
-    /**
-     * Creates a new instance of Rational.  Rational objects are immutable, so
-     * once you've set your numerator and denominator values here, you're stuck
-     * with them!
-     */
-    public Rational(long numerator, long denominator)
-    {
-        _numerator = numerator;
-        _denominator = denominator;
-    }
-
-    /**
-     * Returns the value of the specified number as a <code>double</code>.
-     * This may involve rounding.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>double</code>.
-     */
-    @Override
-    public double doubleValue()
-    {
-        return _numerator == 0
-            ? 0.0
-            : (double) _numerator / (double) _denominator;
-    }
-
-    /**
-     * Returns the value of the specified number as a <code>float</code>.
-     * This may involve rounding.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>float</code>.
-     */
-    @Override
-    public float floatValue()
-    {
-        return _numerator == 0
-            ? 0.0f
-            : (float) _numerator / (float) _denominator;
-    }
-
-    /**
-     * Returns the value of the specified number as a <code>byte</code>.
-     * This may involve rounding or truncation.  This implementation simply
-     * casts the result of {@link Rational#doubleValue} to <code>byte</code>.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>byte</code>.
-     */
-    @Override
-    public final byte byteValue()
-    {
-        return (byte) doubleValue();
-    }
-
-    /**
-     * Returns the value of the specified number as an <code>int</code>.
-     * This may involve rounding or truncation.  This implementation simply
-     * casts the result of {@link Rational#doubleValue} to <code>int</code>.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>int</code>.
-     */
-    @Override
-    public final int intValue()
-    {
-        return (int) doubleValue();
-    }
-
-    /**
-     * Returns the value of the specified number as a <code>long</code>.
-     * This may involve rounding or truncation.  This implementation simply
-     * casts the result of {@link Rational#doubleValue} to <code>long</code>.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>long</code>.
-     */
-    @Override
-    public final long longValue()
-    {
-        return (long) doubleValue();
-    }
-
-    /**
-     * Returns the value of the specified number as a <code>short</code>.
-     * This may involve rounding or truncation.  This implementation simply
-     * casts the result of {@link Rational#doubleValue} to <code>short</code>.
-     *
-     * @return the numeric value represented by this object after conversion
-     *         to type <code>short</code>.
-     */
-    @Override
-    public final short shortValue()
-    {
-        return (short) doubleValue();
-    }
-
-
-    /** Returns the denominator. */
-    public final long getDenominator()
-    {
-        return this._denominator;
-    }
-
-    /** Returns the numerator. */
-    public final long getNumerator()
-    {
-        return this._numerator;
-    }
-
-    /**
-     * Returns the reciprocal value of this object as a new Rational.
-     *
-     * @return the reciprocal in a new object
-     */
-    @NotNull
-    public Rational getReciprocal()
-    {
-        return new Rational(this._denominator, this._numerator);
-    }
-
-    /** Checks if this {@link Rational} number is an Integer, either positive or negative. */
-    public boolean isInteger()
-    {
-        return _denominator == 1 ||
-                (_denominator != 0 && (_numerator % _denominator == 0)) ||
-                (_denominator == 0 && _numerator == 0);
-    }
-
-    /** Checks if either the numerator or denominator are zero. */
-    public boolean isZero()
-    {
-        return _numerator == 0 || _denominator == 0;
-    }
-
-    /**
-     * Returns a string representation of the object of form <code>numerator/denominator</code>.
-     *
-     * @return a string representation of the object.
-     */
-    @Override
-    @NotNull
-    public String toString()
-    {
-        return _numerator + "/" + _denominator;
-    }
-
-    /** Returns the simplest representation of this {@link Rational}'s value possible. */
-    @NotNull
-    public String toSimpleString(boolean allowDecimal)
-    {
-        if (_denominator == 0 && _numerator != 0) {
-            return toString();
-        } else if (isInteger()) {
-            return Integer.toString(intValue());
-        } else if (_numerator != 1 && _denominator % _numerator == 0) {
-            // common factor between denominator and numerator
-            long newDenominator = _denominator / _numerator;
-            return new Rational(1, newDenominator).toSimpleString(allowDecimal);
-        } else {
-            Rational simplifiedInstance = getSimplifiedInstance();
-            if (allowDecimal) {
-                String doubleString = Double.toString(simplifiedInstance.doubleValue());
-                if (doubleString.length() < 5) {
-                    return doubleString;
-                }
-            }
-            return simplifiedInstance.toString();
-        }
-    }
-
-    /**
-     * Compares two {@link Rational} instances, returning true if they are mathematically
-     * equivalent (in consistence with {@link Rational#equals(Object)} method).
-     *
-     * @param that the {@link Rational} to compare this instance to.
-     * @return the value {@code 0} if this {@link Rational} is
-     *         equal to the argument {@link Rational} mathematically; a value less
-     *         than {@code 0} if this {@link Rational} is less
-     *         than the argument {@link Rational}; and a value greater
-     *         than {@code 0} if this {@link Rational} is greater than the argument
-     *         {@link Rational}.
-     */
-    public int compareTo(@NotNull Rational that) {
-        return Double.compare(this.doubleValue(), that.doubleValue());
-    }
-
-    /**
-     * Indicates whether this instance and <code>other</code> are numerically equal,
-     * even if their representations differ.
-     *
-     * For example, 1/2 is equal to 10/20 by this method.
-     * Similarly, 1/0 is equal to 100/0 by this method.
-     * To test equal representations, use EqualsExact.
-     *
-     * @param other The rational value to compare with
-     */
-    public boolean equals(Rational other) {
-        return other.doubleValue() == doubleValue();
-    }
-
-    /**
-     * Indicates whether this instance and <code>other</code> have identical
-     * Numerator and Denominator.
-     * <p>
-     * For example, 1/2 is not equal to 10/20 by this method.
-     * Similarly, 1/0 is not equal to 100/0 by this method.
-     * To test numerically equivalence, use Equals(Rational).</p>
-     *
-     * @param other The rational value to compare with
-     */
-    public boolean equalsExact(Rational other) {
-        return getDenominator() == other.getDenominator() && getNumerator() == other.getNumerator();
-    }
-
-    /**
-     * Compares two {@link Rational} instances, returning true if they are mathematically
-     * equivalent.
-     *
-     * @param obj the {@link Rational} to compare this instance to.
-     * @return true if instances are mathematically equivalent, otherwise false.  Will also
-     *         return false if <code>obj</code> is not an instance of {@link Rational}.
-     */
-    @Override
-    public boolean equals(@Nullable Object obj)
-    {
-        if (obj==null || !(obj instanceof Rational))
-            return false;
-        Rational that = (Rational) obj;
-        return this.doubleValue() == that.doubleValue();
-    }
-
-    @Override
-    public int hashCode()
-    {
-        return (23 * (int)_denominator) + (int)_numerator;
-    }
-
-    /**
-     * <p>
-     * Simplifies the representation of this {@link Rational} number.</p>
-     * <p>
-     * For example, 5/10 simplifies to 1/2 because both Numerator
-     * and Denominator share a common factor of 5.</p>
-     * <p>
-     * Uses the Euclidean Algorithm to find the greatest common divisor.</p>
-     *
-     * @return A simplified instance if one exists, otherwise a copy of the original value.
-     */
-    @NotNull
-    public Rational getSimplifiedInstance()
-    {
-        long gcd = GCD(_numerator, _denominator);
-
-        return new Rational(_numerator / gcd, _denominator / gcd);
-    }
-
-    private static long GCD(long a, long b)
-    {
-        if (a < 0)
-            a = -a;
-        if (b < 0)
-            b = -b;
-
-        while (a != 0 && b != 0)
-        {
-            if (a > b)
-                a %= b;
-            else
-                b %= a;
-        }
-
-        return a == 0 ? b : a;
-    }
-}
Index: trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 15217)
+++ 	(revision )
@@ -1,130 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-
-import java.io.EOFException;
-import java.io.IOException;
-
-/**
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public class SequentialByteArrayReader extends SequentialReader
-{
-    @NotNull
-    private final byte[] _bytes;
-    private int _index;
-
-    @Override
-    public long getPosition()
-    {
-        return _index;
-    }
-
-    public SequentialByteArrayReader(@NotNull byte[] bytes)
-    {
-        this(bytes, 0);
-    }
-
-    @SuppressWarnings("ConstantConditions")
-    public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex)
-    {
-        if (bytes == null)
-            throw new NullPointerException();
-
-        _bytes = bytes;
-        _index = baseIndex;
-    }
-
-    @Override
-    public byte getByte() throws IOException
-    {
-        if (_index >= _bytes.length) {
-            throw new EOFException("End of data reached.");
-        }
-        return _bytes[_index++];
-    }
-
-    @NotNull
-    @Override
-    public byte[] getBytes(int count) throws IOException
-    {
-        if (_index + count > _bytes.length) {
-            throw new EOFException("End of data reached.");
-        }
-
-        byte[] bytes = new byte[count];
-        System.arraycopy(_bytes, _index, bytes, 0, count);
-        _index += count;
-
-        return bytes;
-    }
-
-    @Override
-    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
-    {
-        if (_index + count > _bytes.length) {
-            throw new EOFException("End of data reached.");
-        }
-
-        System.arraycopy(_bytes, _index, buffer, offset, count);
-        _index += count;
-    }
-
-    @Override
-    public void skip(long n) throws IOException
-    {
-        if (n < 0) {
-            throw new IllegalArgumentException("n must be zero or greater.");
-        }
-
-        if (_index + n > _bytes.length) {
-            throw new EOFException("End of data reached.");
-        }
-
-        _index += n;
-    }
-
-    @Override
-    public boolean trySkip(long n) throws IOException
-    {
-        if (n < 0) {
-            throw new IllegalArgumentException("n must be zero or greater.");
-        }
-
-        _index += n;
-
-        if (_index > _bytes.length) {
-            _index = _bytes.length;
-            return false;
-        }
-
-        return true;
-    }
-
-    @Override
-    public int available() {
-        return _bytes.length - _index;
-    }
-}
Index: trunk/src/com/drew/lang/SequentialReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialReader.java	(revision 15217)
+++ 	(revision )
@@ -1,389 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-import com.drew.metadata.StringValue;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-@SuppressWarnings("WeakerAccess")
-public abstract class SequentialReader
-{
-    // TODO review whether the masks are needed (in both this and RandomAccessReader)
-
-    private boolean _isMotorolaByteOrder = true;
-
-    public abstract long getPosition() throws IOException;
-
-    /**
-     * Gets the next byte in the sequence.
-     *
-     * @return The read byte value
-     */
-    public abstract byte getByte() throws IOException;
-
-    /**
-     * Returns the required number of bytes from the sequence.
-     *
-     * @param count The number of bytes to be returned
-     * @return The requested bytes
-     */
-    @NotNull
-    public abstract byte[] getBytes(int count) throws IOException;
-
-    /**
-     * Retrieves bytes, writing them into a caller-provided buffer.
-     * @param buffer The array to write bytes to.
-     * @param offset The starting position within buffer to write to.
-     * @param count The number of bytes to be written.
-     */
-    public abstract void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException;
-
-    /**
-     * Skips forward in the sequence. If the sequence ends, an {@link EOFException} is thrown.
-     *
-     * @param n the number of byte to skip. Must be zero or greater.
-     * @throws EOFException the end of the sequence is reached.
-     * @throws IOException an error occurred reading from the underlying source.
-     */
-    public abstract void skip(long n) throws IOException;
-
-    /**
-     * Skips forward in the sequence, returning a boolean indicating whether the skip succeeded, or whether the sequence ended.
-     *
-     * @param n the number of byte to skip. Must be zero or greater.
-     * @return a boolean indicating whether the skip succeeded, or whether the sequence ended.
-     * @throws IOException an error occurred reading from the underlying source.
-     */
-    public abstract boolean trySkip(long n) throws IOException;
-
-    /**
-     * Returns an estimate of the number of bytes that can be read (or skipped
-     * over) from this {@link SequentialReader} without blocking by the next
-     * invocation of a method for this input stream. A single read or skip of
-     * this many bytes will not block, but may read or skip fewer bytes.
-     * <p>
-     * Note that while some implementations of {@link SequentialReader} like
-     * {@link SequentialByteArrayReader} will return the total remaining number
-     * of bytes in the stream, others will not. It is never correct to use the
-     * return value of this method to allocate a buffer intended to hold all
-     * data in this stream.
-     *
-     * @return an estimate of the number of bytes that can be read (or skipped
-     *         over) from this {@link SequentialReader} without blocking or
-     *         {@code 0} when it reaches the end of the input stream.
-     */
-    public abstract int available();
-
-    /**
-     * Sets the endianness of this reader.
-     * <ul>
-     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
-     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
-     * </ul>
-     *
-     * @param motorolaByteOrder <code>true</code> for Motorola/big endian, <code>false</code> for Intel/little endian
-     */
-    public void setMotorolaByteOrder(boolean motorolaByteOrder)
-    {
-        _isMotorolaByteOrder = motorolaByteOrder;
-    }
-
-    /**
-     * Gets the endianness of this reader.
-     * <ul>
-     * <li><code>true</code> for Motorola (or big) endianness (also known as network byte order), with MSB before LSB.</li>
-     * <li><code>false</code> for Intel (or little) endianness, with LSB before MSB.</li>
-     * </ul>
-     */
-    public boolean isMotorolaByteOrder()
-    {
-        return _isMotorolaByteOrder;
-    }
-
-    /**
-     * Returns an unsigned 8-bit int calculated from the next byte of the sequence.
-     *
-     * @return the 8 bit int value, between 0 and 255
-     */
-    public short getUInt8() throws IOException
-    {
-        return (short) (getByte() & 0xFF);
-    }
-
-    /**
-     * Returns a signed 8-bit int calculated from the next byte the sequence.
-     *
-     * @return the 8 bit int value, between 0x00 and 0xFF
-     */
-    public byte getInt8() throws IOException
-    {
-        return getByte();
-    }
-
-    /**
-     * Returns an unsigned 16-bit int calculated from the next two bytes of the sequence.
-     *
-     * @return the 16 bit int value, between 0x0000 and 0xFFFF
-     */
-    public int getUInt16() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return (getByte() << 8 & 0xFF00) |
-                   (getByte()      & 0xFF);
-        } else {
-            // Intel ordering - LSB first
-            return (getByte()      & 0xFF) |
-                   (getByte() << 8 & 0xFF00);
-        }
-    }
-
-    /**
-     * Returns a signed 16-bit int calculated from two bytes of data (MSB, LSB).
-     *
-     * @return the 16 bit int value, between 0x0000 and 0xFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request
-     */
-    public short getInt16() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return (short) (((short)getByte() << 8 & (short)0xFF00) |
-                            ((short)getByte()      & (short)0xFF));
-        } else {
-            // Intel ordering - LSB first
-            return (short) (((short)getByte()      & (short)0xFF) |
-                            ((short)getByte() << 8 & (short)0xFF00));
-        }
-    }
-
-    /**
-     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
-     *
-     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request
-     */
-    public long getUInt32() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first (big endian)
-            return (((long)getByte()) << 24 & 0xFF000000L) |
-                   (((long)getByte()) << 16 & 0xFF0000L) |
-                   (((long)getByte()) << 8  & 0xFF00L) |
-                   (((long)getByte())       & 0xFFL);
-        } else {
-            // Intel ordering - LSB first (little endian)
-            return (((long)getByte())       & 0xFFL) |
-                   (((long)getByte()) << 8  & 0xFF00L) |
-                   (((long)getByte()) << 16 & 0xFF0000L) |
-                   (((long)getByte()) << 24 & 0xFF000000L);
-        }
-    }
-
-    /**
-     * Returns a signed 32-bit integer from four bytes of data.
-     *
-     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request
-     */
-    public int getInt32() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first (big endian)
-            return (getByte() << 24 & 0xFF000000) |
-                   (getByte() << 16 & 0xFF0000) |
-                   (getByte() << 8  & 0xFF00) |
-                   (getByte()       & 0xFF);
-        } else {
-            // Intel ordering - LSB first (little endian)
-            return (getByte()       & 0xFF) |
-                   (getByte() << 8  & 0xFF00) |
-                   (getByte() << 16 & 0xFF0000) |
-                   (getByte() << 24 & 0xFF000000);
-        }
-    }
-
-    /**
-     * Get a signed 64-bit integer from the buffer.
-     *
-     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
-     * @throws IOException the buffer does not contain enough bytes to service the request
-     */
-    public long getInt64() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            // Motorola - MSB first
-            return ((long)getByte() << 56 & 0xFF00000000000000L) |
-                   ((long)getByte() << 48 & 0xFF000000000000L) |
-                   ((long)getByte() << 40 & 0xFF0000000000L) |
-                   ((long)getByte() << 32 & 0xFF00000000L) |
-                   ((long)getByte() << 24 & 0xFF000000L) |
-                   ((long)getByte() << 16 & 0xFF0000L) |
-                   ((long)getByte() << 8  & 0xFF00L) |
-                   ((long)getByte()       & 0xFFL);
-        } else {
-            // Intel ordering - LSB first
-            return ((long)getByte()       & 0xFFL) |
-                   ((long)getByte() << 8  & 0xFF00L) |
-                   ((long)getByte() << 16 & 0xFF0000L) |
-                   ((long)getByte() << 24 & 0xFF000000L) |
-                   ((long)getByte() << 32 & 0xFF00000000L) |
-                   ((long)getByte() << 40 & 0xFF0000000000L) |
-                   ((long)getByte() << 48 & 0xFF000000000000L) |
-                   ((long)getByte() << 56 & 0xFF00000000000000L);
-        }
-    }
-
-    /**
-     * Gets a s15.16 fixed point float from the buffer.
-     * <p>
-     * This particular fixed point encoding has one sign bit, 15 numerator bits and 16 denominator bits.
-     *
-     * @return the floating point value
-     * @throws IOException the buffer does not contain enough bytes to service the request
-     */
-    public float getS15Fixed16() throws IOException
-    {
-        if (_isMotorolaByteOrder) {
-            float res = (getByte() & 0xFF) << 8 |
-                        (getByte() & 0xFF);
-            int d =     (getByte() & 0xFF) << 8 |
-                        (getByte() & 0xFF);
-            return (float)(res + d/65536.0);
-        } else {
-            // this particular branch is untested
-            int d =     (getByte() & 0xFF) |
-                        (getByte() & 0xFF) << 8;
-            float res = (getByte() & 0xFF) |
-                        (getByte() & 0xFF) << 8;
-            return (float)(res + d/65536.0);
-        }
-    }
-
-    public float getFloat32() throws IOException
-    {
-        return Float.intBitsToFloat(getInt32());
-    }
-
-    public double getDouble64() throws IOException
-    {
-        return Double.longBitsToDouble(getInt64());
-    }
-
-    @NotNull
-    public String getString(int bytesRequested) throws IOException
-    {
-        return new String(getBytes(bytesRequested));
-    }
-
-    @NotNull
-    public String getString(int bytesRequested, String charset) throws IOException
-    {
-        byte[] bytes = getBytes(bytesRequested);
-        try {
-            return new String(bytes, charset);
-        } catch (UnsupportedEncodingException e) {
-            return new String(bytes);
-        }
-    }
-
-    @NotNull
-    public String getString(int bytesRequested, @NotNull Charset charset) throws IOException
-    {
-        byte[] bytes = getBytes(bytesRequested);
-        return new String(bytes, charset);
-    }
-
-    @NotNull
-    public StringValue getStringValue(int bytesRequested, @Nullable Charset charset) throws IOException
-    {
-        return new StringValue(getBytes(bytesRequested), charset);
-    }
-
-    /**
-     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
-     *
-     * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
-     *                       reading will stop and the string will be truncated to this length.
-     * @return The read string.
-     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
-     */
-    @NotNull
-    public String getNullTerminatedString(int maxLengthBytes, Charset charset) throws IOException
-    {
-       return getNullTerminatedStringValue(maxLengthBytes, charset).toString();
-    }
-
-    /**
-     * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
-     *
-     * @param maxLengthBytes The maximum number of bytes to read.  If a <code>\0</code> byte is not reached within this limit,
-     *                       reading will stop and the string will be truncated to this length.
-     * @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding
-     *                is unknown
-     * @return The read string.
-     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
-     */
-    @NotNull
-    public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset) throws IOException
-    {
-        byte[] bytes = getNullTerminatedBytes(maxLengthBytes);
-
-        return new StringValue(bytes, charset);
-    }
-
-    /**
-     * Returns the sequence of bytes punctuated by a <code>\0</code> value.
-     *
-     * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
-     * the returned array will be <code>maxLengthBytes</code> long.
-     * @return The read byte array, excluding the null terminator.
-     * @throws IOException The buffer does not contain enough bytes to satisfy this request.
-     */
-    @NotNull
-    public byte[] getNullTerminatedBytes(int maxLengthBytes) throws IOException
-    {
-        byte[] buffer = new byte[maxLengthBytes];
-
-        // Count the number of non-null bytes
-        int length = 0;
-        while (length < buffer.length && (buffer[length] = getByte()) != 0)
-            length++;
-
-        if (length == maxLengthBytes)
-            return buffer;
-
-        byte[] bytes = new byte[length];
-        if (length > 0)
-            System.arraycopy(buffer, 0, bytes, 0, length);
-        return bytes;
-    }
-}
Index: trunk/src/com/drew/lang/StreamReader.java
===================================================================
--- trunk/src/com/drew/lang/StreamReader.java	(revision 15217)
+++ 	(revision )
@@ -1,140 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- *
- * @author Drew Noakes https://drewnoakes.com
- */
-public class StreamReader extends SequentialReader
-{
-    @NotNull
-    private final InputStream _stream;
-
-    private long _pos;
-
-    @Override
-    public long getPosition()
-    {
-        return _pos;
-    }
-
-    @SuppressWarnings("ConstantConditions")
-    public StreamReader(@NotNull InputStream stream)
-    {
-        if (stream == null)
-            throw new NullPointerException();
-
-        _stream = stream;
-        _pos = 0;
-    }
-
-    @Override
-    public byte getByte() throws IOException
-    {
-        int value = _stream.read();
-        if (value == -1)
-            throw new EOFException("End of data reached.");
-        _pos++;
-        return (byte)value;
-    }
-
-    @NotNull
-    @Override
-    public byte[] getBytes(int count) throws IOException
-    {
-        byte[] bytes = new byte[count];
-        getBytes(bytes, 0, count);
-        return bytes;
-    }
-
-    @Override
-    public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException
-    {
-        int totalBytesRead = 0;
-        while (totalBytesRead != count)
-        {
-            final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead);
-            if (bytesRead == -1)
-                throw new EOFException("End of data reached.");
-            totalBytesRead += bytesRead;
-            assert(totalBytesRead <= count);
-        }
-        _pos += totalBytesRead;
-    }
-
-    @Override
-    public void skip(long n) throws IOException
-    {
-        if (n < 0)
-            throw new IllegalArgumentException("n must be zero or greater.");
-
-        long skippedCount = skipInternal(n);
-
-        if (skippedCount != n)
-            throw new EOFException(String.format("Unable to skip. Requested %d bytes but skipped %d.", n, skippedCount));
-    }
-
-    @Override
-    public boolean trySkip(long n) throws IOException
-    {
-        if (n < 0)
-            throw new IllegalArgumentException("n must be zero or greater.");
-
-        return skipInternal(n) == n;
-    }
-
-    @Override
-    public int available() {
-        try {
-            return _stream.available();
-        } catch (IOException e) {
-            return 0;
-        }
-    }
-
-    private long skipInternal(long n) throws IOException
-    {
-        // It seems that for some streams, such as BufferedInputStream, that skip can return
-        // some smaller number than was requested. So loop until we either skip enough, or
-        // InputStream.skip returns zero.
-        //
-        // See http://stackoverflow.com/questions/14057720/robust-skipping-of-data-in-a-java-io-inputstream-and-its-subtypes
-        //
-        long skippedTotal = 0;
-        while (skippedTotal != n) {
-            long skipped = _stream.skip(n - skippedTotal);
-            assert(skipped >= 0);
-            skippedTotal += skipped;
-            if (skipped == 0)
-                break;
-        }
-        _pos += skippedTotal;
-        return skippedTotal;
-    }
-}
Index: trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- trunk/src/com/drew/lang/StringUtil.java	(revision 15217)
+++ 	(revision )
@@ -1,115 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang;
-
-import com.drew.lang.annotations.NotNull;
-import com.drew.lang.annotations.Nullable;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.Iterator;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-public final class StringUtil
-{
-    @NotNull
-    public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
-    {
-        int capacity = 0;
-        int delimLength = delimiter.length();
-
-        Iterator<? extends CharSequence> iter = strings.iterator();
-        if (iter.hasNext())
-            capacity += iter.next().length() + delimLength;
-
-        StringBuilder buffer = new StringBuilder(capacity);
-        iter = strings.iterator();
-        if (iter.hasNext()) {
-            buffer.append(iter.next());
-            while (iter.hasNext()) {
-                buffer.append(delimiter);
-                buffer.append(iter.next());
-            }
-        }
-        return buffer.toString();
-    }
-
-    @NotNull
-    public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter)
-    {
-        int capacity = 0;
-        int delimLength = delimiter.length();
-        for (T value : strings)
-            capacity += value.length() + delimLength;
-
-        StringBuilder buffer = new StringBuilder(capacity);
-        boolean first = true;
-        for (T value : strings) {
-            if (!first) {
-                buffer.append(delimiter);
-            } else {
-                first = false;
-            }
-            buffer.append(value);
-        }
-        return buffer.toString();
-    }
-
-    @NotNull
-    public static String fromStream(@NotNull InputStream stream) throws IOException
-    {
-        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
-        StringBuilder sb = new StringBuilder();
-        String line;
-        while ((line = reader.readLine()) != null) {
-            sb.append(line);
-        }
-        return sb.toString();
-    }
-
-    public static int compare(@Nullable String s1, @Nullable String s2)
-    {
-        boolean null1 = s1 == null;
-        boolean null2 = s2 == null;
-
-        if (null1 && null2) {
-            return 0;
-        } else if (null1) {
-            return -1;
-        } else if (null2) {
-            return 1;
-        } else {
-            return s1.compareTo(s2);
-        }
-    }
-
-    @NotNull
-    public static String urlEncode(@NotNull String name)
-    {
-        // Sufficient for now, it seems
-        return name.replace(" ", "%20");
-    }
-}
Index: trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- trunk/src/com/drew/lang/annotations/NotNull.java	(revision 15217)
+++ 	(revision )
@@ -1,29 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang.annotations;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-public @interface NotNull
-{
-}
Index: trunk/src/com/drew/lang/annotations/Nullable.java
===================================================================
--- trunk/src/com/drew/lang/annotations/Nullable.java	(revision 15217)
+++ 	(revision )
@@ -1,29 +1,0 @@
-/*
- * Copyright 2002-2019 Drew Noakes and contributors
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang.annotations;
-
-/**
- * @author Drew Noakes https://drewnoakes.com
- */
-public @interface Nullable
-{
-}
Index: trunk/src/com/drew/lang/annotations/SuppressWarnings.java
===================================================================
--- trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 15217)
+++ 	(revision )
@@ -1,42 +1,0 @@
-/*
- * Copyright 2002-2011 Andreas Ziermann
- *
- *    Licensed under the Apache License, Version 2.0 (the "License");
- *    you may not use this file except in compliance with the License.
- *    You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *    Unless required by applicable law or agreed to in writing, software
- *    distributed under the License is distributed on an "AS IS" BASIS,
- *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *    See the License for the specific language governing permissions and
- *    limitations under the License.
- *
- * More information about this project is available at:
- *
- *    https://drewnoakes.com/code/exif/
- *    https://github.com/drewnoakes/metadata-extractor
- */
-
-package com.drew.lang.annotations;
-
-/**
- * Used to suppress specific code analysis warnings produced by the Findbugs tool.
- *
- * @author Andreas Ziermann
- */
-public @interface SuppressWarnings
-{
-    /**
-     * The name of the warning to be suppressed.
-     * @return The name of the warning to be suppressed.
-     */
-    @NotNull String value();
-
-    /**
-     * An explanation of why it is valid to suppress the warning in a particular situation/context.
-     * @return An explanation of why it is valid to suppress the warning in a particular situation/context.
-     */
-    @NotNull String justification();
-}
Index: trunk/src/com/drew/lang/annotations/package-info.java
===================================================================
--- trunk/src/com/drew/lang/annotations/package-info.java	(revision 15217)
+++ 	(revision )
@@ -1,5 +1,0 @@
-/**
- * Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA
- * to provide design-time warnings about potential run-time errors.
- */
-package com.drew.lang.annotations;
Index: trunk/src/com/drew/lang/package-info.java
===================================================================
--- trunk/src/com/drew/lang/package-info.java	(revision 15217)
+++ 	(revision )
@@ -1,4 +1,0 @@
-/**
- * Contains classes of generic utility.
- */
-package com.drew.lang;
