diff --git a/python/src/reader.rs b/python/src/reader.rs
index 73580d4..31c49b4 100644
--- a/python/src/reader.rs
+++ b/python/src/reader.rs
@@ -115,11 +115,15 @@ struct ObspecReader {
 }
 
 impl AsyncFileReader for ObspecReader {
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
         self.backend.get_range_wrapper(&self.path, range).boxed()
     }
 
-    fn get_byte_ranges(
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.backend.get_range_wrapper(&self.path, range).boxed()
+    }
+
+    fn get_image_byte_ranges(
         &self,
         ranges: Vec<Range<u64>>,
     ) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>> {
diff --git a/src/ifd.rs b/src/ifd.rs
index d522826..f390342 100644
--- a/src/ifd.rs
+++ b/src/ifd.rs
@@ -2,12 +2,12 @@ use std::collections::HashMap;
 use std::io::Read;
 use std::ops::Range;
 
-use bytes::Bytes;
+use bytes::{buf::Buf, Bytes};
 use num_enum::TryFromPrimitive;
 
 use crate::error::{AsyncTiffError, AsyncTiffResult};
 use crate::geo::{GeoKeyDirectory, GeoKeyTag};
-use crate::reader::{AsyncCursor, AsyncFileReader};
+use crate::reader::{AsyncCursor, AsyncFileReader, EndianAwareReader};
 use crate::tiff::tags::{
     CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit,
     SampleFormat, Tag, Type,
@@ -779,7 +779,7 @@ impl ImageFileDirectory {
         let range = self
             .get_tile_byte_range(x, y)
             .ok_or(AsyncTiffError::General("Not a tiled TIFF".to_string()))?;
-        let compressed_bytes = reader.get_bytes(range).await?;
+        let compressed_bytes = reader.get_image_bytes(range).await?;
         Ok(Tile {
             x,
             y,
@@ -810,7 +810,7 @@ impl ImageFileDirectory {
             .collect::<AsyncTiffResult<Vec<_>>>()?;
 
         // 2: Fetch using `get_ranges
-        let buffers = reader.get_byte_ranges(byte_ranges).await?;
+        let buffers = reader.get_image_byte_ranges(byte_ranges).await?;
 
         // 3: Create tile objects
         let mut tiles = vec![];
@@ -839,8 +839,6 @@ impl ImageFileDirectory {
 
 /// Read a single tag from the cursor
 async fn read_tag(cursor: &mut AsyncCursor, bigtiff: bool) -> AsyncTiffResult<(Tag, Value)> {
-    let start_cursor_position = cursor.position();
-
     let tag_name = Tag::from_u16_exhaustive(cursor.read_u16().await?);
 
     let tag_type_code = cursor.read_u16().await?;
@@ -855,10 +853,6 @@ async fn read_tag(cursor: &mut AsyncCursor, bigtiff: bool) -> AsyncTiffResult<(T
 
     let tag_value = read_tag_value(cursor, tag_type, count, bigtiff).await?;
 
-    // TODO: better handle management of cursor state
-    let ifd_entry_size = if bigtiff { 20 } else { 12 };
-    cursor.seek(start_cursor_position + ifd_entry_size);
-
     Ok((tag_name, tag_value))
 }
 
@@ -892,42 +886,43 @@ async fn read_tag_value(
 
     let value_byte_length = count.checked_mul(tag_size).unwrap();
 
+    // prefetch all tag data
+    let mut data = if (bigtiff && value_byte_length <= 8) || value_byte_length <= 4 {
+        // value fits in offset field
+        let res = cursor.read(value_byte_length).await?;
+        if bigtiff {
+            cursor.advance(8 - value_byte_length);
+        } else {
+            cursor.advance(4 - value_byte_length);
+        }
+        res
+    } else {
+        // Seek cursor
+        let offset = if bigtiff {
+            cursor.read_u64().await?
+        } else {
+            cursor.read_u32().await?.into()
+        };
+        let reader = cursor
+            .reader()
+            .get_metadata_bytes(offset..offset + value_byte_length)
+            .await?
+            .reader();
+        EndianAwareReader::new(reader, cursor.endianness())
+    };
     // Case 2: there is one value.
     if count == 1 {
-        // 2a: the value is 5-8 bytes and we're in BigTiff mode.
-        if bigtiff && value_byte_length > 4 && value_byte_length <= 8 {
-            let mut data = cursor.read(value_byte_length).await?;
-
-            return Ok(match tag_type {
-                Type::LONG8 => Value::UnsignedBig(data.read_u64()?),
-                Type::SLONG8 => Value::SignedBig(data.read_i64()?),
-                Type::DOUBLE => Value::Double(data.read_f64()?),
-                Type::RATIONAL => Value::Rational(data.read_u32()?, data.read_u32()?),
-                Type::SRATIONAL => Value::SRational(data.read_i32()?, data.read_i32()?),
-                Type::IFD8 => Value::IfdBig(data.read_u64()?),
-                Type::BYTE
-                | Type::SBYTE
-                | Type::ASCII
-                | Type::UNDEFINED
-                | Type::SHORT
-                | Type::SSHORT
-                | Type::LONG
-                | Type::SLONG
-                | Type::FLOAT
-                | Type::IFD => unreachable!(),
-            });
-        }
-
-        // NOTE: we should only be reading value_byte_length when it's 4 bytes or fewer. Right now
-        // we're reading even if it's 8 bytes, but then only using the first 4 bytes of this
-        // buffer.
-        let mut data = cursor.read(value_byte_length).await?;
-
-        // 2b: the value is at most 4 bytes or doesn't fit in the offset field.
         return Ok(match tag_type {
+            Type::LONG8 => Value::UnsignedBig(data.read_u64()?),
+            Type::SLONG8 => Value::SignedBig(data.read_i64()?),
+            Type::DOUBLE => Value::Double(data.read_f64()?),
+            Type::RATIONAL => Value::Rational(data.read_u32()?, data.read_u32()?),
+            Type::SRATIONAL => Value::SRational(data.read_i32()?, data.read_i32()?),
+            Type::IFD8 => Value::IfdBig(data.read_u64()?),
             Type::BYTE | Type::UNDEFINED => Value::Byte(data.read_u8()?),
             Type::SBYTE => Value::Signed(data.read_i8()? as i32),
             Type::SHORT => Value::Short(data.read_u16()?),
+            Type::IFD => Value::Ifd(data.read_u32()?),
             Type::SSHORT => Value::Signed(data.read_i16()? as i32),
             Type::LONG => Value::Unsigned(data.read_u32()?),
             Type::SLONG => Value::Signed(data.read_i32()?),
@@ -940,266 +935,120 @@ async fn read_tag_value(
                     // return Err(TiffError::FormatError(TiffFormatError::InvalidTag));
                 }
             }
-            Type::LONG8 => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                Value::UnsignedBig(cursor.read_u64().await?)
-            }
-            Type::SLONG8 => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                Value::SignedBig(cursor.read_i64().await?)
-            }
-            Type::DOUBLE => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                Value::Double(cursor.read_f64().await?)
-            }
-            Type::RATIONAL => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                let numerator = cursor.read_u32().await?;
-                let denominator = cursor.read_u32().await?;
-                Value::Rational(numerator, denominator)
-            }
-            Type::SRATIONAL => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                let numerator = cursor.read_i32().await?;
-                let denominator = cursor.read_i32().await?;
-                Value::SRational(numerator, denominator)
-            }
-            Type::IFD => Value::Ifd(data.read_u32()?),
-            Type::IFD8 => {
-                let offset = data.read_u32()?;
-                cursor.seek(offset as _);
-                Value::IfdBig(cursor.read_u64().await?)
-            }
         });
     }
 
-    // Case 3: There is more than one value, but it fits in the offset field.
-    if value_byte_length <= 4 || bigtiff && value_byte_length <= 8 {
-        let mut data = cursor.read(value_byte_length).await?;
-        if bigtiff {
-            cursor.advance(8 - value_byte_length);
-        } else {
-            cursor.advance(4 - value_byte_length);
-        }
-
-        match tag_type {
-            Type::BYTE | Type::UNDEFINED => {
-                return {
-                    Ok(Value::List(
-                        (0..count)
-                            .map(|_| Value::Byte(data.read_u8().unwrap()))
-                            .collect(),
-                    ))
-                };
-            }
-            Type::SBYTE => {
-                return {
-                    Ok(Value::List(
-                        (0..count)
-                            .map(|_| Value::Signed(data.read_i8().unwrap() as i32))
-                            .collect(),
-                    ))
-                }
-            }
-            Type::ASCII => {
-                let mut buf = vec![0; count as usize];
-                data.read_exact(&mut buf)?;
-                if buf.is_ascii() && buf.ends_with(&[0]) {
-                    let v = std::str::from_utf8(&buf)
-                        .map_err(|err| AsyncTiffError::General(err.to_string()))?;
-                    let v = v.trim_matches(char::from(0));
-                    return Ok(Value::Ascii(v.into()));
-                } else {
-                    panic!("Invalid tag");
-                    // return Err(TiffError::FormatError(TiffFormatError::InvalidTag));
-                }
-            }
-            Type::SHORT => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Short(data.read_u16()?));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::SSHORT => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Signed(i32::from(data.read_i16()?)));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::LONG => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Unsigned(data.read_u32()?));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::SLONG => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Signed(data.read_i32()?));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::FLOAT => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Float(data.read_f32()?));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::IFD => {
-                let mut v = Vec::new();
-                for _ in 0..count {
-                    v.push(Value::Ifd(data.read_u32()?));
-                }
-                return Ok(Value::List(v));
-            }
-            Type::LONG8
-            | Type::SLONG8
-            | Type::RATIONAL
-            | Type::SRATIONAL
-            | Type::DOUBLE
-            | Type::IFD8 => {
-                unreachable!()
-            }
-        }
-    }
-
-    // Seek cursor
-    let offset = if bigtiff {
-        cursor.read_u64().await?
-    } else {
-        cursor.read_u32().await?.into()
-    };
-    cursor.seek(offset);
-
-    // Case 4: there is more than one value, and it doesn't fit in the offset field.
     match tag_type {
-        // TODO check if this could give wrong results
-        // at a different endianess of file/computer.
         Type::BYTE | Type::UNDEFINED => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Byte(cursor.read_u8().await?))
+                v.push(Value::Byte(data.read_u8()?));
             }
             Ok(Value::List(v))
         }
         Type::SBYTE => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Signed(cursor.read_i8().await? as i32))
+                v.push(Value::Signed(data.read_i8()? as i32));
             }
             Ok(Value::List(v))
         }
+        Type::ASCII => {
+            let mut buf = vec![0; count as usize];
+            data.read_exact(&mut buf)?;
+            if buf.is_ascii() && buf.ends_with(&[0]) {
+                let v = std::str::from_utf8(&buf)
+                    .map_err(|err| AsyncTiffError::General(err.to_string()))?;
+                let v = v.trim_matches(char::from(0));
+                Ok(Value::Ascii(v.into()))
+            } else {
+                panic!("Invalid tag");
+                // return Err(TiffError::FormatError(TiffFormatError::InvalidTag));
+            }
+        }
         Type::SHORT => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Short(cursor.read_u16().await?))
+                v.push(Value::Short(data.read_u16()?));
             }
             Ok(Value::List(v))
         }
         Type::SSHORT => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Signed(cursor.read_i16().await? as i32))
+                v.push(Value::Signed(i32::from(data.read_i16()?)));
             }
             Ok(Value::List(v))
         }
         Type::LONG => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Unsigned(cursor.read_u32().await?))
+                v.push(Value::Unsigned(data.read_u32()?));
             }
             Ok(Value::List(v))
         }
         Type::SLONG => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Signed(cursor.read_i32().await?))
+                v.push(Value::Signed(data.read_i32()?));
             }
             Ok(Value::List(v))
         }
         Type::FLOAT => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Float(cursor.read_f32().await?))
+                v.push(Value::Float(data.read_f32()?));
             }
             Ok(Value::List(v))
         }
         Type::DOUBLE => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Double(cursor.read_f64().await?))
+                v.push(Value::Double(data.read_f64()?))
             }
             Ok(Value::List(v))
         }
         Type::RATIONAL => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Rational(
-                    cursor.read_u32().await?,
-                    cursor.read_u32().await?,
-                ))
+                v.push(Value::Rational(data.read_u32()?, data.read_u32()?))
             }
             Ok(Value::List(v))
         }
         Type::SRATIONAL => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::SRational(
-                    cursor.read_i32().await?,
-                    cursor.read_i32().await?,
-                ))
+                v.push(Value::SRational(data.read_i32()?, data.read_i32()?))
             }
             Ok(Value::List(v))
         }
         Type::LONG8 => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::UnsignedBig(cursor.read_u64().await?))
+                v.push(Value::UnsignedBig(data.read_u64()?))
             }
             Ok(Value::List(v))
         }
         Type::SLONG8 => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::SignedBig(cursor.read_i64().await?))
+                v.push(Value::SignedBig(data.read_i64()?))
             }
             Ok(Value::List(v))
         }
         Type::IFD => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::Ifd(cursor.read_u32().await?))
+                v.push(Value::Ifd(data.read_u32()?))
             }
             Ok(Value::List(v))
         }
         Type::IFD8 => {
             let mut v = Vec::with_capacity(count as _);
             for _ in 0..count {
-                v.push(Value::IfdBig(cursor.read_u64().await?))
+                v.push(Value::IfdBig(data.read_u64()?))
             }
             Ok(Value::List(v))
         }
-        Type::ASCII => {
-            let mut out = vec![0; count as _];
-            let mut buf = cursor.read(count).await?;
-            buf.read_exact(&mut out)?;
-
-            // Strings may be null-terminated, so we trim anything downstream of the null byte
-            if let Some(first) = out.iter().position(|&b| b == 0) {
-                out.truncate(first);
-            }
-            Ok(Value::Ascii(
-                String::from_utf8(out).map_err(|err| AsyncTiffError::General(err.to_string()))?,
-            ))
-        }
     }
 }
diff --git a/src/reader.rs b/src/reader.rs
index 1757dad..9a77997 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -30,12 +30,15 @@ use crate::error::{AsyncTiffError, AsyncTiffResult};
 ///
 /// [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html
 pub trait AsyncFileReader: Debug + Send + Sync {
-    /// Retrieve the bytes in `range`
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>>;
+    /// Retrieve the bytes in `range` as part of a request for header metadata.
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>>;
 
-    /// Retrieve multiple byte ranges. The default implementation will call `get_bytes`
-    /// sequentially
-    fn get_byte_ranges(
+    /// Retrieve the bytes in `range` as part of a request for image data, not header metadata.
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>>;
+
+    /// Retrieve multiple byte ranges as part of a request for image data, not header metadata. The
+    /// default implementation will call `get_image_bytes` sequentially
+    fn get_image_byte_ranges(
         &self,
         ranges: Vec<Range<u64>>,
     ) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>> {
@@ -43,7 +46,7 @@ pub trait AsyncFileReader: Debug + Send + Sync {
             let mut result = Vec::with_capacity(ranges.len());
 
             for range in ranges.into_iter() {
-                let data = self.get_bytes(range).await?;
+                let data = self.get_image_bytes(range).await?;
                 result.push(data);
             }
 
@@ -55,15 +58,19 @@ pub trait AsyncFileReader: Debug + Send + Sync {
 
 /// This allows Box<dyn AsyncFileReader + '_> to be used as an AsyncFileReader,
 impl AsyncFileReader for Box<dyn AsyncFileReader + '_> {
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
-        self.as_ref().get_bytes(range)
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.as_ref().get_metadata_bytes(range)
+    }
+
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.as_ref().get_image_bytes(range)
     }
 
-    fn get_byte_ranges(
+    fn get_image_byte_ranges(
         &self,
         ranges: Vec<Range<u64>>,
     ) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>> {
-        self.as_ref().get_byte_ranges(ranges)
+        self.as_ref().get_image_byte_ranges(ranges)
     }
 }
 
@@ -89,31 +96,36 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin + Send + Debug> Toki
     pub fn new(inner: T) -> Self {
         Self(tokio::sync::Mutex::new(inner))
     }
-}
 
-#[cfg(feature = "tokio")]
-impl<T: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin + Send + Debug> AsyncFileReader
-    for TokioReader<T>
-{
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+    async fn make_range_request(&self, range: Range<u64>) -> AsyncTiffResult<Bytes> {
         use std::io::SeekFrom;
         use tokio::io::{AsyncReadExt, AsyncSeekExt};
 
-        async move {
-            let mut file = self.0.lock().await;
-
-            file.seek(SeekFrom::Start(range.start)).await?;
+        let mut file = self.0.lock().await;
 
-            let to_read = range.end - range.start;
-            let mut buffer = Vec::with_capacity(to_read as usize);
-            let read = file.read(&mut buffer).await? as u64;
-            if read != to_read {
-                return Err(AsyncTiffError::EndOfFile(to_read, read));
-            }
+        file.seek(SeekFrom::Start(range.start)).await?;
 
-            Ok(buffer.into())
+        let to_read = range.end - range.start;
+        let mut buffer = Vec::with_capacity(to_read as usize);
+        let read = file.read(&mut buffer).await? as u64;
+        if read != to_read {
+            return Err(AsyncTiffError::EndOfFile(to_read, read));
         }
-        .boxed()
+
+        Ok(buffer.into())
+    }
+}
+
+#[cfg(feature = "tokio")]
+impl<T: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin + Send + Debug> AsyncFileReader
+    for TokioReader<T>
+{
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range).boxed()
+    }
+
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range).boxed()
     }
 }
 
@@ -133,19 +145,30 @@ impl ObjectReader {
     pub fn new(store: Arc<dyn object_store::ObjectStore>, path: object_store::path::Path) -> Self {
         Self { store, path }
     }
-}
 
-#[cfg(feature = "object_store")]
-impl AsyncFileReader for ObjectReader {
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+    async fn make_range_request(&self, range: Range<u64>) -> AsyncTiffResult<Bytes> {
         let range = range.start as _..range.end as _;
         self.store
             .get_range(&self.path, range)
             .map_err(|e| e.into())
-            .boxed()
+            .await
     }
+}
 
-    fn get_byte_ranges(&self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>>
+#[cfg(feature = "object_store")]
+impl AsyncFileReader for ObjectReader {
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range).boxed()
+    }
+
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range).boxed()
+    }
+
+    fn get_image_byte_ranges(
+        &self,
+        ranges: Vec<Range<u64>>,
+    ) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>>
     where
         Self: Send,
     {
@@ -177,11 +200,8 @@ impl ReqwestReader {
     pub fn new(client: reqwest::Client, url: reqwest::Url) -> Self {
         Self { client, url }
     }
-}
 
-#[cfg(feature = "reqwest")]
-impl AsyncFileReader for ReqwestReader {
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+    fn make_range_request(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
         let url = self.url.clone();
         let client = self.client.clone();
         // HTTP range is inclusive, so we need to subtract 1 from the end
@@ -195,6 +215,17 @@ impl AsyncFileReader for ReqwestReader {
     }
 }
 
+#[cfg(feature = "reqwest")]
+impl AsyncFileReader for ReqwestReader {
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range)
+    }
+
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        self.make_range_request(range)
+    }
+}
+
 /// An AsyncFileReader that caches the first `prefetch` bytes of a file.
 #[derive(Debug)]
 pub struct PrefetchReader {
@@ -205,13 +236,13 @@ pub struct PrefetchReader {
 impl PrefetchReader {
     /// Construct a new PrefetchReader, catching the first `prefetch` bytes of the file.
     pub async fn new(reader: Arc<dyn AsyncFileReader>, prefetch: u64) -> AsyncTiffResult<Self> {
-        let buffer = reader.get_bytes(0..prefetch).await?;
+        let buffer = reader.get_metadata_bytes(0..prefetch).await?;
         Ok(Self { reader, buffer })
     }
 }
 
 impl AsyncFileReader for PrefetchReader {
-    fn get_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+    fn get_metadata_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
         if range.start < self.buffer.len() as _ {
             if range.end < self.buffer.len() as _ {
                 let usize_range = range.start as usize..range.end as usize;
@@ -219,20 +250,29 @@ impl AsyncFileReader for PrefetchReader {
                 async { Ok(result) }.boxed()
             } else {
                 // TODO: reuse partial internal buffer
-                self.reader.get_bytes(range)
+                self.reader.get_metadata_bytes(range)
             }
         } else {
-            self.reader.get_bytes(range)
+            self.reader.get_metadata_bytes(range)
         }
     }
 
-    fn get_byte_ranges(&self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>>
+    fn get_image_bytes(&self, range: Range<u64>) -> BoxFuture<'_, AsyncTiffResult<Bytes>> {
+        // In practice, get_image_bytes is only used for fetching tiles, which are unlikely
+        // to overlap a metadata prefetch.
+        self.reader.get_image_bytes(range)
+    }
+
+    fn get_image_byte_ranges(
+        &self,
+        ranges: Vec<Range<u64>>,
+    ) -> BoxFuture<'_, AsyncTiffResult<Vec<Bytes>>>
     where
         Self: Send,
     {
-        // In practice, get_byte_ranges is only used for fetching tiles, which are unlikely to
-        // overlap a metadata prefetch.
-        self.reader.get_byte_ranges(ranges)
+        // In practice, get_image_byte_ranges is only used for fetching tiles, which are unlikely
+        // to overlap a metadata prefetch.
+        self.reader.get_image_byte_ranges(ranges)
     }
 }
 
@@ -293,7 +333,7 @@ impl AsyncCursor {
     pub(crate) async fn read(&mut self, length: u64) -> AsyncTiffResult<EndianAwareReader> {
         let range = self.offset as _..(self.offset + length) as _;
         self.offset += length;
-        let bytes = self.reader.get_bytes(range).await?;
+        let bytes = self.reader.get_metadata_bytes(range).await?;
         Ok(EndianAwareReader {
             reader: bytes.reader(),
             endianness: self.endianness,
@@ -301,31 +341,37 @@ impl AsyncCursor {
     }
 
     /// Read a u8 from the cursor, advancing the internal state by 1 byte.
+    #[allow(dead_code)]
     pub(crate) async fn read_u8(&mut self) -> AsyncTiffResult<u8> {
         self.read(1).await?.read_u8()
     }
 
     /// Read a i8 from the cursor, advancing the internal state by 1 byte.
+    #[allow(dead_code)]
     pub(crate) async fn read_i8(&mut self) -> AsyncTiffResult<i8> {
         self.read(1).await?.read_i8()
     }
 
     /// Read a u16 from the cursor, advancing the internal state by 2 bytes.
+    #[allow(dead_code)]
     pub(crate) async fn read_u16(&mut self) -> AsyncTiffResult<u16> {
         self.read(2).await?.read_u16()
     }
 
     /// Read a i16 from the cursor, advancing the internal state by 2 bytes.
+    #[allow(dead_code)]
     pub(crate) async fn read_i16(&mut self) -> AsyncTiffResult<i16> {
         self.read(2).await?.read_i16()
     }
 
     /// Read a u32 from the cursor, advancing the internal state by 4 bytes.
+    #[allow(dead_code)]
     pub(crate) async fn read_u32(&mut self) -> AsyncTiffResult<u32> {
         self.read(4).await?.read_u32()
     }
 
     /// Read a i32 from the cursor, advancing the internal state by 4 bytes.
+    #[allow(dead_code)]
     pub(crate) async fn read_i32(&mut self) -> AsyncTiffResult<i32> {
         self.read(4).await?.read_i32()
     }
@@ -336,24 +382,25 @@ impl AsyncCursor {
     }
 
     /// Read a i64 from the cursor, advancing the internal state by 8 bytes.
+    #[allow(dead_code)]
     pub(crate) async fn read_i64(&mut self) -> AsyncTiffResult<i64> {
         self.read(8).await?.read_i64()
     }
 
+    #[allow(dead_code)]
     pub(crate) async fn read_f32(&mut self) -> AsyncTiffResult<f32> {
         self.read(4).await?.read_f32()
     }
 
+    #[allow(dead_code)]
     pub(crate) async fn read_f64(&mut self) -> AsyncTiffResult<f64> {
         self.read(8).await?.read_f64()
     }
 
-    #[allow(dead_code)]
     pub(crate) fn reader(&self) -> &Arc<dyn AsyncFileReader> {
         &self.reader
     }
 
-    #[allow(dead_code)]
     pub(crate) fn endianness(&self) -> Endianness {
         self.endianness
     }
@@ -367,6 +414,7 @@ impl AsyncCursor {
         self.offset = offset;
     }
 
+    #[allow(dead_code)]
     pub(crate) fn position(&self) -> u64 {
         self.offset
     }
@@ -378,6 +426,9 @@ pub(crate) struct EndianAwareReader {
 }
 
 impl EndianAwareReader {
+    pub(crate) fn new(reader: Reader<Bytes>, endianness: Endianness) -> Self {
+        Self { reader, endianness }
+    }
     /// Read a u8 from the cursor, advancing the internal state by 1 byte.
     pub(crate) fn read_u8(&mut self) -> AsyncTiffResult<u8> {
         Ok(self.reader.read_u8()?)