Index: trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- trunk/src/com/drew/lang/BufferBoundsException.java	(revision 10862)
+++ trunk/src/com/drew/lang/BufferBoundsException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/ByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/ByteArrayReader.java	(revision 10862)
+++ trunk/src/com/drew/lang/ByteArrayReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,7 +22,7 @@
 package com.drew.lang;
 
+import com.drew.lang.annotations.NotNull;
+
 import java.io.IOException;
-
-import com.drew.lang.annotations.NotNull;
 
 /**
@@ -39,11 +39,30 @@
     @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;
     }
 
@@ -51,11 +70,12 @@
     public long getLength()
     {
-        return _buffer.length;
+        return _buffer.length - _baseOffset;
     }
 
     @Override
-    protected byte getByte(int index) throws IOException
+    public byte getByte(int index) throws IOException
     {
-        return _buffer[index];
+        validateIndex(index, 1);
+        return _buffer[index + _baseOffset];
     }
 
@@ -64,5 +84,5 @@
     {
         if (!isValidIndex(index, bytesRequested))
-            throw new BufferBoundsException(index, bytesRequested, _buffer.length);
+            throw new BufferBoundsException(toUnshiftedOffset(index), bytesRequested, _buffer.length);
     }
 
@@ -72,5 +92,5 @@
         return bytesRequested >= 0
             && index >= 0
-            && (long)index + (long)bytesRequested - 1L < _buffer.length;
+            && (long)index + (long)bytesRequested - 1L < getLength();
     }
 
@@ -82,5 +102,5 @@
 
         byte[] bytes = new byte[count];
-        System.arraycopy(_buffer, index, bytes, 0, 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 13061)
+++ trunk/src/com/drew/lang/Charsets.java	(revision 13061)
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2017 Drew Noakes
+ *
+ *    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");
+}
Index: trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- trunk/src/com/drew/lang/CompoundException.java	(revision 10862)
+++ trunk/src/com/drew/lang/CompoundException.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- trunk/src/com/drew/lang/GeoLocation.java	(revision 10862)
+++ trunk/src/com/drew/lang/GeoLocation.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/RandomAccessReader.java
===================================================================
--- trunk/src/com/drew/lang/RandomAccessReader.java	(revision 10862)
+++ trunk/src/com/drew/lang/RandomAccessReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,8 +22,11 @@
 package com.drew.lang;
 
-import com.drew.lang.annotations.NotNull;
-
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.StringValue;
 
 /**
@@ -36,5 +39,4 @@
  * <ul>
  *     <li>{@link ByteArrayReader}</li>
- *     <li>{@link RandomAccessStreamReader}</li>
  * </ul>
  *
@@ -45,4 +47,6 @@
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract int toUnshiftedOffset(int localOffset);
+
     /**
      * Gets the byte value at the specified byte <code>index</code>.
@@ -57,5 +61,5 @@
      * @throws IOException if the byte is unable to be read
      */
-    protected abstract byte getByte(int index) throws IOException;
+    public abstract byte getByte(int index) throws IOException;
 
     /**
@@ -89,9 +93,9 @@
      * Returns the length of the data source in bytes.
      * <p>
-     * This is a simple operation for implementations (such as {@link RandomAccessFileReader} and
+     * 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 such as
-     * {@link RandomAccessStreamReader} will have to read and buffer the entire data source in
+     * 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.
      *
@@ -207,10 +211,10 @@
         if (_isMotorolaByteOrder) {
             // Motorola - MSB first
-            return (short) (((short)getByte(index    ) << 8 & (short)0xFF00) |
-                            ((short)getByte(index + 1)      & (short)0xFF));
+            return (short) ((getByte(index    ) << 8 & (short)0xFF00) |
+                            (getByte(index + 1)      & (short)0xFF));
         } else {
             // Intel ordering - LSB first
-            return (short) (((short)getByte(index + 1) << 8 & (short)0xFF00) |
-                            ((short)getByte(index    )      & (short)0xFF));
+            return (short) ((getByte(index + 1) << 8 & (short)0xFF00) |
+                            (getByte(index    )      & (short)0xFF));
         }
     }
@@ -229,12 +233,12 @@
         if (_isMotorolaByteOrder) {
             // Motorola - MSB first (big endian)
-            return (((int)getByte(index    )) << 16 & 0xFF0000) |
-                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
-                   (((int)getByte(index + 2))       & 0xFF);
+            return ((getByte(index    )) << 16 & 0xFF0000) |
+                   ((getByte(index + 1)) << 8  & 0xFF00) |
+                   ((getByte(index + 2))       & 0xFF);
         } else {
             // Intel ordering - LSB first (little endian)
-            return (((int)getByte(index + 2)) << 16 & 0xFF0000) |
-                   (((int)getByte(index + 1)) << 8  & 0xFF00) |
-                   (((int)getByte(index    ))       & 0xFF);
+            return ((getByte(index + 2)) << 16 & 0xFF0000) |
+                   ((getByte(index + 1)) << 8  & 0xFF00) |
+                   ((getByte(index    ))       & 0xFF);
         }
     }
@@ -256,5 +260,5 @@
                    (((long)getByte(index + 1)) << 16 & 0xFF0000L) |
                    (((long)getByte(index + 2)) << 8  & 0xFF00L) |
-                   (((long)getByte(index + 3))       & 0xFFL);
+                   ((getByte(index + 3))       & 0xFFL);
         } else {
             // Intel ordering - LSB first (little endian)
@@ -262,5 +266,5 @@
                    (((long)getByte(index + 2)) << 16 & 0xFF0000L) |
                    (((long)getByte(index + 1)) << 8  & 0xFF00L) |
-                   (((long)getByte(index    ))       & 0xFFL);
+                   ((getByte(index    ))       & 0xFFL);
         }
     }
@@ -312,5 +316,5 @@
                    ((long)getByte(index + 5) << 16 & 0xFF0000L) |
                    ((long)getByte(index + 6) << 8  & 0xFF00L) |
-                   ((long)getByte(index + 7)       & 0xFFL);
+                   (getByte(index + 7)       & 0xFFL);
         } else {
             // Intel ordering - LSB first
@@ -322,5 +326,5 @@
                    ((long)getByte(index + 2) << 16 & 0xFF0000L) |
                    ((long)getByte(index + 1) << 8  & 0xFF00L) |
-                   ((long)getByte(index    )       & 0xFFL);
+                   (getByte(index    )       & 0xFFL);
         }
     }
@@ -365,11 +369,17 @@
 
     @NotNull
-    public String getString(int index, int bytesRequested) throws IOException
-    {
-        return new String(getBytes(index, bytesRequested));
-    }
-
-    @NotNull
-    public String getString(int index, int bytesRequested, String charset) throws IOException
+    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);
@@ -392,16 +402,43 @@
      */
     @NotNull
-    public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException
-    {
-        // NOTE currently only really suited to single-byte character strings
-
-        byte[] bytes = getBytes(index, maxLengthBytes);
+    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 < bytes.length && bytes[length] != '\0')
+        while (length < buffer.length && buffer[length] != 0)
             length++;
 
-        return new String(bytes, 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 10862)
+++ trunk/src/com/drew/lang/Rational.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,5 +36,6 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class Rational extends java.lang.Number implements Serializable
+@SuppressWarnings("WeakerAccess")
+public class Rational extends java.lang.Number implements Comparable<Rational>, Serializable
 {
     private static final long serialVersionUID = 510688928138848770L;
@@ -173,4 +174,10 @@
                 (_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;
     }
 
@@ -212,14 +219,45 @@
 
     /**
-     * Decides whether a brute-force simplification calculation should be avoided
-     * by comparing the maximum number of possible calculations with some threshold.
-     *
-     * @return true if the simplification should be performed, otherwise false
-     */
-    private boolean tooComplexForSimplification()
-    {
-        double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2);
-        final int maxSimplificationCalculations = 1000;
-        return maxPossibleCalculations > maxSimplificationCalculations;
+     * 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();
     }
 
@@ -249,48 +287,37 @@
     /**
      * <p>
-     * Simplifies the {@link Rational} number.</p>
+     * Simplifies the representation of this {@link Rational} number.</p>
      * <p>
-     * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p>
+     * For example, 5/10 simplifies to 1/2 because both Numerator
+     * and Denominator share a common factor of 5.</p>
      * <p>
-     * To reduce a rational, need to see if both numerator and denominator are divisible
-     * by a common factor.  Using the prime number series in ascending order guarantees
-     * the minimum number of checks required.</p>
-     * <p>
-     * However, generating the prime number series seems to be a hefty task.  Perhaps
-     * it's simpler to check if both d &amp; n are divisible by all numbers from 2 {@literal ->}
-     * (Math.min(denominator, numerator) / 2).  In doing this, one can check for 2
-     * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5.
-     * This leaves four numbers from every ten to check.</p>
-     * <p>
-     * Therefore, the max number of pairs of modulus divisions required will be:</p>
-     * <pre><code>
-     *    4   Math.min(denominator, numerator) - 1
-     *   -- * ------------------------------------ + 2
-     *   10                    2
-     *
-     *   Math.min(denominator, numerator) - 1
-     * = ------------------------------------ + 2
-     *                  5
-     * </code></pre>
-     *
-     * @return a simplified instance, or if the Rational could not be simplified,
-     *         returns itself (unchanged)
+     * 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()
     {
-        if (tooComplexForSimplification()) {
-            return this;
+        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;
         }
-        for (int factor = 2; factor <= Math.min(_denominator, _numerator); factor++) {
-            if ((factor % 2 == 0 && factor > 2) || (factor % 5 == 0 && factor > 5)) {
-                continue;
-            }
-            if (_denominator % factor == 0 && _numerator % factor == 0) {
-                // found a common factor
-                return new Rational(_numerator / factor, _denominator / factor);
-            }
-        }
-        return this;
+
+        return a == 0 ? b : a;
     }
 }
Index: trunk/src/com/drew/lang/SequentialByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 10862)
+++ trunk/src/com/drew/lang/SequentialByteArrayReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +37,10 @@
     private int _index;
 
+    @Override
+    public long getPosition()
+    {
+        return _index;
+    }
+
     public SequentialByteArrayReader(@NotNull byte[] bytes)
     {
@@ -42,4 +48,5 @@
     }
 
+    @SuppressWarnings("ConstantConditions")
     public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex)
     {
@@ -52,5 +59,5 @@
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         if (_index >= _bytes.length) {
@@ -73,4 +80,15 @@
 
         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;
     }
 
@@ -105,3 +123,8 @@
         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 10862)
+++ trunk/src/com/drew/lang/SequentialReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,12 +23,16 @@
 
 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
 {
@@ -37,4 +41,6 @@
     private boolean _isMotorolaByteOrder = true;
 
+    public abstract long getPosition() throws IOException;
+
     /**
      * Gets the next byte in the sequence.
@@ -42,5 +48,5 @@
      * @return The read byte value
      */
-    protected abstract byte getByte() throws IOException;
+    public abstract byte getByte() throws IOException;
 
     /**
@@ -52,4 +58,12 @@
     @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;
 
     /**
@@ -70,4 +84,22 @@
      */
     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();
 
     /**
@@ -284,4 +316,17 @@
     }
 
+    @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>.
@@ -293,16 +338,52 @@
      */
     @NotNull
-    public String getNullTerminatedString(int maxLengthBytes) throws IOException
-    {
-        // NOTE currently only really suited to single-byte character strings
-
-        byte[] bytes = new byte[maxLengthBytes];
+    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 < bytes.length && (bytes[length] = getByte()) != '\0')
+        while (length < buffer.length && (buffer[length] = getByte()) != 0)
             length++;
 
-        return new String(bytes, 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 10862)
+++ trunk/src/com/drew/lang/StreamReader.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +37,13 @@
     private final InputStream _stream;
 
+    private long _pos;
+
+    @Override
+    public long getPosition()
+    {
+        return _pos;
+    }
+
+    @SuppressWarnings("ConstantConditions")
     public StreamReader(@NotNull InputStream stream)
     {
@@ -43,12 +52,14 @@
 
         _stream = stream;
+        _pos = 0;
     }
 
     @Override
-    protected byte getByte() throws IOException
+    public byte getByte() throws IOException
     {
         int value = _stream.read();
         if (value == -1)
             throw new EOFException("End of data reached.");
+        _pos++;
         return (byte)value;
     }
@@ -59,8 +70,15 @@
     {
         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(bytes, totalBytesRead, count - totalBytesRead);
+        while (totalBytesRead != count)
+        {
+            final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead);
             if (bytesRead == -1)
                 throw new EOFException("End of data reached.");
@@ -68,6 +86,5 @@
             assert(totalBytesRead <= count);
         }
-
-        return bytes;
+        _pos += totalBytesRead;
     }
 
@@ -93,4 +110,13 @@
     }
 
+    @Override
+    public int available() {
+        try {
+            return _stream.available();
+        } catch (IOException e) {
+            return 0;
+        }
+    }
+
     private long skipInternal(long n) throws IOException
     {
@@ -109,4 +135,5 @@
                 break;
         }
+        _pos += skippedTotal;
         return skippedTotal;
     }
Index: trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- trunk/src/com/drew/lang/StringUtil.java	(revision 10862)
+++ trunk/src/com/drew/lang/StringUtil.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,5 +34,5 @@
  * @author Drew Noakes https://drewnoakes.com
  */
-public class StringUtil
+public final class StringUtil
 {
     @NotNull
Index: trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- trunk/src/com/drew/lang/annotations/NotNull.java	(revision 10862)
+++ trunk/src/com/drew/lang/annotations/NotNull.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/Nullable.java
===================================================================
--- trunk/src/com/drew/lang/annotations/Nullable.java	(revision 10862)
+++ trunk/src/com/drew/lang/annotations/Nullable.java	(revision 13061)
@@ -1,4 +1,4 @@
 /*
- * Copyright 2002-2016 Drew Noakes
+ * Copyright 2002-2017 Drew Noakes
  *
  *    Licensed under the Apache License, Version 2.0 (the "License");
Index: trunk/src/com/drew/lang/annotations/SuppressWarnings.java
===================================================================
--- trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 13061)
+++ trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 13061)
@@ -0,0 +1,42 @@
+/*
+ * 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 13061)
+++ trunk/src/com/drew/lang/annotations/package-info.java	(revision 13061)
@@ -0,0 +1,5 @@
+/**
+ * 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/annotations/package.html
===================================================================
--- trunk/src/com/drew/lang/annotations/package.html	(revision 10862)
+++ 	(revision )
@@ -1,34 +1,0 @@
-<!--
-  ~ Copyright 2002-2016 Drew Noakes
-  ~
-  ~    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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-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.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
Index: trunk/src/com/drew/lang/package-info.java
===================================================================
--- trunk/src/com/drew/lang/package-info.java	(revision 13061)
+++ trunk/src/com/drew/lang/package-info.java	(revision 13061)
@@ -0,0 +1,4 @@
+/**
+ * Contains classes of generic utility.
+ */
+package com.drew.lang;
Index: trunk/src/com/drew/lang/package.html
===================================================================
--- trunk/src/com/drew/lang/package.html	(revision 10862)
+++ 	(revision )
@@ -1,33 +1,0 @@
-<!--
-  ~ Copyright 2002-2016 Drew Noakes
-  ~
-  ~    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
-  -->
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
-</head>
-<body bgcolor="white">
-
-Contains classes of generic utility.
-
-<!-- Put @see and @since tags down here. -->
-
-</body>
-</html>
